diff --git a/.github/workflows/flutter-linux-native.yml b/.github/workflows/flutter-linux-native.yml new file mode 100644 index 00000000..18ce63d5 --- /dev/null +++ b/.github/workflows/flutter-linux-native.yml @@ -0,0 +1,620 @@ +name: Linux native acceptance + +# Automatic CI reuses the ordinary package and desktop checks. Stress campaigns +# remain explicit release acceptance; native fatal exits still fail their checks. +on: + workflow_dispatch: + inputs: + run-stress: + description: Include hundred-close, compiled-close and debug/profile campaigns + type: boolean + default: true + workflow_call: + inputs: + run-stress: + type: boolean + default: false + +permissions: + contents: read + +jobs: + verify: + name: Standalone release stress + if: ${{ inputs.run-stress }} + runs-on: ubuntu-24.04 + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Install Linux build dependencies + id: linux-dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + apparmor-profiles \ + at-spi2-core \ + bubblewrap \ + clang \ + cmake \ + dbus-daemon \ + gsettings-desktop-schemas \ + dconf-cli \ + fonts-noto-core \ + fonts-noto-cjk \ + fonts-noto-mono \ + ninja-build \ + pkg-config \ + libgtk-3-dev \ + libglib2.0-dev \ + libpango1.0-dev \ + libhandy-1-dev \ + libsecret-1-dev \ + libwebkit2gtk-4.1-dev \ + python3-gi \ + rsync \ + xvfb \ + xz-utils + + - name: Enable Bubblewrap sandbox for WebKitGTK tests + id: web-sandbox + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} + run: | + probe_bwrap() { + bwrap --ro-bind / / --unshare-user --unshare-pid \ + --unshare-net /usr/bin/true + } + if ! probe_bwrap; then + profile=/usr/share/apparmor/extra-profiles/bwrap-userns-restrict + test -f "$profile" + sudo install -m 0644 "$profile" \ + /etc/apparmor.d/bwrap-userns-restrict + sudo apparmor_parser --replace \ + /etc/apparmor.d/bwrap-userns-restrict + fi + probe_bwrap + + - name: Set up Node.js for bundled web engines + id: node + if: ${{ !cancelled() }} + uses: actions/setup-node@v7 + with: + node-version: '22' + cache: npm + cache-dependency-path: tools/visualization/package-lock.json + + - name: Select official pinned Flutter SDK + id: flutter-sdk + if: ${{ !cancelled() }} + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: '3.47.5' + cache: true + + - name: Enable Linux desktop + if: ${{ !cancelled() && steps.flutter-sdk.outcome == 'success' }} + run: flutter config --enable-linux-desktop + + - name: Resolve locked dependencies + id: flutter-packages + if: ${{ !cancelled() && steps.flutter-sdk.outcome == 'success' }} + run: flutter pub get --enforce-lockfile + + - name: Build Linux release + id: release-build + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' && steps.node.outcome == 'success' && steps.flutter-packages.outcome == 'success' }} + run: flutter build linux --release + + # Explicit status conditions keep independent checks running after a + # native failure. Failed checks still fail the job; none is optional. + - name: Require 100 natural closes of the shipping release + if: ${{ !cancelled() && steps.release-build.outcome == 'success' }} + run: | + xvfb-run -a -s '-screen 0 1280x1024x24' \ + dbus-run-session -- env GDK_BACKEND=x11 \ + /usr/bin/python3 -u tools/application_close_smoke.py \ + --output "${RUNNER_TEMP}/close-release" --cycles 100 --plain \ + --expected-engine build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so -- \ + build/linux/x64/release/bundle/busymark + + + - name: Build compiled-close fixture + if: ${{ !cancelled() && steps.release-build.outcome == 'success' }} + id: release-fixture + run: flutter build linux --release --no-pub -t tools/window_close_smoke.dart + + - name: Exercise live application close, persistence, media and reopening + if: ${{ !cancelled() && steps.release-fixture.outcome == 'success' && steps.web-sandbox.outcome == 'success' }} + run: | + xvfb-run -a -s '-screen 0 1280x1024x24' \ + dbus-run-session -- env GDK_BACKEND=x11 \ + /usr/bin/python3 -u tools/application_close_smoke.py \ + --output "${RUNNER_TEMP}/close-live-application" --cycles 3 --fresh-profile \ + --expected-engine build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so \ + --scenarios plain visualization pending-initialization \ + dirty-save dirty-discard dirty-cancel video \ + --video test/fixtures/media/close-testsrc.mp4 -- \ + build/linux/x64/release/bundle/busymark + + - name: Preserve natural-close evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: natural-close-release + path: | + ${{ runner.temp }}/close-release + ${{ runner.temp }}/close-live-application + if-no-files-found: ignore + + + snap: + name: Native release acceptance (snap) + runs-on: ubuntu-24.04 + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Install Snap artifact inspection prerequisites + id: snap-inspection + run: | + sudo apt-get update + sudo apt-get install -y squashfs-tools + + - name: Prepare clean Snap artifact selection + run: | + rm -f ./*.snap + touch "${RUNNER_TEMP}/busymark-snap-build-start" + + - name: Build strict Snap from a clean core24 environment (attempt 1) + id: snapcraft + uses: snapcore/action-build@v1 + continue-on-error: true + + - name: Retry strict Snap build after an infrastructure failure + id: snapcraft-retry + if: steps.snapcraft.outcome == 'failure' + uses: snapcore/action-build@v1 + + - name: Select built Snap + if: ${{ !cancelled() && steps.snap-inspection.outcome == 'success' && (steps.snapcraft.outcome == 'success' || steps.snapcraft-retry.outcome == 'success') }} + id: snap-artifact + env: + PRIMARY_SNAP: ${{ steps.snapcraft.outputs.snap }} + PRIMARY_OUTCOME: ${{ steps.snapcraft.outcome }} + RETRY_SNAP: ${{ steps.snapcraft-retry.outputs.snap }} + RETRY_OUTCOME: ${{ steps.snapcraft-retry.outcome }} + run: | + if [ "$PRIMARY_OUTCOME" = success ]; then + snap_path="$PRIMARY_SNAP" + else + test "$PRIMARY_OUTCOME" = failure + test "$RETRY_OUTCOME" = success + snap_path="$RETRY_SNAP" + fi + test -n "$snap_path" + test -f "$snap_path" + test "$snap_path" -nt "${RUNNER_TEMP}/busymark-snap-build-start" + snap_path="$(realpath "$snap_path")" + case "$snap_path" in + "$GITHUB_WORKSPACE"/*.snap) ;; + *) echo "Refusing Snap outside the clean checkout: $snap_path" >&2; exit 1 ;; + esac + test "$(unsquashfs -cat "$snap_path" snap/snapcraft.yaml | sha256sum | cut -d' ' -f1)" = \ + "$(sha256sum snap/snapcraft.yaml | cut -d' ' -f1)" + echo "snap=$snap_path" >> "$GITHUB_OUTPUT" + + - name: Upload Snap artifact + if: ${{ !cancelled() && steps.snap-artifact.outcome == 'success' }} + uses: actions/upload-artifact@v7 + with: + name: busymark-snap + path: ${{ steps.snap-artifact.outputs.snap }} + + - name: Install desktop smoke dependencies + id: strict-desktop + if: ${{ !cancelled() && steps.snap-artifact.outcome == 'success' }} + run: | + sudo apt-get update + sudo apt-get install -y weston xvfb python3-gi gir1.2-gtk-3.0 gsettings-desktop-schemas dconf-cli dbus-daemon dbus-user-session + + - name: Install strict Snap + id: strict-install + if: ${{ !cancelled() && steps.snap-artifact.outcome == 'success' && steps.strict-desktop.outcome == 'success' }} + run: >- + sudo snap install --dangerous + "${{ steps.snap-artifact.outputs.snap }}" + + - name: Prepare strict-Snap user session + if: ${{ !cancelled() && steps.strict-install.outcome == 'success' }} + id: strict-session + run: | + session_uid="$(id -u)" + sudo systemctl start "user@${session_uid}.service" + runtime_dir="/run/user/${session_uid}" + test -S "$runtime_dir/bus" + echo "XDG_RUNTIME_DIR=$runtime_dir" >> "$GITHUB_ENV" + echo "DBUS_SESSION_BUS_ADDRESS=unix:path=$runtime_dir/bus" >> "$GITHUB_ENV" + + - name: Enable parallel Snap instances + if: ${{ !cancelled() && inputs.run-stress && steps.strict-install.outcome == 'success' }} + id: strict-parallel + run: sudo snap set system experimental.parallel-instances=true + + - name: Install disposable close instance + if: ${{ !cancelled() && inputs.run-stress && steps.strict-install.outcome == 'success' && steps.strict-parallel.outcome == 'success' }} + id: strict-close-install + run: | + sudo snap install --dangerous --name=busymark_close \ + "${{ steps.snap-artifact.outputs.snap }}" + + - name: Require 100 natural closes under strict confinement + if: ${{ !cancelled() && inputs.run-stress && steps.strict-close-install.outcome == 'success' && steps.strict-session.outcome == 'success' }} + run: | + xvfb-run -a -s '-screen 0 1280x1024x24' \ + env GDK_BACKEND=x11 \ + /usr/bin/python3 -u tools/application_close_smoke.py \ + --output "${RUNNER_TEMP}/close-strict-snap" --cycles 100 \ + --snap-instance busymark_close \ + --expected-engine /snap/busymark_close/current/lib/libflutter_linux_gtk.so + + - name: Preserve strict natural-close evidence + if: ${{ always() && inputs.run-stress }} + uses: actions/upload-artifact@v7 + with: + name: natural-close-strict-snap + path: ${{ runner.temp }}/close-strict-snap + if-no-files-found: ignore + + - name: Verify GTK SVG icon loader and portable icon caches + if: ${{ !cancelled() && steps.strict-install.outcome == 'success' && steps.strict-session.outcome == 'success' }} + run: | + snap run --shell busymark -c ' + set -eu + test ! -e \ + "$SNAP/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/librsvg-2.so.2" + query="$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" + loader="$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader_svg.so" + "$query" "$loader" | grep -q "\"svg\" 6 \"gdk-pixbuf\"" + cache_tool="$SNAP_DESKTOP_RUNTIME/usr/bin/gtk-update-icon-cache" + test -x "$cache_tool" + for icon_theme in \ + "$SNAP"/usr/share/icons/* "$SNAP"/share/icons/*; do + if [ -f "$icon_theme/index.theme" ]; then + if [ -f "$icon_theme/icon-theme.cache" ]; then + "$cache_tool" --validate "$icon_theme" + fi + fi + done + test -s "$SNAP/usr/share/icons/Yaru/icon-theme.cache" + ' + + - name: Verify shared runtimes, packaged tools, media, and resources + if: ${{ !cancelled() && steps.strict-install.outcome == 'success' && steps.strict-session.outcome == 'success' }} + run: | + snap run --shell busymark -c ' + set -eu + inspect_dependencies() { + dependency_object="$1" + if dependency_output="$(ldd "$dependency_object" 2>&1)"; then + : + else + printf "Dependency inspection failed: %s\n%s\n" \ + "$dependency_object" "$dependency_output" >&2 + exit 1 + fi + case "$dependency_output" in + *"not found"*) + printf "Unresolved dependencies: %s\n%s\n" \ + "$dependency_object" "$dependency_output" >&2 + exit 1 + ;; + esac + } + require_resolution() { + required_library="$1" + required_path="$2" + resolved_path="$(printf "%s\n" "$dependency_output" | \ + awk -v name="$required_library" \ + "\$1 == name && \$2 == \"=>\" { print \$3; exit }")" + if [ -z "$resolved_path" ] || \ + ! expected_real="$(readlink -e "$required_path")" || \ + ! resolved_real="$(readlink -e "$resolved_path")" || \ + [ "$resolved_real" != "$expected_real" ]; then + printf "Unexpected resolution for %s in %s; expected %s\n%s\n" \ + "$required_library" "$dependency_object" \ + "$required_path" "$dependency_output" >&2 + exit 1 + fi + } + app_lib="$SNAP/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET" + inspect_dependencies "$SNAP/busymark" + for library in \ + libhandy-1.so.0 libsecret-1.so.0 libwebkit2gtk-4.1.so.0 \ + libgtk-3.so.0 libglib-2.0.so.0 libpango-1.0.so.0; do + test ! -e "$app_lib/$library" + require_resolution "$library" \ + "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/$library" + done + graphics_lib="$SNAP/gpu-2404/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET" + for library in \ + libX11.so.6 libXdamage.so.1 libXext.so.6 libXfixes.so.3 \ + libxcb-shm.so.0 libxcb.so.1 libwayland-client.so.0 \ + libwayland-cursor.so.0 libwayland-egl.so.1; do + test ! -e "$app_lib/$library" + test -e "$graphics_lib/$library" + require_resolution "$library" "$graphics_lib/$library" + done + for helper in WebKitWebProcess WebKitNetworkProcess WebKitGPUProcess; do + test -x "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/webkit2gtk-4.1/$helper" + done + test -x "$SNAP/usr/bin/git" + test -x "$SNAP/usr/bin/ssh" + test -x "$SNAP/usr/bin/setsid" + inspect_dependencies "$SNAP/usr/lib/git-core/git-remote-http" + require_resolution libcurl-gnutls.so.4 \ + "$app_lib/libcurl-gnutls.so.4" + GIT_EXEC_PATH="$SNAP/usr/lib/git-core" \ + "$SNAP/usr/bin/git" --exec-path | grep -Fx "$SNAP/usr/lib/git-core" + "$SNAP/usr/bin/ssh" -V + provider_plugins="$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gstreamer-1.0" + for plugin in libgstavi.so libgstisomp4.so libgstmatroska.so \ + libgstogg.so libgsttheora.so libgstvpx.so; do + test -f "$provider_plugins/$plugin" + inspect_dependencies "$provider_plugins/$plugin" + done + private_plugins="$SNAP/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gstreamer-1.0" + for plugin in libgstavi.so libgstisomp4.so libgstmatroska.so \ + libgstogg.so libgsttheora.so libgstvpx.so; do + test ! -e "$private_plugins/$plugin" + done + for plugin in libgstlibav.so libgstvideoparsersbad.so \ + libgstmpeg2dec.so; do + test -f "$private_plugins/$plugin" + inspect_dependencies "$private_plugins/$plugin" + done + test -L "$SNAP/share/busymark/fonts" + test "$(readlink "$SNAP/share/busymark/fonts")" = \ + ../../usr/share/fonts/truetype/noto + test -f "$SNAP/share/busymark/fonts/NotoSans-Regular.ttf" + test -f "$SNAP/usr/share/doc/fonts-noto-core/copyright" + test -f "$SNAP/usr/share/doc/fonts-noto-mono/copyright" + for resource_kind in themes icons; do + for resource_name in Yaru Yaru-dark; do + resource="$SNAP/share/$resource_kind/$resource_name" + test -L "$resource" + test -e "$resource" + done + done + ' + + - name: Install one strict-Snap dictionary and verify refresh/offline use + if: ${{ !cancelled() && steps.strict-install.outcome == 'success' && steps.strict-session.outcome == 'success' }} + run: | + report="$HOME/snap/busymark/common/spelling-release.json" + rm -f "$report" + xvfb-run -a -s '-screen 0 1280x1024x24' \ + env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ + LIBGL_ALWAYS_SOFTWARE=1 snap run busymark \ + --spelling-release-smoke="$report" + python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert report["checks"]["bundledDictionaryFileCount"] == 0, report; assert report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["installedDictionaryCount"] == 1, report' "$report" + test -d "$HOME/snap/busymark/common/spelling/dictionaries/downloaded/en-US" + before_revision="$(snap list busymark | awk 'NR == 2 {print $3}')" + sudo snap install --dangerous \ + "${{ steps.snap-artifact.outputs.snap }}" + after_revision="$(snap list busymark | awk 'NR == 2 {print $3}')" + test -n "$before_revision" + test -n "$after_revision" + test "$before_revision" != "$after_revision" + sudo snap disconnect busymark:network + trap 'sudo snap connect busymark:network >/dev/null' EXIT + xvfb-run -a -s '-screen 0 1280x1024x24' \ + env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ + LIBGL_ALWAYS_SOFTWARE=1 snap run busymark \ + --spelling-release-smoke="$report" + python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert not report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["installedDictionaryCount"] == 1, report; assert report["checks"]["personalWordPresentBeforeRun"], report' "$report" + sudo snap connect busymark:network + trap - EXIT + + - name: Exercise strict Snap visualization and PDF paths under X11 + if: ${{ !cancelled() && steps.strict-install.outcome == 'success' && steps.strict-session.outcome == 'success' }} + run: | + report="$HOME/snap/busymark/common/visualization-release-x11.json" + mkdir -p "$(dirname "$report")" + timeout --signal=TERM 300s \ + xvfb-run -a -s '-screen 0 1280x1024x24' \ + env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ + LIBGL_ALWAYS_SOFTWARE=1 snap run busymark \ + --visualization-release-smoke="$report" + python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report' "$report" + test -s "$HOME/snap/busymark/common/visualization-smoke.pdf" + + - name: Exercise strict Snap spelling and visualization under Wayland + if: ${{ !cancelled() && steps.strict-install.outcome == 'success' && steps.strict-session.outcome == 'success' }} + run: | + runtime_dir="/run/user/$(id -u)" + sudo install -d -m 700 -o "$(id -u)" -g "$(id -g)" "$runtime_dir" + XDG_RUNTIME_DIR="$runtime_dir" \ + weston --backend=headless-backend.so --socket=wayland-99 \ + --idle-time=0 --log="${RUNNER_TEMP}/weston-snap.log" & + weston_pid=$! + sudo snap disconnect busymark:network + cleanup_wayland() { + result=$? + trap - EXIT + if ! sudo snap connect busymark:network >/dev/null; then + result=1 + fi + kill "$weston_pid" 2>/dev/null || true + wait "$weston_pid" 2>/dev/null || true + return "$result" + } + trap cleanup_wayland EXIT + for attempt in {1..40}; do + if [ -S "$runtime_dir/wayland-99" ]; then + break + fi + if ! kill -0 "$weston_pid" 2>/dev/null; then + cat "${RUNNER_TEMP}/weston-snap.log" + exit 1 + fi + sleep 0.25 + done + test -S "$runtime_dir/wayland-99" + export runtime_dir + checks_failed=0 + bash -euo pipefail <<'SPELLING' || checks_failed=1 + spelling_report="$HOME/snap/busymark/common/spelling-release-wayland.json" + XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ + GDK_BACKEND=wayland BUSYMARK_RELEASE_SMOKE=1 \ + LIBGL_ALWAYS_SOFTWARE=1 \ + timeout --signal=TERM 300s snap run busymark \ + --spelling-release-smoke="$spelling_report" + python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert report["checks"]["sourceEditorTransaction"], report; assert report["checks"]["formattedRichEditorTransaction"], report; assert report["checks"]["tableCellEditorTransaction"], report' "$spelling_report" + SPELLING + bash -euo pipefail <<'COMPILED' || checks_failed=1 + report="$HOME/snap/busymark/common/visualization-release-wayland.json" + XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ + GDK_BACKEND=wayland BUSYMARK_RELEASE_SMOKE=1 \ + LIBGL_ALWAYS_SOFTWARE=1 \ + timeout --signal=TERM 300s snap run busymark \ + --visualization-release-smoke="$report" + python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report' "$report" + test -s "$HOME/snap/busymark/common/visualization-smoke.pdf" + COMPILED + exit "$checks_failed" + + - name: Prepare a separate compiled-close fixture package + if: ${{ !cancelled() && inputs.run-stress && steps.strict-install.outcome == 'success' }} + id: strict-fixture-source + run: | + cp "${{ steps.snap-artifact.outputs.snap }}" "${RUNNER_TEMP}/shipping-main.snap" + cp snap/snapcraft.yaml "${RUNNER_TEMP}/shipping-snapcraft.yaml" + rm -f ./*.snap + python3 - <<'PYCODE' + from pathlib import Path + recipe = Path('snap/snapcraft.yaml') + text = recipe.read_text() + assert text.count('--target lib/main.dart') == 1 + recipe.write_text(text.replace('--target lib/main.dart', '--target tools/window_close_smoke.dart')) + PYCODE + - name: Build genuine strict compiled-close fixture + if: ${{ !cancelled() && inputs.run-stress && steps.strict-fixture-source.outcome == 'success' }} + id: close-fixture-snap + uses: snapcore/action-build@v1 + - name: Install strict compiled-close fixture + if: ${{ !cancelled() && inputs.run-stress && steps.close-fixture-snap.outcome == 'success' && steps.strict-parallel.outcome == 'success' }} + id: strict-fixture-install + run: | + fixture="${{ steps.close-fixture-snap.outputs.snap }}" + test -f "$fixture" + test "$(unsquashfs -cat "$fixture" snap/snapcraft.yaml | sha256sum | cut -d' ' -f1)" = \ + "$(sha256sum snap/snapcraft.yaml | cut -d' ' -f1)" + sudo snap install --dangerous --name=busymark_fixture "$fixture" + fixture_root="$HOME/snap/busymark_fixture/common" + mkdir -p "$fixture_root/fixtures" + cp test/fixtures/media/close-testsrc.mp4 "$fixture_root/fixtures/" + + - name: Verify strict Save Discard Cancel media rendering and reopening + if: ${{ !cancelled() && inputs.run-stress && steps.strict-fixture-install.outcome == 'success' && steps.strict-session.outcome == 'success' }} + run: | + fixture_root="$HOME/snap/busymark_fixture/common" + xvfb-run -a /usr/bin/python3 tools/application_close_smoke.py \ + --output "$fixture_root/close-live" --cycles 3 --fresh-profile \ + --snap-instance busymark_fixture --snap-fixture \ + --expected-engine /snap/busymark_fixture/current/lib/libflutter_linux_gtk.so \ + --scenarios plain visualization pending-initialization dirty-save dirty-discard dirty-cancel video \ + --video "$fixture_root/fixtures/close-testsrc.mp4" + - name: Preserve strict compiled-close evidence + if: ${{ always() && inputs.run-stress }} + uses: actions/upload-artifact@v7 + with: + name: natural-close-strict-fixture + path: | + ~/snap/busymark_fixture/common/close-live + if-no-files-found: ignore + + development-modes: + name: Native release acceptance (${{ matrix.mode }}) + if: ${{ inputs.run-stress }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + mode: [debug, profile] + steps: + - uses: actions/checkout@v7 + - name: Install Linux application prerequisites + id: linux-dependencies + run: | + sudo apt-get update + sudo apt-get install -y apparmor-profiles bubblewrap clang cmake ninja-build pkg-config curl xz-utils \ + libgtk-3-dev libhandy-1-dev libsecret-1-dev libwebkit2gtk-4.1-dev \ + fonts-noto-core fonts-noto-mono gsettings-desktop-schemas dconf-cli python3-gi gir1.2-webkit2-4.1 xvfb dbus-daemon + - name: Enable Bubblewrap sandbox for WebKitGTK tests + id: web-sandbox + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} + run: | + probe_bwrap() { + bwrap --ro-bind / / --unshare-user --unshare-pid \ + --unshare-net /usr/bin/true + } + if ! probe_bwrap; then + profile=/usr/share/apparmor/extra-profiles/bwrap-userns-restrict + test -f "$profile" + sudo install -m 0644 "$profile" \ + /etc/apparmor.d/bwrap-userns-restrict + sudo apparmor_parser --replace \ + /etc/apparmor.d/bwrap-userns-restrict + fi + probe_bwrap + + - uses: actions/setup-node@v7 + id: node + if: ${{ !cancelled() }} + with: + node-version: '22' + - name: Select official pinned Flutter SDK + id: flutter-sdk + if: ${{ !cancelled() }} + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: '3.47.5' + cache: true + - run: flutter pub get --enforce-lockfile + id: flutter-packages + if: ${{ !cancelled() && steps.flutter-sdk.outcome == 'success' }} + - name: Build official development mode + id: mode-build + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' && steps.node.outcome == 'success' && steps.flutter-packages.outcome == 'success' }} + run: flutter build linux --${{ matrix.mode }} --no-pub + - name: Verify development mode GTK engine and natural close + if: ${{ !cancelled() && steps.mode-build.outcome == 'success' }} + run: | + xvfb-run -a dbus-run-session -- /usr/bin/python3 tools/application_close_smoke.py \ + --output "${RUNNER_TEMP}/close-${{ matrix.mode }}" --cycles 10 --fresh-profile --plain \ + --expected-engine build/linux/x64/${{ matrix.mode }}/bundle/lib/libflutter_linux_gtk.so -- \ + build/linux/x64/${{ matrix.mode }}/bundle/busymark + - name: Build development-mode compiled-close fixture + if: ${{ !cancelled() && steps.mode-build.outcome == 'success' }} + id: mode-fixture + run: flutter build linux --${{ matrix.mode }} --no-pub -t tools/window_close_smoke.dart + - name: Verify development-mode documents media and reopening + if: ${{ !cancelled() && steps.mode-fixture.outcome == 'success' && steps.web-sandbox.outcome == 'success' }} + run: | + xvfb-run -a dbus-run-session -- /usr/bin/python3 tools/application_close_smoke.py \ + --output "${RUNNER_TEMP}/features-${{ matrix.mode }}" --cycles 1 --fresh-profile \ + --expected-engine build/linux/x64/${{ matrix.mode }}/bundle/lib/libflutter_linux_gtk.so \ + --scenarios plain visualization pending-initialization dirty-save dirty-discard dirty-cancel video \ + --video test/fixtures/media/close-testsrc.mp4 -- \ + build/linux/x64/${{ matrix.mode }}/bundle/busymark + - uses: actions/upload-artifact@v7 + if: always() + with: + name: development-mode-${{ matrix.mode }} + path: | + ${{ runner.temp }}/close-${{ matrix.mode }} + ${{ runner.temp }}/features-${{ matrix.mode }} diff --git a/.github/workflows/flutter-linux.yml b/.github/workflows/flutter-linux.yml index 58532845..85df4900 100644 --- a/.github/workflows/flutter-linux.yml +++ b/.github/workflows/flutter-linux.yml @@ -4,11 +4,20 @@ on: pull_request: branches: - main + - release/v0.6.3 push: branches: - main +permissions: + contents: read + jobs: + native-product: + uses: ./.github/workflows/flutter-linux-native.yml + with: + run-stress: false + writerside-conformance: runs-on: ubuntu-24.04 @@ -27,6 +36,7 @@ jobs: uses: actions/checkout@v7 - name: Install Linux build dependencies + id: linux-dependencies run: | sudo apt-get update sudo apt-get install -y \ @@ -36,6 +46,8 @@ jobs: clang \ cmake \ dbus-daemon \ + gsettings-desktop-schemas \ + dconf-cli \ fonts-noto-core \ fonts-noto-cjk \ fonts-noto-mono \ @@ -51,11 +63,15 @@ jobs: poppler-utils \ python3-gi \ python3-gi-cairo \ + python3-yaml \ + rsync \ xvfb \ weston \ xz-utils - name: Enable Bubblewrap sandbox for WebKitGTK tests + id: web-sandbox + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} run: | probe_bwrap() { bwrap --ro-bind / / --unshare-user --unshare-pid \ @@ -72,106 +88,102 @@ jobs: probe_bwrap - name: Set up Node.js for bundled web engines + id: node + if: ${{ !cancelled() }} uses: actions/setup-node@v7 with: node-version: '22' cache: npm cache-dependency-path: tools/visualization/package-lock.json - - name: Set up Flutter + - name: Select official pinned Flutter SDK + id: flutter-sdk + if: ${{ !cancelled() }} uses: subosito/flutter-action@v2 with: channel: stable - flutter-version-file: pubspec.yaml + flutter-version: '3.47.5' cache: true - name: Enable Linux desktop + if: ${{ !cancelled() && steps.flutter-sdk.outcome == 'success' }} run: flutter config --enable-linux-desktop - name: Resolve locked dependencies + id: flutter-packages + if: ${{ !cancelled() && steps.flutter-sdk.outcome == 'success' }} run: flutter pub get --enforce-lockfile - name: Test production GTK accent lookup and observer + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} run: xvfb-run -a env GDK_BACKEND=x11 bash tools/test_gtk_accent.sh - name: Test secure credential namespace policy + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} run: bash tools/test_secure_credential_policy.sh + - name: Test native shortcut metadata + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} + run: xvfb-run -a env GDK_BACKEND=x11 bash tools/test_native_shortcut_metadata.sh + - name: Generate localizations + if: ${{ !cancelled() && steps.flutter-packages.outcome == 'success' }} run: flutter gen-l10n - name: Audit localization catalogs + if: ${{ !cancelled() && steps.flutter-packages.outcome == 'success' }} run: flutter test test/src/localization_audit_test.dart - name: Verify generated files are committed + if: ${{ !cancelled() && steps.flutter-packages.outcome == 'success' }} run: git diff --exit-code - name: Check formatting + if: ${{ !cancelled() && steps.flutter-packages.outcome == 'success' }} run: dart format --set-exit-if-changed . - name: Analyze + if: ${{ !cancelled() && steps.flutter-packages.outcome == 'success' }} run: flutter analyze - name: Test native Hunspell package explicitly + if: ${{ !cancelled() && steps.flutter-packages.outcome == 'success' && steps.linux-dependencies.outcome == 'success' }} working-directory: packages/busymark_spellcheck_native run: | dart pub get dart test - name: Prepare all-language spelling test resources + id: spelling-resources + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} run: tools/fetch_spelling_dictionaries.sh build/spelling-test - name: Validate every supported dictionary with the native engine + if: ${{ !cancelled() && steps.spelling-resources.outcome == 'success' && steps.flutter-packages.outcome == 'success' }} env: BUSYMARK_TEST_SPELLING_ROOT: ${{ github.workspace }}/build/spelling-test run: flutter test test/src/spelling_bundle_integration_test.dart - name: Build Linux release + if: ${{ !cancelled() && steps.flutter-packages.outcome == 'success' && steps.linux-dependencies.outcome == 'success' && steps.node.outcome == 'success' }} + id: release-build run: flutter build linux --release - name: Test production GTK accent channel host and lifecycle + if: ${{ !cancelled() && steps.release-build.outcome == 'success' }} run: xvfb-run -a env GDK_BACKEND=x11 bash tools/test_gtk_accent_host.sh - - name: Verify release ships catalog and notices without language pairs - run: | - spelling_root="build/linux/x64/release/bundle/share/busymark/spelling" - test -s "$spelling_root/dictionaries.json" - test -s "$spelling_root/NOTICE" - test -z "$(find "$spelling_root" -type f \( -name '*.aff' -o -name '*.dic' \) -print -quit)" - - - name: Install one dictionary and exercise it after an offline restart - run: | - report="${RUNNER_TEMP}/spelling-release.json" - profile="${RUNNER_TEMP}/spelling-profile" - mkdir -p "$profile" - xvfb-run -a -s '-screen 0 1280x1024x24' \ - env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ - XDG_DATA_HOME="$profile" LIBGL_ALWAYS_SOFTWARE=1 \ - build/linux/x64/release/bundle/busymark \ - --spelling-release-smoke="$report" - python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert report["checks"]["bundledDictionaryFileCount"] == 0, report; assert report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["installedDictionaryCount"] == 1, report' "$report" - sudo unshare --net --fork -- \ - runuser -u "$(id -un)" -- \ - xvfb-run -a -s '-screen 0 1280x1024x24' \ - env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ - XDG_DATA_HOME="$profile" LIBGL_ALWAYS_SOFTWARE=1 \ - build/linux/x64/release/bundle/busymark \ - --spelling-release-smoke="$report" - python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert not report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["personalWordPresentBeforeRun"], report' "$report" - - - name: Test including bundled D2 and Typst paths - env: - BUSYMARK_D2_PATH: ${{ github.workspace }}/build/linux/x64/release/bundle/libexec/busymark/d2 - BUSYMARK_TYPST_PATH: ${{ github.workspace }}/build/linux/x64/release/bundle/libexec/busymark/typst - run: flutter test - name: Exercise demand-driven native visualization lifecycle + if: ${{ !cancelled() && steps.release-build.outcome == 'success' && steps.web-sandbox.outcome == 'success' }} run: | xvfb-run -a -s '-screen 0 1280x1024x24' \ env GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 NO_AT_BRIDGE=1 \ bash tools/test_web_render_host.sh + - name: Run real WebKit, PlantUML, Mermaid, OpenAPI, and D2 smoke tests + if: ${{ !cancelled() && steps.release-build.outcome == 'success' && steps.web-sandbox.outcome == 'success' }} run: | xvfb-run -a -s '-screen 0 1280x1024x24' \ env GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 \ @@ -181,19 +193,53 @@ jobs: --demo test/fixtures/markdown/basic.md \ --plantuml-corpus test/fixtures/visualization/plantuml-conformance.md + - name: Exercise the release binary visualization and PDF paths under X11 + if: ${{ !cancelled() && steps.release-build.outcome == 'success' && steps.web-sandbox.outcome == 'success' }} run: | report="${RUNNER_TEMP}/visualization-release-x11.json" timeout --signal=TERM 300s \ xvfb-run -a -s '-screen 0 1280x1024x24' \ env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ + XDG_DATA_HOME="${RUNNER_TEMP}/visualization-profile" \ LIBGL_ALWAYS_SOFTWARE=1 \ build/linux/x64/release/bundle/busymark \ --visualization-release-smoke="$report" python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report' "$report" test -s "${RUNNER_TEMP}/visualization-smoke.pdf" + + - name: Verify release ships catalog and notices without language pairs + if: ${{ !cancelled() && steps.release-build.outcome == 'success' }} + run: | + spelling_root="build/linux/x64/release/bundle/share/busymark/spelling" + test -s "$spelling_root/dictionaries.json" + test -s "$spelling_root/NOTICE" + test -z "$(find "$spelling_root" -type f \( -name '*.aff' -o -name '*.dic' \) -print -quit)" + + - name: Install one dictionary and exercise it after an offline restart + if: ${{ !cancelled() && steps.release-build.outcome == 'success' }} + run: | + report="${RUNNER_TEMP}/spelling-release.json" + profile="${RUNNER_TEMP}/spelling-profile" + mkdir -p "$profile" + xvfb-run -a -s '-screen 0 1280x1024x24' \ + dbus-run-session -- env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ + XDG_DATA_HOME="$profile" LIBGL_ALWAYS_SOFTWARE=1 \ + build/linux/x64/release/bundle/busymark \ + --spelling-release-smoke="$report" + python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert report["checks"]["bundledDictionaryFileCount"] == 0, report; assert report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["installedDictionaryCount"] == 1, report' "$report" + sudo unshare --net --fork -- \ + runuser -u "$(id -un)" -- \ + xvfb-run -a -s '-screen 0 1280x1024x24' \ + dbus-run-session -- env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ + XDG_DATA_HOME="$profile" LIBGL_ALWAYS_SOFTWARE=1 \ + build/linux/x64/release/bundle/busymark \ + --spelling-release-smoke="$report" + python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert not report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["personalWordPresentBeforeRun"], report' "$report" + - name: Run spelling and visualization smoke tests under Wayland + if: ${{ !cancelled() && steps.release-build.outcome == 'success' && steps.web-sandbox.outcome == 'success' }} run: | runtime_dir="/run/user/$(id -u)" sudo install -d -m 700 -o "$(id -u)" -g "$(id -g)" "$runtime_dir" @@ -201,7 +247,7 @@ jobs: weston --backend=headless-backend.so --socket=wayland-99 \ --idle-time=0 --log="${RUNNER_TEMP}/weston.log" & weston_pid=$! - trap 'kill "$weston_pid" 2>/dev/null || true' EXIT + trap 'kill "$weston_pid" 2>/dev/null || true; wait "$weston_pid" 2>/dev/null || true' EXIT for attempt in {1..40}; do if [ -S "$runtime_dir/wayland-99" ]; then break @@ -213,25 +259,35 @@ jobs: sleep 0.25 done test -S "$runtime_dir/wayland-99" + export runtime_dir + checks_failed=0 + bash -euo pipefail <<'SPELLING' || checks_failed=1 spelling_report="${RUNNER_TEMP}/spelling-release-wayland.json" spelling_profile="${RUNNER_TEMP}/spelling-profile" sudo unshare --net --fork -- \ runuser -u "$(id -un)" -- \ + dbus-run-session -- \ env XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ XDG_DATA_HOME="$spelling_profile" GDK_BACKEND=wayland \ BUSYMARK_RELEASE_SMOKE=1 LIBGL_ALWAYS_SOFTWARE=1 \ build/linux/x64/release/bundle/busymark \ --spelling-release-smoke="$spelling_report" python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert report["checks"]["sourceEditorTransaction"], report; assert report["checks"]["formattedRichEditorTransaction"], report; assert report["checks"]["tableCellEditorTransaction"], report' "$spelling_report" - XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ + SPELLING + bash -euo pipefail <<'RENDERERS' || checks_failed=1 + dbus-run-session -- env \ + XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ GDK_BACKEND=wayland LIBGL_ALWAYS_SOFTWARE=1 \ /usr/bin/python3 -u tools/visualization_smoke.py \ --assets build/linux/x64/release/bundle/share/busymark/visualization \ --d2 build/linux/x64/release/bundle/libexec/busymark/d2 \ --demo test/fixtures/markdown/basic.md \ --plantuml-corpus test/fixtures/visualization/plantuml-conformance.md + RENDERERS + bash -euo pipefail <<'COMPILED' || checks_failed=1 report="${RUNNER_TEMP}/visualization-release-wayland.json" - XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ + dbus-run-session -- env \ + XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ GDK_BACKEND=wayland BUSYMARK_RELEASE_SMOKE=1 \ LIBGL_ALWAYS_SOFTWARE=1 \ timeout --signal=TERM 300s \ @@ -239,254 +295,33 @@ jobs: --visualization-release-smoke="$report" python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report' "$report" test -s "${RUNNER_TEMP}/visualization-smoke.pdf" + COMPILED + exit "$checks_failed" - snap: - runs-on: ubuntu-24.04 - - steps: - - name: Check out repository - uses: actions/checkout@v7 - - - name: Prepare clean Snap artifact selection + # Independent application checks retain their own failure outcomes. + - name: Test process ownership and application lifecycle tooling + if: ${{ !cancelled() && steps.linux-dependencies.outcome == 'success' }} run: | - rm -f ./*.snap - touch "${RUNNER_TEMP}/busymark-snap-build-start" - - - name: Build strict Snap from a clean core24 environment (attempt 1) - id: snapcraft - uses: snapcore/action-build@v1 - continue-on-error: true - - - name: Retry strict Snap build after an infrastructure failure - id: snapcraft-retry - if: steps.snapcraft.outcome == 'failure' - uses: snapcore/action-build@v1 + /usr/bin/python3 -m unittest tools/test_owned_processes.py \ + tools/test_application_close_smoke.py \ + tools/test_run_owned_fixture.py \ + tools/test_official_flutter_workflow.py - - name: Select built Snap - id: snap-artifact + - name: Test including bundled D2 and Typst paths + if: ${{ !cancelled() && steps.release-build.outcome == 'success' }} env: - PRIMARY_SNAP: ${{ steps.snapcraft.outputs.snap }} - PRIMARY_OUTCOME: ${{ steps.snapcraft.outcome }} - RETRY_SNAP: ${{ steps.snapcraft-retry.outputs.snap }} - RETRY_OUTCOME: ${{ steps.snapcraft-retry.outcome }} - run: | - if [ "$PRIMARY_OUTCOME" = success ]; then - snap_path="$PRIMARY_SNAP" - else - test "$PRIMARY_OUTCOME" = failure - test "$RETRY_OUTCOME" = success - snap_path="$RETRY_SNAP" - fi - test -n "$snap_path" - test -f "$snap_path" - test "$snap_path" -nt "${RUNNER_TEMP}/busymark-snap-build-start" - snap_path="$(realpath "$snap_path")" - case "$snap_path" in - "$GITHUB_WORKSPACE"/*.snap) ;; - *) echo "Refusing Snap outside the clean checkout: $snap_path" >&2; exit 1 ;; - esac - test "$(unsquashfs -cat "$snap_path" snap/snapcraft.yaml | sha256sum | cut -d' ' -f1)" = \ - "$(sha256sum snap/snapcraft.yaml | cut -d' ' -f1)" - echo "snap=$snap_path" >> "$GITHUB_OUTPUT" - - - name: Upload Snap artifact + BUSYMARK_D2_PATH: ${{ github.workspace }}/build/linux/x64/release/bundle/libexec/busymark/d2 + BUSYMARK_TYPST_PATH: ${{ github.workspace }}/build/linux/x64/release/bundle/libexec/busymark/typst + # Real durable fixtures share the runner's filesystem. Keep the full + # suite bounded independently of a developer machine's CPU count. + run: flutter test --concurrency=2 + + - name: Preserve compiled visualization report + if: always() uses: actions/upload-artifact@v7 with: - name: busymark-snap - path: ${{ steps.snap-artifact.outputs.snap }} - - - name: Install desktop smoke dependencies - run: | - sudo apt-get update - sudo apt-get install -y weston xvfb - - - name: Install strict Snap - run: >- - sudo snap install --dangerous - "${{ steps.snap-artifact.outputs.snap }}" - - - name: Verify GTK SVG icon loader - run: | - snap run --shell busymark -c ' - test ! -e \ - "$SNAP/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/librsvg-2.so.2" - query="$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" - loader="$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader_svg.so" - "$query" "$loader" | grep -q "\"svg\" 6 \"gdk-pixbuf\"" - ' - - - name: Verify shared runtimes, packaged tools, media, and resources - run: | - snap run --shell busymark -c ' - set -eu - inspect_dependencies() { - dependency_object="$1" - if dependency_output="$(ldd "$dependency_object" 2>&1)"; then - : - else - printf "Dependency inspection failed: %s\n%s\n" \ - "$dependency_object" "$dependency_output" >&2 - exit 1 - fi - case "$dependency_output" in - *"not found"*) - printf "Unresolved dependencies: %s\n%s\n" \ - "$dependency_object" "$dependency_output" >&2 - exit 1 - ;; - esac - } - require_resolution() { - required_library="$1" - required_path="$2" - resolved_path="$(printf "%s\n" "$dependency_output" | \ - awk -v name="$required_library" \ - "\$1 == name && \$2 == \"=>\" { print \$3; exit }")" - if [ -z "$resolved_path" ] || \ - ! expected_real="$(readlink -e "$required_path")" || \ - ! resolved_real="$(readlink -e "$resolved_path")" || \ - [ "$resolved_real" != "$expected_real" ]; then - printf "Unexpected resolution for %s in %s; expected %s\n%s\n" \ - "$required_library" "$dependency_object" \ - "$required_path" "$dependency_output" >&2 - exit 1 - fi - } - app_lib="$SNAP/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET" - inspect_dependencies "$SNAP/busymark" - for library in \ - libhandy-1.so.0 libsecret-1.so.0 libwebkit2gtk-4.1.so.0 \ - libgtk-3.so.0 libglib-2.0.so.0 libpango-1.0.so.0; do - test ! -e "$app_lib/$library" - require_resolution "$library" \ - "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/$library" - done - graphics_lib="$SNAP/gpu-2404/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET" - for library in \ - libX11.so.6 libXdamage.so.1 libXext.so.6 libXfixes.so.3 \ - libxcb-shm.so.0 libxcb.so.1 libwayland-client.so.0 \ - libwayland-cursor.so.0 libwayland-egl.so.1; do - test ! -e "$app_lib/$library" - test -e "$graphics_lib/$library" - require_resolution "$library" "$graphics_lib/$library" - done - for helper in WebKitWebProcess WebKitNetworkProcess WebKitGPUProcess; do - test -x "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/webkit2gtk-4.1/$helper" - done - test -x "$SNAP/usr/bin/git" - test -x "$SNAP/usr/bin/ssh" - test -x "$SNAP/usr/bin/setsid" - inspect_dependencies "$SNAP/usr/lib/git-core/git-remote-http" - require_resolution libcurl-gnutls.so.4 \ - "$app_lib/libcurl-gnutls.so.4" - GIT_EXEC_PATH="$SNAP/usr/lib/git-core" \ - "$SNAP/usr/bin/git" --exec-path | grep -Fx "$SNAP/usr/lib/git-core" - "$SNAP/usr/bin/ssh" -V - provider_plugins="$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gstreamer-1.0" - for plugin in libgstavi.so libgstisomp4.so libgstmatroska.so \ - libgstogg.so libgsttheora.so libgstvpx.so; do - test -f "$provider_plugins/$plugin" - inspect_dependencies "$provider_plugins/$plugin" - done - private_plugins="$SNAP/usr/lib/$SNAP_LAUNCHER_ARCH_TRIPLET/gstreamer-1.0" - for plugin in libgstavi.so libgstisomp4.so libgstmatroska.so \ - libgstogg.so libgsttheora.so libgstvpx.so; do - test ! -e "$private_plugins/$plugin" - done - for plugin in libgstlibav.so libgstvideoparsersbad.so \ - libgstmpeg2dec.so; do - test -f "$private_plugins/$plugin" - inspect_dependencies "$private_plugins/$plugin" - done - test -L "$SNAP/share/busymark/fonts" - test "$(readlink "$SNAP/share/busymark/fonts")" = \ - ../../usr/share/fonts/truetype/noto - test -f "$SNAP/share/busymark/fonts/NotoSans-Regular.ttf" - test -f "$SNAP/usr/share/doc/fonts-noto-core/copyright" - test -f "$SNAP/usr/share/doc/fonts-noto-mono/copyright" - for resource_kind in themes icons; do - for resource_name in Yaru Yaru-dark; do - resource="$SNAP/share/$resource_kind/$resource_name" - test -L "$resource" - test -e "$resource" - done - done - ' - - - name: Install one strict-Snap dictionary and verify refresh/offline use - run: | - report="$HOME/snap/busymark/common/spelling-release.json" - rm -f "$report" - xvfb-run -a -s '-screen 0 1280x1024x24' \ - env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ - LIBGL_ALWAYS_SOFTWARE=1 snap run busymark \ - --spelling-release-smoke="$report" - python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert report["checks"]["bundledDictionaryFileCount"] == 0, report; assert report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["installedDictionaryCount"] == 1, report' "$report" - test -d "$HOME/snap/busymark/common/spelling/dictionaries/downloaded/en-US" - before_revision="$(snap list busymark | awk 'NR == 2 {print $3}')" - sudo snap install --dangerous \ - "${{ steps.snap-artifact.outputs.snap }}" - after_revision="$(snap list busymark | awk 'NR == 2 {print $3}')" - test -n "$before_revision" - test -n "$after_revision" - test "$before_revision" != "$after_revision" - sudo snap disconnect busymark:network - trap 'sudo snap connect busymark:network >/dev/null' EXIT - xvfb-run -a -s '-screen 0 1280x1024x24' \ - env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ - LIBGL_ALWAYS_SOFTWARE=1 snap run busymark \ - --spelling-release-smoke="$report" - python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert not report["checks"]["dictionaryInstalledDuringRun"], report; assert report["checks"]["installedDictionaryCount"] == 1, report; assert report["checks"]["personalWordPresentBeforeRun"], report' "$report" - sudo snap connect busymark:network - trap - EXIT - - - name: Exercise strict Snap visualization and PDF paths under X11 - run: | - report="$HOME/snap/busymark/common/visualization-release-x11.json" - mkdir -p "$(dirname "$report")" - timeout --signal=TERM 300s \ - xvfb-run -a -s '-screen 0 1280x1024x24' \ - env GDK_BACKEND=x11 BUSYMARK_RELEASE_SMOKE=1 \ - LIBGL_ALWAYS_SOFTWARE=1 snap run busymark \ - --visualization-release-smoke="$report" - python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report' "$report" - test -s "$HOME/snap/busymark/common/visualization-smoke.pdf" - - - name: Exercise strict Snap spelling and visualization under Wayland - run: | - runtime_dir="/run/user/$(id -u)" - sudo install -d -m 700 -o "$(id -u)" -g "$(id -g)" "$runtime_dir" - XDG_RUNTIME_DIR="$runtime_dir" \ - weston --backend=headless-backend.so --socket=wayland-99 \ - --idle-time=0 --log="${RUNNER_TEMP}/weston-snap.log" & - weston_pid=$! - sudo snap disconnect busymark:network - trap 'sudo snap connect busymark:network >/dev/null; kill "$weston_pid" 2>/dev/null || true' EXIT - for attempt in {1..40}; do - if [ -S "$runtime_dir/wayland-99" ]; then - break - fi - if ! kill -0 "$weston_pid" 2>/dev/null; then - cat "${RUNNER_TEMP}/weston-snap.log" - exit 1 - fi - sleep 0.25 - done - test -S "$runtime_dir/wayland-99" - spelling_report="$HOME/snap/busymark/common/spelling-release-wayland.json" - XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ - GDK_BACKEND=wayland BUSYMARK_RELEASE_SMOKE=1 \ - LIBGL_ALWAYS_SOFTWARE=1 \ - timeout --signal=TERM 300s snap run busymark \ - --spelling-release-smoke="$spelling_report" - python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report; assert report["checks"]["sourceEditorTransaction"], report; assert report["checks"]["formattedRichEditorTransaction"], report; assert report["checks"]["tableCellEditorTransaction"], report' "$spelling_report" - report="$HOME/snap/busymark/common/visualization-release-wayland.json" - XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \ - GDK_BACKEND=wayland BUSYMARK_RELEASE_SMOKE=1 \ - LIBGL_ALWAYS_SOFTWARE=1 \ - timeout --signal=TERM 300s snap run busymark \ - --visualization-release-smoke="$report" - python3 -c 'import json,sys; report=json.load(open(sys.argv[1], encoding="utf-8")); assert report["ok"], report' "$report" - test -s "$HOME/snap/busymark/common/visualization-smoke.pdf" - sudo snap connect busymark:network - trap 'kill "$weston_pid" 2>/dev/null || true' EXIT + name: visualization-release + path: | + ${{ runner.temp }}/visualization-release-x11.json + ${{ runner.temp }}/visualization-smoke.pdf + if-no-files-found: ignore diff --git a/README.md b/README.md index 28f8ad60..8aec2d51 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Markdown, Writerside, and Nextcloud Notes editor for Linux. * **Writerside projects** — Open and create Writerside-compatible projects; edit Markdown and XML topics; manage instances, tables of contents, reusable TOC libraries, and project structure. * **Nextcloud Notes** — Connect to a Nextcloud server, create and edit notes offline, synchronize changes, search and filter notes, manage categories and favorites, and work with attachments. * **References and links** — Incoming/outgoing references, document/topic/heading target chooser, reviewed local Markdown rename/move, and portable Nextcloud note URLs. See [reference workflows](docs/references.md). -* **Navigation and search** — Files, Topics, and Outline views, tabbed documents, command palette, keyboard shortcuts, document search and replace, reviewed replacement across local workspaces, and Markdown TOC generation. +* **Navigation and search** — Files, Topics, and Outline views, tabbed documents, Actions search, configurable keyboard shortcuts, document search and replace, reviewed replacement across local workspaces, and Markdown TOC generation. * **Technical documentation** — Local rendering of Mermaid, PlantUML, D2, fenced OpenAPI specifications, and MathJax mathematical expressions. * **PDF and HTML publishing** — Export Markdown documents and Writerside instances to configurable PDF or portable offline HTML, with controls for layout, typography, tables of contents, heading numbering, and HTML styling. * **Git integration for local workspaces** — Review changes and diffs, stage and unstage files, commit, create and switch branches, fetch, pull, push, inspect file and project history, compare historical versions, and restore earlier file versions. @@ -58,6 +58,7 @@ sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ ninja-build \ pkg-config \ + python3 \ xz-utils ``` @@ -67,17 +68,16 @@ components. Prepare and run the application: ```bash -flutter doctor -flutter pub get +flutter pub get --enforce-lockfile flutter run -d linux ``` -Pass a Markdown file or documentation directory after `--` to open it at -startup: +Pass a Markdown file or documentation directory as a Dart entrypoint argument +to open it at startup: ```bash -flutter run -d linux -- /absolute/path/to/document.md -flutter run -d linux -- /absolute/path/to/documentation +flutter run -d linux --dart-entrypoint-args=/absolute/path/to/document.md +flutter run -d linux --dart-entrypoint-args=/absolute/path/to/documentation ``` Build and run the standard checks with: @@ -91,9 +91,16 @@ flutter test The build assembles BusyMark's runtime components. Packaged users do not need separate Typst, Java, Node.js, or Chromium installations. +Linux development and packaging use the official Flutter SDK version pinned in +`pubspec.yaml` (currently 3.47.5). No custom Flutter engine is built or selected. +See [Linux development](docs/development/linux-engine.md) for build modes and +[compiled application checks](docs/development/application-smoke-tests.md) for +native and document regression commands. A successful widget suite or build +does not establish native release acceptance. + The CI workflow installs additional packages for headless X11/Wayland, browser, PDF, and sandbox verification. Those packages are not required for an ordinary -local build; see [the Linux workflow](.github/workflows/flutter-linux.yml) when +local build; see [native acceptance](.github/workflows/flutter-linux-native.yml) when reproducing release verification. ## Snap release and security builds @@ -113,7 +120,7 @@ sha256sum ./busymark_*.snap Inspect and test the exact file emitted by the final `snapcraft` command. A failed build must not fall back to an older `.snap`. The `snap` job in -[the Linux workflow](.github/workflows/flutter-linux.yml) follows this route, +[native acceptance](.github/workflows/flutter-linux-native.yml) follows this route, installs that selected artifact, and runs the strict-confinement smoke checks. Build information embeds the original recipe for the workflow's identity check. @@ -122,6 +129,8 @@ the Flutter payload in an installed Snap scaffold for quick development and may retain that scaffold's Ubuntu libraries and bundled tools. Even with `--no-install`, its output is not a dependency refresh and must not be used for a release, a security update, or validation of `stage-packages` changes. +It selects Flutter from PATH; `BUSYMARK_FLUTTER_BIN` can select an executable +by command name or path. The selected SDK must match the pinned version. ## Screenshots @@ -145,9 +154,10 @@ a release, a security update, or validation of `stage-packages` changes. Reading view for rendered documentation. - BusyMark keyboard shortcuts dialog + Keyboard Shortcuts in Settings
- Keyboard shortcuts reference. + Find actions or record a combination, assign shortcuts, resolve conflicts, + and restore defaults. Shortcut guide. @@ -202,17 +212,17 @@ BusyMark can open a **Nextcloud Notes** workspace alongside its local Markdown a ### Connect your account 1. Ensure the **Notes** app is enabled on your Nextcloud server. -2. On BusyMark’s welcome screen, choose **Nextcloud Notes**. You can also open **Settings → Nextcloud Notes**. +2. On BusyMark’s welcome screen, choose **Browse Notes** or **New note** (both Nextcloud actions). You can also open **Settings → Nextcloud Notes**. 3. Enter the full **HTTPS** address of your Nextcloud server and click **Connect**. 4. Complete authorization in your default web browser. BusyMark verifies Notes compatibility and opens the note workspace. -Once connected, select **Nextcloud Notes** on the welcome screen to reopen the workspace. Use **Settings → Nextcloud Notes** to open it, reconnect, or disconnect the account. **One Nextcloud account can be connected at a time.** +Once connected, select **Browse Notes** on Welcome to reopen normal Notes navigation, or **New note** to create and open one note. Individual notes also reopen through Recent or Documents search. Use **Settings → Nextcloud Notes** to open it, reconnect, or disconnect the account. **One Nextcloud account can be connected at a time.** ### Work with notes * **Create and edit:** Create notes from the Notes sidebar and edit them using the normal Markdown editor. Use each note’s menu to change its title, category, or favorite status, save a separate local copy, or delete the note. * **Find and organize:** Use All notes, Favorites, Uncategorized, Recovery, or a category and its descendants. Sort by activity or title. Ctrl-click toggles selection, Shift-click selects a range, and Ctrl+A selects the current list; the selected-notes action reviews batch favorite/category changes and reports partial outcomes. -* **Quick Open and search:** Ctrl+P finds titles/categories, including unopened cached notes; Ctrl+Shift+P opens the command palette. Notes search combines terms with AND and supports quoted phrases, `title:` / `category:` restrictions, whole words, snippets, and navigation to the match. Unsaved open text is included without forcing a save. +* **Sidebar search:** Ctrl+Shift+D searches Documents by titles/categories, including unopened cached notes; Ctrl+Shift+P searches Actions. Ctrl+F searches only the current note’s buffer, while Ctrl+Shift+F searches the active account. Notes search combines terms with AND and supports quoted phrases, `title:` / `category:` restrictions, snippets, and navigation to the match. Unsaved open text is included without forcing a save. * **Work offline:** Edits and new notes are saved locally before synchronization and survive restart. **Make available offline** retains a note or category/subtree's supported managed media. Its status lists available/missing bytes and offers deliberate retry/cancellation. Offline availability, freshness, and acknowledged synchronization are separate states; external dependencies remain identified. * **Review conflicts:** If a note changes on the server while you have local edits, BusyMark preserves the competing versions for review instead of silently overwriting one. The comparison workflow supports choosing a version, merging changes, or recovering content as a separate note. Ambiguous note-creation results also require explicit review. * **Use attachments:** Add files to notes, view supported images and media, and include managed resources when exporting Markdown or HTML. PDF export includes supported images but does not embed arbitrary file attachments. New uploads are limited to **100 MiB per file**. @@ -231,7 +241,7 @@ See the [Notes workspace guide](docs/development/nextcloud-notes.md#everyday-not BusyMark supports active-document replacement and reviewed workspace-wide replacement for local workspaces. Nextcloud Notes supports replacement within individual notes, but not workspace-wide replacement. See [Search and replace](docs/search-and-replace.md) for search -options, scope, preview behavior, and stale-result handling. +matching, scopes, preview behavior, and stale-result handling. ## Diagrams and mathematics diff --git a/docs/development/application-smoke-tests.md b/docs/development/application-smoke-tests.md new file mode 100644 index 00000000..aa7d3ff6 --- /dev/null +++ b/docs/development/application-smoke-tests.md @@ -0,0 +1,60 @@ +# Compiled application checks + +Use the official pinned Flutter SDK and a disposable desktop with isolated +profiles. Never install test builds over an existing application. + +`tools/application_close_smoke.py` requires natural process exit, durable clean +shutdown and verified descendant retirement. A Dart checkpoint, disappearing +window or forced cleanup does not pass. It identifies only the launched +application's window and records failed prerequisites and native exits. +`--expected-engine` accepts the actual official `libflutter_linux_gtk.so` and +verifies its mapped inode, device and bytes through retained descriptors. + +```sh +python3 tools/application_close_smoke.py \ + --output /absolute/new/shipping-close --cycles 3 --plain \ + --expected-engine /path/to/official/libflutter_linux_gtk.so -- \ + build/linux/x64/release/bundle/busymark + +flutter build linux --release -t tools/window_close_smoke.dart +python3 tools/application_close_smoke.py \ + --output /absolute/new/document-close --cycles 1 --fresh-profile \ + --scenarios plain visualization pending-initialization \ + dirty-save dirty-discard dirty-cancel video \ + --video test/fixtures/media/close-testsrc.mp4 -- \ + build/linux/x64/release/bundle/busymark +flutter build linux --release -t lib/main.dart +``` + +The alternate entry point exercises the real application, editors and close +listener. It verifies Save/Discard/Cancel, saved-file reopening and rendered +content before closing. Frame callbacks confirm raster activity; video checks +require changing decoded frames. The pending-initialization case requires +`visualization-shutdown initPending=1`, using a harness that withholds readiness. + +The `window-controls` scenario also tests Yaru maximize, restore, minimize, close +and titlebar dragging. It requires a window manager and the explicit +`--disposable-display` option because it injects pointer input. + +`--snap-instance busymark_close` tests an owned parallel instance in a disposable +machine through its ordinary strict launcher and enforced AppArmor label. Use +the normal systemd user-session bus. Standalone checks can use a private D-Bus +session. Keep WebKit sandboxing and ordinary graphics settings enabled. + +`tools/owned_processes.py` and `tools/run_owned_fixture.py` are also used by +History and browser regressions. Each supervisor owns a dedicated child tree; +PID/start-time checks and held descriptors prevent signalling reused or unrelated +processes. Incomplete observation or unresolved cleanup fails verification and +prevents another case. History fixtures compile once per test file before their +operation deadlines, preserving interprocess lock and cancellation checks. + +```sh +python3 -m unittest tools/test_owned_processes.py \ + tools/test_application_close_smoke.py tools/test_run_owned_fixture.py \ + tools/test_official_flutter_workflow.py +``` + +These tooling tests require Clang and PyYAML in addition to system Python. +Automatic CI runs bounded native, X11/Wayland, dictionary/offline restart and +strict-Snap product checks. Hundred-close and debug/profile campaigns remain +explicitly selected release checks; see the existing workflows. diff --git a/docs/development/export-verification.md b/docs/development/export-verification.md index e1fcfef8..5286bc5f 100644 --- a/docs/development/export-verification.md +++ b/docs/development/export-verification.md @@ -140,7 +140,11 @@ do not certify physical-printer or assistive-technology behavior. ## CI environment -The Linux workflow in `.github/workflows/flutter-linux.yml` is authoritative for -native packages, environment flags, X11 and Wayland runs, and the -strict Snap verification. Keep ordinary build prerequisites in the main README; +The automatic `.github/workflows/flutter-linux.yml` workflow runs bounded X11 +host, renderer and compiled visualization/PDF checks on pull requests to `main` +and `release/v0.6.3`, and pushes to `main`. The same job reuses its release build for dictionary and Wayland checks. It +calls `.github/workflows/flutter-linux-native.yml` for genuine strict-Snap +checks; hundred-close and debug/profile campaigns remain opt-in. +Each workflow declares its native +packages and desktop prerequisites. Keep ordinary build prerequisites in the main README; do not copy the larger CI-only dependency set into user installation steps. diff --git a/docs/development/history.md b/docs/development/history.md index ab34f5fe..442c8c0c 100644 --- a/docs/development/history.md +++ b/docs/development/history.md @@ -134,8 +134,8 @@ the newly written or protective record. Local History scopes ordinary browsing to the active buffer. Scope, snapshot, and query generations prevent a delayed read or search from publishing into a -new document scope. Store-wide document search is a temporary discovery mode -for retained identities that no longer have an open file. +new document scope. Shared Local History search discovers retained identities independently of +ordinary browsing, including identities that no longer have an open file. `SourceComparisonInput` identifies immutable source versions. Comparison uses patience anchors, bounded local LCS, and intraline refinement in Dart UTF-16 @@ -214,3 +214,38 @@ JSON reports for inspection. Its optional `--application` mode opens a supplied file in the full application and requires the exact synthetic table fixture checked by the driver; use an isolated application profile and disposable files for that mode. + +Session and recovery JSON publication protects each staged inode before its +atomic rename. Linux performs permission changes off the UI isolate through a +held descriptor. Configured directory links are followed and their targets +pinned; final state-file links are rejected. Retargeting a pathname cannot +redirect an admitted permission change. Unreadable inodes use the same held +identity through procfs. Permission or inspection failures fail the transaction; +private modes, lock ownership, flush and recovery ordering remain mandatory. + +Startup capture and pruning wait for the saved History policy. Restoration +reads at most four files concurrently, preserves session order and drains +admitted reads after a failure. Adopted recovery remains protected until +restoration and required persistence finish, including when close is requested +before an editor is published. + +Opening can display validated content while Recent persistence completes. A +failed Recent update remains a warning after a successful open or save. Settings +writes retain ordered completion and retry failed publications; shutdown drains +the queue before retiring recovery ownership. + +## Shared Local History search + +`SearchCategory.localHistory` owns the header query and optional retained-document +ID. `findRetained()` reads the existing store and returns bounded document or +revision metadata without changing browsing, comparison, recording or buffer +ownership. Each request checks cancellation and the retained snapshot generation; +clear and account removal invalidate affected results. Revision bodies are read +sequentially and discarded after matching, rather than kept in widget state. + +Activation revalidates stable document/revision IDs and checksum before the +existing comparison workflow reads a revision. Its selection callback also checks +the originating search request after the read. Ordinary History navigation keeps +the complete timeline and has no search entry points or private query controller. +The shared coordinator retains explicit removable document scopes independently +of the browsing panel. diff --git a/docs/development/linux-engine.md b/docs/development/linux-engine.md new file mode 100644 index 00000000..bcd93ee5 --- /dev/null +++ b/docs/development/linux-engine.md @@ -0,0 +1,44 @@ +# Linux development with official Flutter + +Use the official Flutter SDK pinned in `pubspec.yaml` (3.47.5) on `PATH`. +BusyMark does not build or select a custom engine or GTK runtime. + +```sh +flutter pub get --enforce-lockfile +flutter run -d linux +flutter run -d linux --profile +flutter run -d linux --release +flutter build linux --debug +flutter build linux --profile +flutter build linux --release +flutter analyze +flutter test +``` + +CI selects the same official version. Snapcraft downloads the checksum-pinned +SDK and packages its unmodified release engine. Follow the README's clean +Snapcraft procedure in a disposable supported environment; do not replace an +installed application for acceptance. The local scaffold repack helper is not +release or confinement validation. Strict-Snap checks use the normal systemd +user-session bus for launcher cgroup tracking, not a private D-Bus session. +Standalone smoke checks use disposable D-Bus sessions to retire desktop helpers. + +`Flutter Linux` automatically runs bounded GTK/WebKit, compiled visualization/PDF, +dictionary installation/offline restart, Wayland and strict-Snap build/install +checks for PRs to `main` and `release/v0.6.3`, and pushes to `main`. It reuses +`Linux native acceptance` with `run-stress: false`. Run that workflow manually +with `run-stress` enabled for hundred-close, compiled document/media and +debug/profile campaigns. See [compiled application checks](application-smoke-tests.md). +A widget-suite pass does not validate the GTK embedder. + +Yaru uses the unmodified dependencies in `pubspec.lock` through Flutter's +generated plugin build. No local plugin source preparation is required. +Compiled window, document and media checks exercise the actual application; +runtime shutdown failures still fail native release acceptance. A disappearing +window or forced cleanup is not successful natural shutdown. + +Startup resolves the GTK accent or its portal fallback before building the UI, +using the existing readiness deadline. A valid GTK accent does not wait for the +portal. Saved preferences are read once before `runApp()` and handed to the +settings controller; failed reads use defaults without acknowledging them as a +persisted snapshot. diff --git a/docs/development/nextcloud-notes.md b/docs/development/nextcloud-notes.md index 051c285b..aae4aae6 100644 --- a/docs/development/nextcloud-notes.md +++ b/docs/development/nextcloud-notes.md @@ -498,23 +498,33 @@ Read-only notes support favorite changes. Partial results retain successes and identify skipped, conflicting or failed notes; resolve conflicts through the existing review workflow. -Ctrl+P opens Quick Open for the active workspace. Cached unopened Notes titles -and categories participate alongside local Markdown/Writerside files in their -own workspaces. Exact and prefix title matches rank first. Arrow keys choose, -Enter opens, and Escape dismisses. Ctrl+Shift+P still opens the command palette. - -The sidebar Search control and workspace search use one indexed Notes search -workflow. Terms combine with AND; quote a phrase, use `title:meeting` or -`category:"Work/Team"`, and select Whole word when needed. Ordinary punctuation, +Ctrl+Shift+D opens Documents in the shared search sidebar for the active workspace. +Cached unopened Notes titles and categories participate within their originating +account; document search never matches note bodies. Exact and prefix title +matches rank first. Arrow keys select, Enter opens, and Escape closes search. +Ctrl+Shift+P opens Actions in the same sidebar. Ctrl+Shift+R searches the shared +Recent documents and workspaces, including individual offline-created notes. + +Ctrl+F searches the current note's in-memory text. Ctrl+Shift+F and the Notes +sidebar Search button explicitly choose Workspace Text and use the +existing indexed Notes search. Terms combine with AND; quote a phrase, use `title:meeting` or +`category:"Work/Team"`. Ordinary punctuation, SQL/FTS operators and wildcard characters are literal. Partial terms, short queries, Unicode and decomposed accents are supported. Results include local unsynchronized text and overlay dirty open buffers without saving. Results show context, individual occurrences, loading/indexing/errors and Show more when -truncated. Opening a changed result rechecks its revision and content digest and -relocates the match. The Source location button explicitly selects source for +truncated. Opening a result rechecks its identity and content digest before +navigating to the exact occurrence; changed sources require fresh results. +The Source location button explicitly selects source for syntax that the rendered view cannot represent. Remote workspace replacement remains disabled. A missing derived index rebuilds without changing notes/outbox. +Welcome offers Nextcloud **New note** and **Browse Notes**. New note creates one +individual note through the existing offline creation workflow; Browse Notes +opens ordinary Notes navigation. Without an account, connection settings retain +the requested operation only until a successful connection or cancellation. +Opening settings alone does not create a note. + Make available offline retains an individual note or a category and descendants. Individual and category requirements are independent and survive restart. Moving notes into a retained category or adding supported managed references schedules @@ -595,3 +605,35 @@ and opens their recovery preview. Ordinary Notes search excludes these records. The preview continues to offer retained Local History revisions and recovery as a new note; it does not access server history or trash. Deleted attachments retained for recovery do not count as live offline availability. + +## Native sidebar search checks + +Build the maintained native probe with +`flutter build linux --release --no-pub -t tools/sidebar_search_smoke.dart`. +Run the resulting executable on an isolated X11 display, passing an absolute, +disposable output directory outside the checkout. Add `--large` to exercise 202 +local documents, 10,001 cached notes in the active account and a second offline +account. The probe uses the application UI, native shortcut delivery and existing +repository workflows; unexpected Flutter or Dart errors fail the run. + +The output directory contains temporary screenshots and timings, including rapid +query changes, account switching and repeated search opening/closing. It contains +only generated fixture data. Rebuild the normal Linux release afterward to +restore the production entry point. + +The desktop target also accepts `--unified-search` for a fresh, disposable +connected profile. Its private credentials JSON includes `secondaryLoginName` +and `secondaryAppPassword` for a second authorized disposable user on the same +server. Supply the test CA through the existing argument. This scenario checks +file/workspace/Documents scope, offline creation followed by synchronization, +stable Recent-note identity, direct reopening across accounts, and removal of the +originating account. It never uses production accounts or embeds credentials. + +The sidebar probe’s application timings observe search-session events, excluding +key injection and polling sleeps while including debounce. They distinguish first +useful results from completion, count file/revision reads, cancel active scans, +and compare file descriptors and RSS across repeated opening/closing. Local +search rereads eligible unopened files for each query; it retains eight-file +read-ahead and worker reuse and publishes bounded progress. Local History reads +revision bodies sequentially through the existing store and retains only result +metadata. Neither probe creates a persistent search cache or index. diff --git a/docs/development/visualizations.md b/docs/development/visualizations.md index 9856797f..0ca91fa1 100644 --- a/docs/development/visualizations.md +++ b/docs/development/visualizations.md @@ -11,6 +11,11 @@ dependency resolution, and bounded memory and disk caches. Cache keys include the engine and sanitizer versions, source, theme, render profile, options, and hashes of local dependencies. +Successful renders are published before optional disk-cache persistence finishes. +The cache warms memory before its first I/O await; the coordinator retains the +render's concurrency slot until persistence settles, bounding pending writes +without putting disk flushes on the visible-result path. + The Linux runner exposes a first-party platform channel backed by WebKitGTK 4.1. One reusable hidden render view handles Mermaid, PlantUML, OpenAPI parsing, SVG rasterization, and MathJax. BusyMark-owned windows are created only for the full @@ -62,9 +67,8 @@ view in the same ephemeral context. Process recovery also creates a fresh registry. Each engine retains its existing validation, configuration, and dependencies. Math includes all offline NewCM font tables. First use still pays for the WebKit process, harness, and requested engine; -subsequent requests reuse them. Those costs must be measured alongside plain -startup when assessing performance, including preparation that an eager -baseline completed before receiving its first request. +subsequent requests reuse them. Measure complete launch-to-output and +request-to-output paths separately. Opening an interactive reference first creates the shared context and its visible view, without loading the offscreen harness. A later render uses that @@ -82,11 +86,9 @@ callbacks retain the host until completion, check its state and view identity, and cannot recreate resources after shutdown. Private resource callbacks own their resource path independently of the host. -Set `BUSYMARK_VISUALIZATION_TRACE=1` for opt-in monotonic timings on stderr: -context construction, view construction, offscreen realization/show, harness -dispatch, and request acceptance, start, JavaScript delivery, and completion. It logs no document source. Moving -construction to first use removes unused startup work; it does not establish -the cause or elimination of a rare WebKit construction stall. +Demand-driven construction avoids initializing unused hosts. Constructor stalls +remain a separate runtime issue from the application's startup and lifecycle +contracts. ## Dependencies and packaging @@ -98,7 +100,8 @@ sources are: - `tools/fetch_d2.sh` for the D2 release, architecture, and verified hashes; - `tools/fetch_typst.sh` for Typst release artifacts and hashes; - `linux/CMakeLists.txt` and `snap/snapcraft.yaml` for bundle layout; and -- `.github/workflows/flutter-linux.yml` for release and Snap verification. +- `.github/workflows/flutter-linux.yml` for automatic native and compiled PDF checks; +- `.github/workflows/flutter-linux-native.yml` for automatic Wayland/Snap checks and opt-in release stress campaigns. The web bundle is built with `npm ci --ignore-scripts`. Build steps verify direct upstream artifacts and stage package metadata, licenses, and notices. Node.js is @@ -151,3 +154,50 @@ requires. Normal startup does not require a WebKit environment override, and Flutter and visible API-reference windows retain their normal rendering settings. Smoke tests must not globally disable WebKit compositing; `LIBGL_ALWAYS_SOFTWARE` is only used to provide Mesa rendering on headless CI displays. + +Host registration remains lightweight; only a validated video `show` request +creates video resources. + +Local video passes its single validated file through WebKit's File API and an +allowed `blob:` URL. This lets WebKit broker the file into the existing media +pipeline; GStreamer's direct `file:` reader cannot see arbitrary local paths +inside the WebProcess sandbox. The fixed local-player shell alone can select +that file. No chooser is shown, no video copy is staged, and the sandbox, CSP, +network restrictions, and separate visualization/video contexts are retained. +Each video player owns its WebKit widget until host cleanup. Closing the parent +window can destroy the overlay first; host cleanup must still disconnect player +callbacks and release that widget safely. The native `video-parent-destroy` +regression exercises this order and observes ownership through shutdown. + +The native regression script accepts individual operations and an optional +repeat count, with a fresh isolated profile for each process: + +```bash +BUSYMARK_NATIVE_PROBE_REPETITIONS=100 \ + bash tools/test_web_render_host.sh renderMermaid +bash tools/test_web_render_host.sh video-no-init video-first video-failure video-view-failure +bash tools/test_web_render_host.sh video-playback video-mini-playback +``` + +Render results are delivered before optional disk-cache publication completes. +Cache publication still occupies a bounded rendering slot, including the awaited +write, atomic rename and LRU maintenance. Derived images do not request `fsync`: +their disk cache is disposable, and invalid or incomplete entries are rejected +and recomputed after interruption. Document, session, recovery and Local History +durability use their separate stores. Full-directory metadata scans run in one short-lived +maintenance isolate at a time per cache, using only its path and numeric limits. +This avoids one main-isolate asynchronous round trip per cached file. The caller +still awaits maintenance: no unlimited background writes or maintenance queue +are introduced. + +## Compiled application lifecycle checks + +See [compiled application checks](application-smoke-tests.md) for natural closure, +Save/Discard/Cancel, reopening, native window controls and video verification. +Keep these product assertions separate from startup measurements. + +Widget tests that perform real filesystem work must start and await that work +with `tester.runAsync`, pumping UI transitions outside that callback. Readiness +helpers advance both real I/O and the widget clock within their existing +budgets. Focused History double-click tests retain pointer-gesture coverage +alongside keyboard navigation. diff --git a/docs/history.md b/docs/history.md index f9046884..338d8524 100644 --- a/docs/history.md +++ b/docs/history.md @@ -6,8 +6,9 @@ document revisions on this device for comparison and recovery. ## Clipboard History -Open **Clipboard History** from the sidebar, command palette, or an editor -context menu. Search examines each item's complete text. Select an item and use +Open **Clipboard History** from the sidebar, Actions search, or an editor +context menu. The panel shows the current clipboard followed by retained entries, +without a search field. Select an item and use **Paste** or **Paste as Plain Text**; Enter and double-click also insert the selected item at the latest editable selection. @@ -48,14 +49,30 @@ plain-text fallbacks outside BusyMark. ## Local History -Open **Local History** from the sidebar, command palette, an editor or document -tab context menu, or a file-tree context menu. The revision list normally follows -the active document. - -Use **Find in Local History…** to locate retained closed, renamed, deleted, and -previously untitled documents by name, path, or revision content. Selecting a -result temporarily inspects that document; use Back to return to the active -document. +Open **Local History** from the sidebar (Ctrl+5 by default), Actions search, an +editor or document-tab context menu, or a file-tree context menu. The revision +list normally follows the active document. + +Use **Find in Local History…** (Ctrl+Shift+L by default) to search retained closed, +renamed, deleted and previously untitled documents by name, current or historical +path, or revision content. The shared header owns the query and results appear +in the search sidebar. Scoped history results retain removable document context; +clear that context to search retained history generally. The ordinary History +panel has no Search or Find action; its management menu provides Refresh and +confirmed clearing operations. +An empty query waits for input and does not enumerate retained documents or read +revision contents. Clearing the query cancels matching and removes its results. + +A document result opens its retained timeline. A content result opens that exact +revision for comparison and leaves the search sidebar available. Inspection never +restores content or opens a current file merely because it has the same path. +Missing revisions produce feedback rather than selecting another revision. + +Opening ordinary Local History shows its full timeline, with no private search +field or remembered filter. Search retains its own query and selection for return +through the Search button. Both views use the same retained identities and +comparison operations. Use Back to return from retained inspection to the active +document. History matching is literal and case-insensitive, without regex options. BusyMark stores complete source text together with its encoding and line-ending policy. It records: diff --git a/docs/references.md b/docs/references.md index 85ae9c96..ef6a7ae7 100644 --- a/docs/references.md +++ b/docs/references.md @@ -1,6 +1,6 @@ # References and links -Open **References** from the sidebar menu or Command Palette. The panel follows +Open **References** from the sidebar menu or Search Actions. The panel follows the current document and shows its workspace/account scope. Switch between Incoming and Outgoing, or choose a parsed heading, topic element, or reusable fragment. Rows show the authored context, destination, and resolution status. diff --git a/docs/search-and-replace.md b/docs/search-and-replace.md index 75119d04..7ce533a7 100644 --- a/docs/search-and-replace.md +++ b/docs/search-and-replace.md @@ -1,45 +1,162 @@ # Search and replace -Press **Ctrl+F**, choose **Search**, or use the command palette to search the -open workspace. For local Markdown and Writerside workspaces, the search controls -use the same query and options for the active document and workspace results. +Search uses one query field in the header and one results sidebar. The compact +native-styled **Search:** selector above the results selects a category. +Normal Files, Topics, Notes and other browsing remain available when search +closes. At narrow window sizes the same sidebar temporarily overlays the leading +side of the document. -## Quick Open and Notes search +| Category | Scope | Shortcut | +| --- | --- | --- | +| Document Text | Active document’s current buffer | Ctrl+F | +| Workspace Text | Current local workspace or Notes account | Ctrl+Shift+F | +| Headings | Headings in the current document | Ctrl+Shift+H | +| Documents | Names, paths or Notes titles/categories in the active workspace | Ctrl+Shift+D | +| Topics | Topics across the current Writerside project, including outside the selected TOC | Ctrl+Shift+T | +| Recent | Chronological document and workspace history | Ctrl+Shift+R | +| Local History | Retained document names, historical paths and revision contents | Ctrl+Shift+L | +| Actions | Application commands, including commands without shortcuts | Ctrl+Shift+P | -**Ctrl+P** opens Quick Open for titles and local paths or Notes categories. -Exact/prefix title matches appear first. Use the arrow keys and Enter to open a -result, or Escape to dismiss; **Ctrl+Shift+P** remains the command palette. +Categories appear in that order; unavailable categories are omitted. Welcome +provides Recent and Actions. Retained documents behind Welcome are not search +or action targets. There is no combined category or search-results dialog. +**Settings → Keyboard Shortcuts** configures bindings. Its Actions command is +unassigned by default and opens the same Settings page, preserving the return +destination. It has no standalone main-menu entry or built-in shortcut. Explicit +user assignments remain available. The shortcuts listed here are defaults; +assignments can be changed. -In a Nextcloud Notes workspace, the sidebar search and workspace Search action -open the same indexed search dialog, including unopened cached notes, saved -pending edits, and unsaved open buffers. Terms use AND; quotes match phrases, +The Search icon toggles search and restores the last applicable category. +A category shortcut always opens its designated category and focuses the query; +Ctrl+F always chooses the current document, even after using Actions or opening +a Nextcloud note. Queries remain separate for each category within the window +session. They are not saved to settings or recovery metadata. +The clear button clears the query. Escape closes an open popup first, then +closes search and restores valid editing/navigation focus. Closing search also +restores the previous sidebar destination and visibility for the current workspace. + +Up and Down select a result without opening it. Enter activates the highlighted +result; Tab and Shift+Tab traverse controls. Disabled or stale Actions do not +execute a different result. Document formatting actions use the editor selection +captured before the query received focus and reject a changed target. +Save, Export and workspace actions also validate their original resource. +Existing Notes synchronization, folder/file import and export workflows are +available through Actions when their Notes workspace is active. + +## Document and workspace search + +**Search in Document** searches the live buffer, including unsaved text, +untitled Markdown and individual Nextcloud notes. **Search in Workspace** explicitly +widens the scope to the known local file inventory or active Notes account. An +unrelated external tab does not enlarge the local workspace scope. Text results +are occurrences; filename discovery belongs to Documents. + +For Nextcloud workspace text, the existing local Notes index includes cached, +unopened notes and dirty-buffer overlays. Terms use AND; quotes match phrases, `title:` restricts a term to the title, and `category:` restricts the category. -For example, `"alpha beta" café category:"Work/子"` combines all three restrictions. -Whole word is optional. Ordinary punctuation and technical identifiers are -literal; Notes search does not expose regular-expression or FTS operators. +For example, `"alpha beta" café category:"Work/子"` combines these restrictions. +The Notes workspace provider keeps its phrase and field query language. +Search in Document for Notes uses the normal buffer matching rules. +Documents searches note titles and categories, never note bodies. + +Loading/indexing, empty results, errors, skipped files and incomplete results are +shown in the sidebar. **Show more results** extends a bounded search. Activating +a text result validates its resource and source digest before applying exact +coordinates. A changed source requires a fresh result. A Notes result also has +an explicit source-location action for content without a rendered counterpart. +Recovery Search uses this same sidebar with an explicit **Recovery** context and +opens the existing recovery preview. Recovery records stay outside ordinary searches. + +## Outline, Topics and Recent + +Ctrl+3 and the Outline sidebar button open ordinary Outline without a query or +remembered filter. Choosing Headings in the search category menu filters the same +outline model. Explicit browsing closes search and focuses the Outline tree. + +Outline search waits for a query, then displays matching headings in document +order with their levels and ancestor context. It uses the same live model and +validated heading navigation, including unsaved headings. Ordinary Outline owns +its independent tree expansion, selection and section actions; search does not +filter or change that browsing state. +Topics searches source titles, topic IDs, filenames and project-relative paths +across modules. Rows distinguish module ownership and open the actual topic. + +Recent rows show a resource name and its containing location on two lines. +Locations inside the actual home use `~`; owned topics show their workspace and +containing folder. Outside locations remain absolute. One measured path elision +keeps useful trailing folders visible in narrow rows. +The tooltip, accessible description and Copy Path retain the complete location. + +Recent is one chronological collection of documents and workspaces, ordered by +last successful opening, with twelve entries in total. Explicit successful +openings move resources to the top. Local topics retain their project owner; individual Notes use the +originating account and stable local note identity, including offline-created +notes that later receive a server ID. Removing or clearing Recent only removes +navigation metadata. Missing resources produce feedback rather than opening a +similarly named document. Removing an account removes its Recent metadata. + +On Welcome, **New note** creates one Nextcloud note through the existing offline +creation workflow; **Browse Notes** opens normal Notes navigation. Both identify +the Nextcloud destination. Without an account they enter connection settings; +a successful connection resumes the requested operation, while cancellation or +changing context cancels it. Merely opening settings does not create a note. -Results show contextual highlights. Select a result to navigate in the current -view where supported, or choose its source-location action for Markdown syntax -that has no rendered counterpart. Changed documents are checked before applying -stored offsets. Loading/indexing, errors, empty results, and **Show more** are -explicit. Workspace-wide replacement remains unavailable for Notes. +## Local History search and browsing -The remaining sections describe local workspace search and replacement. +**Find in Local History…** opens the shared header query and sidebar. It searches +retained documents across the application, including closed, renamed, deleted and +previously untitled owners. Names and current or historical paths lead to the +retained timeline. Content results identify an exact revision and timestamp and +open its existing comparison view while keeping search results available. -## Search options +When History search has an explicit document scope, its name appears as +removable context. Clear that context to search all retained history. The +ordinary History panel has no Search or Find controls. Matching is literal and +case-insensitive. An empty History query waits for input. +Search reads retained revisions without recording, restoring, rebinding a live +buffer or substituting a same-named file. Missing revisions report failure. -- **Case sensitive** distinguishes uppercase and lowercase letters. -- **Whole word** excludes matches that are part of a longer word. -- **Regex** treats the query as a Dart regular expression. Invalid expressions - are reported instead of being searched. +The Local History sidebar button and its navigation shortcut show the ordinary, +unfiltered timeline. The panel has no private search field or hidden filter. +Returning through Search restores the shared query and selection. Clearing or +pruning retained history invalidates affected results and comparisons. -Zero-length regular-expression matches, such as `^`, `$`, or a lookahead that -matches only a position, are not supported for search or replacement. +## Literal matching and initial lists + +Document and local-workspace text search use case-insensitive literal substring +matching. `cat` finds `Cat` and `catalog`; `.*` finds those literal characters. +Whitespace is searchable. Source coordinates remain offsets into the original +buffer. Replacement text is literal too: `$1` inserts `$1`, without capture-group +substitution. There are no case, whole-word or regex toggles in the sidebar or +Source/Split controls. + +Document Text, Workspace Text, Headings, Documents, Topics, Recent and Local +History initially show **Enter text to search**. Empty queries do not start +matching, scan the workspace, enumerate topic/document inventories or read History +revision contents. Clearing cancels outstanding work and removes its results and +selection. **No results** appears only after a nonempty query completes without +matches; loading, errors and unavailable resources have distinct feedback. + +Welcome displays its ordinary chronological Recent list. Recent search waits for +input and filters that collection without changing its order. Actions initially +displays the command catalog grouped in the same categories as Keyboard Shortcuts +Settings. +Within each group, available commands come first, then localized names. Group +headings are not selectable. With text entered, Actions shows one flat list ranked +by exact name, name prefix/token matches, then weaker name or metadata matches. +Commands without shortcuts remain visible. Selection follows command identity +when metadata changes; unavailable actions cannot execute or redirect Enter. +Each category selector option shows its current effective shortcut, including +custom assignments. Unassigned categories show no shortcut. ## Active document -In Source or Split view, the search panel shows the match count and provides -previous and next navigation. Its replacement field supports: +In Source or Split view, the search panel shows the full document match count. +Previous and next navigate every occurrence, including folded and off-screen +content, and wrap only at the document boundaries. Previous without a selected +match selects the final occurrence. The sidebar automatically loads and reveals +the selected result; **Show more results** is not required for this navigation. +Its replacement field supports: - replacing the current match; - replacing the current match and moving to the next one; and @@ -49,14 +166,18 @@ These commands edit the current in-memory document. They do not save it immediately unless autosave is enabled, and they participate in normal undo and save behavior. -Editor and Reading views show workspace search results and navigate to matching +Replace and find next waits for matches in the modified document before moving +the caret. Changing the category, query, document or selection cancels +that pending navigation. + +Editor and Reading views show sidebar search results and navigate to matching content, but the active-document replacement controls are available in Source or Split view. ## Workspace search -The sidebar groups matches by file. Selecting a result opens that document and -navigates to the matching source. Workspace search covers supported text files +The sidebar shows occurrences with their file and line. Selecting a result opens +that document and navigates to the matching source. Workspace search covers supported text files already known to the open workspace, including Markdown, Writerside Markdown and XML topics, trees, configuration, variables, categories, `.gitignore`, and text resources. Images and unknown file types are excluded. @@ -69,7 +190,8 @@ the query when the result list is incomplete. ## Replace in the workspace -Workspace replacement is always reviewed: +Workspace replacement applies to supported local workspaces and is always reviewed. +It is unavailable for Notes workspaces and recovery content. 1. Run a workspace search and choose **Replace in Workspace**. 2. Enter the replacement text and choose **Review replacements**. @@ -77,10 +199,6 @@ Workspace replacement is always reviewed: exclude them. 4. Choose **Apply replacements**. -With Regex enabled, replacement text supports `$1` through `$99` for numbered -capture groups, `${name}` for named groups, `$&` for the complete match, and -`$$` for a literal dollar sign. - BusyMark rechecks every selected file and open buffer against the reviewed preview before changing it. Replacements in open documents update their editor buffers and follow normal save or autosave behavior. Replacements in unopened @@ -98,3 +216,41 @@ BusyMark does not apply an incomplete preview. If a concurrent change prevents a safe file transaction, the result identifies the affected path and, when necessary, the preserved displaced content. Always read the final replacement summary before continuing. + +## Keyboard shortcut settings + +Use **Settings → Keyboard Shortcuts** to browse the complete action catalog by +category. The page has no search or shortcut filter. Scroll to an action and +select it to record and assign a shortcut, +remove its assignment, or restore its default. **Restore all defaults** asks for +confirmation. Local text input and traversal operations remain local controls. +While typing in a search field, Select All, Cut, Copy, Paste and Undo keep editing +that field, including when those combinations are assigned to application actions. + +A custom assignment replaces the default and its aliases, including numeric-keypad +variants and Ctrl+Insert / Shift+Insert / Shift+Delete for Copy / Paste / Cut. +The Settings context line identifies default aliases. Removing +an assignment leaves the action available from its UI and Actions search. Changes +update editor dispatch, Actions badges, menus and header metadata together. +Writerside Source retains its existing Ctrl+B Go to Declaration precedence over +formatting. The declaration, usages and rename bindings are configurable too. +Select All uses the full source document, including folded content, in Source +and Split. Modified Enter combinations reach their assigned commands; ordinary +Enter and autocomplete retain their local editing behavior. + +Settings stores only overrides keyed by command identity. Logical keys respect +keyboard layout; display text is not used to identify a key. + +Conflicts name actions whose receiving contexts overlap, including commands that +are currently unavailable. Reassignment explicitly removes those conflicting +bindings in the same update. Resetting one action also checks for conflicts. +Changed search defaults yield to conflicting saved assignments; +Settings identifies the conflict and supports explicit reassignment or reset. +Separate Editor-only and Source-only operations can share a combination; each +keeps its own action and context in the list. +Capture ignores modifier-only presses and repeats. Escape cancels; held keys must +be released before normal command dispatch resumes. A desktop may reserve some +combinations that BusyMark cannot override. + +If saving fails, the current bindings remain active and Settings reports that +failure. Retry saving before relying on those assignments after restarting. diff --git a/docs/snap-confinement.md b/docs/snap-confinement.md index 882b7238..2bd1456f 100644 --- a/docs/snap-confinement.md +++ b/docs/snap-confinement.md @@ -4,6 +4,11 @@ The Snap Store build uses strict confinement. BusyMark bundles Git, OpenSSH, Typst, D2, WebKitGTK, and its other runtime dependencies rather than executing arbitrary host programs. +The build generates and validates portable GTK icon-theme caches with the GNOME +SDK. The GNOME launcher can reuse each bundled theme's index on first launch. +These indices describe the packaged icons; graphics and media preparation still +occurs in the confined application profile. + The application plugs the standard desktop, display, audio, network, `home`, `removable-media`, `ssh-keys`, and `password-manager-service` interfaces. This supports documentation projects in ordinary home-directory locations and diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 21cc4151..16e60d9b 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "ملاحظة جديدة من النص المحتفَظ به", + "notesAccountThrottled": "طلب Nextcloud التوقف مؤقتًا. انتظر قبل بدء تسجيل الدخول مجددًا.", + "notesSettingsRefreshing": "جارٍ تحديث Notes بعد تأكيد الإعدادات", + "notesExportOperationFailed": "فشل التصدير قبل النشر. تحقق من المصدر والوجهة ثم أعد التصدير.", + "notesRecoveryCreatedNotOpened": "حُفظت الملاحظة المستردة لكن تعذر فتحها. عالج تعديلات المحرر أو الخصائص المعلقة ثم افتح هذه الملاحظة الموجودة.", + "notesSettingsPendingNotes": "تمنع الملاحظات المعلقة أو التعارضات أو عمليات الإنشاء غير المؤكدة هذا التغيير. زامنها أو راجعها في Notes ثم أعد حفظ مسودة الإعدادات المحفوظة.", + "notesSettingsPendingAttachments": "تمنع عمليات المرفقات المعلقة هذا التغيير. راجع المرفقات في Notes ثم أعد حفظ مسودة الإعدادات المحفوظة.", + "notesAccountServerAddress": "استخدم عنوان HTTPS الأساسي لخادم Nextcloud وأعد تسجيل الدخول. تعذر التحقق من العنوان أو إعادة التوجيه.", + "notesAccountUnsupported": "Notes غير متاح أو واجهته غير مدعومة. فعّل Nextcloud Notes بواجهة API ذات الإصدار الرئيسي 1 والفرعي 4 أو أحدث.", + "notesAccountAuthorizeAgain": "رفض Nextcloud بيانات الاعتماد. أعد تفويض الحساب أو الاتصال به.", + "notesAccountCheckForbidden": "رفض Nextcloud فحص إمكانات Notes. اطلب من مسؤول الخادم السماح به ثم أعد المحاولة.", + "notesAccountMalformed": "تعذر التحقق من استجابة Nextcloud. تحقق من تثبيت الخادم ثم أعد تسجيل الدخول.", + "notesAccountTimedOut": "انتهت مهلة تسجيل الدخول. ابدأ مجددًا وأكمل التفويض في المتصفح.", + "notesAccountBrowserUnavailable": "تعذر فتح متصفح النظام. تحقق من المتصفح الافتراضي ثم أعد تسجيل الدخول.", + "notesAccountNetwork": "تعذر الوصول إلى Nextcloud. تحقق من الاتصال والخادم ثم أعد تسجيل الدخول.", + "notesAccountRejected": "رفض Nextcloud طلب الإمكانات. تحقق من صلاحية الوصول مع المسؤول ثم أعد المحاولة.", + "notesAccountFailed": "تعذر إكمال إعداد الحساب. أعد محاولة تسجيل الدخول.", + "notesTransferSourceChanged": "تغير المصدر بعد المراجعة. راجع الملف الحالي أو التقط نسخة جديدة قبل إعادة المحاولة.", + "notesTransferUnreadable": "تعذرت قراءة المصدر. تحقق من الوصول إلى الملف أو المجلد ثم راجعه مجددًا.", + "notesTransferInvalidSnapshot": "النسخة غير صالحة أو غير مدعومة. اختر نسخة BusyMark Notes صالحة أو مصدر Markdown.", + "notesTransferMediaLimit": "تتجاوز الوسائط حد الاستيراد. قلل وسائط العنصر المتأثر ثم راجعه مجددًا.", + "notesTransferUnsafeSource": "يحتوي المصدر على مسار غير آمن. اختر ملفات داخل مجلد المصدر ثم راجعها مجددًا.", + "notesTransferDestination": "تعذرت كتابة التصدير. تحقق من الوصول والمساحة المتاحة في الوجهة ثم أعد التصدير.", + "notesTransferOperationFailed": "تعذر إكمال النقل. راجع العناصر المتأثرة وأعد المحاولة. تبقى العناصر المستوردة سابقًا محفوظة.", + "notesExportPublishedWithOmissions": "التصدير متاح مع عناصر مستثناة", + "notesBatchUnchanged": "القيم المطلوبة مضبوطة بالفعل.", + "notesBatchReadOnly": "هذه الملاحظة للقراءة فقط. لا يمكن تغيير الخصائص المحمية؛ ما زال تغيير المفضلة مسموحًا.", + "notesBatchReviewChanged": "تغيرت هذه الملاحظة بعد المراجعة. راجع حالتها الحالية قبل تطبيق الإجراء مجددًا.", + "notesBatchMissing": "لم تعد هذه الملاحظة متاحة. حدّث القائمة وراجع العناصر المستهدفة مجددًا.", + "notesBatchStorage": "فشل الحفظ المحلي. تحقق من التخزين وأعد محاولة هذا العنصر؛ تبقى العناصر الناجحة محفوظة.", + "notesRemoteReviewChanged": "تغيرت ملاحظة الخادم. مسودة الدمج محفوظة. راجع المقارنة المحدثة واختر طريقة الحل مجددًا.", + "notesReviewRemote": "راجعت إصدار الخادم الحالي", "wsModuleCapsNotPublished": "لا يُنشر تنسيق أحرف الوحدة.", "wsTitleOverridesAction": "عناوين المثيل وجدول المحتويات…", "wsHeadingNotPublished": "لا ينشرها هذا المنشئ.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "التاريخ", "pdfTitlePageTitleRequired": "أدخل عنوانًا لصفحة العنوان.", "pdfTocSectionNote": "يبدأ في صفحة منفصلة وقد يمتد إلى عدة صفحات.", - "sourceSearchZeroLengthUnsupported": "لا تُدعم مطابقات التعبير النمطي ذات الطول الصفري (مثل ^ أو $ أو الاستباق الذي يطابق موضعًا فقط).", "workspaceSearchIncomplete": "تظهر بعض النتائج فقط. اعرض المزيد أو ضيّق نطاق البحث.", "workspaceSearchShowMore": "عرض المزيد من النتائج", "workspaceSearchSkippedFiles": "البحث غير مكتمل. تجاوز حجم هذه الملفات 1 MiB أو تعذّرت قراءتها:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "إظهار مرجع اختصارات لوحة المفاتيح", + "shortcutKeyboardShortcutsDescription": "تخصيص اختصارات لوحة مفاتيح التطبيق", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "مسح مساحات العمل الأخيرة", + "clearRecentWorkspaces": "مسح السجل الأخير", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "حساس لحالة الأحرف", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "الكلمة بالكامل", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "تعبير نمطي", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "تعبير نمطي غير صالح", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "ملف كبير: تم إيقاف التمييز والطي مؤقتًا", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "تعذر تحديث العناصر الأخيرة: {error}.", "workspaceErrorOpenFailed": "فشل الفتح: ⁨{error}⁩", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "نص الرابط فارغ؛ أدخل اسمًا ميسّرًا يصف الغرض منه.", "diagnosticMarkdownLinkReviewText": "راجع ما إذا كان نص الرابط «⁨{text}⁩» يصف غرضه ضمن السياق.", "diagnosticMarkdownTableEmptyHeader": "يجب أن تعرّف رؤوس الجدول أعمدتها؛ أكمل كل رأس فارغ.", - "commandPalette": "لوحة الأوامر", - "commandPaletteHint": "اكتب أمرًا", - "commandPaletteEmpty": "لا توجد أوامر مطابقة", "tableAlignmentUnspecified": "المحاذاة: غير محددة", "tableAlignmentLeft": "المحاذاة: إلى اليسار", "tableAlignmentCenter": "المحاذاة: إلى الوسط", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "اختر ملف .css مقروءًا بترميز UTF-8 حتى 256 KiB، دون استيراد أو عناوين موارد أو HTML أو CSS قابل للتنفيذ أو يحتوي على محارف هروب.", "exportInvalidRange": "{field}: أدخل رقمًا بين {minimum} و{maximum}.", "clipboardHistory": "سجل النسخ واللصق", - "clipboardHistorySearchHint": "البحث في سجل الحافظة", "clipboardHistoryDescription": "افتح سجل النسخ واللصق الخاص بالجلسة.", "clipboardCurrent": "النسخة واللصق الحالية", "clipboardCurrentExternal": "النسخة واللصق من تطبيق آخر", "clipboardDestination": "الصق في {name}", "clipboardSessionOnly": "مخزن فقط لهذه جلسة BusyMark.", "clipboardNoItems": "سجل النسخ واللصق فارغ.", - "clipboardNoMatches": "لا توجد عناصر في سجل النسخ واللصق تطابق هذا البحث.", "clipboardDisabled": "مجموعة سجل النسخ واللصق معطلة في الإعدادات.", "clipboardUnavailable": "محتوى النسخة واللصق الحالي غير متاح أو غير مدعوم.", "clipboardTooLarge": "هذا العنصر في النسخة واللصق كبير جدًا بحيث لا يمكن الاحتفاظ به.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "البحث في مستندات التاريخ المحلي ومحتوى الإصدارات.", "localHistoryNoDocuments": "لا توجد مستندات لديها تاريخ محلي.", "localHistoryNoRevisions": "هذا المستند ليس لديه إصدارات في التاريخ المحلي.", - "localHistoryNoMatches": "لا تتطابق أي إصدارات في التاريخ المحلي مع هذا البحث.", - "localHistorySearchHint": "البحث في السجل المحلي", "localHistorySelectedRevision": "الإصدار المحدد", "localHistoryCurrentEditor": "محتوى المحرر الحالي", "localHistoryCurrentDisk": "المحتوى الحالي على القرص", @@ -3610,5 +3619,125 @@ "wsInsertElements": "إدراج عناصر", "wsAdmonitionsAndQuotes": "تنبيهات واقتباسات", "wsSemanticText": "نص دلالي", - "wsPropertiesUnavailable": "العنصر الذي يتم فحصه غير متاح. افحص عنصرًا للمتابعة." + "wsPropertiesUnavailable": "العنصر الذي يتم فحصه غير متاح. افحص عنصرًا للمتابعة.", + "searchDocumentsCategory": "المستندات", + "searchTopicsCategory": "المواضيع", + "searchCurrentDocument": "المستند الحالي", + "searchCurrentWorkspace": "مساحة العمل الحالية", + "browseNotes": "تصفح الملاحظات", + "searchActionsCommand": "البحث عن إجراءات", + "searchDocumentsCommand": "البحث عن مستندات", + "searchTopicsCommand": "البحث عن مواضيع", + "searchRecentCommand": "البحث في الأخيرة", + "searchResourceUnavailable": "هذا المورد غير متاح.", + "notesSync": "مزامنة الملاحظات", + "searchInWorkspace": "البحث في مساحة العمل", + "searchCategory": "فئة البحث", + "clearHistorySearchScope": "البحث في سجل المستندات", + "searchInDocument": "البحث في المستند", + "searchDocumentHint": "ابحث في هذا المستند…", + "searchWorkspaceHint": "ابحث في مساحة العمل هذه…", + "searchOutlineHint": "ابحث في العناوين…", + "searchHistoryHint": "ابحث في السجل المحلي…", + "searchEnterText": "أدخل نصًا للبحث", + "searchAccountUnavailable": "الحساب غير متاح", + "searchDocumentsHint": "البحث عن مستندات…", + "searchTopicsHint": "البحث عن مواضيع…", + "searchRecentHint": "البحث في الأخيرة…", + "searchActionsHint": "البحث عن إجراءات…", + "searchRequiresNotesWorkspace": "يتطلب مساحة عمل للملاحظات", + "searchSidebarLabel": "بحث:", + "searchDocumentTextCategory": "نص المستند", + "searchWorkspaceTextCategory": "نص مساحة العمل", + "searchHeadingsCategory": "العناوين", + "shortcutUnassigned": "غير معيّن", + "shortcutPressKeys": "اضغط مجموعة مفاتيح…", + "shortcutRecord": "تسجيل اختصار", + "shortcutRestoreAll": "استعادة جميع الإعدادات الافتراضية", + "shortcutRestoreAllQuestion": "هل تريد استعادة الاختصارات الافتراضية لجميع الإجراءات؟ ستُزال التعيينات المخصصة.", + "shortcutSaveFailed": "الاختصارات مفعّلة ولكن تعذر حفظها. أعد المحاولة للاحتفاظ بها بعد إعادة التشغيل.", + "shortcutRetry": "إعادة محاولة الحفظ", + "shortcutChanged": "تغيرت التعيينات. راجع الاختصارات الحالية وحاول مجددًا.", + "shortcutInvalid": "لا يمكن تعيين هذه المجموعة في هذا السياق.", + "shortcutSystemWarning": "قد يستخدم سطح المكتب هذه المجموعة. لا يستطيع BusyMark تجاوز اختصارات النظام.", + "shortcutConflicts": "يتعارض هذا الاختصار مع الإجراءات التالية:", + "shortcutRemove": "إزالة التعيين", + "shortcutRestoreDefault": "استعادة الافتراضي", + "shortcutAssign": "تعيين", + "shortcutReassign": "إعادة التعيين", + "shortcutContextApplication": "التطبيق", + "shortcutContextDocument": "طرق عرض المستند", + "shortcutContextEditor": "المحرر والمصدر", + "shortcutContextWritersideSource": "مصدر Writerside", + "shortcutContextWorkspace": "التنقل في مساحة العمل", + "shortcutContextTree": "أشجار الشريط الجانبي", + "searchHeadingsCommand": "البحث عن العناوين", + "shortcutFixed": "يتولاه المحرر؛ غير قابل للتخصيص.", + "shortcutDefaultConflict": "يتعارض الاختصار الافتراضي مع تعيين مخصص.", + "shortcutCurrentBinding": "الاختصار الحالي", + "shortcutProposedBinding": "الاختصار المقترح", + "notesActions": "إجراءات الملاحظات", + "noteActions": "إجراءات الملاحظة", + "noteProperties": "خصائص الملاحظة", + "notesOfflinePause": "إيقاف التنزيلات مؤقتًا", + "notesOfflineResume": "استئناف التنزيلات", + "notesEmptyAll": "لا توجد ملاحظات بعد. أنشئ ملاحظة للبدء.", + "notesEmptyFavorites": "لا توجد ملاحظات مفضلة.", + "notesEmptyCategory": "لا توجد ملاحظات في هذه الفئة.", + "notesEmptyUncategorized": "لا توجد ملاحظات غير مصنفة.", + "notesEmptyRecovery": "لا توجد ملاحظات محفوظة لاستعادتها.", + "notesStartingConnection": "جارٍ بدء الاتصال بـ Nextcloud…", + "notesVerifyingConnection": "جارٍ التحقق من الوصول إلى الملاحظات…", + "notesCancellingConnection": "جارٍ إلغاء تسجيل الدخول والتنظيف…", + "notesRemovingConnection": "جارٍ إزالة الحساب المحلي…", + "notesSettingsLeave": "حفظ تغييرات إعدادات Nextcloud قبل المغادرة؟", + "notesSettingsReload": "إعادة تحميل إعدادات الخادم…", + "notesScanning": "جارٍ فحص الملفات…", + "notesCancelling": "جارٍ الإلغاء… في انتظار انتهاء العملية الحالية.", + "notesCancelled": "أُلغي", + "notesPartial": "اكتمل جزئيًا", + "notesNotStarted": "لم يبدأ", + "notesCreated": "أُنشئ", + "notesCurrentText": "النص المحلي المحفوظ حاليًا", + "notesRecovering": "جارٍ الاستعادة كملاحظة جديدة…", + "notesSort": "ترتيب الملاحظات", + "notesOfflineRetentionHelp": "إزالة هذا المتطلب تبقي المحتوى المخزن مؤقتًا. قد يظل متطلب الفئة أو الحساب يحتفظ بالملاحظة.", + "notesOfflineBroader": "تُحتفظ بهذه الملاحظة أيضًا بسبب متطلب الفئة أو الحساب.", + "notesOfflineExternal": "تتطلب الوسائط الخارجية اتصالًا. يحتفظ BusyMark بنص الملاحظة والمرفقات المُدارة فقط.", + "notesOperationFailed": "تعذر إكمال العملية. المسودة محفوظة. راجع الحفظ والتعارضات ثم أعد المحاولة.", + "notesRemovedWarning": "أُزيل حساب Nextcloud المحلي. لم يُؤكد إبطال كلمة مرور التطبيق. احذف كلمة مرور BusyMark من إعدادات أمان Nextcloud.", + "notesBuffersNotSaved": "تعذر حفظ جميع الملاحظات المفتوحة محليًا. راجع الحفظ والتعارضات أو التسوية في مساحة العمل. قد تظل التغييرات المحلية بحاجة إلى المزامنة مع Nextcloud.", + "notesSettingsReloadHelp": "تحل إعدادات Nextcloud الحالية محل المسودة عند إعادة التحميل. أتريد تجاهل المسودة وإعادة التحميل؟", + "notesSettingsRefreshFailed": "قَبِل Nextcloud الإعدادات، لكن تعذر تحديث الملاحظات. القيم المعروضة هي ما أكده الخادم. أعد التحديث من الملاحظات.", + "notesStorageFull": "مساحة Nextcloud ممتلئة. أفرغ مساحة على الخادم ثم أعد المحاولة.", + "notesRejected": "رفض Nextcloud هذا التغيير. راجع الملاحظة واحفظ نسخة محلية قبل إعادة المحاولة.", + "notesThrottled": "طلب Nextcloud الانتظار. تُستأنف المزامنة بعد انتهاء مهلة إعادة المحاولة.", + "notesImportPartialHelp": "قد تكون العناصر السابقة محفوظة محليًا بالفعل. استأنف هذا الاستيراد للمتابعة دون تكرارها.", + "notesExportAll": "تصدير ملاحظات · كل الملاحظات…", + "notesExportCategory": "تصدير ملاحظات · الفئة…", + "notesOfflineAll": "إتاحة دون اتصال · كل الملاحظات", + "notesOfflineCategory": "إتاحة دون اتصال · الفئة", + "notesOfflineRemoveAll": "إزالة الاحتفاظ دون اتصال · كل الملاحظات", + "notesOfflineRemoveCategory": "إزالة الاحتفاظ دون اتصال · الفئة", + "notesOfflineKeep": "إتاحة دون اتصال", + "notesLoadFailed": "Nextcloud Notes · فشل. إعادة المحاولة", + "notesInspectionFailed": "المرفقات · فشل. إعادة المحاولة", + "notesSettingsLeaveHelp": "إعدادات Notes على الخادم · تغييرات غير محفوظة", + "notesUnavailable": "غير متصل · النص متاح محليًا · محفوظ محليًا · المزامنة معلقة", + "notesRecoveryRequired": "تعارضات · مقارنة", + "notesTransferFailed": "فشل. إعادة المحاولة", + "notesRemoteDeleteUnavailable": "عنوان الخادم · غير مدعوم · حذف · المرفقات", + "notesReadOnlyHelp": "للقراءة فقط · المفضلة · حفظ نسخة محلية…", + "notesPathRelative": "مسار مجلد الملاحظات نسبي إلى مساحة تخزين Nextcloud الخاصة بك.", + "notesSettingsStale": "تغيرت الإعدادات في Nextcloud. أعد تحميل قيم الخادم وراجعها قبل استبدال المسودة.", + "notesDownloadsPaused": "التنزيلات متوقفة مؤقتًا", + "notesExportNotPublished": "لم يُنشر أي تصدير.", + "notesKeyringUnavailable": "افتح قفل سلسلة مفاتيح سطح المكتب وأعد المحاولة.", + "notesConnectFailed": "تعذر الاتصال بملاحظات Nextcloud. تحقق من عنوان الخادم وأعد المحاولة.", + "notesAccountMismatch": "سجل الدخول إلى حساب Nextcloud نفسه. أزل هذا الحساب أولًا للاتصال بحساب مختلف.", + "notesLocalRemovalFailed": "تعذر تنظيف الحساب المحلي. احتُفظ بالحساب لتتمكن من إعادة محاولة إزالته.", + "notesTransferring": "جارٍ نقل الملاحظات…", + "notesTechnicalDetails": "تفاصيل تقنية", + "notesPauseRequirement": "إيقاف متطلب الإتاحة دون اتصال مؤقتًا", + "notesResumeRequirement": "استئناف متطلب الإتاحة دون اتصال" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 643e1188..77aac8b7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Neue Notiz aus aufbewahrtem Text", + "notesAccountThrottled": "Nextcloud hat um eine Pause gebeten. Warte, bevor du die Anmeldung erneut startest.", + "notesSettingsRefreshing": "Notes wird nach der bestätigten Einstellungsänderung aktualisiert", + "notesExportOperationFailed": "Der Export wurde nicht veröffentlicht. Quelle und Ziel prüfen und erneut exportieren.", + "notesRecoveryCreatedNotOpened": "Die wiederhergestellte Notiz wurde gespeichert, konnte aber nicht geöffnet werden. Ausstehende Editor- oder Eigenschaftsänderungen klären und diese Notiz öffnen.", + "notesSettingsPendingNotes": "Ausstehende Notizen, Konflikte oder ungewisse Erstellungen blockieren diese Änderung. In Notes synchronisieren oder prüfen, dann den erhaltenen Einstellungsentwurf erneut speichern.", + "notesSettingsPendingAttachments": "Ausstehende Anhangsvorgänge blockieren diese Änderung. Anhänge in Notes prüfen, dann den erhaltenen Einstellungsentwurf erneut speichern.", + "notesAccountServerAddress": "Die kanonische HTTPS-Adresse des Nextcloud-Servers verwenden und die Anmeldung neu starten. Adresse oder Weiterleitung konnten nicht verifiziert werden.", + "notesAccountUnsupported": "Notes ist nicht verfügbar oder die API wird nicht unterstützt. Nextcloud Notes mit API-Hauptversion 1 ab Nebenversion 4 aktivieren.", + "notesAccountAuthorizeAgain": "Nextcloud hat die Zugangsdaten abgelehnt. Das Konto erneut autorisieren oder verbinden.", + "notesAccountCheckForbidden": "Nextcloud hat die Notes-Funktionsprüfung verweigert. Die Serveradministration um Freigabe bitten und erneut versuchen.", + "notesAccountMalformed": "Die Nextcloud-Antwort konnte nicht verifiziert werden. Die Serverinstallation prüfen und die Anmeldung neu starten.", + "notesAccountTimedOut": "Die Anmeldung ist abgelaufen. Erneut starten und im Browser autorisieren.", + "notesAccountBrowserUnavailable": "Der Systembrowser konnte nicht geöffnet werden. Den Standardbrowser prüfen und die Anmeldung neu starten.", + "notesAccountNetwork": "Nextcloud ist nicht erreichbar. Verbindung und Server prüfen und die Anmeldung erneut versuchen.", + "notesAccountRejected": "Nextcloud hat die Funktionsanfrage abgelehnt. Den Serverzugriff mit der Administration prüfen und erneut versuchen.", + "notesAccountFailed": "Die Kontoeinrichtung konnte nicht abgeschlossen werden. Die Anmeldung erneut versuchen.", + "notesTransferSourceChanged": "Die Quelle wurde nach der Prüfung geändert. Die aktuelle Datei erneut prüfen oder einen neuen Schnappschuss erstellen.", + "notesTransferUnreadable": "Die Quelle konnte nicht gelesen werden. Den Datei- oder Ordnerzugriff prüfen und erneut einlesen.", + "notesTransferInvalidSnapshot": "Der Schnappschuss ist ungültig oder wird nicht unterstützt. Einen gültigen BusyMark-Notes-Schnappschuss oder eine Markdown-Quelle wählen.", + "notesTransferMediaLimit": "Medien überschreiten das Importlimit. Die Medien des betroffenen Eintrags verkleinern und erneut prüfen.", + "notesTransferUnsafeSource": "Die Quelle enthält einen unsicheren Pfad. Dateien innerhalb des Quellordners wählen und erneut prüfen.", + "notesTransferDestination": "Der Export konnte nicht geschrieben werden. Zugriff und freien Speicher am Ziel prüfen und erneut exportieren.", + "notesTransferOperationFailed": "Die Übertragung ist fehlgeschlagen. Betroffene Einträge prüfen und erneut versuchen. Bereits importierte Einträge bleiben gespeichert.", + "notesExportPublishedWithOmissions": "Export mit Auslassungen verfügbar", + "notesBatchUnchanged": "Die angeforderten Werte waren bereits gesetzt.", + "notesBatchReadOnly": "Diese Notiz ist schreibgeschützt. Geschützte Eigenschaften können nicht geändert werden; Favorit bleibt erlaubt.", + "notesBatchReviewChanged": "Diese Notiz wurde nach der Prüfung geändert. Vor einem erneuten Versuch den aktuellen Stand prüfen.", + "notesBatchMissing": "Diese Notiz ist nicht mehr verfügbar. Die Liste aktualisieren und die Ziele erneut prüfen.", + "notesBatchStorage": "Lokales Speichern fehlgeschlagen. Speicher prüfen und diesen Eintrag erneut versuchen; erfolgreiche Einträge bleiben gespeichert.", + "notesRemoteReviewChanged": "Die Servernotiz wurde geändert. Der Zusammenführungsentwurf bleibt erhalten. Den neuen Vergleich prüfen und die Auflösung erneut wählen.", + "notesReviewRemote": "Ich habe die aktuelle Serverversion geprüft", "wsModuleCapsNotPublished": "Modul-Großschreibung wird nicht veröffentlicht.", "wsTitleOverridesAction": "Instanz- und Inhaltsverzeichnistitel…", "wsHeadingNotPublished": "Wird von diesem Builder nicht veröffentlicht.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Datum", "pdfTitlePageTitleRequired": "Geben Sie einen Titel für die Titelseite ein.", "pdfTocSectionNote": "Beginnt auf einer separaten Seite und kann mehrere Seiten umfassen.", - "sourceSearchZeroLengthUnsupported": "Regex-Treffer ohne Zeichen werden nicht unterstützt (z. B. ^, $ oder Lookaheads, die nur eine Position treffen).", "workspaceSearchIncomplete": "Es wird nur ein Teil der Ergebnisse angezeigt. Zeigen Sie mehr an oder grenzen Sie die Suche ein.", "workspaceSearchShowMore": "Weitere Ergebnisse anzeigen", "workspaceSearchSkippedFiles": "Die Suche ist unvollständig. Diese Dateien sind größer als 1 MiB oder konnten nicht gelesen werden:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Diese Tastaturkürzelübersicht anzeigen", + "shortcutKeyboardShortcutsDescription": "Tastenkürzel der Anwendung anpassen", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Zuletzt verwendete Arbeitsbereiche löschen", + "clearRecentWorkspaces": "Zuletzt verwendete Einträge löschen", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Groß-/Kleinschreibung beachten", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Nur ganze Wörter", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Regulärer Ausdruck", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Ungültiger regulärer Ausdruck", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Große Datei: Hervorhebung und Faltung sind pausiert", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Zuletzt verwendete Elemente konnten nicht aktualisiert werden: {error}.", "workspaceErrorOpenFailed": "Öffnen fehlgeschlagen: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "Der Linktext ist leer; geben Sie einen zugänglichen Namen an, der den Zweck beschreibt.", "diagnosticMarkdownLinkReviewText": "Prüfen Sie, ob der Linktext „{text}“ seinen Zweck im Kontext beschreibt.", "diagnosticMarkdownTableEmptyHeader": "Tabellenüberschriften müssen ihre Spalten bezeichnen; füllen Sie jede leere Überschrift aus.", - "commandPalette": "Befehlspalette", - "commandPaletteHint": "Befehl eingeben", - "commandPaletteEmpty": "Keine passenden Befehle", "tableAlignmentUnspecified": "Ausrichtung: Nicht festgelegt", "tableAlignmentLeft": "Ausrichtung: Links", "tableAlignmentCenter": "Ausrichtung: Mitte", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "Wählen Sie eine lesbare UTF-8-CSS-Datei bis 256 KiB ohne Importe, Ressourcen-URLs, HTML sowie ausführbares oder maskiertes CSS.", "exportInvalidRange": "{field}: Geben Sie eine Zahl zwischen {minimum} und {maximum} ein.", "clipboardHistory": "Zwischenablage-Historie", - "clipboardHistorySearchHint": "Zwischenablageverlauf durchsuchen", "clipboardHistoryDescription": "Öffne die Sitzungs-spezifische Zwischenablage-Historie.", "clipboardCurrent": "Aktuelle Zwischenablage", "clipboardCurrentExternal": "Aktuelle Zwischenablage aus einer anderen Anwendung", "clipboardDestination": "Einfügen in {name}", "clipboardSessionOnly": "Nur für diese BusyMark-Sitzung gespeichert.", "clipboardNoItems": "Zwischenablage-Historie ist leer.", - "clipboardNoMatches": "Keine Zwischenablagen-Elemente passen zu dieser Suche.", "clipboardDisabled": "Die Sammlung \"Zwischenablage-Historie\" ist in den Einstellungen deaktiviert.", "clipboardUnavailable": "Der aktuelle Zwischenablage-Inhalt ist nicht verfügbar oder wird nicht unterstützt.", "clipboardTooLarge": "Dieses Zwischenablagen-Element ist zu groß, um es zu speichern.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "Lokale Historie Dokumente und Revisionen durchsuchen.", "localHistoryNoDocuments": "Es gibt noch keine Dokumente mit Lokaler Historie.", "localHistoryNoRevisions": "Dieses Dokument hat keine Lokalen Historie Revisionen.", - "localHistoryNoMatches": "Keine Lokalen Historie Revisionen passen zu dieser Suche.", - "localHistorySearchHint": "Lokalen Verlauf durchsuchen", "localHistorySelectedRevision": "Ausgewählte Revision", "localHistoryCurrentEditor": "Aktueller Editor Inhalt", "localHistoryCurrentDisk": "Aktueller Inhalt auf der Festplatte", @@ -3610,5 +3619,125 @@ "wsInsertElements": "Elemente einfügen", "wsAdmonitionsAndQuotes": "Hinweise und Zitate", "wsSemanticText": "Semantischer Text", - "wsPropertiesUnavailable": "Das betrachtete Element ist nicht verfügbar. Wählen Sie ein Element zum Fortfahren." + "wsPropertiesUnavailable": "Das betrachtete Element ist nicht verfügbar. Wählen Sie ein Element zum Fortfahren.", + "searchDocumentsCategory": "Dokumente", + "searchTopicsCategory": "Themen", + "searchCurrentDocument": "Aktuelles Dokument", + "searchCurrentWorkspace": "Aktueller Arbeitsbereich", + "browseNotes": "Notizen durchsuchen", + "searchActionsCommand": "Aktionen suchen", + "searchDocumentsCommand": "Dokumente suchen", + "searchTopicsCommand": "Themen suchen", + "searchRecentCommand": "Zuletzt geöffnet suchen", + "searchResourceUnavailable": "Diese Ressource ist nicht verfügbar.", + "notesSync": "Notizen synchronisieren", + "searchInWorkspace": "Im Arbeitsbereich suchen", + "searchCategory": "Suchkategorie", + "clearHistorySearchScope": "Verlauf aller Dokumente durchsuchen", + "searchInDocument": "Im Dokument suchen", + "searchDocumentHint": "Dieses Dokument durchsuchen…", + "searchWorkspaceHint": "Diesen Arbeitsbereich durchsuchen…", + "searchOutlineHint": "Überschriften suchen…", + "searchHistoryHint": "Lokalen Verlauf durchsuchen…", + "searchEnterText": "Suchtext eingeben", + "searchAccountUnavailable": "Konto nicht verfügbar", + "searchDocumentsHint": "Dokumente suchen…", + "searchTopicsHint": "Themen suchen…", + "searchRecentHint": "Zuletzt geöffnet suchen…", + "searchActionsHint": "Aktionen suchen…", + "searchRequiresNotesWorkspace": "Erfordert einen Notizen-Arbeitsbereich", + "searchSidebarLabel": "Suchen:", + "searchDocumentTextCategory": "Dokumenttext", + "searchWorkspaceTextCategory": "Arbeitsbereichstext", + "searchHeadingsCategory": "Überschriften", + "shortcutUnassigned": "Nicht zugewiesen", + "shortcutPressKeys": "Tastenkombination drücken…", + "shortcutRecord": "Tastenkürzel aufnehmen", + "shortcutRestoreAll": "Alle Vorgaben wiederherstellen", + "shortcutRestoreAllQuestion": "Standardkürzel aller Aktionen wiederherstellen? Eigene Zuweisungen werden entfernt.", + "shortcutSaveFailed": "Tastenkürzel sind aktiv, konnten aber nicht gespeichert werden. Zum Beibehalten nach Neustart erneut speichern.", + "shortcutRetry": "Speichern wiederholen", + "shortcutChanged": "Zuweisungen geändert. Aktuelle Tastenkürzel prüfen und erneut versuchen.", + "shortcutInvalid": "Diese Kombination kann hier nicht zugewiesen werden.", + "shortcutSystemWarning": "Diese Kombination wird möglicherweise vom Desktop verwendet. BusyMark kann Systemkürzel nicht überschreiben.", + "shortcutConflicts": "Dieses Tastenkürzel überschneidet sich mit diesen Aktionen:", + "shortcutRemove": "Zuweisung entfernen", + "shortcutRestoreDefault": "Vorgabe wiederherstellen", + "shortcutAssign": "Zuweisen", + "shortcutReassign": "Neu zuweisen", + "shortcutContextApplication": "Anwendung", + "shortcutContextDocument": "Dokumentansichten", + "shortcutContextEditor": "Editor und Quelltext", + "shortcutContextWritersideSource": "Writerside-Quelltext", + "shortcutContextWorkspace": "Arbeitsbereichsnavigation", + "shortcutContextTree": "Seitenleistenbäume", + "searchHeadingsCommand": "Überschriften suchen", + "shortcutFixed": "Vom Editor verwaltet; nicht konfigurierbar.", + "shortcutDefaultConflict": "Das Standardkürzel steht mit einer eigenen Zuweisung in Konflikt.", + "shortcutCurrentBinding": "Aktuelles Tastenkürzel", + "shortcutProposedBinding": "Vorgeschlagenes Tastenkürzel", + "notesActions": "Notizaktionen", + "noteActions": "Aktionen für diese Notiz", + "noteProperties": "Notizeigenschaften", + "notesOfflinePause": "Downloads pausieren", + "notesOfflineResume": "Downloads fortsetzen", + "notesEmptyAll": "Noch keine Notizen. Erstellen Sie eine Notiz.", + "notesEmptyFavorites": "Keine favorisierten Notizen.", + "notesEmptyCategory": "Keine Notizen in dieser Kategorie.", + "notesEmptyUncategorized": "Keine nicht kategorisierten Notizen.", + "notesEmptyRecovery": "Keine aufbewahrten Notizen zur Wiederherstellung.", + "notesStartingConnection": "Nextcloud-Verbindung wird gestartet…", + "notesVerifyingConnection": "Zugriff auf Notizen wird geprüft…", + "notesCancellingConnection": "Anmeldung wird abgebrochen und bereinigt…", + "notesRemovingConnection": "Lokales Konto wird entfernt…", + "notesSettingsLeave": "Änderungen an den Nextcloud-Einstellungen vor dem Verlassen speichern?", + "notesSettingsReload": "Servereinstellungen neu laden…", + "notesScanning": "Dateien werden durchsucht…", + "notesCancelling": "Abbruch läuft… Der aktuelle Vorgang wird noch beendet.", + "notesCancelled": "Abgebrochen", + "notesPartial": "Teilweise abgeschlossen", + "notesNotStarted": "Nicht gestartet", + "notesCreated": "Erstellt", + "notesCurrentText": "Aktuell aufbewahrter Text", + "notesRecovering": "Wiederherstellung als neue Notiz…", + "notesSort": "Notizen sortieren", + "notesOfflineRetentionHelp": "Das Entfernen dieser Anforderung löscht keine zwischengespeicherten Inhalte. Eine Kategorie- oder Kontoanforderung kann die Notiz weiter aufbewahren.", + "notesOfflineBroader": "Diese Notiz wird auch durch eine Kategorie- oder Kontoanforderung aufbewahrt.", + "notesOfflineExternal": "Externe Medien benötigen eine Verbindung. BusyMark bewahrt nur Notiztext und verwaltete Anhänge auf.", + "notesOperationFailed": "Der Vorgang ist fehlgeschlagen. Ihr Entwurf bleibt erhalten. Prüfen Sie Speichern und Konflikte und versuchen Sie es erneut.", + "notesRemovedWarning": "Das lokale Nextcloud-Konto wurde entfernt. Der Widerruf des App-Passworts wurde nicht bestätigt. Entfernen Sie BusyMarks App-Passwort in den Nextcloud-Sicherheitseinstellungen.", + "notesBuffersNotSaved": "Nicht alle offenen Notizen konnten lokal gespeichert werden. Prüfen Sie Speichern, Konflikte oder Normalisierung im Arbeitsbereich. Lokale Änderungen müssen eventuell noch mit Nextcloud synchronisiert werden.", + "notesSettingsReloadHelp": "Neu laden ersetzt Ihren Entwurf durch die aktuellen Nextcloud-Einstellungen. Entwurf verwerfen und neu laden?", + "notesSettingsRefreshFailed": "Nextcloud hat die Einstellungen angenommen, aber die Notizen konnten nicht aktualisiert werden. Angezeigt werden die bestätigten Serverwerte. Aktualisieren Sie die Notizen erneut.", + "notesStorageFull": "Der Nextcloud-Speicher ist voll. Geben Sie auf dem Server Speicherplatz frei und versuchen Sie es erneut.", + "notesRejected": "Nextcloud hat diese Änderung abgelehnt. Prüfen Sie die Notiz und speichern Sie vor einem neuen Versuch eine lokale Kopie.", + "notesThrottled": "Nextcloud hat eine Wartezeit angefordert. Die Synchronisierung wird nach Ablauf dieser Frist fortgesetzt.", + "notesImportPartialHelp": "Frühere Einträge können bereits lokal gespeichert sein. Setzen Sie diesen Import fort, um Duplikate zu vermeiden.", + "notesExportAll": "Notizen exportieren · Alle Notizen…", + "notesExportCategory": "Notizen exportieren · Kategorie…", + "notesOfflineAll": "Offline verfügbar machen · Alle Notizen", + "notesOfflineCategory": "Offline verfügbar machen · Kategorie", + "notesOfflineRemoveAll": "Offline-Aufbewahrung entfernen · Alle Notizen", + "notesOfflineRemoveCategory": "Offline-Aufbewahrung entfernen · Kategorie", + "notesOfflineKeep": "Offline verfügbar machen", + "notesLoadFailed": "Nextcloud Notes · Fehlgeschlagen. Erneut versuchen", + "notesInspectionFailed": "Anhänge · Fehlgeschlagen. Erneut versuchen", + "notesSettingsLeaveHelp": "Notes-Einstellungen auf dem Server · Ungespeicherte Änderungen", + "notesUnavailable": "Nicht verbunden · Text lokal verfügbar · Lokal gespeichert · Synchronisierung ausstehend", + "notesRecoveryRequired": "Konflikte · Vergleichen", + "notesTransferFailed": "Fehlgeschlagen. Erneut versuchen", + "notesRemoteDeleteUnavailable": "Server-URL · Nicht unterstützt · Löschen · Anhänge", + "notesReadOnlyHelp": "Schreibgeschützt · Favoriten · Lokale Kopie speichern…", + "notesPathRelative": "Der Notizordnerpfad ist relativ zu Ihrem Nextcloud-Speicher.", + "notesSettingsStale": "Die Einstellungen haben sich auf Nextcloud geändert. Laden und prüfen Sie die Serverwerte, bevor Sie den Entwurf ersetzen.", + "notesDownloadsPaused": "Downloads pausiert", + "notesExportNotPublished": "Es wurde kein Export veröffentlicht.", + "notesKeyringUnavailable": "Entsperren Sie den Desktop-Schlüsselbund und versuchen Sie es erneut.", + "notesConnectFailed": "Keine Verbindung zu Nextcloud Notes. Prüfen Sie die Serveradresse und versuchen Sie es erneut.", + "notesAccountMismatch": "Melden Sie sich beim selben Nextcloud-Konto an. Entfernen Sie dieses Konto zuerst, um ein anderes zu verbinden.", + "notesLocalRemovalFailed": "Die lokale Kontobereinigung ist fehlgeschlagen. Das Konto bleibt für einen erneuten Entfernungsversuch erhalten.", + "notesTransferring": "Notizen werden übertragen…", + "notesTechnicalDetails": "Technische Details", + "notesPauseRequirement": "Offline-Anforderung pausieren", + "notesResumeRequirement": "Offline-Anforderung fortsetzen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fa1d3be4..25700efc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "New note from retained text", + "notesAccountThrottled": "Nextcloud requested a pause. Wait before starting sign-in again.", + "notesSettingsRefreshing": "Refreshing Notes after the acknowledged settings update", + "notesExportOperationFailed": "The export failed before publication. Check the source and destination, then export again.", + "notesRecoveryCreatedNotOpened": "The recovered note was saved. Opening it failed. Resolve pending editor or property changes, then open this existing note.", + "notesSettingsPendingNotes": "Pending notes, conflicts, or uncertain creations block this collection change. Open Notes and synchronize or review them, then retry this retained settings draft.", + "notesSettingsPendingAttachments": "Pending attachment operations block this collection change. Open Notes and review the attachments, then retry this retained settings draft.", + "notesAccountServerAddress": "Use the canonical HTTPS address of your Nextcloud server, then start sign-in again. The address or redirect could not be verified.", + "notesAccountUnsupported": "Notes is unavailable or its API is unsupported. Enable Nextcloud Notes with API major version 1, minor version 4 or later.", + "notesAccountAuthorizeAgain": "Nextcloud rejected the credentials. Authorize this account again or reconnect.", + "notesAccountCheckForbidden": "Nextcloud denied the Notes capability check. Ask the server administrator to allow it, then try again.", + "notesAccountMalformed": "The Nextcloud response could not be verified. Check the server installation, then start sign-in again.", + "notesAccountTimedOut": "Sign-in timed out. Start sign-in again and complete authorization in your browser.", + "notesAccountBrowserUnavailable": "The system browser could not open. Check your default browser, then start sign-in again.", + "notesAccountNetwork": "BusyMark could not reach Nextcloud. Check your connection and the server, then try signing in again.", + "notesAccountRejected": "Nextcloud rejected the capability request. Check server access with your administrator, then try again.", + "notesAccountFailed": "BusyMark could not complete account setup. Try signing in again.", + "notesTransferSourceChanged": "The source changed after review. Review the current file or capture a new snapshot before retrying.", + "notesTransferUnreadable": "The source could not be read. Check access to the file or folder, then review it again.", + "notesTransferInvalidSnapshot": "The snapshot is invalid or unsupported. Choose a valid BusyMark Notes snapshot or Markdown source.", + "notesTransferMediaLimit": "Media exceeds the import size limit. Reduce the affected item's media before reviewing it again.", + "notesTransferUnsafeSource": "The source contains an unsafe path. Choose files within the selected source folder and review again.", + "notesTransferDestination": "The export could not be written. Check access and free space at the destination, then export again.", + "notesTransferOperationFailed": "The transfer could not finish. Review the affected items and try again. Earlier imported items remain saved.", + "notesExportPublishedWithOmissions": "Export available with omissions", + "notesBatchUnchanged": "The requested values were already set.", + "notesBatchReadOnly": "This note is read-only. Protected properties cannot change; Favorite is still permitted.", + "notesBatchReviewChanged": "This note changed after review. Review its current state before applying this action again.", + "notesBatchMissing": "This note is no longer available. Refresh the list and review the targets again.", + "notesBatchStorage": "The local save failed. Check local storage and retry this item; successful items remain saved.", + "notesRemoteReviewChanged": "The server note changed. Your merge draft is retained. Review the updated comparison and choose how to resolve it again.", + "notesReviewRemote": "I have reviewed the current server version", "wsModuleCapsNotPublished": "Module capitalization is not published.", "wsTitleOverridesAction": "Instance and TOC titles…", "wsHeadingNotPublished": "Not published by this builder.", @@ -358,12 +390,6 @@ "insert": "Insert", "@insert": {"description": "Insert button label."}, "keyboardShortcuts": "Keyboard Shortcuts", - "commandPalette": "Command Palette", - "@commandPalette": {"description": "Title and command label for the searchable command palette."}, - "commandPaletteHint": "Type a command", - "@commandPaletteHint": {"description": "Search hint in the command palette."}, - "commandPaletteEmpty": "No matching commands", - "@commandPaletteEmpty": {"description": "Empty state in the command palette."}, "commandUnavailableInContext": "Unavailable in the current editor context", "@commandUnavailableInContext": {"description":"Reason shown for a disabled contextual command."}, "@keyboardShortcuts": {"description": "Keyboard shortcuts dialog title and menu item."}, @@ -507,7 +533,7 @@ "@shortcutSaveDescription": {"description": "Keyboard shortcut description for saving content."}, "shortcutSearchDescription": "Search the current workspace", "@shortcutSearchDescription": {"description": "Keyboard shortcut description for search."}, - "shortcutKeyboardShortcutsDescription": "Show this keyboard shortcut reference", + "shortcutKeyboardShortcutsDescription": "Customize application keyboard shortcuts", "@shortcutKeyboardShortcutsDescription": {"description": "Keyboard shortcut description for opening shortcuts."}, "shortcutSyntaxReferenceDescription": "Open the Syntax Reference", "@shortcutSyntaxReferenceDescription": {"description": "Keyboard shortcut description for opening the Syntax Reference."}, @@ -663,8 +689,8 @@ "@editorFontSize": {"description": "Editor font size setting label."}, "validateOnEdit": "Validate on edit", "@validateOnEdit": {"description": "Validation setting label."}, - "clearRecentWorkspaces": "Clear recent workspaces", - "@clearRecentWorkspaces": {"description": "Settings action label for clearing recent workspaces."}, + "clearRecentWorkspaces": "Clear recent history", + "@clearRecentWorkspaces": {"description": "Settings action for clearing Recent navigation metadata in both document and workspace sections."}, "editingButtonsPosition": "Editing buttons position", "@editingButtonsPosition": {"description": "Settings label for WYSIWYG editing button position."}, "editingButtonsPositionDescription": "Choose where the floating WYSIWYG editing buttons appear.", @@ -1162,12 +1188,6 @@ "@sourceSearchPreviousMatch": {"description": "Tooltip for moving to the previous source search match."}, "sourceSearchNextMatch": "Next match", "@sourceSearchNextMatch": {"description": "Tooltip for moving to the next source search match."}, - "sourceSearchCaseSensitive": "Case sensitive", - "@sourceSearchCaseSensitive": {"description": "Tooltip for toggling case-sensitive source search."}, - "sourceSearchWholeWord": "Whole word", - "@sourceSearchWholeWord": {"description": "Tooltip for toggling whole-word source search."}, - "sourceSearchRegex": "Regex", - "@sourceSearchRegex": {"description": "Tooltip for toggling regular-expression source search."}, "sourceSearchReplacement": "Replace with", "@sourceSearchReplacement": {"description": "Placeholder for the active-document replacement field."}, "sourceSearchReplaceCurrent": "Replace current match", @@ -1249,12 +1269,9 @@ "@keepMine": {"description": "Action that keeps the editor buffer after an external change."}, "saveAs": "Save As", "@saveAs": {"description": "Action that saves the current document to another path."}, - "sourceSearchInvalidRegex": "Invalid regular expression", - "sourceSearchZeroLengthUnsupported": "Zero-length regex matches are not supported (for example ^, $, or lookaheads that match only a position).", "workspaceSearchIncomplete": "Only some results are shown. Show more or narrow your search.", "workspaceSearchShowMore": "Show more results", "workspaceSearchSkippedFiles": "Search is incomplete. These files exceeded 1 MiB or could not be read:", - "@sourceSearchInvalidRegex": {"description": "Source search status shown when the regular expression is invalid."}, "sourceLargeFileFeaturesPaused": "Large file: highlighting and folding are paused", "@sourceLargeFileFeaturesPaused": {"description": "Status banner shown when source highlighting and folding are disabled for a large file."}, "nothingToRead": "Nothing to read", @@ -1326,6 +1343,11 @@ "documentKindResourceFile": "Resource file", "@documentKindResourceFile": {"description": "Search result document kind label."}, + "workspaceErrorRecentWorkspaceUpdateFailed": "Could not update Recent items: {error}.", + "@workspaceErrorRecentWorkspaceUpdateFailed": { + "description": "Warning when updating Recent items fails after a successful open or save.", + "placeholders": {"error": {"type": "String"}} + }, "workspaceErrorOpenFailed": "Open failed: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -2720,7 +2742,6 @@ "exportInvalidColor": "Enter a color as #RRGGBB.", "exportInvalidCss": "Choose a readable UTF-8 .css file up to 256 KiB, without imports, resource URLs, HTML, or executable or escaped CSS.", "clipboardHistory": "Clipboard History", - "clipboardHistorySearchHint": "Search clipboard history", "clipboardHistoryDescription": "Open the session-local clipboard history.", "clipboardCurrent": "Current clipboard", "clipboardCurrentExternal": "Current clipboard from another application", @@ -2728,7 +2749,6 @@ "@clipboardDestination": {"placeholders": {"name": {"type": "String"}}}, "clipboardSessionOnly": "Stored for this BusyMark session only.", "clipboardNoItems": "Clipboard history is empty.", - "clipboardNoMatches": "No clipboard items match this search.", "clipboardDisabled": "Clipboard History collection is disabled in Settings.", "clipboardUnavailable": "The current clipboard content is unavailable or unsupported.", "clipboardTooLarge": "This clipboard item is too large to retain.", @@ -2746,8 +2766,6 @@ "findLocalHistoryDescription": "Search Local History documents and revision contents.", "localHistoryNoDocuments": "No documents have Local History yet.", "localHistoryNoRevisions": "This document has no Local History revisions.", - "localHistoryNoMatches": "No Local History revisions match this search.", - "localHistorySearchHint": "Search local history", "localHistorySelectedRevision": "Selected revision", "localHistoryCurrentEditor": "Current editor content", "localHistoryCurrentDisk": "Current content on disk", @@ -3157,5 +3175,151 @@ "wsInsertElements": "Insert elements", "wsAdmonitionsAndQuotes": "Admonitions and quotes", "wsSemanticText": "Semantic text", - "wsPropertiesUnavailable": "The inspected element is unavailable. Inspect an element to continue." + "wsPropertiesUnavailable": "The inspected element is unavailable. Inspect an element to continue.", + "searchDocumentsCategory": "Documents", + "searchTopicsCategory": "Topics", + "searchCurrentDocument": "Current document", + "searchCurrentWorkspace": "Current workspace", + "browseNotes": "Browse Notes", + "searchActionsCommand": "Search Actions", + "searchDocumentsCommand": "Search Documents", + "searchTopicsCommand": "Search Topics", + "searchRecentCommand": "Search Recent", + "searchResourceUnavailable": "This resource is unavailable.", + "notesSync": "Sync notes", + "@notesSync": {"description": "Command to synchronize the active Nextcloud Notes account."}, + "searchInWorkspace": "Search in Workspace", + "searchCategory": "Search category", + "clearHistorySearchScope": "Search history across documents", + "searchInDocument": "Search in Document", + "searchDocumentHint": "Search this document…", + "searchWorkspaceHint": "Search this workspace…", + "searchOutlineHint": "Search headings…", + "searchHistoryHint": "Search Local History…", + "searchEnterText": "Enter text to search", + "searchAccountUnavailable": "Account unavailable", + "searchDocumentsHint": "Search documents…", + "searchTopicsHint": "Search topics…", + "searchRecentHint": "Search recent resources…", + "searchActionsHint": "Search actions…", + "searchRequiresNotesWorkspace": "Requires a Notes workspace", + "searchSidebarLabel": "Search:", + "@searchSidebarLabel": {"description": "Search and keyboard shortcut settings: searchSidebarLabel"}, + "searchDocumentTextCategory": "Document Text", + "@searchDocumentTextCategory": {"description": "Search and keyboard shortcut settings: searchDocumentTextCategory"}, + "searchWorkspaceTextCategory": "Workspace Text", + "@searchWorkspaceTextCategory": {"description": "Search and keyboard shortcut settings: searchWorkspaceTextCategory"}, + "searchHeadingsCategory": "Headings", + "@searchHeadingsCategory": {"description": "Search and keyboard shortcut settings: searchHeadingsCategory"}, + "shortcutUnassigned": "Unassigned", + "@shortcutUnassigned": {"description": "Search and keyboard shortcut settings: shortcutUnassigned"}, + "shortcutPressKeys": "Press a key combination…", + "@shortcutPressKeys": {"description": "Search and keyboard shortcut settings: shortcutPressKeys"}, + "shortcutRecord": "Record shortcut", + "@shortcutRecord": {"description": "Search and keyboard shortcut settings: shortcutRecord"}, + "shortcutRestoreAll": "Restore all defaults", + "@shortcutRestoreAll": {"description": "Search and keyboard shortcut settings: shortcutRestoreAll"}, + "shortcutRestoreAllQuestion": "Restore every action’s default shortcut? Custom assignments will be removed.", + "@shortcutRestoreAllQuestion": {"description": "Search and keyboard shortcut settings: shortcutRestoreAllQuestion"}, + "shortcutSaveFailed": "Shortcuts are active but could not be saved. Retry to keep them after restart.", + "@shortcutSaveFailed": {"description": "Search and keyboard shortcut settings: shortcutSaveFailed"}, + "shortcutRetry": "Retry saving", + "@shortcutRetry": {"description": "Search and keyboard shortcut settings: shortcutRetry"}, + "shortcutChanged": "Assignments changed. Review the current shortcuts and try again.", + "@shortcutChanged": {"description": "Search and keyboard shortcut settings: shortcutChanged"}, + "shortcutInvalid": "This combination cannot be assigned in this context.", + "@shortcutInvalid": {"description": "Search and keyboard shortcut settings: shortcutInvalid"}, + "shortcutSystemWarning": "This combination may be used by your desktop. BusyMark cannot override system shortcuts.", + "@shortcutSystemWarning": {"description": "Search and keyboard shortcut settings: shortcutSystemWarning"}, + "shortcutConflicts": "This shortcut overlaps these actions:", + "@shortcutConflicts": {"description": "Search and keyboard shortcut settings: shortcutConflicts"}, + "shortcutRemove": "Remove assignment", + "@shortcutRemove": {"description": "Search and keyboard shortcut settings: shortcutRemove"}, + "shortcutRestoreDefault": "Restore default", + "@shortcutRestoreDefault": {"description": "Search and keyboard shortcut settings: shortcutRestoreDefault"}, + "shortcutAssign": "Assign", + "@shortcutAssign": {"description": "Search and keyboard shortcut settings: shortcutAssign"}, + "shortcutReassign": "Reassign", + "@shortcutReassign": {"description": "Search and keyboard shortcut settings: shortcutReassign"}, + "shortcutContextApplication": "Application", + "@shortcutContextApplication": {"description": "Search and keyboard shortcut settings: shortcutContextApplication"}, + "shortcutContextDocument": "Document views", + "@shortcutContextDocument": {"description": "Search and keyboard shortcut settings: shortcutContextDocument"}, + "shortcutContextEditor": "Editor and Source", + "@shortcutContextEditor": {"description": "Search and keyboard shortcut settings: shortcutContextEditor"}, + "shortcutContextWritersideSource": "Writerside Source", + "@shortcutContextWritersideSource": {"description": "Search and keyboard shortcut settings: shortcutContextWritersideSource"}, + "shortcutContextWorkspace": "Workspace navigation", + "@shortcutContextWorkspace": {"description": "Search and keyboard shortcut settings: shortcutContextWorkspace"}, + "shortcutContextTree": "Sidebar trees", + "@shortcutContextTree": {"description": "Search and keyboard shortcut settings: shortcutContextTree"}, + "searchHeadingsCommand": "Search Headings", + "shortcutFixed": "Handled by the editor; not configurable.", + "shortcutDefaultConflict": "Default shortcut conflicts with a custom assignment.", + "shortcutCurrentBinding": "Current shortcut", + "shortcutProposedBinding": "Proposed shortcut", + "notesActions": "Notes actions", + "noteActions": "Note actions", + "noteProperties": "Note Properties", + "notesExportAll": "Export All Notes…", + "notesExportCategory": "Export Category…", + "notesOfflineAll": "Keep All Notes Available Offline", + "notesOfflineCategory": "Keep Category Available Offline", + "notesOfflineRemoveAll": "Remove All Notes Offline Requirement", + "notesOfflineRemoveCategory": "Remove Category Offline Requirement", + "notesOfflinePause": "Pause Downloads", + "notesOfflineResume": "Resume Downloads", + "notesOfflineKeep": "Keep this note available offline", + "notesOfflineRetentionHelp": "Removing this requirement keeps existing cached content. A category or account requirement may still retain this note.", + "notesOfflineBroader": "This note is also retained by a category or account requirement.", + "notesOfflineExternal": "External media requires a connection. BusyMark only retains note text and managed attachments.", + "notesEmptyAll": "No notes yet. Create a note to get started.", + "notesEmptyFavorites": "No favorite notes.", + "notesEmptyCategory": "No notes in this category.", + "notesEmptyUncategorized": "No uncategorized notes.", + "notesEmptyRecovery": "No retained notes to recover.", + "notesLoadFailed": "BusyMark could not load the local Notes store. Retry to open your cached notes.", + "notesOperationFailed": "The operation could not finish. Your draft is retained. Review Save and any conflicts, then try again.", + "notesInspectionFailed": "BusyMark could not inspect attachments. Retry to review availability.", + "notesStartingConnection": "Starting Nextcloud connection…", + "notesVerifyingConnection": "Verifying Notes access…", + "notesCancellingConnection": "Cancelling login and cleaning up…", + "notesRemovingConnection": "Removing the local account…", + "notesRemovedWarning": "The local Nextcloud account was removed. App-password revocation was not confirmed. Remove BusyMark’s app password in Nextcloud Security settings.", + "notesBuffersNotSaved": "BusyMark could not save all open notes locally. Review Save, conflicts, or normalization in the workspace before trying again. Pending local changes may still need to synchronize with Nextcloud.", + "notesSettingsLeave": "Save changes to Nextcloud settings before leaving?", + "notesSettingsLeaveHelp": "Your Notes folder and suffix changes have not been sent to Nextcloud.", + "notesSettingsReload": "Reload Server Settings…", + "notesSettingsReloadHelp": "Reloading replaces your draft with the current settings on Nextcloud. Discard this draft and reload?", + "notesSettingsRefreshFailed": "Nextcloud accepted the settings update, but refreshing notes failed. The displayed settings are the server’s acknowledged values. Retry the refresh from Notes.", + "notesUnavailable": "Nextcloud is unavailable. Cached notes remain available; local changes are waiting to synchronize.", + "notesStorageFull": "Nextcloud storage is full. Free space on the server, then retry.", + "notesRejected": "Nextcloud rejected this change. Review the note and save a local copy before retrying.", + "notesThrottled": "Nextcloud asked BusyMark to wait. Synchronization resumes after the retry deadline.", + "notesRecoveryRequired": "This change requires review before synchronization can continue.", + "notesScanning": "Scanning files…", + "notesCancelling": "Cancelling… Waiting for the current operation to settle.", + "notesCancelled": "Cancelled", + "notesPartial": "Partially completed", + "notesTransferFailed": "Transfer failed. Review the outcome before retrying.", + "notesImportPartialHelp": "Earlier items may already be saved locally. Resume this import to continue without duplicating them.", + "notesNotStarted": "Not started", + "notesCreated": "Created", + "notesRemoteDeleteUnavailable": "This server does not support deleting remote attachments.", + "notesCurrentText": "Current retained text", + "notesRecovering": "Recovering as a new note…", + "notesReadOnlyHelp": "This note is read-only. You can change its favorite status or save a local copy.", + "notesSort": "Sort notes", + "notesPathRelative": "The Notes folder path is relative to your Nextcloud storage.", + "notesSettingsStale": "The settings changed on Nextcloud. Reload and review the server values before replacing your draft.", + "notesDownloadsPaused": "Downloads paused", + "notesExportNotPublished": "No export was published.", + "notesKeyringUnavailable": "Unlock the desktop keyring and try again.", + "notesConnectFailed": "Could not connect to Nextcloud Notes. Check the server address and try again.", + "notesAccountMismatch": "Sign in to the same Nextcloud account. Remove this account first to connect a different one.", + "notesLocalRemovalFailed": "Local account cleanup failed. The account is retained so you can retry removal.", + "notesTransferring": "Transferring notes…", + "notesTechnicalDetails": "Technical details", + "notesPauseRequirement": "Pause Offline Requirement", + "notesResumeRequirement": "Resume Offline Requirement" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 38458714..04538d12 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nueva nota del texto conservado", + "notesAccountThrottled": "Nextcloud solicitó una pausa. Espere antes de iniciar sesión de nuevo.", + "notesSettingsRefreshing": "Actualizando Notes tras confirmar los ajustes", + "notesExportOperationFailed": "La exportación falló antes de publicarse. Compruebe el origen y el destino y vuelva a exportar.", + "notesRecoveryCreatedNotOpened": "La nota recuperada se guardó, pero no pudo abrirse. Resuelva los cambios pendientes del editor o de propiedades y abra esta nota existente.", + "notesSettingsPendingNotes": "Hay notas pendientes, conflictos o creaciones inciertas que bloquean este cambio. Sincronícelos o revíselos en Notes y vuelva a guardar este borrador conservado.", + "notesSettingsPendingAttachments": "Hay operaciones de adjuntos que bloquean este cambio. Revise los adjuntos en Notes y vuelva a guardar este borrador conservado.", + "notesAccountServerAddress": "Use la dirección HTTPS canónica de Nextcloud y vuelva a iniciar sesión. No se pudo verificar la dirección o la redirección.", + "notesAccountUnsupported": "Notes no está disponible o su API no es compatible. Active Nextcloud Notes con la API de versión principal 1 y secundaria 4 o posterior.", + "notesAccountAuthorizeAgain": "Nextcloud rechazó las credenciales. Vuelva a autorizar la cuenta o a conectarla.", + "notesAccountCheckForbidden": "Nextcloud denegó la comprobación de funciones de Notes. Solicite permiso al administrador y reinténtelo.", + "notesAccountMalformed": "No se pudo verificar la respuesta de Nextcloud. Compruebe la instalación del servidor y vuelva a iniciar sesión.", + "notesAccountTimedOut": "El inicio de sesión caducó. Reinícielo y complete la autorización en el navegador.", + "notesAccountBrowserUnavailable": "No se pudo abrir el navegador del sistema. Compruebe el navegador predeterminado y vuelva a iniciar sesión.", + "notesAccountNetwork": "No se pudo contactar con Nextcloud. Compruebe la conexión y el servidor y vuelva a iniciar sesión.", + "notesAccountRejected": "Nextcloud rechazó la solicitud de funciones. Revise el acceso con el administrador y reinténtelo.", + "notesAccountFailed": "No se pudo configurar la cuenta. Vuelva a iniciar sesión.", + "notesTransferSourceChanged": "El origen cambió desde la revisión. Revise el archivo actual o capture otra instantánea antes de reintentar.", + "notesTransferUnreadable": "No se pudo leer el origen. Compruebe el acceso al archivo o carpeta y vuelva a revisarlo.", + "notesTransferInvalidSnapshot": "La instantánea no es válida o compatible. Elija una instantánea válida de BusyMark Notes o un origen Markdown.", + "notesTransferMediaLimit": "Los medios superan el límite de importación. Reduzca los del elemento afectado y vuelva a revisarlo.", + "notesTransferUnsafeSource": "El origen contiene una ruta insegura. Elija archivos dentro de la carpeta de origen y revíselos.", + "notesTransferDestination": "No se pudo escribir la exportación. Compruebe el acceso y el espacio libre en el destino y reinténtelo.", + "notesTransferOperationFailed": "La transferencia falló. Revise los elementos afectados y reinténtelo. Los ya importados siguen guardados.", + "notesExportPublishedWithOmissions": "Exportación disponible con omisiones", + "notesBatchUnchanged": "Los valores solicitados ya estaban establecidos.", + "notesBatchReadOnly": "Esta nota es de solo lectura. Las propiedades protegidas no pueden cambiar; Favorito sigue permitido.", + "notesBatchReviewChanged": "Esta nota cambió desde la revisión. Revise su estado actual antes de repetir la acción.", + "notesBatchMissing": "Esta nota ya no está disponible. Actualice la lista y revise los destinos.", + "notesBatchStorage": "Falló el guardado local. Compruebe el almacenamiento y reintente este elemento; los correctos siguen guardados.", + "notesRemoteReviewChanged": "La nota del servidor cambió. Se conserva su borrador de combinación. Revise la nueva comparación y decida de nuevo cómo resolverla.", + "notesReviewRemote": "He revisado la versión actual del servidor", "wsModuleCapsNotPublished": "No se publica la capitalización del módulo.", "wsTitleOverridesAction": "Títulos de instancia e índice…", "wsHeadingNotPublished": "Este generador no lo publica.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Fecha", "pdfTitlePageTitleRequired": "Introduzca un título para la portada.", "pdfTocSectionNote": "Comienza en una página independiente y puede ocupar varias páginas.", - "sourceSearchZeroLengthUnsupported": "No se admiten coincidencias de longitud cero (por ejemplo, ^, $ o anticipaciones que solo coinciden con una posición).", "workspaceSearchIncomplete": "Solo se muestran algunos resultados. Muestre más o limite la búsqueda.", "workspaceSearchShowMore": "Mostrar más resultados", "workspaceSearchSkippedFiles": "La búsqueda está incompleta. Estos archivos superan 1 MiB o no se pudieron leer:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Mostrar esta referencia de atajos de teclado", + "shortcutKeyboardShortcutsDescription": "Personalizar los atajos de teclado de la aplicación", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Borrar espacios de trabajo recientes", + "clearRecentWorkspaces": "Borrar el historial reciente", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Distinguir mayúsculas y minúsculas", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Palabra completa", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Expresión regular", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Expresión regular no válida", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Archivo grande: el resaltado y el plegado están en pausa", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "No se pudieron actualizar los elementos recientes: {error}.", "workspaceErrorOpenFailed": "Error al abrir: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "El texto del enlace está vacío; proporcione un nombre accesible que describa su propósito.", "diagnosticMarkdownLinkReviewText": "Revise si el texto del enlace «{text}» describe su propósito en contexto.", "diagnosticMarkdownTableEmptyHeader": "Los encabezados de tabla deben identificar sus columnas; complete cada encabezado vacío.", - "commandPalette": "Paleta de comandos", - "commandPaletteHint": "Escribe un comando", - "commandPaletteEmpty": "No hay comandos coincidentes", "tableAlignmentUnspecified": "Alineación: Sin especificar", "tableAlignmentLeft": "Alineación: Izquierda", "tableAlignmentCenter": "Alineación: Centro", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "Elija un archivo .css UTF-8 legible de hasta 256 KiB, sin importaciones, URL de recursos, HTML ni CSS ejecutable o escapado.", "exportInvalidRange": "{field}: introduzca un número entre {minimum} y {maximum}.", "clipboardHistory": "Historial de la Copia", - "clipboardHistorySearchHint": "Buscar en el historial del portapapeles", "clipboardHistoryDescription": "Abrir el historial de la copia local de la sesión.", "clipboardCurrent": "Copia actual", "clipboardCurrentExternal": "Copia actual de otra aplicación", "clipboardDestination": "Pegar en {name}", "clipboardSessionOnly": "Almacenado solo para esta sesión de BusyMark.", "clipboardNoItems": "El historial de la copia está vacío.", - "clipboardNoMatches": "Ningún elemento de la copia coincide con esta búsqueda.", "clipboardDisabled": "La colección de Historial de la Copia está deshabilitada en la configuración.", "clipboardUnavailable": "El contenido de la copia actual no está disponible o no es compatible.", "clipboardTooLarge": "Este elemento de la copia es demasiado grande para conservarlo.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "Buscar documentos y contenido de revisiones en el Historial Local.", "localHistoryNoDocuments": "Ningún documento tiene Historial Local.", "localHistoryNoRevisions": "Este documento no tiene revisiones en el Historial Local.", - "localHistoryNoMatches": "Ninguna revisión del Historial Local coincide con esta búsqueda.", - "localHistorySearchHint": "Buscar en el historial local", "localHistorySelectedRevision": "Revisión seleccionada", "localHistoryCurrentEditor": "Contenido del editor actual", "localHistoryCurrentDisk": "Contenido actual en el disco", @@ -3610,5 +3619,125 @@ "wsInsertElements": "Insertar elementos", "wsAdmonitionsAndQuotes": "Avisos y citas", "wsSemanticText": "Texto semántico", - "wsPropertiesUnavailable": "El elemento inspeccionado no está disponible. Inspeccione otro elemento para continuar." + "wsPropertiesUnavailable": "El elemento inspeccionado no está disponible. Inspeccione otro elemento para continuar.", + "searchDocumentsCategory": "Documentos", + "searchTopicsCategory": "Temas", + "searchCurrentDocument": "Documento actual", + "searchCurrentWorkspace": "Espacio de trabajo actual", + "browseNotes": "Explorar notas", + "searchActionsCommand": "Buscar acciones", + "searchDocumentsCommand": "Buscar documentos", + "searchTopicsCommand": "Buscar temas", + "searchRecentCommand": "Buscar recientes", + "searchResourceUnavailable": "Este recurso no está disponible.", + "notesSync": "Sincronizar notas", + "searchInWorkspace": "Buscar en espacio de trabajo", + "searchCategory": "Categoría de búsqueda", + "clearHistorySearchScope": "Buscar en el historial de documentos", + "searchInDocument": "Buscar en el documento", + "searchDocumentHint": "Buscar en este documento…", + "searchWorkspaceHint": "Buscar en este espacio de trabajo…", + "searchOutlineHint": "Buscar encabezados…", + "searchHistoryHint": "Buscar en el historial local…", + "searchEnterText": "Introduce texto para buscar", + "searchAccountUnavailable": "Cuenta no disponible", + "searchDocumentsHint": "Buscar documentos…", + "searchTopicsHint": "Buscar temas…", + "searchRecentHint": "Buscar recientes…", + "searchActionsHint": "Buscar acciones…", + "searchRequiresNotesWorkspace": "Requiere un espacio de trabajo de Notas", + "searchSidebarLabel": "Buscar:", + "searchDocumentTextCategory": "Texto del documento", + "searchWorkspaceTextCategory": "Texto del espacio de trabajo", + "searchHeadingsCategory": "Encabezados", + "shortcutUnassigned": "Sin asignar", + "shortcutPressKeys": "Pulse una combinación de teclas…", + "shortcutRecord": "Capturar atajo", + "shortcutRestoreAll": "Restaurar todos los valores predeterminados", + "shortcutRestoreAllQuestion": "¿Restaurar los atajos predeterminados de todas las acciones? Se eliminarán las asignaciones personalizadas.", + "shortcutSaveFailed": "Los atajos están activos, pero no se pudieron guardar. Reintente para conservarlos tras reiniciar.", + "shortcutRetry": "Reintentar guardado", + "shortcutChanged": "Las asignaciones cambiaron. Revise los atajos actuales y reintente.", + "shortcutInvalid": "Esta combinación no se puede asignar en este contexto.", + "shortcutSystemWarning": "El escritorio puede usar esta combinación. BusyMark no puede sustituir atajos del sistema.", + "shortcutConflicts": "Este atajo entra en conflicto con estas acciones:", + "shortcutRemove": "Quitar asignación", + "shortcutRestoreDefault": "Restaurar valor predeterminado", + "shortcutAssign": "Asignar", + "shortcutReassign": "Reasignar", + "shortcutContextApplication": "Aplicación", + "shortcutContextDocument": "Vistas del documento", + "shortcutContextEditor": "Editor y código fuente", + "shortcutContextWritersideSource": "Código fuente de Writerside", + "shortcutContextWorkspace": "Navegación del espacio de trabajo", + "shortcutContextTree": "Árboles laterales", + "searchHeadingsCommand": "Buscar encabezados", + "shortcutFixed": "Lo gestiona el editor; no se puede configurar.", + "shortcutDefaultConflict": "El atajo predeterminado entra en conflicto con una asignación personalizada.", + "shortcutCurrentBinding": "Atajo actual", + "shortcutProposedBinding": "Atajo propuesto", + "notesActions": "Acciones de notas", + "noteActions": "Acciones de la nota", + "noteProperties": "Propiedades de la nota", + "notesOfflinePause": "Pausar descargas", + "notesOfflineResume": "Reanudar descargas", + "notesEmptyAll": "Aún no hay notas. Cree una para empezar.", + "notesEmptyFavorites": "No hay notas favoritas.", + "notesEmptyCategory": "No hay notas en esta categoría.", + "notesEmptyUncategorized": "No hay notas sin categoría.", + "notesEmptyRecovery": "No hay notas conservadas para recuperar.", + "notesStartingConnection": "Iniciando la conexión con Nextcloud…", + "notesVerifyingConnection": "Verificando el acceso a Notas…", + "notesCancellingConnection": "Cancelando el inicio de sesión y limpiando…", + "notesRemovingConnection": "Eliminando la cuenta local…", + "notesSettingsLeave": "¿Guardar los cambios de ajustes de Nextcloud antes de salir?", + "notesSettingsReload": "Volver a cargar los ajustes del servidor…", + "notesScanning": "Examinando archivos…", + "notesCancelling": "Cancelando… Esperando a que termine la operación actual.", + "notesCancelled": "Cancelado", + "notesPartial": "Completado parcialmente", + "notesNotStarted": "Sin iniciar", + "notesCreated": "Creado", + "notesCurrentText": "Texto conservado actual", + "notesRecovering": "Recuperando como nota nueva…", + "notesSort": "Ordenar notas", + "notesOfflineRetentionHelp": "Quitar este requisito conserva el contenido en caché. Un requisito de categoría o cuenta puede seguir reteniendo la nota.", + "notesOfflineBroader": "Esta nota también se conserva por un requisito de categoría o cuenta.", + "notesOfflineExternal": "El contenido multimedia externo necesita conexión. BusyMark solo conserva el texto y los adjuntos gestionados.", + "notesOperationFailed": "La operación no pudo terminar. El borrador se conserva. Revise el guardado y los conflictos antes de reintentarlo.", + "notesRemovedWarning": "Se eliminó la cuenta local de Nextcloud. No se confirmó la revocación de la contraseña de aplicación. Elimine la contraseña de BusyMark en los ajustes de seguridad de Nextcloud.", + "notesBuffersNotSaved": "No se pudieron guardar todas las notas abiertas localmente. Revise el guardado, los conflictos o la normalización en el espacio de trabajo. Los cambios pendientes aún pueden necesitar sincronización con Nextcloud.", + "notesSettingsReloadHelp": "La recarga sustituye el borrador por los ajustes actuales de Nextcloud. ¿Descartar el borrador y recargar?", + "notesSettingsRefreshFailed": "Nextcloud aceptó los ajustes, pero falló la actualización de las notas. Se muestran los valores confirmados por el servidor. Reintente actualizar desde Notas.", + "notesStorageFull": "El almacenamiento de Nextcloud está lleno. Libere espacio en el servidor y reinténtelo.", + "notesRejected": "Nextcloud rechazó este cambio. Revise la nota y guarde una copia local antes de reintentarlo.", + "notesThrottled": "Nextcloud ha pedido esperar. La sincronización continuará al finalizar el plazo de espera.", + "notesImportPartialHelp": "Los elementos anteriores pueden estar guardados localmente. Reanude esta importación para continuar sin duplicarlos.", + "notesExportAll": "Exportar notas · Todas las notas…", + "notesExportCategory": "Exportar notas · Categoría…", + "notesOfflineAll": "Hacer disponible sin conexión · Todas las notas", + "notesOfflineCategory": "Hacer disponible sin conexión · Categoría", + "notesOfflineRemoveAll": "Quitar conservación sin conexión · Todas las notas", + "notesOfflineRemoveCategory": "Quitar conservación sin conexión · Categoría", + "notesOfflineKeep": "Hacer disponible sin conexión", + "notesLoadFailed": "Nextcloud Notes · Fallido. Reintentar", + "notesInspectionFailed": "Adjuntos · Fallido. Reintentar", + "notesSettingsLeaveHelp": "Ajustes de Notes del servidor · Cambios sin guardar", + "notesUnavailable": "Sin conexión · Texto disponible localmente · Guardado localmente · sincronización pendiente", + "notesRecoveryRequired": "Conflictos · Comparar", + "notesTransferFailed": "Fallido. Reintentar", + "notesRemoteDeleteUnavailable": "URL del servidor · No compatible · Eliminar · Adjuntos", + "notesReadOnlyHelp": "Solo lectura · Favoritos · Guardar copia local…", + "notesPathRelative": "La ruta de la carpeta de notas es relativa a su almacenamiento Nextcloud.", + "notesSettingsStale": "Los ajustes cambiaron en Nextcloud. Recargue y revise los valores del servidor antes de sustituir el borrador.", + "notesDownloadsPaused": "Descargas pausadas", + "notesExportNotPublished": "No se publicó ninguna exportación.", + "notesKeyringUnavailable": "Desbloquee el llavero del escritorio y reinténtelo.", + "notesConnectFailed": "No se pudo conectar con Nextcloud Notes. Compruebe la dirección del servidor y reinténtelo.", + "notesAccountMismatch": "Inicie sesión con la misma cuenta Nextcloud. Elimínela primero para conectar otra.", + "notesLocalRemovalFailed": "Falló la limpieza local. La cuenta se conserva para poder reintentar su eliminación.", + "notesTransferring": "Transfiriendo notas…", + "notesTechnicalDetails": "Detalles técnicos", + "notesPauseRequirement": "Pausar requisito sin conexión", + "notesResumeRequirement": "Reanudar requisito sin conexión" } diff --git a/lib/l10n/app_et.arb b/lib/l10n/app_et.arb index 0a9e9b77..a339cd60 100644 --- a/lib/l10n/app_et.arb +++ b/lib/l10n/app_et.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Uus märge säilitatud tekstist", + "notesAccountThrottled": "Nextcloud palus pausi. Oota enne uuesti sisselogimist.", + "notesSettingsRefreshing": "Notes värskendamine pärast seadete kinnitamist", + "notesExportOperationFailed": "Eksport nurjus enne avaldamist. Kontrolli allikat ja sihtkohta ning ekspordi uuesti.", + "notesRecoveryCreatedNotOpened": "Taastatud märge on salvestatud, kuid ei avanenud. Lahenda ootel redaktori- või omaduste muudatused ning ava see olemasolev märge.", + "notesSettingsPendingNotes": "Ootel märkmed, konfliktid või ebakindlad loomised takistavad muudatust. Sünkrooni või vaata need Notes’is üle ning salvesta säilitatud seadete mustand uuesti.", + "notesSettingsPendingAttachments": "Ootel manuste toimingud takistavad muudatust. Vaata manused Notes’is üle ning salvesta säilitatud seadete mustand uuesti.", + "notesAccountServerAddress": "Kasuta Nextcloudi kanoonilist HTTPS-aadressi ja logi uuesti sisse. Aadressi või ümbersuunamist ei saanud kinnitada.", + "notesAccountUnsupported": "Notes pole saadaval või API pole toetatud. Luba Nextcloud Notes API põhiversiooniga 1, alamversiooniga 4 või uuemaga.", + "notesAccountAuthorizeAgain": "Nextcloud lükkas pääsuandmed tagasi. Volita konto uuesti või ühenda uuesti.", + "notesAccountCheckForbidden": "Nextcloud keelas Notes’i võimaluste kontrolli. Palu administraatoril see lubada ja proovi uuesti.", + "notesAccountMalformed": "Nextcloudi vastust ei saanud kinnitada. Kontrolli serveri paigaldust ja logi uuesti sisse.", + "notesAccountTimedOut": "Sisselogimise aeg sai läbi. Alusta uuesti ja lõpeta volitamine brauseris.", + "notesAccountBrowserUnavailable": "Süsteemi brauser ei avanenud. Kontrolli vaikebrauserit ja logi uuesti sisse.", + "notesAccountNetwork": "Nextcloudiga ei saadud ühendust. Kontrolli võrku ja serverit ning logi uuesti sisse.", + "notesAccountRejected": "Nextcloud lükkas võimaluste päringu tagasi. Kontrolli administraatoriga ligipääsu ja proovi uuesti.", + "notesAccountFailed": "Konto seadistamine nurjus. Proovi uuesti sisse logida.", + "notesTransferSourceChanged": "Allikas muutus pärast ülevaatust. Vaata praegune fail üle või loo uus hetktõmmis.", + "notesTransferUnreadable": "Allikat ei saanud lugeda. Kontrolli faili või kausta ligipääsu ja vaata uuesti üle.", + "notesTransferInvalidSnapshot": "Hetktõmmis on vigane või toetamata. Vali kehtiv BusyMark Notes’i hetktõmmis või Markdown-allikas.", + "notesTransferMediaLimit": "Meedia ületab impordi mahupiiri. Vähenda vastava kirje meediat ja vaata uuesti üle.", + "notesTransferUnsafeSource": "Allikas sisaldab ohtlikku teed. Vali failid allikakausta seest ja vaata uuesti üle.", + "notesTransferDestination": "Eksporti ei saanud kirjutada. Kontrolli sihtkoha ligipääsu ja vaba ruumi ning proovi uuesti.", + "notesTransferOperationFailed": "Edastus ei lõppenud. Vaata vastavad kirjed üle ja proovi uuesti. Varem imporditud kirjed on salvestatud.", + "notesExportPublishedWithOmissions": "Eksport on saadaval koos väljajättudega", + "notesBatchUnchanged": "Soovitud väärtused olid juba määratud.", + "notesBatchReadOnly": "Märge on kirjutuskaitstud. Kaitstud omadusi muuta ei saa; lemmikuks märkimine on lubatud.", + "notesBatchReviewChanged": "Märge muutus pärast ülevaatust. Vaata selle praegune olek enne toimingu kordamist üle.", + "notesBatchMissing": "Märge pole enam saadaval. Värskenda loendit ja vaata sihtkirjed uuesti üle.", + "notesBatchStorage": "Kohalik salvestamine nurjus. Kontrolli andmeruumi ja proovi seda kirjet uuesti; õnnestunud kirjed on salvestatud.", + "notesRemoteReviewChanged": "Serveri märge muutus. Liitmise mustand säilis. Vaata uus võrdlus üle ja vali lahendus uuesti.", + "notesReviewRemote": "Olen serveri praeguse versiooni üle vaadanud", "wsModuleCapsNotPublished": "Mooduli suurtähereeglit ei avaldata.", "wsTitleOverridesAction": "Eksemplari ja sisukorra pealkirjad…", "wsHeadingNotPublished": "See koostaja ei avalda seda.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Kuupäev", "pdfTitlePageTitleRequired": "Sisesta tiitellehe pealkiri.", "pdfTocSectionNote": "Algab eraldi lehelt ja võib hõlmata mitut lehte.", - "sourceSearchZeroLengthUnsupported": "Nullpikkusega regulaaravaldiste vasteid ei toetata (näiteks ^, $ või ainult asukohta sobitavad ettevaated).", "workspaceSearchIncomplete": "Kuvatakse ainult osa tulemustest. Kuva rohkem või kitsenda otsingut.", "workspaceSearchShowMore": "Kuva rohkem tulemusi", "workspaceSearchSkippedFiles": "Otsing pole täielik. Need failid on suuremad kui 1 MiB või neid ei saanud lugeda:", @@ -673,7 +704,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Kuva kiirklahvide loend", + "shortcutKeyboardShortcutsDescription": "Kohanda rakenduse kiirklahve", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -970,7 +1001,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Tühjenda hiljutiste tööruumide loend", + "clearRecentWorkspaces": "Tühjenda hiljutiste kirjete loend", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1855,22 +1886,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Tõstutundlik", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Terve sõna", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Regulaaravaldis", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Vigane regulaaravaldis", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Suur fail: esiletõstmine ja voltimine on peatatud", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2004,6 +2019,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Hiljutisi üksusi ei saanud uuendada: {error}.", "workspaceErrorOpenFailed": "Avamine nurjus: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3668,9 +3684,6 @@ "diagnosticMarkdownLinkEmptyText": "Lingi tekst on tühi; lisa ligipääsetav nimi, mis kirjeldab selle otstarvet.", "diagnosticMarkdownLinkReviewText": "Kontrolli, kas lingi tekst „{text}” kirjeldab kontekstis selle otstarvet.", "diagnosticMarkdownTableEmptyHeader": "Tabelipäised peavad veerge kirjeldama; täida kõik tühjad päised.", - "commandPalette": "Käskude palett", - "commandPaletteHint": "Sisesta käsk", - "commandPaletteEmpty": "Sobivaid käske pole", "tableAlignmentUnspecified": "Joondus: määramata", "tableAlignmentLeft": "Joondus: vasakule", "tableAlignmentCenter": "Joondus: keskele", @@ -3785,14 +3798,12 @@ "exportInvalidCss": "Vali loetav UTF-8 .css-fail kuni 256 KiB, ilma importide, ressursside URL-ide, HTML-i ning käivitatava või paojadadega CSS-ita.", "exportInvalidRange": "{field}: sisesta arv vahemikus {minimum} kuni {maximum}.", "clipboardHistory": "Kleepimiskredi ajalugu", - "clipboardHistorySearchHint": "Otsi lõikepuhvri ajaloost", "clipboardHistoryDescription": "Ava sessiooni-spetsiifiline kleepimiskredi ajalugu.", "clipboardCurrent": "Praegune kleepimiskredi", "clipboardCurrentExternal": "Praegune kleepimiskredi teistest rakendusest", "clipboardDestination": "Kleepi {name}", "clipboardSessionOnly": "Salvestatud ainult sellele BusyMark sessioonile.", "clipboardNoItems": "Kleepimiskredi ajalugu on tühi.", - "clipboardNoMatches": "Ei leitud kleepimiskredi elemente, mis vastavad sellele otsingule.", "clipboardDisabled": "Kleepimiskredi ajalugu on peatatud seadetes.", "clipboardUnavailable": "Praegune kleepimiskredi sisu pole saadaval või toetatud.", "clipboardTooLarge": "See kleepimiskredi element on liiga suur, et seda säilitada.", @@ -3809,8 +3820,6 @@ "findLocalHistoryDescription": "Otsi kohalises ajaloos ja versioonide sisu.", "localHistoryNoDocuments": "Mida ei ole kohalist ajaloos.", "localHistoryNoRevisions": "Selle dokumendil pole kohalist ajaloos.", - "localHistoryNoMatches": "Ei leidunud kohalise ajalooga versioone.", - "localHistorySearchHint": "Otsi kohalikust ajaloost", "localHistorySelectedRevision": "Valitud versioon", "localHistoryCurrentEditor": "Praegune redaktoori sisu", "localHistoryCurrentDisk": "Praegune sisu faile", @@ -4070,5 +4079,125 @@ "wsInsertElements": "Lisa elemendid", "wsAdmonitionsAndQuotes": "Märkused ja tsitaadid", "wsSemanticText": "Semantiline tekst", - "wsPropertiesUnavailable": "Vaadeldav element pole saadaval. Jätkamiseks vali element." + "wsPropertiesUnavailable": "Vaadeldav element pole saadaval. Jätkamiseks vali element.", + "searchDocumentsCategory": "Dokumendid", + "searchTopicsCategory": "Teemad", + "searchCurrentDocument": "Praegune dokument", + "searchCurrentWorkspace": "Praegune tööruum", + "browseNotes": "Sirvi märkmeid", + "searchActionsCommand": "Otsi toiminguid", + "searchDocumentsCommand": "Otsi dokumente", + "searchTopicsCommand": "Otsi teemasid", + "searchRecentCommand": "Otsi hiljutisi", + "searchResourceUnavailable": "See ressurss pole saadaval.", + "notesSync": "Sünkrooni märkmed", + "searchInWorkspace": "Otsi tööalast", + "searchCategory": "Otsingu kategooria", + "clearHistorySearchScope": "Otsi dokumentide ajaloost", + "searchInDocument": "Otsi dokumendist", + "searchDocumentHint": "Otsi sellest dokumendist…", + "searchWorkspaceHint": "Otsi sellest tööruumist…", + "searchOutlineHint": "Otsi pealkirju…", + "searchHistoryHint": "Otsi kohalikust ajaloost…", + "searchEnterText": "Sisesta otsitav tekst", + "searchAccountUnavailable": "Konto pole saadaval", + "searchDocumentsHint": "Otsi dokumente…", + "searchTopicsHint": "Otsi teemasid…", + "searchRecentHint": "Otsi hiljutisi…", + "searchActionsHint": "Otsi toiminguid…", + "searchRequiresNotesWorkspace": "Vajab märkmete tööruumi", + "searchSidebarLabel": "Otsi:", + "searchDocumentTextCategory": "Dokumendi tekst", + "searchWorkspaceTextCategory": "Tööala tekst", + "searchHeadingsCategory": "Pealkirjad", + "shortcutUnassigned": "Määramata", + "shortcutPressKeys": "Vajuta klahvikombinatsiooni…", + "shortcutRecord": "Salvesta kiirklahv", + "shortcutRestoreAll": "Taasta kõik vaikeväärtused", + "shortcutRestoreAllQuestion": "Kas taastada kõigi toimingute vaikekiirklahvid? Kohandatud määrangud eemaldatakse.", + "shortcutSaveFailed": "Kiirklahvid on aktiivsed, kuid neid ei saanud salvestada. Proovi uuesti, et säilitada need pärast taaskäivitamist.", + "shortcutRetry": "Proovi salvestamist uuesti", + "shortcutChanged": "Määrangud on muutunud. Vaata praegused kiirklahvid üle ja proovi uuesti.", + "shortcutInvalid": "Seda kombinatsiooni ei saa selles kontekstis määrata.", + "shortcutSystemWarning": "Töölaud võib seda kombinatsiooni kasutada. BusyMark ei saa süsteemi kiirklahve alistada.", + "shortcutConflicts": "See kiirklahv kattub järgmiste toimingutega:", + "shortcutRemove": "Eemalda määrang", + "shortcutRestoreDefault": "Taasta vaikeväärtus", + "shortcutAssign": "Määra", + "shortcutReassign": "Määra ümber", + "shortcutContextApplication": "Rakendus", + "shortcutContextDocument": "Dokumendivaated", + "shortcutContextEditor": "Redaktor ja lähtekood", + "shortcutContextWritersideSource": "Writerside’i lähtekood", + "shortcutContextWorkspace": "Tööala navigeerimine", + "shortcutContextTree": "Külgriba puud", + "searchHeadingsCommand": "Otsi pealkirju", + "shortcutFixed": "Redaktori hallatav; pole seadistatav.", + "shortcutDefaultConflict": "Vaikimisi otsetee on vastuolus kohandatud määranguga.", + "shortcutCurrentBinding": "Praegune otsetee", + "shortcutProposedBinding": "Pakutud otsetee", + "notesActions": "Märkmete toimingud", + "noteActions": "Märkme toimingud", + "noteProperties": "Märkme omadused", + "notesOfflinePause": "Peata allalaadimised", + "notesOfflineResume": "Jätka allalaadimisi", + "notesEmptyAll": "Märkmeid veel pole. Alustamiseks loo märge.", + "notesEmptyFavorites": "Lemmikmärkmeid pole.", + "notesEmptyCategory": "Selles kategoorias pole märkmeid.", + "notesEmptyUncategorized": "Kategooriata märkmeid pole.", + "notesEmptyRecovery": "Taastamiseks säilitatud märkmeid pole.", + "notesStartingConnection": "Nextcloudi ühenduse alustamine…", + "notesVerifyingConnection": "Märkmete ligipääsu kontrollimine…", + "notesCancellingConnection": "Sisselogimise tühistamine ja puhastamine…", + "notesRemovingConnection": "Kohaliku konto eemaldamine…", + "notesSettingsLeave": "Kas salvestada Nextcloudi seadete muudatused enne lahkumist?", + "notesSettingsReload": "Laadi serveri seaded uuesti…", + "notesScanning": "Failide uurimine…", + "notesCancelling": "Tühistamine… Praeguse toimingu lõpu ootamine.", + "notesCancelled": "Tühistatud", + "notesPartial": "Osaliselt lõpetatud", + "notesNotStarted": "Alustamata", + "notesCreated": "Loodud", + "notesCurrentText": "Praegu säilitatud tekst", + "notesRecovering": "Taastamine uue märkmena…", + "notesSort": "Sordi märkmeid", + "notesOfflineRetentionHelp": "Selle nõude eemaldamine jätab vahemälu alles. Kategooria või konto nõue võib märget endiselt säilitada.", + "notesOfflineBroader": "Seda märget säilitatakse ka kategooria või konto nõude tõttu.", + "notesOfflineExternal": "Väline meedia vajab ühendust. BusyMark säilitab vaid märkmete teksti ja hallatavad manused.", + "notesOperationFailed": "Toiming ei lõppenud. Mustand on alles. Kontrolli salvestamist ja konflikte ning proovi uuesti.", + "notesRemovedWarning": "Kohalik Nextcloudi konto eemaldati. Rakenduse parooli tühistamist ei kinnitatud. Eemalda BusyMarki parool Nextcloudi turvaseadetest.", + "notesBuffersNotSaved": "Kõiki avatud märkmeid ei saanud kohalikult salvestada. Kontrolli tööruumis salvestamist, konflikte või normaliseerimist. Kohalikud muudatused võivad veel vajada Nextcloudiga sünkroonimist.", + "notesSettingsReloadHelp": "Uuesti laadimine asendab mustandi Nextcloudi praeguste seadetega. Kas loobuda mustandist ja laadida uuesti?", + "notesSettingsRefreshFailed": "Nextcloud kinnitas seaded, kuid märkmete värskendamine ebaõnnestus. Kuvatakse serveri kinnitatud väärtused. Proovi märkmete värskendamist uuesti.", + "notesStorageFull": "Nextcloudi salvestusruum on täis. Vabasta serveris ruumi ja proovi uuesti.", + "notesRejected": "Nextcloud lükkas muudatuse tagasi. Kontrolli märget ja salvesta enne uut katset kohalik koopia.", + "notesThrottled": "Nextcloud palus oodata. Sünkroonimine jätkub pärast ooteaja lõppu.", + "notesImportPartialHelp": "Varasemad üksused võivad olla juba kohalikult salvestatud. Jätka sama importi, et vältida duplikaate.", + "notesExportAll": "Ekspordi märkmed · Kõik märkmed…", + "notesExportCategory": "Ekspordi märkmed · Kategooria…", + "notesOfflineAll": "Tee võrguta kättesaadavaks · Kõik märkmed", + "notesOfflineCategory": "Tee võrguta kättesaadavaks · Kategooria", + "notesOfflineRemoveAll": "Eemalda võrguta säilitamine · Kõik märkmed", + "notesOfflineRemoveCategory": "Eemalda võrguta säilitamine · Kategooria", + "notesOfflineKeep": "Tee võrguta kättesaadavaks", + "notesLoadFailed": "Nextcloud Notes · Ebaõnnestus. Proovi uuesti", + "notesInspectionFailed": "Manused · Ebaõnnestus. Proovi uuesti", + "notesSettingsLeaveHelp": "Serveri Notesi seaded · Salvestamata muudatused", + "notesUnavailable": "Ühenduseta · Tekst on kohalikult saadaval · Salvestatud kohalikult · sünkroonimine ootel", + "notesRecoveryRequired": "Konfliktid · Võrdle", + "notesTransferFailed": "Ebaõnnestus. Proovi uuesti", + "notesRemoteDeleteUnavailable": "Serveri aadress · Pole toetatud · Kustuta · Manused", + "notesReadOnlyHelp": "Kirjutuskaitstud · Lemmikud · Salvesta kohalik koopia…", + "notesPathRelative": "Märkmete kaustatee on suhteline sinu Nextcloudi salvestusruumi suhtes.", + "notesSettingsStale": "Seaded muutusid Nextcloudis. Laadi serveri väärtused uuesti ja vaata üle enne mustandi asendamist.", + "notesDownloadsPaused": "Allalaadimised peatatud", + "notesExportNotPublished": "Eksporti ei avaldatud.", + "notesKeyringUnavailable": "Ava töölaua võtmehoidla ja proovi uuesti.", + "notesConnectFailed": "Nextcloudi märkmetega ei saanud ühendust. Kontrolli serveri aadressi ja proovi uuesti.", + "notesAccountMismatch": "Logi sisse samale Nextcloudi kontole. Teise konto ühendamiseks eemalda esmalt see konto.", + "notesLocalRemovalFailed": "Kohaliku konto puhastamine ebaõnnestus. Konto on alles, et saaksid eemaldamist korrata.", + "notesTransferring": "Märkmete teisaldamine…", + "notesTechnicalDetails": "Tehnilised üksikasjad", + "notesPauseRequirement": "Peata võrguühenduseta säilitamise nõue", + "notesResumeRequirement": "Jätka võrguühenduseta säilitamise nõuet" } diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index a009276a..ef524903 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "یادداشت جدید از متن نگه‌داری‌شده", + "notesAccountThrottled": "Nextcloud درخواست مکث کرد. پیش از آغاز دوبارهٔ ورود منتظر بمانید.", + "notesSettingsRefreshing": "در حال تازه‌سازی Notes پس از تأیید تنظیمات", + "notesExportOperationFailed": "برون‌بری پیش از انتشار شکست خورد. مبدأ و مقصد را بررسی و دوباره برون‌بری کنید.", + "notesRecoveryCreatedNotOpened": "یادداشت بازیابی‌شده ذخیره شد اما باز نشد. تغییرات معلق ویرایشگر یا ویژگی‌ها را رفع و همین یادداشت را باز کنید.", + "notesSettingsPendingNotes": "یادداشت‌های معلق، تعارض‌ها یا ایجادهای نامطمئن مانع این تغییرند. در Notes همگام‌سازی یا بازبینی کنید، سپس ذخیرهٔ پیش‌نویس حفظ‌شده را تکرار کنید.", + "notesSettingsPendingAttachments": "عملیات معلق پیوست‌ها مانع این تغییرند. پیوست‌ها را در Notes بازبینی و ذخیرهٔ پیش‌نویس حفظ‌شده را تکرار کنید.", + "notesAccountServerAddress": "از نشانی HTTPS اصلی Nextcloud استفاده و ورود را دوباره آغاز کنید. نشانی یا تغییرمسیر تأیید نشد.", + "notesAccountUnsupported": "Notes در دسترس نیست یا API پشتیبانی نمی‌شود. Nextcloud Notes را با نسخهٔ اصلی API برابر 1 و فرعی 4 یا بالاتر فعال کنید.", + "notesAccountAuthorizeAgain": "Nextcloud اطلاعات ورود را رد کرد. حساب را دوباره مجاز یا متصل کنید.", + "notesAccountCheckForbidden": "Nextcloud بررسی قابلیت‌های Notes را نپذیرفت. از مدیر سرور مجوز بخواهید و دوباره تلاش کنید.", + "notesAccountMalformed": "پاسخ Nextcloud تأیید نشد. نصب سرور را بررسی و ورود را دوباره آغاز کنید.", + "notesAccountTimedOut": "مهلت ورود تمام شد. دوباره آغاز و مجوز را در مرورگر تکمیل کنید.", + "notesAccountBrowserUnavailable": "مرورگر سیستم باز نشد. مرورگر پیش‌فرض را بررسی و ورود را دوباره آغاز کنید.", + "notesAccountNetwork": "دسترسی به Nextcloud ممکن نشد. اتصال و سرور را بررسی و دوباره وارد شوید.", + "notesAccountRejected": "Nextcloud درخواست قابلیت‌ها را رد کرد. دسترسی را با مدیر بررسی و دوباره تلاش کنید.", + "notesAccountFailed": "راه‌اندازی حساب کامل نشد. دوباره وارد شوید.", + "notesTransferSourceChanged": "مبدأ پس از بازبینی تغییر کرده است. فایل فعلی را دوباره بازبینی یا تصویر تازه‌ای تهیه کنید.", + "notesTransferUnreadable": "مبدأ خوانده نشد. دسترسی فایل یا پوشه را بررسی و دوباره بازبینی کنید.", + "notesTransferInvalidSnapshot": "تصویر نامعتبر یا پشتیبانی‌نشده است. یک تصویر معتبر BusyMark Notes یا مبدأ Markdown انتخاب کنید.", + "notesTransferMediaLimit": "رسانه از محدودیت اندازهٔ درون‌بری بیشتر است. رسانهٔ مورد مربوط را کوچک‌تر و دوباره بازبینی کنید.", + "notesTransferUnsafeSource": "مبدأ مسیر ناامن دارد. فایل‌هایی از داخل پوشهٔ مبدأ انتخاب و دوباره بازبینی کنید.", + "notesTransferDestination": "برون‌بری نوشته نشد. دسترسی و فضای آزاد مقصد را بررسی و دوباره برون‌بری کنید.", + "notesTransferOperationFailed": "انتقال کامل نشد. موارد مربوط را بازبینی و دوباره تلاش کنید. موارد درون‌برده‌شدهٔ قبلی ذخیره می‌مانند.", + "notesExportPublishedWithOmissions": "برون‌بری با موارد حذف‌شده در دسترس است", + "notesBatchUnchanged": "مقادیر خواسته‌شده از قبل تنظیم بودند.", + "notesBatchReadOnly": "این یادداشت فقط‌خواندنی است. ویژگی‌های محافظت‌شده تغییر نمی‌کنند؛ تغییر علاقه‌مندی همچنان مجاز است.", + "notesBatchReviewChanged": "یادداشت پس از بازبینی تغییر کرده است. پیش از اجرای دوبارهٔ عمل، وضعیت فعلی را بازبینی کنید.", + "notesBatchMissing": "این یادداشت دیگر در دسترس نیست. فهرست و اهداف را دوباره بررسی کنید.", + "notesBatchStorage": "ذخیرهٔ محلی شکست خورد. فضای ذخیره‌سازی را بررسی و این مورد را تکرار کنید؛ موارد موفق ذخیره می‌مانند.", + "notesRemoteReviewChanged": "یادداشت سرور تغییر کرد. پیش‌نویس ادغام حفظ شده است. مقایسهٔ به‌روز را بازبینی و راه‌حل را دوباره انتخاب کنید.", + "notesReviewRemote": "نسخهٔ فعلی سرور را بازبینی کردم", "wsModuleCapsNotPublished": "سبک حروف ماژول منتشر نمی‌شود.", "wsTitleOverridesAction": "عنوان‌های نمونه و فهرست…", "wsHeadingNotPublished": "این سازنده آن را منتشر نمی‌کند.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "تاریخ", "pdfTitlePageTitleRequired": "عنوانی برای صفحهٔ عنوان وارد کنید.", "pdfTocSectionNote": "از صفحه‌ای جداگانه شروع می‌شود و ممکن است چند صفحه ادامه یابد.", - "sourceSearchZeroLengthUnsupported": "تطبیق‌های با طول صفر در عبارت منظم پشتیبانی نمی‌شوند (مانند ^، $ یا نگاه‌به‌جلو که فقط با یک موقعیت تطبیق دارد).", "workspaceSearchIncomplete": "فقط بخشی از نتایج نمایش داده می‌شود. نتایج بیشتری نمایش دهید یا جستجو را محدود کنید.", "workspaceSearchShowMore": "نمایش نتایج بیشتر", "workspaceSearchSkippedFiles": "جستجو کامل نیست. حجم این فایل‌ها بیش از 1 MiB است یا خواندن آن‌ها ممکن نبود:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "نمایش مرجع میانبرهای صفحه‌کلید", + "shortcutKeyboardShortcutsDescription": "سفارشی‌سازی میان‌برهای صفحه‌کلید برنامه", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "پاک کردن فضاهای کاری اخیر", + "clearRecentWorkspaces": "پاک کردن تاریخچهٔ اخیر", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1569,22 +1600,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "حساس به بزرگی و کوچکی حروف", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "تمام کلمه", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "عبارت منظم", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "عبارت منظم نامعتبر است", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "فایل بزرگ: برجسته‌سازی و جمع‌کردن موقتاً متوقف شده‌اند", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1719,6 +1734,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "به‌روزرسانی موارد اخیر ممکن نشد: {error}.", "workspaceErrorOpenFailed": "باز کردن ناموفق بود: ⁨{error}⁩", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3250,9 +3266,6 @@ "diagnosticMarkdownLinkEmptyText": "متن پیوند خالی است؛ نام دسترس‌پذیری وارد کنید که هدف آن را توضیح دهد.", "diagnosticMarkdownLinkReviewText": "بررسی کنید که آیا متن پیوند «⁨{text}⁩» هدف آن را در زمینه توضیح می‌دهد.", "diagnosticMarkdownTableEmptyHeader": "سرستون‌های جدول باید ستون‌های خود را مشخص کنند؛ هر سرستون خالی را تکمیل کنید.", - "commandPalette": "پالت فرمان", - "commandPaletteHint": "یک فرمان وارد کنید", - "commandPaletteEmpty": "هیچ فرمان منطبقی وجود ندارد", "tableAlignmentUnspecified": "تراز: مشخص‌نشده", "tableAlignmentLeft": "تراز: چپ", "tableAlignmentCenter": "تراز: وسط", @@ -3367,14 +3380,12 @@ "exportInvalidCss": "یک فایل .css خوانا با UTF-8 تا ۲۵۶ KiB انتخاب کنید، بدون واردسازی، نشانی منابع، HTML یا CSS اجرایی یا دارای نویسهٔ گریز.", "exportInvalidRange": "{field}: عددی بین {minimum} و {maximum} وارد کنید.", "clipboardHistory": "تاریخچه کپی و پیست", - "clipboardHistorySearchHint": "جستجو در تاریخچه کلیپ‌بورد", "clipboardHistoryDescription": "باز کردن تاریخچه کپی و پیست مخصوص جلسه.", "clipboardCurrent": "کپی و پیست فعلی", "clipboardCurrentExternal": "کپی و پیست از برنامه دیگر", "clipboardDestination": "کپی و پیست در {name}", "clipboardSessionOnly": "فقط برای این جلسه BusyMark ذخیره می شود.", "clipboardNoItems": "تاریخچه کپی و پیست خالی است.", - "clipboardNoMatches": "هیچ آیتم کپی و پیستی با این جستجو مطابقت ندارد.", "clipboardDisabled": "مجموعه تاریخچه کپی و پیست در تنظیمات غیرفعال است.", "clipboardUnavailable": "محتوای کپی و پیست فعلی در دسترس نیست یا پشتیبانی نمی شود.", "clipboardTooLarge": "این آیتم کپی شده خیلی بزرگ است و نمی‌توان آن را نگه داشت.", @@ -3391,8 +3402,6 @@ "findLocalHistoryDescription": "اسناد و محتوای نسخه‌های تاریخ محلی را جستجو کنید.", "localHistoryNoDocuments": "هنوز هیچ سند دارای تاریخ محلی نیست.", "localHistoryNoRevisions": "این سند دارای هیچ نسخه‌ای از تاریخ محلی نیست.", - "localHistoryNoMatches": "هیچ نسخه‌ای از تاریخ محلی با این جستجو مطابقت ندارد.", - "localHistorySearchHint": "جستجو در تاریخچه محلی", "localHistorySelectedRevision": "نسخه انتخاب شده", "localHistoryCurrentEditor": "محتوای فعلی ویرایشگر", "localHistoryCurrentDisk": "محتوای فعلی روی دیسک", @@ -3652,5 +3661,125 @@ "wsInsertElements": "درج عناصر", "wsAdmonitionsAndQuotes": "یادآوری‌ها و نقل‌قول‌ها", "wsSemanticText": "متن معنایی", - "wsPropertiesUnavailable": "عنصر بررسی‌شده در دسترس نیست. برای ادامه عنصری را بررسی کنید." + "wsPropertiesUnavailable": "عنصر بررسی‌شده در دسترس نیست. برای ادامه عنصری را بررسی کنید.", + "searchDocumentsCategory": "سندها", + "searchTopicsCategory": "موضوع‌ها", + "searchCurrentDocument": "سند کنونی", + "searchCurrentWorkspace": "فضای کاری کنونی", + "browseNotes": "مرور یادداشت‌ها", + "searchActionsCommand": "جستجوی کنش‌ها", + "searchDocumentsCommand": "جستجوی سندها", + "searchTopicsCommand": "جستجوی موضوع‌ها", + "searchRecentCommand": "جستجوی موارد اخیر", + "searchResourceUnavailable": "این منبع در دسترس نیست.", + "notesSync": "همگام‌سازی یادداشت‌ها", + "searchInWorkspace": "جستجو در فضای کاری", + "searchCategory": "دستهٔ جستجو", + "clearHistorySearchScope": "جستجو در تاریخچهٔ سندها", + "searchInDocument": "جستجو در سند", + "searchDocumentHint": "جستجو در این سند…", + "searchWorkspaceHint": "جستجو در این فضای کاری…", + "searchOutlineHint": "جستجوی عنوان‌ها…", + "searchHistoryHint": "جستجو در تاریخچهٔ محلی…", + "searchEnterText": "متن جستجو را وارد کنید", + "searchAccountUnavailable": "حساب در دسترس نیست", + "searchDocumentsHint": "جستجوی سندها…", + "searchTopicsHint": "جستجوی موضوع‌ها…", + "searchRecentHint": "جستجوی موارد اخیر…", + "searchActionsHint": "جستجوی کنش‌ها…", + "searchRequiresNotesWorkspace": "به فضای کاری یادداشت‌ها نیاز دارد", + "searchSidebarLabel": "جستجو:", + "searchDocumentTextCategory": "متن سند", + "searchWorkspaceTextCategory": "متن فضای کاری", + "searchHeadingsCategory": "عنوان‌ها", + "shortcutUnassigned": "تخصیص داده نشده", + "shortcutPressKeys": "یک ترکیب کلید را فشار دهید…", + "shortcutRecord": "ثبت میان‌بر", + "shortcutRestoreAll": "بازیابی همهٔ پیش‌فرض‌ها", + "shortcutRestoreAllQuestion": "میان‌برهای پیش‌فرض همهٔ کنش‌ها بازیابی شوند؟ تخصیص‌های سفارشی حذف می‌شوند.", + "shortcutSaveFailed": "میان‌برها فعال‌اند اما ذخیره نشدند. برای حفظ آن‌ها پس از راه‌اندازی مجدد، دوباره ذخیره کنید.", + "shortcutRetry": "تلاش دوباره برای ذخیره", + "shortcutChanged": "تخصیص‌ها تغییر کرده‌اند. میان‌برهای فعلی را بررسی و دوباره تلاش کنید.", + "shortcutInvalid": "این ترکیب را نمی‌توان در این زمینه تخصیص داد.", + "shortcutSystemWarning": "ممکن است میزکار از این ترکیب استفاده کند. BusyMark نمی‌تواند میان‌برهای سیستم را بازنویسی کند.", + "shortcutConflicts": "این میان‌بر با کنش‌های زیر تداخل دارد:", + "shortcutRemove": "حذف تخصیص", + "shortcutRestoreDefault": "بازیابی پیش‌فرض", + "shortcutAssign": "تخصیص", + "shortcutReassign": "تخصیص مجدد", + "shortcutContextApplication": "برنامه", + "shortcutContextDocument": "نماهای سند", + "shortcutContextEditor": "ویرایشگر و منبع", + "shortcutContextWritersideSource": "منبع Writerside", + "shortcutContextWorkspace": "پیمایش فضای کاری", + "shortcutContextTree": "درخت‌های نوار کناری", + "searchHeadingsCommand": "جستجوی عنوان‌ها", + "shortcutFixed": "توسط ویرایشگر مدیریت می‌شود؛ قابل تنظیم نیست.", + "shortcutDefaultConflict": "میان‌بر پیش‌فرض با تخصیص سفارشی تداخل دارد.", + "shortcutCurrentBinding": "میان‌بر فعلی", + "shortcutProposedBinding": "میان‌بر پیشنهادی", + "notesActions": "کنش‌های یادداشت‌ها", + "noteActions": "کنش‌های یادداشت", + "noteProperties": "ویژگی‌های یادداشت", + "notesOfflinePause": "مکث بارگیری‌ها", + "notesOfflineResume": "ادامهٔ بارگیری‌ها", + "notesEmptyAll": "هنوز یادداشتی نیست. یک یادداشت بسازید.", + "notesEmptyFavorites": "یادداشت دلخواهی نیست.", + "notesEmptyCategory": "در این دسته یادداشتی نیست.", + "notesEmptyUncategorized": "یادداشت بدون دسته‌ای نیست.", + "notesEmptyRecovery": "یادداشت نگهداری‌شده‌ای برای بازیابی نیست.", + "notesStartingConnection": "در حال آغاز اتصال Nextcloud…", + "notesVerifyingConnection": "در حال بررسی دسترسی به یادداشت‌ها…", + "notesCancellingConnection": "در حال لغو ورود و پاک‌سازی…", + "notesRemovingConnection": "در حال حذف حساب محلی…", + "notesSettingsLeave": "پیش از خروج تغییرات تنظیمات Nextcloud ذخیره شوند؟", + "notesSettingsReload": "بارگیری دوبارهٔ تنظیمات کارساز…", + "notesScanning": "در حال بررسی پرونده‌ها…", + "notesCancelling": "در حال لغو… منتظر پایان عملیات جاری.", + "notesCancelled": "لغو شد", + "notesPartial": "تا حدی کامل شد", + "notesNotStarted": "آغاز نشده", + "notesCreated": "ایجاد شد", + "notesCurrentText": "متن محلی نگهداری‌شدهٔ کنونی", + "notesRecovering": "در حال بازیابی به‌عنوان یادداشت تازه…", + "notesSort": "مرتب‌سازی یادداشت‌ها", + "notesOfflineRetentionHelp": "حذف این نیازمندی محتوای نهان‌شده را نگه می‌دارد. نیازمندی دسته یا حساب ممکن است همچنان یادداشت را نگه دارد.", + "notesOfflineBroader": "این یادداشت به‌دلیل نیازمندی دسته یا حساب نیز نگهداری می‌شود.", + "notesOfflineExternal": "رسانهٔ خارجی به اتصال نیاز دارد. BusyMark فقط متن یادداشت و پیوست‌های مدیریت‌شده را نگه می‌دارد.", + "notesOperationFailed": "عملیات کامل نشد. پیش‌نویس نگهداری شده است. ذخیره و تعارض‌ها را بررسی کنید و دوباره تلاش کنید.", + "notesRemovedWarning": "حساب محلی Nextcloud حذف شد. لغو گذرواژهٔ برنامه تأیید نشد. گذرواژهٔ BusyMark را در تنظیمات امنیتی Nextcloud حذف کنید.", + "notesBuffersNotSaved": "همهٔ یادداشت‌های باز به‌صورت محلی ذخیره نشدند. ذخیره، تعارض‌ها یا نرمال‌سازی را در فضای کار بررسی کنید. تغییرات محلی ممکن است هنوز به همگام‌سازی با Nextcloud نیاز داشته باشند.", + "notesSettingsReloadHelp": "بارگیری دوباره پیش‌نویس را با تنظیمات فعلی Nextcloud جایگزین می‌کند. پیش‌نویس دور ریخته و دوباره بارگیری شود؟", + "notesSettingsRefreshFailed": "Nextcloud تنظیمات را پذیرفت ولی تازه‌سازی یادداشت‌ها ناموفق بود. مقادیر نمایش‌داده‌شده تأییدشدهٔ کارسازند. از یادداشت‌ها دوباره تازه‌سازی کنید.", + "notesStorageFull": "فضای Nextcloud پر است. در کارساز جا باز کنید و دوباره تلاش کنید.", + "notesRejected": "Nextcloud این تغییر را نپذیرفت. یادداشت را بررسی کنید و پیش از تلاش دوباره نسخه‌ای محلی ذخیره کنید.", + "notesThrottled": "Nextcloud درخواست انتظار کرد. همگام‌سازی پس از پایان مهلت دوباره ادامه می‌یابد.", + "notesImportPartialHelp": "موارد قبلی ممکن است از پیش محلی ذخیره شده باشند. همین درون‌ریزی را ادامه دهید تا تکراری ایجاد نشود.", + "notesExportAll": "صادر کردن یادداشت‌ها · همه یادداشت‌ها…", + "notesExportCategory": "صادر کردن یادداشت‌ها · دسته‌بندی…", + "notesOfflineAll": "در دسترس کردن آفلاین · همه یادداشت‌ها", + "notesOfflineCategory": "در دسترس کردن آفلاین · دسته‌بندی", + "notesOfflineRemoveAll": "حذف نگهداری آفلاین · همه یادداشت‌ها", + "notesOfflineRemoveCategory": "حذف نگهداری آفلاین · دسته‌بندی", + "notesOfflineKeep": "در دسترس کردن آفلاین", + "notesLoadFailed": "Nextcloud Notes · ناموفق. تلاش دوباره", + "notesInspectionFailed": "پیوست‌ها · ناموفق. تلاش دوباره", + "notesSettingsLeaveHelp": "تنظیمات Notes سرور · تغییرات ذخیره‌نشده", + "notesUnavailable": "بدون اتصال · متن به صورت محلی در دسترس است · ذخیرهٔ محلی · در انتظار همگام‌سازی", + "notesRecoveryRequired": "تداخل‌ها · مقایسه", + "notesTransferFailed": "ناموفق. تلاش دوباره", + "notesRemoteDeleteUnavailable": "نشانی سرور · پشتیبانی نمی‌شود · حذف · پیوست‌ها", + "notesReadOnlyHelp": "فقط خواندنی · علاقه‌مندی‌ها · ذخیرهٔ نسخهٔ محلی…", + "notesPathRelative": "مسیر پوشهٔ یادداشت‌ها نسبت به فضای ذخیره‌سازی Nextcloud شماست.", + "notesSettingsStale": "تنظیمات در Nextcloud تغییر کرده‌اند. پیش از جایگزینی پیش‌نویس، مقادیر کارساز را دوباره بارگیری و بررسی کنید.", + "notesDownloadsPaused": "بارگیری‌ها مکث شده‌اند", + "notesExportNotPublished": "هیچ برون‌بری منتشر نشد.", + "notesKeyringUnavailable": "قفل دسته‌کلید میزکار را باز کنید و دوباره تلاش کنید.", + "notesConnectFailed": "اتصال به یادداشت‌های Nextcloud ناموفق بود. نشانی کارساز را بررسی و دوباره تلاش کنید.", + "notesAccountMismatch": "به همان حساب Nextcloud وارد شوید. برای اتصال به حساب دیگر ابتدا این حساب را حذف کنید.", + "notesLocalRemovalFailed": "پاک‌سازی حساب محلی ناموفق بود. حساب برای تلاش دوبارهٔ حذف نگه داشته شده است.", + "notesTransferring": "در حال انتقال یادداشت‌ها…", + "notesTechnicalDetails": "جزئیات فنی", + "notesPauseRequirement": "مکث نیازمندی آفلاین", + "notesResumeRequirement": "ادامهٔ نیازمندی آفلاین" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 230fd23b..c42b9e3b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nouvelle note à partir du texte conservé", + "notesAccountThrottled": "Nextcloud demande une pause. Attendez avant de relancer la connexion.", + "notesSettingsRefreshing": "Actualisation de Notes après confirmation des paramètres", + "notesExportOperationFailed": "L’export a échoué avant publication. Vérifiez la source et la destination, puis réessayez.", + "notesRecoveryCreatedNotOpened": "La note récupérée est enregistrée, mais son ouverture a échoué. Réglez les modifications en attente dans l’éditeur ou les propriétés, puis ouvrez cette note.", + "notesSettingsPendingNotes": "Des notes en attente, conflits ou créations incertaines bloquent ce changement. Synchronisez ou examinez-les dans Notes, puis réessayez ce brouillon conservé.", + "notesSettingsPendingAttachments": "Des opérations de pièces jointes bloquent ce changement. Examinez-les dans Notes, puis réessayez ce brouillon de paramètres conservé.", + "notesAccountServerAddress": "Utilisez l’adresse HTTPS canonique de Nextcloud et relancez la connexion. L’adresse ou la redirection n’a pas pu être vérifiée.", + "notesAccountUnsupported": "Notes est indisponible ou son API incompatible. Activez Nextcloud Notes avec l’API majeure 1, mineure 4 ou ultérieure.", + "notesAccountAuthorizeAgain": "Nextcloud a refusé les identifiants. Autorisez à nouveau ce compte ou reconnectez-le.", + "notesAccountCheckForbidden": "Nextcloud a interdit la vérification des fonctionnalités Notes. Demandez son autorisation à l’administrateur, puis réessayez.", + "notesAccountMalformed": "La réponse Nextcloud n’a pas pu être vérifiée. Vérifiez l’installation du serveur, puis relancez la connexion.", + "notesAccountTimedOut": "La connexion a expiré. Relancez-la et terminez l’autorisation dans le navigateur.", + "notesAccountBrowserUnavailable": "Impossible d’ouvrir le navigateur système. Vérifiez le navigateur par défaut, puis relancez la connexion.", + "notesAccountNetwork": "Nextcloud est injoignable. Vérifiez la connexion et le serveur, puis réessayez.", + "notesAccountRejected": "Nextcloud a refusé la demande de fonctionnalités. Vérifiez l’accès avec l’administrateur, puis réessayez.", + "notesAccountFailed": "La configuration du compte a échoué. Réessayez de vous connecter.", + "notesTransferSourceChanged": "La source a changé depuis son examen. Examinez le fichier actuel ou créez un nouvel instantané avant de réessayer.", + "notesTransferUnreadable": "Impossible de lire la source. Vérifiez l’accès au fichier ou au dossier, puis examinez-la à nouveau.", + "notesTransferInvalidSnapshot": "Instantané invalide ou incompatible. Choisissez un instantané BusyMark Notes valide ou une source Markdown.", + "notesTransferMediaLimit": "Les médias dépassent la limite d’importation. Réduisez ceux de l’élément concerné avant de le réexaminer.", + "notesTransferUnsafeSource": "La source contient un chemin non sûr. Choisissez des fichiers dans le dossier source, puis réexaminez-les.", + "notesTransferDestination": "Impossible d’écrire l’export. Vérifiez l’accès et l’espace libre à destination, puis réessayez.", + "notesTransferOperationFailed": "Le transfert a échoué. Examinez les éléments concernés et réessayez. Les éléments déjà importés restent enregistrés.", + "notesExportPublishedWithOmissions": "Export disponible avec des omissions", + "notesBatchUnchanged": "Les valeurs demandées étaient déjà définies.", + "notesBatchReadOnly": "Cette note est en lecture seule. Ses propriétés protégées ne peuvent pas changer ; le favori reste modifiable.", + "notesBatchReviewChanged": "Cette note a changé depuis son examen. Examinez son état actuel avant de réappliquer l’action.", + "notesBatchMissing": "Cette note n’est plus disponible. Actualisez la liste et vérifiez les cibles.", + "notesBatchStorage": "Échec de l’enregistrement local. Vérifiez le stockage et réessayez cet élément ; les éléments réussis restent enregistrés.", + "notesRemoteReviewChanged": "La note du serveur a changé. Votre brouillon de fusion est conservé. Examinez la comparaison actualisée et choisissez à nouveau la résolution.", + "notesReviewRemote": "J’ai examiné la version actuelle du serveur", "wsModuleCapsNotPublished": "La casse du module n’est pas publiée.", "wsTitleOverridesAction": "Titres de l’instance et du sommaire…", "wsHeadingNotPublished": "Non publié par ce générateur.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Date", "pdfTitlePageTitleRequired": "Saisissez un titre pour la page de titre.", "pdfTocSectionNote": "Commence sur une page distincte et peut occuper plusieurs pages.", - "sourceSearchZeroLengthUnsupported": "Les correspondances de longueur nulle ne sont pas prises en charge (par exemple ^, $ ou les assertions qui ne correspondent qu’à une position).", "workspaceSearchIncomplete": "Seule une partie des résultats est affichée. Affichez-en davantage ou affinez la recherche.", "workspaceSearchShowMore": "Afficher plus de résultats", "workspaceSearchSkippedFiles": "La recherche est incomplète. Ces fichiers dépassent 1 MiB ou n’ont pas pu être lus :", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Afficher cette liste de raccourcis clavier", + "shortcutKeyboardShortcutsDescription": "Personnaliser les raccourcis clavier de l’application", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Effacer les espaces de travail récents", + "clearRecentWorkspaces": "Effacer l’historique récent", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Respecter la casse", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Mot entier", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Expression régulière", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Expression régulière non valide", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Fichier volumineux : la coloration et le repliage sont suspendus", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Impossible de mettre à jour les éléments récents : {error}.", "workspaceErrorOpenFailed": "Échec de l’ouverture : {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "Le texte du lien est vide ; fournissez un nom accessible qui décrit son objectif.", "diagnosticMarkdownLinkReviewText": "Vérifiez si le texte du lien « {text} » décrit son objectif dans le contexte.", "diagnosticMarkdownTableEmptyHeader": "Les en-têtes de tableau doivent identifier leurs colonnes ; complétez chaque en-tête vide.", - "commandPalette": "Palette de commandes", - "commandPaletteHint": "Saisissez une commande", - "commandPaletteEmpty": "Aucune commande correspondante", "tableAlignmentUnspecified": "Alignement : non spécifié", "tableAlignmentLeft": "Alignement : gauche", "tableAlignmentCenter": "Alignement : centre", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "Choisissez un fichier .css UTF-8 lisible de 256 KiB maximum, sans imports, URL de ressources, HTML ni CSS exécutable ou échappé.", "exportInvalidRange": "{field} : saisissez un nombre entre {minimum} et {maximum}.", "clipboardHistory": "Historique de la presse-papiers", - "clipboardHistorySearchHint": "Rechercher dans l’historique du presse-papiers", "clipboardHistoryDescription": "Ouvrir l'historique de la presse-papiers local à la session.", "clipboardCurrent": "Presse-papiers actuel", "clipboardCurrentExternal": "Presse-papiers actuel provenant d'une autre application", "clipboardDestination": "Coller dans {name}", "clipboardSessionOnly": "Stocké uniquement pour cette session BusyMark.", "clipboardNoItems": "L'historique de la presse-papiers est vide.", - "clipboardNoMatches": "Aucun élément de presse-papiers ne correspond à cette recherche.", "clipboardDisabled": "La collection d'Historique de la presse-papiers est désactivée dans les paramètres.", "clipboardUnavailable": "Le contenu actuel du presse-papiers n'est pas disponible ou non pris en charge.", "clipboardTooLarge": "Cet élément du presse-papiers est trop grand pour être conservé.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "Rechercher les documents et le contenu des révisions dans l'Historique Local.", "localHistoryNoDocuments": "Aucun document n'a Historique Local.", "localHistoryNoRevisions": "Ce document n'a pas de révisions dans l'Historique Local.", - "localHistoryNoMatches": "Aucune révision n'est trouvée dans cette recherche.", - "localHistorySearchHint": "Rechercher dans l’historique local", "localHistorySelectedRevision": "Révision sélectionnée", "localHistoryCurrentEditor": "Contenu de l'éditeur actuel", "localHistoryCurrentDisk": "Contenu actuel sur le disque", @@ -3610,5 +3619,125 @@ "wsInsertElements": "Insérer des éléments", "wsAdmonitionsAndQuotes": "Avertissements et citations", "wsSemanticText": "Texte sémantique", - "wsPropertiesUnavailable": "L’élément inspecté est indisponible. Inspectez un élément pour continuer." + "wsPropertiesUnavailable": "L’élément inspecté est indisponible. Inspectez un élément pour continuer.", + "searchDocumentsCategory": "Documents", + "searchTopicsCategory": "Rubriques", + "searchCurrentDocument": "Document actuel", + "searchCurrentWorkspace": "Espace de travail actuel", + "browseNotes": "Parcourir les notes", + "searchActionsCommand": "Rechercher des actions", + "searchDocumentsCommand": "Rechercher des documents", + "searchTopicsCommand": "Rechercher des rubriques", + "searchRecentCommand": "Rechercher les éléments récents", + "searchResourceUnavailable": "Cette ressource est indisponible.", + "notesSync": "Synchroniser les notes", + "searchInWorkspace": "Rechercher dans l’espace de travail", + "searchCategory": "Catégorie de recherche", + "clearHistorySearchScope": "Rechercher dans l’historique des documents", + "searchInDocument": "Rechercher dans le document", + "searchDocumentHint": "Rechercher dans ce document…", + "searchWorkspaceHint": "Rechercher dans cet espace de travail…", + "searchOutlineHint": "Rechercher des titres…", + "searchHistoryHint": "Rechercher dans l’historique local…", + "searchEnterText": "Saisissez du texte à rechercher", + "searchAccountUnavailable": "Compte indisponible", + "searchDocumentsHint": "Rechercher des documents…", + "searchTopicsHint": "Rechercher des rubriques…", + "searchRecentHint": "Rechercher les éléments récents…", + "searchActionsHint": "Rechercher des actions…", + "searchRequiresNotesWorkspace": "Nécessite un espace de travail Notes", + "searchSidebarLabel": "Rechercher:", + "searchDocumentTextCategory": "Texte du document", + "searchWorkspaceTextCategory": "Texte de l’espace de travail", + "searchHeadingsCategory": "Titres", + "shortcutUnassigned": "Non attribué", + "shortcutPressKeys": "Appuyez sur une combinaison de touches…", + "shortcutRecord": "Enregistrer un raccourci", + "shortcutRestoreAll": "Rétablir tous les raccourcis", + "shortcutRestoreAllQuestion": "Rétablir les raccourcis par défaut de toutes les actions ? Les affectations personnalisées seront supprimées.", + "shortcutSaveFailed": "Les raccourcis sont actifs mais n’ont pas été sauvegardés. Réessayez pour les conserver après redémarrage.", + "shortcutRetry": "Réessayer la sauvegarde", + "shortcutChanged": "Les affectations ont changé. Vérifiez les raccourcis actuels et réessayez.", + "shortcutInvalid": "Cette combinaison ne peut pas être attribuée dans ce contexte.", + "shortcutSystemWarning": "Cette combinaison peut être utilisée par votre bureau. BusyMark ne peut pas remplacer les raccourcis système.", + "shortcutConflicts": "Ce raccourci entre en conflit avec ces actions :", + "shortcutRemove": "Supprimer l’affectation", + "shortcutRestoreDefault": "Rétablir le raccourci par défaut", + "shortcutAssign": "Attribuer", + "shortcutReassign": "Réattribuer", + "shortcutContextApplication": "Application entière", + "shortcutContextDocument": "Vues du document", + "shortcutContextEditor": "Éditeur et source", + "shortcutContextWritersideSource": "Source Writerside", + "shortcutContextWorkspace": "Navigation de l’espace de travail", + "shortcutContextTree": "Arborescences latérales", + "searchHeadingsCommand": "Rechercher des titres", + "shortcutFixed": "Géré par l’éditeur ; non configurable.", + "shortcutDefaultConflict": "Le raccourci par défaut entre en conflit avec une affectation personnalisée.", + "shortcutCurrentBinding": "Raccourci actuel", + "shortcutProposedBinding": "Raccourci proposé", + "notesActions": "Actions des notes", + "noteActions": "Actions de la note", + "noteProperties": "Propriétés de la note", + "notesOfflinePause": "Suspendre les téléchargements", + "notesOfflineResume": "Reprendre les téléchargements", + "notesEmptyAll": "Aucune note. Créez-en une pour commencer.", + "notesEmptyFavorites": "Aucune note favorite.", + "notesEmptyCategory": "Aucune note dans cette catégorie.", + "notesEmptyUncategorized": "Aucune note sans catégorie.", + "notesEmptyRecovery": "Aucune note conservée à récupérer.", + "notesStartingConnection": "Démarrage de la connexion Nextcloud…", + "notesVerifyingConnection": "Vérification de l’accès aux notes…", + "notesCancellingConnection": "Annulation de la connexion et nettoyage…", + "notesRemovingConnection": "Suppression du compte local…", + "notesSettingsLeave": "Enregistrer les modifications des paramètres Nextcloud avant de quitter ?", + "notesSettingsReload": "Recharger les paramètres du serveur…", + "notesScanning": "Analyse des fichiers…", + "notesCancelling": "Annulation… En attente de la fin de l’opération en cours.", + "notesCancelled": "Annulé", + "notesPartial": "Partiellement terminé", + "notesNotStarted": "Non démarré", + "notesCreated": "Créé", + "notesCurrentText": "Texte actuellement conservé", + "notesRecovering": "Récupération dans une nouvelle note…", + "notesSort": "Trier les notes", + "notesOfflineRetentionHelp": "Retirer cette exigence conserve le contenu en cache. Une exigence de catégorie ou de compte peut encore conserver cette note.", + "notesOfflineBroader": "Cette note est aussi conservée par une exigence de catégorie ou de compte.", + "notesOfflineExternal": "Les médias externes nécessitent une connexion. BusyMark conserve uniquement le texte et les pièces jointes gérées.", + "notesOperationFailed": "L’opération a échoué. Votre brouillon est conservé. Vérifiez l’enregistrement et les conflits avant de réessayer.", + "notesRemovedWarning": "Le compte Nextcloud local a été supprimé. La révocation du mot de passe d’application n’a pas été confirmée. Supprimez le mot de passe de BusyMark dans les paramètres de sécurité Nextcloud.", + "notesBuffersNotSaved": "Toutes les notes ouvertes n’ont pas pu être enregistrées localement. Vérifiez l’enregistrement, les conflits ou la normalisation dans l’espace de travail. Des changements locaux peuvent encore nécessiter une synchronisation Nextcloud.", + "notesSettingsReloadHelp": "Le rechargement remplace votre brouillon par les paramètres actuels de Nextcloud. Abandonner le brouillon et recharger ?", + "notesSettingsRefreshFailed": "Nextcloud a accepté les paramètres, mais l’actualisation des notes a échoué. Les valeurs affichées sont confirmées par le serveur. Réessayez l’actualisation depuis Notes.", + "notesStorageFull": "Le stockage Nextcloud est plein. Libérez de l’espace sur le serveur et réessayez.", + "notesRejected": "Nextcloud a rejeté cette modification. Vérifiez la note et enregistrez une copie locale avant de réessayer.", + "notesThrottled": "Nextcloud a demandé d’attendre. La synchronisation reprendra à l’expiration du délai.", + "notesImportPartialHelp": "Des éléments précédents peuvent déjà être enregistrés localement. Reprenez cette importation pour continuer sans les dupliquer.", + "notesExportAll": "Exporter des notes · Toutes les notes…", + "notesExportCategory": "Exporter des notes · Catégorie…", + "notesOfflineAll": "Rendre disponible hors ligne · Toutes les notes", + "notesOfflineCategory": "Rendre disponible hors ligne · Catégorie", + "notesOfflineRemoveAll": "Retirer la conservation hors ligne · Toutes les notes", + "notesOfflineRemoveCategory": "Retirer la conservation hors ligne · Catégorie", + "notesOfflineKeep": "Rendre disponible hors ligne", + "notesLoadFailed": "Nextcloud Notes · Échec. Réessayer", + "notesInspectionFailed": "Pièces jointes · Échec. Réessayer", + "notesSettingsLeaveHelp": "Paramètres Notes du serveur · Modifications non enregistrées", + "notesUnavailable": "Hors ligne · Texte disponible localement · Enregistré localement · synchronisation en attente", + "notesRecoveryRequired": "Conflits · Comparer", + "notesTransferFailed": "Échec. Réessayer", + "notesRemoteDeleteUnavailable": "URL du serveur · Non prise en charge · Supprimer · Pièces jointes", + "notesReadOnlyHelp": "Lecture seule · Favoris · Enregistrer une copie locale…", + "notesPathRelative": "Le chemin du dossier des notes est relatif à votre stockage Nextcloud.", + "notesSettingsStale": "Les paramètres ont changé sur Nextcloud. Rechargez et vérifiez les valeurs du serveur avant de remplacer le brouillon.", + "notesDownloadsPaused": "Téléchargements suspendus", + "notesExportNotPublished": "Aucune exportation n’a été publiée.", + "notesKeyringUnavailable": "Déverrouillez le trousseau du bureau et réessayez.", + "notesConnectFailed": "Connexion à Nextcloud Notes impossible. Vérifiez l’adresse du serveur et réessayez.", + "notesAccountMismatch": "Connectez-vous au même compte Nextcloud. Supprimez d’abord ce compte pour en connecter un autre.", + "notesLocalRemovalFailed": "Le nettoyage local a échoué. Le compte est conservé pour permettre une nouvelle tentative de suppression.", + "notesTransferring": "Transfert des notes…", + "notesTechnicalDetails": "Détails techniques", + "notesPauseRequirement": "Suspendre l’exigence hors ligne", + "notesResumeRequirement": "Reprendre l’exigence hors ligne" } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 4c9b5781..1c9e4e2d 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "सहेजे गए पाठ से नया नोट", + "notesAccountThrottled": "Nextcloud ने विराम माँगा है। फिर साइन इन शुरू करने से पहले प्रतीक्षा करें।", + "notesSettingsRefreshing": "पुष्टि किए गए सेटिंग बदलाव के बाद Notes ताज़ा हो रहा है", + "notesExportOperationFailed": "प्रकाशन से पहले निर्यात विफल हुआ। स्रोत और गंतव्य जाँचकर फिर निर्यात करें।", + "notesRecoveryCreatedNotOpened": "पुनर्प्राप्त नोट सहेजा गया, पर खुल नहीं सका। संपादक या गुणों के लंबित बदलाव सुलझाएँ, फिर इसी मौजूदा नोट को खोलें।", + "notesSettingsPendingNotes": "लंबित नोट, विरोध या अनिश्चित निर्माण इस बदलाव को रोक रहे हैं। Notes में समन्वय या समीक्षा करें, फिर सुरक्षित रखा गया सेटिंग मसौदा दोबारा सहेजें।", + "notesSettingsPendingAttachments": "लंबित संलग्नक कार्य इस बदलाव को रोक रहे हैं। Notes में संलग्नकों की समीक्षा करें, फिर सुरक्षित रखा गया सेटिंग मसौदा दोबारा सहेजें।", + "notesAccountServerAddress": "Nextcloud का मानक HTTPS पता उपयोग करें और फिर साइन इन करें। पते या रीडायरेक्ट की पुष्टि नहीं हो सकी।", + "notesAccountUnsupported": "Notes उपलब्ध नहीं है या API समर्थित नहीं है। मुख्य API संस्करण 1 और गौण संस्करण 4 या नए वाला Nextcloud Notes सक्षम करें।", + "notesAccountAuthorizeAgain": "Nextcloud ने पहचान जानकारी अस्वीकार की। खाते को फिर अधिकृत करें या दोबारा कनेक्ट करें।", + "notesAccountCheckForbidden": "Nextcloud ने Notes क्षमता जाँच की अनुमति नहीं दी। व्यवस्थापक से अनुमति लेकर फिर प्रयास करें।", + "notesAccountMalformed": "Nextcloud के उत्तर की पुष्टि नहीं हो सकी। सर्वर स्थापना जाँचें और फिर साइन इन करें।", + "notesAccountTimedOut": "साइन इन का समय समाप्त हुआ। फिर शुरू करें और ब्राउज़र में अनुमति पूरी करें।", + "notesAccountBrowserUnavailable": "सिस्टम ब्राउज़र नहीं खुल सका। डिफ़ॉल्ट ब्राउज़र जाँचें और फिर साइन इन करें।", + "notesAccountNetwork": "Nextcloud से संपर्क नहीं हो सका। कनेक्शन और सर्वर जाँचें, फिर साइन इन करें।", + "notesAccountRejected": "Nextcloud ने क्षमता अनुरोध अस्वीकार किया। व्यवस्थापक के साथ पहुँच जाँचकर फिर प्रयास करें।", + "notesAccountFailed": "खाता सेटअप पूरा नहीं हुआ। फिर साइन इन करें।", + "notesTransferSourceChanged": "समीक्षा के बाद स्रोत बदल गया। फिर प्रयास से पहले मौजूदा फ़ाइल की समीक्षा करें या नया स्नैपशॉट लें।", + "notesTransferUnreadable": "स्रोत पढ़ा नहीं जा सका। फ़ाइल या फ़ोल्डर की पहुँच जाँचकर फिर समीक्षा करें।", + "notesTransferInvalidSnapshot": "स्नैपशॉट अमान्य या असमर्थित है। मान्य BusyMark Notes स्नैपशॉट या Markdown स्रोत चुनें।", + "notesTransferMediaLimit": "मीडिया आयात आकार सीमा से बड़ा है। प्रभावित आइटम का मीडिया छोटा करके फिर समीक्षा करें।", + "notesTransferUnsafeSource": "स्रोत में असुरक्षित पथ है। स्रोत फ़ोल्डर के अंदर की फ़ाइलें चुनकर फिर समीक्षा करें।", + "notesTransferDestination": "निर्यात लिखा नहीं जा सका। गंतव्य की पहुँच और खाली स्थान जाँचकर फिर निर्यात करें।", + "notesTransferOperationFailed": "स्थानांतरण पूरा नहीं हुआ। प्रभावित आइटम की समीक्षा करके फिर प्रयास करें। पहले आयात किए आइटम सहेजे हुए हैं।", + "notesExportPublishedWithOmissions": "कुछ चीज़ें छूटी हैं, निर्यात उपलब्ध है", + "notesBatchUnchanged": "माँगे गए मान पहले से तय हैं।", + "notesBatchReadOnly": "यह नोट केवल पढ़ने योग्य है। सुरक्षित गुण नहीं बदल सकते; पसंदीदा स्थिति बदलना अनुमत है।", + "notesBatchReviewChanged": "समीक्षा के बाद नोट बदल गया। कार्य दोबारा लागू करने से पहले उसकी मौजूदा स्थिति की समीक्षा करें।", + "notesBatchMissing": "यह नोट अब उपलब्ध नहीं है। सूची ताज़ा करके लक्ष्यों की समीक्षा करें।", + "notesBatchStorage": "स्थानीय सहेजना विफल हुआ। संग्रहण जाँचकर इस आइटम पर फिर प्रयास करें; सफल आइटम सहेजे हुए हैं।", + "notesRemoteReviewChanged": "सर्वर का नोट बदल गया। आपका विलय मसौदा सुरक्षित है। नई तुलना की समीक्षा करें और समाधान फिर चुनें।", + "notesReviewRemote": "मैंने सर्वर के मौजूदा संस्करण की समीक्षा की है", "wsModuleCapsNotPublished": "मॉड्यूल का अक्षर-शैली नियम प्रकाशित नहीं होता।", "wsTitleOverridesAction": "इंस्टेंस और विषय-सूची के शीर्षक…", "wsHeadingNotPublished": "यह बिल्डर इसे प्रकाशित नहीं करता।", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "तारीख", "pdfTitlePageTitleRequired": "शीर्षक पृष्ठ के लिए शीर्षक दर्ज करें।", "pdfTocSectionNote": "अलग पृष्ठ से शुरू होता है और कई पृष्ठों तक जा सकता है।", - "sourceSearchZeroLengthUnsupported": "शून्य लंबाई वाले रेगेक्स मिलान समर्थित नहीं हैं (जैसे ^, $ या केवल स्थिति से मेल खाने वाले लुकअहेड)।", "workspaceSearchIncomplete": "केवल कुछ परिणाम दिखाए गए हैं। अधिक परिणाम देखें या खोज को सीमित करें।", "workspaceSearchShowMore": "अधिक परिणाम दिखाएँ", "workspaceSearchSkippedFiles": "खोज अधूरी है। ये फ़ाइलें 1 MiB से बड़ी हैं या पढ़ी नहीं जा सकीं:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "यह कीबोर्ड शॉर्टकट संदर्भ दिखाएँ", + "shortcutKeyboardShortcutsDescription": "अनुप्रयोग के कीबोर्ड शॉर्टकट अनुकूलित करें", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "हालिया कार्यस्थान साफ़ करें", + "clearRecentWorkspaces": "हाल का इतिहास साफ़ करें", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "बड़े-छोटे अक्षरों के प्रति संवेदनशील", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "पूरा शब्द", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "रेगुलर एक्सप्रेशन", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "अमान्य रेगुलर एक्सप्रेशन", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "बड़ी फ़ाइल: हाइलाइटिंग और फ़ोल्डिंग अस्थायी रूप से रुकी हुई हैं", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "हाल के आइटम अपडेट नहीं किए जा सके: {error}.", "workspaceErrorOpenFailed": "खोलना विफल रहा: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "लिंक टेक्स्ट खाली है; उसके उद्देश्य का वर्णन करने वाला सुलभ नाम दें।", "diagnosticMarkdownLinkReviewText": "जाँचें कि लिंक टेक्स्ट “{text}” संदर्भ में उसके उद्देश्य का वर्णन करता है या नहीं।", "diagnosticMarkdownTableEmptyHeader": "तालिका शीर्षकों को अपने कॉलम पहचानने चाहिए; हर खाली शीर्षक पूरा करें।", - "commandPalette": "कमांड पैलेट", - "commandPaletteHint": "कमांड लिखें", - "commandPaletteEmpty": "कोई मेल खाता कमांड नहीं", "tableAlignmentUnspecified": "संरेखण: अनिर्दिष्ट", "tableAlignmentLeft": "संरेखण: बायाँ", "tableAlignmentCenter": "संरेखण: मध्य", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "256 KiB तक की पढ़ने योग्य UTF-8 .css फ़ाइल चुनें, जिसमें इंपोर्ट, संसाधन URL, HTML और निष्पादन योग्य या एस्केप वाला CSS न हो।", "exportInvalidRange": "{field}: {minimum} से {maximum} के बीच संख्या दर्ज करें।", "clipboardHistory": "क्लिपबोर्ड इतिहास", - "clipboardHistorySearchHint": "क्लिपबोर्ड इतिहास खोजें", "clipboardHistoryDescription": "सत्र-विशिष्ट क्लिपबोर्ड इतिहास खोलें।", "clipboardCurrent": "वर्तमान क्लिपबोर्ड", "clipboardCurrentExternal": "किसी अन्य एप्लिकेशन से वर्तमान क्लिपबोर्ड", "clipboardDestination": "{name} में पेस्ट करें", "clipboardSessionOnly": "केवल इस BusyMark सत्र के लिए संग्रहीत।", "clipboardNoItems": "क्लिपबोर्ड इतिहास खाली है।", - "clipboardNoMatches": "कोई क्लिपबोर्ड आइटम इस खोज से मेल नहीं खाता।", "clipboardDisabled": "क्लिपबोर्ड इतिहास संग्रह सेटिंग्स में अक्षम है।", "clipboardUnavailable": "वर्तमान क्लिपबोर्ड सामग्री अनुपलब्ध या समर्थित नहीं है।", "clipboardTooLarge": "यह क्लिपबोर्ड आइटम बहुत बड़ा है और इसे बनाए रखना संभव नहीं है।", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "स्थानीय इतिहास में दस्तावेज़ और संशोधन सामग्री खोजें।", "localHistoryNoDocuments": "अभी तक किसी भी दस्तावेज़ में स्थानीय इतिहास नहीं है।", "localHistoryNoRevisions": "इस दस्तावेज़ में स्थानीय इतिहास में कोई संशोधन नहीं है।", - "localHistoryNoMatches": "यह खोज स्थानीय इतिहास संशोधनों से मेल नहीं खाती।", - "localHistorySearchHint": "स्थानीय इतिहास खोजें", "localHistorySelectedRevision": "चयनित संशोधन", "localHistoryCurrentEditor": "वर्तमान संपादक सामग्री", "localHistoryCurrentDisk": "डिस्क पर वर्तमान सामग्री", @@ -3610,5 +3619,125 @@ "wsInsertElements": "तत्व जोड़ें", "wsAdmonitionsAndQuotes": "सूचनाएँ और उद्धरण", "wsSemanticText": "अर्थपूर्ण पाठ", - "wsPropertiesUnavailable": "जाँचा गया तत्व उपलब्ध नहीं है। जारी रखने के लिए कोई तत्व जाँचें।" + "wsPropertiesUnavailable": "जाँचा गया तत्व उपलब्ध नहीं है। जारी रखने के लिए कोई तत्व जाँचें।", + "searchDocumentsCategory": "दस्तावेज़", + "searchTopicsCategory": "विषय", + "searchCurrentDocument": "वर्तमान दस्तावेज़", + "searchCurrentWorkspace": "वर्तमान कार्यक्षेत्र", + "browseNotes": "नोट ब्राउज़ करें", + "searchActionsCommand": "क्रियाएँ खोजें", + "searchDocumentsCommand": "दस्तावेज़ खोजें", + "searchTopicsCommand": "विषय खोजें", + "searchRecentCommand": "हाल के आइटम खोजें", + "searchResourceUnavailable": "यह संसाधन उपलब्ध नहीं है।", + "notesSync": "नोट सिंक करें", + "searchInWorkspace": "कार्यस्थान में खोजें", + "searchCategory": "खोज श्रेणी", + "clearHistorySearchScope": "दस्तावेज़ों के इतिहास में खोजें", + "searchInDocument": "दस्तावेज़ में खोजें", + "searchDocumentHint": "इस दस्तावेज़ में खोजें…", + "searchWorkspaceHint": "इस कार्यक्षेत्र में खोजें…", + "searchOutlineHint": "शीर्षक खोजें…", + "searchHistoryHint": "स्थानीय इतिहास में खोजें…", + "searchEnterText": "खोजने के लिए पाठ दर्ज करें", + "searchAccountUnavailable": "खाता उपलब्ध नहीं है", + "searchDocumentsHint": "दस्तावेज़ खोजें…", + "searchTopicsHint": "विषय खोजें…", + "searchRecentHint": "हाल के आइटम खोजें…", + "searchActionsHint": "क्रियाएँ खोजें…", + "searchRequiresNotesWorkspace": "नोट्स कार्यक्षेत्र आवश्यक है", + "searchSidebarLabel": "खोजें:", + "searchDocumentTextCategory": "दस्तावेज़ पाठ", + "searchWorkspaceTextCategory": "कार्यस्थान पाठ", + "searchHeadingsCategory": "शीर्षक", + "shortcutUnassigned": "असाइन नहीं किया गया", + "shortcutPressKeys": "कुंजी संयोजन दबाएँ…", + "shortcutRecord": "शॉर्टकट रिकॉर्ड करें", + "shortcutRestoreAll": "सभी डिफ़ॉल्ट पुनर्स्थापित करें", + "shortcutRestoreAllQuestion": "सभी क्रियाओं के डिफ़ॉल्ट शॉर्टकट पुनर्स्थापित करें? कस्टम असाइनमेंट हटा दिए जाएँगे।", + "shortcutSaveFailed": "शॉर्टकट सक्रिय हैं लेकिन सहेजे नहीं जा सके। पुनः प्रारंभ के बाद रखने के लिए दोबारा सहेजें।", + "shortcutRetry": "सहेजने का पुनः प्रयास करें", + "shortcutChanged": "असाइनमेंट बदल गए हैं। वर्तमान शॉर्टकट जाँचें और पुनः प्रयास करें।", + "shortcutInvalid": "इस संदर्भ में यह संयोजन असाइन नहीं किया जा सकता।", + "shortcutSystemWarning": "आपका डेस्कटॉप इस संयोजन का उपयोग कर सकता है। BusyMark सिस्टम शॉर्टकट नहीं बदल सकता।", + "shortcutConflicts": "यह शॉर्टकट इन क्रियाओं से टकराता है:", + "shortcutRemove": "असाइनमेंट हटाएँ", + "shortcutRestoreDefault": "डिफ़ॉल्ट पुनर्स्थापित करें", + "shortcutAssign": "असाइन करें", + "shortcutReassign": "फिर से असाइन करें", + "shortcutContextApplication": "अनुप्रयोग", + "shortcutContextDocument": "दस्तावेज़ दृश्य", + "shortcutContextEditor": "संपादक और स्रोत", + "shortcutContextWritersideSource": "Writerside स्रोत", + "shortcutContextWorkspace": "कार्यस्थान नेविगेशन", + "shortcutContextTree": "साइडबार ट्री", + "searchHeadingsCommand": "शीर्षक खोजें", + "shortcutFixed": "संपादक द्वारा प्रबंधित; कॉन्फ़िगर नहीं किया जा सकता।", + "shortcutDefaultConflict": "डिफ़ॉल्ट शॉर्टकट का कस्टम असाइनमेंट से टकराव है।", + "shortcutCurrentBinding": "वर्तमान शॉर्टकट", + "shortcutProposedBinding": "प्रस्तावित शॉर्टकट", + "notesActions": "नोटों की कार्रवाइयाँ", + "noteActions": "नोट की कार्रवाइयाँ", + "noteProperties": "नोट के गुण", + "notesOfflinePause": "डाउनलोड रोकें", + "notesOfflineResume": "डाउनलोड जारी रखें", + "notesEmptyAll": "अभी कोई नोट नहीं है। शुरू करने के लिए नोट बनाएँ।", + "notesEmptyFavorites": "कोई पसंदीदा नोट नहीं है।", + "notesEmptyCategory": "इस श्रेणी में कोई नोट नहीं है।", + "notesEmptyUncategorized": "कोई बिना श्रेणी वाला नोट नहीं है।", + "notesEmptyRecovery": "पुनर्प्राप्ति के लिए कोई सहेजा हुआ नोट नहीं है।", + "notesStartingConnection": "Nextcloud कनेक्शन शुरू हो रहा है…", + "notesVerifyingConnection": "नोटों तक पहुँच जाँची जा रही है…", + "notesCancellingConnection": "साइन-इन रद्द करके सफ़ाई की जा रही है…", + "notesRemovingConnection": "स्थानीय खाता हटाया जा रहा है…", + "notesSettingsLeave": "जाने से पहले Nextcloud सेटिंग के बदलाव सहेजें?", + "notesSettingsReload": "सर्वर सेटिंग फिर लोड करें…", + "notesScanning": "फ़ाइलें जाँची जा रही हैं…", + "notesCancelling": "रद्द हो रहा है… वर्तमान कार्रवाई समाप्त होने की प्रतीक्षा है।", + "notesCancelled": "रद्द किया गया", + "notesPartial": "आंशिक रूप से पूरा", + "notesNotStarted": "शुरू नहीं हुआ", + "notesCreated": "बनाया गया", + "notesCurrentText": "वर्तमान सुरक्षित पाठ", + "notesRecovering": "नए नोट के रूप में पुनर्प्राप्त हो रहा है…", + "notesSort": "नोट क्रमबद्ध करें", + "notesOfflineRetentionHelp": "यह आवश्यकता हटाने से कैश की मौजूदा सामग्री नहीं हटती। श्रेणी या खाते की आवश्यकता अब भी यह नोट सुरक्षित रख सकती है।", + "notesOfflineBroader": "यह नोट श्रेणी या खाते की आवश्यकता के कारण भी सुरक्षित है।", + "notesOfflineExternal": "बाहरी मीडिया को कनेक्शन चाहिए। BusyMark केवल नोट का पाठ और प्रबंधित संलग्नक सुरक्षित रखता है।", + "notesOperationFailed": "कार्रवाई पूरी नहीं हुई। आपका मसौदा सुरक्षित है। सहेजने और विरोधों की जाँच करके फिर प्रयास करें।", + "notesRemovedWarning": "स्थानीय Nextcloud खाता हटा दिया गया। ऐप पासवर्ड रद्द होने की पुष्टि नहीं हुई। Nextcloud की सुरक्षा सेटिंग में BusyMark का ऐप पासवर्ड हटाएँ।", + "notesBuffersNotSaved": "सभी खुले नोट स्थानीय रूप से नहीं सहेजे जा सके। कार्यक्षेत्र में सहेजने, विरोधों या सामान्यीकरण की समीक्षा करें। स्थानीय बदलावों को अब भी Nextcloud से समन्वय की ज़रूरत हो सकती है।", + "notesSettingsReloadHelp": "फिर लोड करने से मसौदे की जगह वर्तमान Nextcloud सेटिंग आएगी। मसौदा छोड़कर फिर लोड करें?", + "notesSettingsRefreshFailed": "Nextcloud ने सेटिंग स्वीकार की, पर नोट रीफ़्रेश नहीं हुए। दिखाई गई सेटिंग सर्वर द्वारा स्वीकृत हैं। नोटों से रीफ़्रेश फिर आज़माएँ।", + "notesStorageFull": "Nextcloud का संग्रहण भर गया है। सर्वर पर जगह खाली करके फिर प्रयास करें।", + "notesRejected": "Nextcloud ने यह बदलाव अस्वीकार कर दिया। फिर प्रयास करने से पहले नोट जाँचें और स्थानीय प्रति सहेजें।", + "notesThrottled": "Nextcloud ने प्रतीक्षा को कहा है। फिर प्रयास की समय-सीमा के बाद समन्वय जारी होगा।", + "notesImportPartialHelp": "पिछले आइटम पहले ही स्थानीय रूप से सहेजे हो सकते हैं। प्रतियाँ बनाए बिना जारी रखने के लिए यही आयात फिर शुरू करें।", + "notesExportAll": "नोट्स निर्यात करें · सभी नोट्स…", + "notesExportCategory": "नोट्स निर्यात करें · श्रेणी…", + "notesOfflineAll": "ऑफ़लाइन उपलब्ध करें · सभी नोट्स", + "notesOfflineCategory": "ऑफ़लाइन उपलब्ध करें · श्रेणी", + "notesOfflineRemoveAll": "ऑफ़लाइन संरक्षण हटाएँ · सभी नोट्स", + "notesOfflineRemoveCategory": "ऑफ़लाइन संरक्षण हटाएँ · श्रेणी", + "notesOfflineKeep": "ऑफ़लाइन उपलब्ध करें", + "notesLoadFailed": "Nextcloud Notes · विफल. फिर प्रयास करें", + "notesInspectionFailed": "अटैचमेंट · विफल. फिर प्रयास करें", + "notesSettingsLeaveHelp": "सर्वर Notes सेटिंग्स · न सहेजे गए बदलाव", + "notesUnavailable": "ऑफ़लाइन · पाठ स्थानीय रूप से उपलब्ध है · स्थानीय रूप से सहेजा · सिंक लंबित", + "notesRecoveryRequired": "टकराव · तुलना करें", + "notesTransferFailed": "विफल. फिर प्रयास करें", + "notesRemoteDeleteUnavailable": "सर्वर का पता · असमर्थित · हटाएँ · अटैचमेंट", + "notesReadOnlyHelp": "केवल पढ़ने योग्य · पसंदीदा · स्थानीय प्रति सहेजें…", + "notesPathRelative": "नोट फ़ोल्डर का पथ आपके Nextcloud संग्रहण के सापेक्ष है।", + "notesSettingsStale": "Nextcloud पर सेटिंग बदल गई है। मसौदा बदलने से पहले सर्वर के मान फिर लोड करके जाँचें।", + "notesDownloadsPaused": "डाउनलोड रुके हुए हैं", + "notesExportNotPublished": "कोई निर्यात प्रकाशित नहीं हुआ।", + "notesKeyringUnavailable": "डेस्कटॉप कीरिंग अनलॉक करके फिर प्रयास करें।", + "notesConnectFailed": "Nextcloud Notes से कनेक्ट नहीं हो सका। सर्वर का पता जाँचें और फिर प्रयास करें।", + "notesAccountMismatch": "उसी Nextcloud खाते में साइन-इन करें। दूसरे खाते से कनेक्ट करने के लिए पहले यह खाता हटाएँ।", + "notesLocalRemovalFailed": "स्थानीय खाते की सफ़ाई विफल रही। हटाने का फिर प्रयास करने के लिए खाता रखा गया है।", + "notesTransferring": "नोट स्थानांतरित हो रहे हैं…", + "notesTechnicalDetails": "तकनीकी विवरण", + "notesPauseRequirement": "ऑफ़लाइन आवश्यकता रोकें", + "notesResumeRequirement": "ऑफ़लाइन आवश्यकता जारी रखें" } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 3441e636..4f901d69 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Catatan baru dari teks tersimpan", + "notesAccountThrottled": "Nextcloud meminta jeda. Tunggu sebelum mulai masuk lagi.", + "notesSettingsRefreshing": "Menyegarkan Notes setelah pembaruan pengaturan dikonfirmasi", + "notesExportOperationFailed": "Ekspor gagal sebelum diterbitkan. Periksa sumber dan tujuan, lalu ekspor lagi.", + "notesRecoveryCreatedNotOpened": "Catatan yang dipulihkan sudah tersimpan, tetapi gagal dibuka. Selesaikan perubahan editor atau properti yang tertunda, lalu buka catatan yang sudah ada ini.", + "notesSettingsPendingNotes": "Catatan tertunda, konflik, atau pembuatan yang belum pasti menghalangi perubahan ini. Sinkronkan atau tinjau di Notes, lalu simpan lagi draf pengaturan yang dipertahankan.", + "notesSettingsPendingAttachments": "Operasi lampiran tertunda menghalangi perubahan ini. Tinjau lampiran di Notes, lalu simpan lagi draf pengaturan yang dipertahankan.", + "notesAccountServerAddress": "Gunakan alamat HTTPS kanonis Nextcloud, lalu masuk lagi. Alamat atau pengalihan tidak dapat diverifikasi.", + "notesAccountUnsupported": "Notes tidak tersedia atau API tidak didukung. Aktifkan Nextcloud Notes dengan API versi utama 1, minor 4 atau lebih baru.", + "notesAccountAuthorizeAgain": "Nextcloud menolak kredensial. Otorisasi ulang atau hubungkan kembali akun ini.", + "notesAccountCheckForbidden": "Nextcloud melarang pemeriksaan kemampuan Notes. Minta administrator mengizinkannya, lalu coba lagi.", + "notesAccountMalformed": "Respons Nextcloud tidak dapat diverifikasi. Periksa pemasangan server, lalu masuk lagi.", + "notesAccountTimedOut": "Waktu masuk habis. Mulai lagi dan selesaikan otorisasi di peramban.", + "notesAccountBrowserUnavailable": "Peramban sistem tidak dapat dibuka. Periksa peramban bawaan, lalu masuk lagi.", + "notesAccountNetwork": "Nextcloud tidak dapat dijangkau. Periksa koneksi dan server, lalu masuk lagi.", + "notesAccountRejected": "Nextcloud menolak permintaan kemampuan. Periksa akses bersama administrator, lalu coba lagi.", + "notesAccountFailed": "Penyiapan akun tidak selesai. Coba masuk lagi.", + "notesTransferSourceChanged": "Sumber berubah setelah ditinjau. Tinjau berkas saat ini atau buat snapshot baru sebelum mencoba lagi.", + "notesTransferUnreadable": "Sumber tidak dapat dibaca. Periksa akses berkas atau folder, lalu tinjau lagi.", + "notesTransferInvalidSnapshot": "Snapshot tidak valid atau tidak didukung. Pilih snapshot BusyMark Notes yang valid atau sumber Markdown.", + "notesTransferMediaLimit": "Media melebihi batas impor. Perkecil media item terkait sebelum meninjaunya lagi.", + "notesTransferUnsafeSource": "Sumber berisi jalur tidak aman. Pilih berkas di dalam folder sumber, lalu tinjau lagi.", + "notesTransferDestination": "Ekspor tidak dapat ditulis. Periksa akses dan ruang kosong di tujuan, lalu ekspor lagi.", + "notesTransferOperationFailed": "Transfer tidak selesai. Tinjau item terkait dan coba lagi. Item yang sudah diimpor tetap tersimpan.", + "notesExportPublishedWithOmissions": "Ekspor tersedia dengan beberapa bagian terlewat", + "notesBatchUnchanged": "Nilai yang diminta sudah ditetapkan.", + "notesBatchReadOnly": "Catatan ini hanya-baca. Properti terlindungi tidak dapat diubah; Favorit tetap diizinkan.", + "notesBatchReviewChanged": "Catatan berubah setelah ditinjau. Tinjau keadaan saat ini sebelum menerapkan tindakan lagi.", + "notesBatchMissing": "Catatan ini tidak lagi tersedia. Segarkan daftar dan tinjau sasaran lagi.", + "notesBatchStorage": "Penyimpanan lokal gagal. Periksa penyimpanan dan coba item ini lagi; item yang berhasil tetap tersimpan.", + "notesRemoteReviewChanged": "Catatan server berubah. Draf penggabungan Anda dipertahankan. Tinjau perbandingan terbaru dan pilih penyelesaian lagi.", + "notesReviewRemote": "Saya telah meninjau versi server saat ini", "wsModuleCapsNotPublished": "Kapitalisasi modul tidak diterbitkan.", "wsTitleOverridesAction": "Judul instans dan daftar isi…", "wsHeadingNotPublished": "Tidak diterbitkan oleh pembuat ini.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Tanggal", "pdfTitlePageTitleRequired": "Masukkan judul untuk halaman judul.", "pdfTocSectionNote": "Dimulai pada halaman terpisah dan dapat mencakup beberapa halaman.", - "sourceSearchZeroLengthUnsupported": "Kecocokan regex dengan panjang nol tidak didukung (misalnya ^, $, atau lookahead yang hanya cocok dengan posisi).", "workspaceSearchIncomplete": "Hanya sebagian hasil yang ditampilkan. Tampilkan lebih banyak atau persempit pencarian.", "workspaceSearchShowMore": "Tampilkan lebih banyak hasil", "workspaceSearchSkippedFiles": "Pencarian belum lengkap. Berkas ini melebihi 1 MiB atau tidak dapat dibaca:", @@ -423,18 +454,6 @@ "@keyboardShortcuts": { "description": "Keyboard shortcuts dialog title and menu item." }, - "commandPalette": "Palet perintah", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "Ketikkan perintah", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "Tidak ada perintah yang cocok", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "Tidak tersedia dalam konteks editor saat ini", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -715,7 +734,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Tampilkan referensi pintasan keyboard ini", + "shortcutKeyboardShortcutsDescription": "Sesuaikan pintasan keyboard aplikasi", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1012,7 +1031,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Hapus ruang kerja terbaru", + "clearRecentWorkspaces": "Hapus riwayat terbaru", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1943,18 +1962,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Peka huruf besar-kecil", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Seluruh kata", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Regex", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "Ganti dengan", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2138,10 +2145,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "Ekspresi reguler tidak valid", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "File besar: penyorotan dan pelipatan dijeda", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2281,6 +2284,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Tidak dapat memperbarui item terbaru: {error}.", "workspaceErrorOpenFailed": "Gagal dibuka: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4864,14 +4868,12 @@ "exportInvalidCss": "Pilih berkas .css UTF-8 yang dapat dibaca hingga 256 KiB, tanpa impor, URL sumber daya, HTML, atau CSS yang dapat dieksekusi atau memakai escape.", "exportInvalidRange": "{field}: masukkan angka antara {minimum} dan {maximum}.", "clipboardHistory": "Sejarah Tempel", - "clipboardHistorySearchHint": "Cari riwayat papan klip", "clipboardHistoryDescription": "Buka riwayat tempel lokal sesi.", "clipboardCurrent": "Tempel saat ini", "clipboardCurrentExternal": "Tempel saat ini dari aplikasi lain", "clipboardDestination": "Tempel ke {name}", "clipboardSessionOnly": "Hanya disimpan untuk sesi BusyMark ini.", "clipboardNoItems": "Riwayat tempel kosong.", - "clipboardNoMatches": "Tidak ada item tempel yang cocok dengan pencarian ini.", "clipboardDisabled": "Koleksi Riwayat Tempel dinonaktifkan di Pengaturan.", "clipboardUnavailable": "Konten tempel saat ini tidak tersedia atau tidak didukung.", "clipboardTooLarge": "Item clipboard ini terlalu besar untuk disimpan.", @@ -4888,8 +4890,6 @@ "findLocalHistoryDescription": "Cari dokumen dan isi revisi dalam Sejarah Lokal.", "localHistoryNoDocuments": "Tidak ada dokumen yang memiliki Sejarah Lokal.", "localHistoryNoRevisions": "Dokumen ini tidak memiliki revisi Sejarah Lokal.", - "localHistoryNoMatches": "Tidak ada revisi Sejarah Lokal yang cocok dengan pencarian ini.", - "localHistorySearchHint": "Cari riwayat lokal", "localHistorySelectedRevision": "Revisi yang dipilih", "localHistoryCurrentEditor": "Konten editor saat ini", "localHistoryCurrentDisk": "Konten saat ini di disk", @@ -5149,5 +5149,125 @@ "wsInsertElements": "Sisipkan elemen", "wsAdmonitionsAndQuotes": "Pemberitahuan dan kutipan", "wsSemanticText": "Teks semantik", - "wsPropertiesUnavailable": "Elemen yang diperiksa tidak tersedia. Periksa elemen untuk melanjutkan." + "wsPropertiesUnavailable": "Elemen yang diperiksa tidak tersedia. Periksa elemen untuk melanjutkan.", + "searchDocumentsCategory": "Dokumen", + "searchTopicsCategory": "Topik", + "searchCurrentDocument": "Dokumen saat ini", + "searchCurrentWorkspace": "Ruang kerja saat ini", + "browseNotes": "Jelajahi catatan", + "searchActionsCommand": "Cari tindakan", + "searchDocumentsCommand": "Cari dokumen", + "searchTopicsCommand": "Cari topik", + "searchRecentCommand": "Cari terbaru", + "searchResourceUnavailable": "Sumber daya ini tidak tersedia.", + "notesSync": "Sinkronkan catatan", + "searchInWorkspace": "Cari dalam Ruang Kerja", + "searchCategory": "Kategori pencarian", + "clearHistorySearchScope": "Cari riwayat seluruh dokumen", + "searchInDocument": "Cari dalam dokumen", + "searchDocumentHint": "Cari dalam dokumen ini…", + "searchWorkspaceHint": "Cari dalam ruang kerja ini…", + "searchOutlineHint": "Cari judul bagian…", + "searchHistoryHint": "Cari dalam riwayat lokal…", + "searchEnterText": "Masukkan teks untuk mencari", + "searchAccountUnavailable": "Akun tidak tersedia", + "searchDocumentsHint": "Cari dokumen…", + "searchTopicsHint": "Cari topik…", + "searchRecentHint": "Cari terbaru…", + "searchActionsHint": "Cari tindakan…", + "searchRequiresNotesWorkspace": "Memerlukan ruang kerja Catatan", + "searchSidebarLabel": "Cari:", + "searchDocumentTextCategory": "Teks dokumen", + "searchWorkspaceTextCategory": "Teks ruang kerja", + "searchHeadingsCategory": "Judul", + "shortcutUnassigned": "Belum ditetapkan", + "shortcutPressKeys": "Tekan kombinasi tombol…", + "shortcutRecord": "Rekam pintasan", + "shortcutRestoreAll": "Pulihkan semua bawaan", + "shortcutRestoreAllQuestion": "Pulihkan pintasan bawaan semua tindakan? Penetapan khusus akan dihapus.", + "shortcutSaveFailed": "Pintasan aktif tetapi tidak dapat disimpan. Coba lagi agar tetap tersedia setelah mulai ulang.", + "shortcutRetry": "Coba simpan lagi", + "shortcutChanged": "Penetapan berubah. Tinjau pintasan saat ini dan coba lagi.", + "shortcutInvalid": "Kombinasi ini tidak dapat ditetapkan dalam konteks ini.", + "shortcutSystemWarning": "Desktop Anda mungkin memakai kombinasi ini. BusyMark tidak dapat mengganti pintasan sistem.", + "shortcutConflicts": "Pintasan ini bertentangan dengan tindakan berikut:", + "shortcutRemove": "Hapus penetapan", + "shortcutRestoreDefault": "Pulihkan bawaan", + "shortcutAssign": "Tetapkan", + "shortcutReassign": "Tetapkan ulang", + "shortcutContextApplication": "Aplikasi", + "shortcutContextDocument": "Tampilan dokumen", + "shortcutContextEditor": "Editor dan sumber", + "shortcutContextWritersideSource": "Sumber Writerside", + "shortcutContextWorkspace": "Navigasi ruang kerja", + "shortcutContextTree": "Pohon bilah samping", + "searchHeadingsCommand": "Cari judul", + "shortcutFixed": "Dikelola oleh editor; tidak dapat dikonfigurasi.", + "shortcutDefaultConflict": "Pintasan bawaan bertentangan dengan penetapan khusus.", + "shortcutCurrentBinding": "Pintasan saat ini", + "shortcutProposedBinding": "Pintasan yang diusulkan", + "notesActions": "Tindakan catatan", + "noteActions": "Tindakan untuk catatan", + "noteProperties": "Properti Catatan", + "notesOfflinePause": "Jeda unduhan", + "notesOfflineResume": "Lanjutkan unduhan", + "notesEmptyAll": "Belum ada catatan. Buat catatan untuk memulai.", + "notesEmptyFavorites": "Tidak ada catatan favorit.", + "notesEmptyCategory": "Tidak ada catatan dalam kategori ini.", + "notesEmptyUncategorized": "Tidak ada catatan tanpa kategori.", + "notesEmptyRecovery": "Tidak ada catatan tersimpan untuk dipulihkan.", + "notesStartingConnection": "Memulai koneksi Nextcloud…", + "notesVerifyingConnection": "Memverifikasi akses Catatan…", + "notesCancellingConnection": "Membatalkan masuk dan membersihkan…", + "notesRemovingConnection": "Menghapus akun lokal…", + "notesSettingsLeave": "Simpan perubahan pengaturan Nextcloud sebelum keluar?", + "notesSettingsReload": "Muat ulang pengaturan server…", + "notesScanning": "Memindai berkas…", + "notesCancelling": "Membatalkan… Menunggu operasi saat ini selesai.", + "notesCancelled": "Dibatalkan", + "notesPartial": "Selesai sebagian", + "notesNotStarted": "Belum dimulai", + "notesCreated": "Dibuat", + "notesCurrentText": "Teks yang saat ini disimpan", + "notesRecovering": "Memulihkan sebagai catatan baru…", + "notesSort": "Urutkan catatan", + "notesOfflineRetentionHelp": "Menghapus persyaratan ini mempertahankan cache yang ada. Persyaratan kategori atau akun mungkin masih menyimpan catatan ini.", + "notesOfflineBroader": "Catatan ini juga disimpan oleh persyaratan kategori atau akun.", + "notesOfflineExternal": "Media eksternal memerlukan koneksi. BusyMark hanya menyimpan teks catatan dan lampiran terkelola.", + "notesOperationFailed": "Operasi tidak dapat diselesaikan. Draf Anda tetap disimpan. Tinjau penyimpanan dan konflik, lalu coba lagi.", + "notesRemovedWarning": "Akun Nextcloud lokal telah dihapus. Pencabutan kata sandi aplikasi tidak dikonfirmasi. Hapus kata sandi BusyMark di pengaturan Keamanan Nextcloud.", + "notesBuffersNotSaved": "Tidak semua catatan terbuka dapat disimpan secara lokal. Tinjau penyimpanan, konflik, atau normalisasi di ruang kerja. Perubahan lokal mungkin masih perlu disinkronkan dengan Nextcloud.", + "notesSettingsReloadHelp": "Memuat ulang mengganti draf dengan pengaturan Nextcloud saat ini. Buang draf dan muat ulang?", + "notesSettingsRefreshFailed": "Nextcloud menerima pengaturan, tetapi penyegaran catatan gagal. Nilai yang ditampilkan telah dikonfirmasi server. Coba segarkan lagi dari Catatan.", + "notesStorageFull": "Penyimpanan Nextcloud penuh. Kosongkan ruang di server, lalu coba lagi.", + "notesRejected": "Nextcloud menolak perubahan ini. Tinjau catatan dan simpan salinan lokal sebelum mencoba lagi.", + "notesThrottled": "Nextcloud meminta BusyMark menunggu. Sinkronisasi berlanjut setelah batas waktu percobaan ulang.", + "notesImportPartialHelp": "Item sebelumnya mungkin telah disimpan secara lokal. Lanjutkan impor ini untuk meneruskan tanpa duplikat.", + "notesExportAll": "Ekspor catatan · Semua catatan…", + "notesExportCategory": "Ekspor catatan · Kategori…", + "notesOfflineAll": "Sediakan secara luring · Semua catatan", + "notesOfflineCategory": "Sediakan secara luring · Kategori", + "notesOfflineRemoveAll": "Hapus penyimpanan luring · Semua catatan", + "notesOfflineRemoveCategory": "Hapus penyimpanan luring · Kategori", + "notesOfflineKeep": "Sediakan secara luring", + "notesLoadFailed": "Nextcloud Notes · Gagal. Coba lagi", + "notesInspectionFailed": "Lampiran · Gagal. Coba lagi", + "notesSettingsLeaveHelp": "Pengaturan Notes server · Perubahan yang belum disimpan", + "notesUnavailable": "Luring · Teks tersedia secara lokal · Disimpan lokal · sinkronisasi tertunda", + "notesRecoveryRequired": "Konflik · Bandingkan", + "notesTransferFailed": "Gagal. Coba lagi", + "notesRemoteDeleteUnavailable": "URL server · Tidak didukung · Hapus · Lampiran", + "notesReadOnlyHelp": "Hanya baca · Favorit · Simpan salinan lokal…", + "notesPathRelative": "Jalur folder catatan relatif terhadap penyimpanan Nextcloud Anda.", + "notesSettingsStale": "Pengaturan berubah di Nextcloud. Muat ulang dan tinjau nilai server sebelum mengganti draf.", + "notesDownloadsPaused": "Unduhan dijeda", + "notesExportNotPublished": "Tidak ada ekspor yang dipublikasikan.", + "notesKeyringUnavailable": "Buka kunci keyring desktop dan coba lagi.", + "notesConnectFailed": "Tidak dapat terhubung ke Nextcloud Notes. Periksa alamat server dan coba lagi.", + "notesAccountMismatch": "Masuk ke akun Nextcloud yang sama. Hapus akun ini terlebih dahulu untuk menghubungkan akun lain.", + "notesLocalRemovalFailed": "Pembersihan akun lokal gagal. Akun dipertahankan agar penghapusan dapat dicoba lagi.", + "notesTransferring": "Mentransfer catatan…", + "notesTechnicalDetails": "Detail teknis", + "notesPauseRequirement": "Jeda persyaratan luring", + "notesResumeRequirement": "Lanjutkan persyaratan luring" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d88067e9..fc5a1fce 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nuova nota dal testo conservato", + "notesAccountThrottled": "Nextcloud richiede una pausa. Attendi prima di accedere nuovamente.", + "notesSettingsRefreshing": "Aggiornamento di Notes dopo la conferma delle impostazioni", + "notesExportOperationFailed": "Esportazione fallita prima della pubblicazione. Controlla origine e destinazione, poi riprova.", + "notesRecoveryCreatedNotOpened": "La nota recuperata è stata salvata, ma non si è aperta. Risolvi le modifiche in sospeso nell’editor o nelle proprietà, poi apri questa nota.", + "notesSettingsPendingNotes": "Note in sospeso, conflitti o creazioni incerte bloccano la modifica. Sincronizzali o esaminali in Notes, poi salva di nuovo la bozza conservata.", + "notesSettingsPendingAttachments": "Operazioni sugli allegati bloccano la modifica. Esamina gli allegati in Notes, poi salva di nuovo la bozza conservata.", + "notesAccountServerAddress": "Usa l’indirizzo HTTPS canonico di Nextcloud e riavvia l’accesso. L’indirizzo o il reindirizzamento non è verificabile.", + "notesAccountUnsupported": "Notes non è disponibile o l’API non è supportata. Attiva Nextcloud Notes con API principale 1, secondaria 4 o successiva.", + "notesAccountAuthorizeAgain": "Nextcloud ha rifiutato le credenziali. Autorizza nuovamente l’account o riconnettilo.", + "notesAccountCheckForbidden": "Nextcloud ha negato la verifica delle funzionalità Notes. Chiedi all’amministratore di consentirla e riprova.", + "notesAccountMalformed": "La risposta di Nextcloud non è verificabile. Controlla l’installazione del server e riavvia l’accesso.", + "notesAccountTimedOut": "Accesso scaduto. Riavvialo e completa l’autorizzazione nel browser.", + "notesAccountBrowserUnavailable": "Impossibile aprire il browser di sistema. Controlla il browser predefinito e riavvia l’accesso.", + "notesAccountNetwork": "Nextcloud non è raggiungibile. Controlla connessione e server e riprova ad accedere.", + "notesAccountRejected": "Nextcloud ha rifiutato la richiesta di funzionalità. Verifica l’accesso con l’amministratore e riprova.", + "notesAccountFailed": "Configurazione dell’account non riuscita. Riprova ad accedere.", + "notesTransferSourceChanged": "L’origine è cambiata dopo la revisione. Esamina il file attuale o crea una nuova istantanea prima di riprovare.", + "notesTransferUnreadable": "Impossibile leggere l’origine. Controlla l’accesso al file o alla cartella e riesaminala.", + "notesTransferInvalidSnapshot": "Istantanea non valida o non supportata. Scegli un’istantanea BusyMark Notes valida o un’origine Markdown.", + "notesTransferMediaLimit": "I contenuti multimediali superano il limite d’importazione. Riduci quelli dell’elemento interessato e riesaminalo.", + "notesTransferUnsafeSource": "L’origine contiene un percorso non sicuro. Scegli file nella cartella di origine e riesaminali.", + "notesTransferDestination": "Impossibile scrivere l’esportazione. Controlla accesso e spazio libero nella destinazione e riprova.", + "notesTransferOperationFailed": "Trasferimento non riuscito. Esamina gli elementi interessati e riprova. Quelli già importati rimangono salvati.", + "notesExportPublishedWithOmissions": "Esportazione disponibile con omissioni", + "notesBatchUnchanged": "I valori richiesti erano già impostati.", + "notesBatchReadOnly": "La nota è di sola lettura. Le proprietà protette non possono cambiare; Preferito rimane modificabile.", + "notesBatchReviewChanged": "La nota è cambiata dopo la revisione. Esamina lo stato attuale prima di ripetere l’azione.", + "notesBatchMissing": "La nota non è più disponibile. Aggiorna l’elenco e verifica gli elementi scelti.", + "notesBatchStorage": "Salvataggio locale fallito. Controlla lo spazio e riprova questo elemento; quelli riusciti rimangono salvati.", + "notesRemoteReviewChanged": "La nota sul server è cambiata. La bozza di unione è conservata. Esamina il nuovo confronto e scegli di nuovo la risoluzione.", + "notesReviewRemote": "Ho esaminato la versione attuale del server", "wsModuleCapsNotPublished": "La capitalizzazione del modulo non viene pubblicata.", "wsTitleOverridesAction": "Titoli dell’istanza e dell’indice…", "wsHeadingNotPublished": "Non pubblicato da questo generatore.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Data", "pdfTitlePageTitleRequired": "Inserisci un titolo per il frontespizio.", "pdfTocSectionNote": "Inizia su una pagina separata e può occupare più pagine.", - "sourceSearchZeroLengthUnsupported": "Le corrispondenze regex di lunghezza zero non sono supportate (ad esempio ^, $ o lookahead che corrispondono solo a una posizione).", "workspaceSearchIncomplete": "È mostrata solo una parte dei risultati. Mostrane altri o restringi la ricerca.", "workspaceSearchShowMore": "Mostra altri risultati", "workspaceSearchSkippedFiles": "La ricerca è incompleta. Questi file superano 1 MiB o non è stato possibile leggerli:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Mostra il riferimento alle scorciatoie da tastiera", + "shortcutKeyboardShortcutsDescription": "Personalizza le scorciatoie da tastiera dell’applicazione", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Cancella le aree di lavoro recenti", + "clearRecentWorkspaces": "Cancella la cronologia recente", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Maiuscole/minuscole", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Parola intera", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Espressione regolare", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Espressione regolare non valida", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "File di grandi dimensioni: evidenziazione e ripiegamento sono sospesi", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Impossibile aggiornare gli elementi recenti: {error}.", "workspaceErrorOpenFailed": "Apertura non riuscita: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "Il testo del collegamento è vuoto; fornisci un nome accessibile che ne descriva lo scopo.", "diagnosticMarkdownLinkReviewText": "Verifica se il testo del collegamento “{text}” ne descrive lo scopo nel contesto.", "diagnosticMarkdownTableEmptyHeader": "Le intestazioni della tabella devono identificare le colonne; completa ogni intestazione vuota.", - "commandPalette": "Tavolozza dei comandi", - "commandPaletteHint": "Digita un comando", - "commandPaletteEmpty": "Nessun comando corrispondente", "tableAlignmentUnspecified": "Allineamento: non specificato", "tableAlignmentLeft": "Allineamento: sinistra", "tableAlignmentCenter": "Allineamento: centro", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "Scegli un file .css UTF-8 leggibile fino a 256 KiB, senza importazioni, URL di risorse, HTML o CSS eseguibile o con escape.", "exportInvalidRange": "{field}: inserisci un numero tra {minimum} e {maximum}.", "clipboardHistory": "Cronologia della Copia", - "clipboardHistorySearchHint": "Cerca nella cronologia degli appunti", "clipboardHistoryDescription": "Apri la cronologia della copia locale della sessione.", "clipboardCurrent": "Copia corrente", "clipboardCurrentExternal": "Copia corrente da un'altra applicazione", "clipboardDestination": "Incolla in {name}", "clipboardSessionOnly": "Memorizzata solo per questa sessione BusyMark.", "clipboardNoItems": "La cronologia della copia è vuota.", - "clipboardNoMatches": "Nessun elemento della copia corrisponde a questa ricerca.", "clipboardDisabled": "La collezione Cronologia della Copia è disabilitata nelle Impostazioni.", "clipboardUnavailable": "Il contenuto della copia corrente non è disponibile o non è supportato.", "clipboardTooLarge": "Questo elemento del segnalibro è troppo grande per essere conservato.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "Cerca documenti e contenuti delle revisioni nella cronologia locale.", "localHistoryNoDocuments": "Nessun documento ha ancora una cronologia locale.", "localHistoryNoRevisions": "Questo documento non ha revisioni nella cronologia locale.", - "localHistoryNoMatches": "Nessuna revisione corrisponde a questa ricerca.", - "localHistorySearchHint": "Cerca nella cronologia locale", "localHistorySelectedRevision": "Revisione selezionata", "localHistoryCurrentEditor": "Contenuto dell'editor corrente", "localHistoryCurrentDisk": "Contenuto corrente sul disco", @@ -3610,5 +3619,125 @@ "wsInsertElements": "Inserisci elementi", "wsAdmonitionsAndQuotes": "Avvisi e citazioni", "wsSemanticText": "Testo semantico", - "wsPropertiesUnavailable": "L’elemento esaminato non è disponibile. Esamina un elemento per continuare." + "wsPropertiesUnavailable": "L’elemento esaminato non è disponibile. Esamina un elemento per continuare.", + "searchDocumentsCategory": "Documenti", + "searchTopicsCategory": "Argomenti", + "searchCurrentDocument": "Documento corrente", + "searchCurrentWorkspace": "Area di lavoro corrente", + "browseNotes": "Sfoglia note", + "searchActionsCommand": "Cerca azioni", + "searchDocumentsCommand": "Cerca documenti", + "searchTopicsCommand": "Cerca argomenti", + "searchRecentCommand": "Cerca recenti", + "searchResourceUnavailable": "Questa risorsa non è disponibile.", + "notesSync": "Sincronizza note", + "searchInWorkspace": "Cerca nello spazio di lavoro", + "searchCategory": "Categoria di ricerca", + "clearHistorySearchScope": "Cerca nella cronologia dei documenti", + "searchInDocument": "Cerca nel documento", + "searchDocumentHint": "Cerca in questo documento…", + "searchWorkspaceHint": "Cerca in questo spazio di lavoro…", + "searchOutlineHint": "Cerca titoli…", + "searchHistoryHint": "Cerca nella cronologia locale…", + "searchEnterText": "Inserisci il testo da cercare", + "searchAccountUnavailable": "Account non disponibile", + "searchDocumentsHint": "Cerca documenti…", + "searchTopicsHint": "Cerca argomenti…", + "searchRecentHint": "Cerca recenti…", + "searchActionsHint": "Cerca azioni…", + "searchRequiresNotesWorkspace": "Richiede uno spazio di lavoro Note", + "searchSidebarLabel": "Ricerca:", + "searchDocumentTextCategory": "Testo del documento", + "searchWorkspaceTextCategory": "Testo dell’area di lavoro", + "searchHeadingsCategory": "Intestazioni", + "shortcutUnassigned": "Non assegnata", + "shortcutPressKeys": "Premi una combinazione di tasti…", + "shortcutRecord": "Acquisisci scorciatoia", + "shortcutRestoreAll": "Ripristina tutte le impostazioni predefinite", + "shortcutRestoreAllQuestion": "Ripristinare le scorciatoie predefinite di tutte le azioni? Le assegnazioni personalizzate verranno rimosse.", + "shortcutSaveFailed": "Le scorciatoie sono attive ma non sono state salvate. Riprova per mantenerle dopo il riavvio.", + "shortcutRetry": "Riprova salvataggio", + "shortcutChanged": "Le assegnazioni sono cambiate. Controlla le scorciatoie attuali e riprova.", + "shortcutInvalid": "Questa combinazione non può essere assegnata in questo contesto.", + "shortcutSystemWarning": "Questa combinazione potrebbe essere usata dal desktop. BusyMark non può sostituire le scorciatoie di sistema.", + "shortcutConflicts": "Questa scorciatoia è in conflitto con queste azioni:", + "shortcutRemove": "Rimuovi assegnazione", + "shortcutRestoreDefault": "Ripristina predefinita", + "shortcutAssign": "Assegna", + "shortcutReassign": "Riassegna", + "shortcutContextApplication": "Applicazione", + "shortcutContextDocument": "Viste del documento", + "shortcutContextEditor": "Editor e sorgente", + "shortcutContextWritersideSource": "Sorgente Writerside", + "shortcutContextWorkspace": "Navigazione dell’area di lavoro", + "shortcutContextTree": "Alberi laterali", + "searchHeadingsCommand": "Cerca titoli", + "shortcutFixed": "Gestito dall’editor; non configurabile.", + "shortcutDefaultConflict": "La scorciatoia predefinita è in conflitto con un’assegnazione personalizzata.", + "shortcutCurrentBinding": "Scorciatoia attuale", + "shortcutProposedBinding": "Scorciatoia proposta", + "notesActions": "Azioni delle note", + "noteActions": "Azioni della nota", + "noteProperties": "Proprietà della nota", + "notesOfflinePause": "Sospendi download", + "notesOfflineResume": "Riprendi download", + "notesEmptyAll": "Nessuna nota. Creane una per iniziare.", + "notesEmptyFavorites": "Nessuna nota preferita.", + "notesEmptyCategory": "Nessuna nota in questa categoria.", + "notesEmptyUncategorized": "Nessuna nota senza categoria.", + "notesEmptyRecovery": "Nessuna nota conservata da recuperare.", + "notesStartingConnection": "Avvio della connessione Nextcloud…", + "notesVerifyingConnection": "Verifica dell’accesso alle note…", + "notesCancellingConnection": "Annullamento dell’accesso e pulizia…", + "notesRemovingConnection": "Rimozione dell’account locale…", + "notesSettingsLeave": "Salvare le modifiche alle impostazioni Nextcloud prima di uscire?", + "notesSettingsReload": "Ricarica impostazioni del server…", + "notesScanning": "Analisi dei file…", + "notesCancelling": "Annullamento… In attesa che termini l’operazione in corso.", + "notesCancelled": "Annullato", + "notesPartial": "Completato parzialmente", + "notesNotStarted": "Non avviato", + "notesCreated": "Creato", + "notesCurrentText": "Testo attualmente conservato", + "notesRecovering": "Recupero come nuova nota…", + "notesSort": "Ordina note", + "notesOfflineRetentionHelp": "Rimuovere questo requisito conserva i contenuti nella cache. Un requisito di categoria o account può ancora conservare la nota.", + "notesOfflineBroader": "La nota è conservata anche da un requisito di categoria o account.", + "notesOfflineExternal": "I contenuti multimediali esterni richiedono una connessione. BusyMark conserva solo testo e allegati gestiti.", + "notesOperationFailed": "L’operazione non è terminata. La bozza è conservata. Controlla il salvataggio e i conflitti, poi riprova.", + "notesRemovedWarning": "L’account Nextcloud locale è stato rimosso. La revoca della password dell’app non è confermata. Rimuovi la password di BusyMark nelle impostazioni di sicurezza di Nextcloud.", + "notesBuffersNotSaved": "Non tutte le note aperte sono state salvate localmente. Controlla salvataggio, conflitti o normalizzazione nell’area di lavoro. Le modifiche locali potrebbero richiedere ancora la sincronizzazione con Nextcloud.", + "notesSettingsReloadHelp": "Il ricaricamento sostituisce la bozza con le impostazioni attuali di Nextcloud. Scartare la bozza e ricaricare?", + "notesSettingsRefreshFailed": "Nextcloud ha accettato le impostazioni, ma l’aggiornamento delle note è fallito. Sono mostrati i valori confermati dal server. Riprova l’aggiornamento dalle note.", + "notesStorageFull": "Lo spazio Nextcloud è esaurito. Libera spazio sul server e riprova.", + "notesRejected": "Nextcloud ha rifiutato questa modifica. Controlla la nota e salva una copia locale prima di riprovare.", + "notesThrottled": "Nextcloud ha chiesto di attendere. La sincronizzazione riprenderà al termine dell’attesa.", + "notesImportPartialHelp": "Gli elementi precedenti potrebbero essere già salvati localmente. Riprendi questa importazione per evitare duplicati.", + "notesExportAll": "Esporta note · Tutte le note…", + "notesExportCategory": "Esporta note · Categoria…", + "notesOfflineAll": "Rendi disponibile offline · Tutte le note", + "notesOfflineCategory": "Rendi disponibile offline · Categoria", + "notesOfflineRemoveAll": "Rimuovi conservazione offline · Tutte le note", + "notesOfflineRemoveCategory": "Rimuovi conservazione offline · Categoria", + "notesOfflineKeep": "Rendi disponibile offline", + "notesLoadFailed": "Nextcloud Notes · Non riuscito. Riprova", + "notesInspectionFailed": "Allegati · Non riuscito. Riprova", + "notesSettingsLeaveHelp": "Impostazioni Notes del server · Modifiche non salvate", + "notesUnavailable": "Non in linea · Testo disponibile localmente · Salvato localmente · sincronizzazione in attesa", + "notesRecoveryRequired": "Conflitti · Confronta", + "notesTransferFailed": "Non riuscito. Riprova", + "notesRemoteDeleteUnavailable": "URL del server · Non supportato · Elimina · Allegati", + "notesReadOnlyHelp": "Sola lettura · Preferiti · Salva copia locale…", + "notesPathRelative": "Il percorso della cartella delle note è relativo al tuo spazio Nextcloud.", + "notesSettingsStale": "Le impostazioni sono cambiate su Nextcloud. Ricarica e controlla i valori del server prima di sostituire la bozza.", + "notesDownloadsPaused": "Download sospesi", + "notesExportNotPublished": "Nessuna esportazione pubblicata.", + "notesKeyringUnavailable": "Sblocca il portachiavi del desktop e riprova.", + "notesConnectFailed": "Impossibile connettersi a Nextcloud Notes. Controlla l’indirizzo del server e riprova.", + "notesAccountMismatch": "Accedi allo stesso account Nextcloud. Rimuovi prima questo account per collegarne un altro.", + "notesLocalRemovalFailed": "La pulizia dell’account locale è fallita. L’account è conservato per riprovare la rimozione.", + "notesTransferring": "Trasferimento delle note…", + "notesTechnicalDetails": "Dettagli tecnici", + "notesPauseRequirement": "Sospendi requisito offline", + "notesResumeRequirement": "Riprendi requisito offline" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 1937b115..4ebfed34 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "保持されたテキストから新規ノートを作成", + "notesAccountThrottled": "Nextcloud が待機を求めています。しばらく待ってから再度ログインしてください。", + "notesSettingsRefreshing": "確認済みの設定に基づいて Notes を更新中", + "notesExportOperationFailed": "公開前にエクスポートが失敗しました。元と保存先を確認して再実行してください。", + "notesRecoveryCreatedNotOpened": "復旧したノートは保存済みですが、開けませんでした。未処理の編集やプロパティ変更を解決し、この既存ノートを開いてください。", + "notesSettingsPendingNotes": "未処理のノート、競合、作成結果の不確定なノートが変更を妨げています。Notes で同期または確認し、保持された設定の下書きを再保存してください。", + "notesSettingsPendingAttachments": "未処理の添付操作が変更を妨げています。Notes で添付を確認し、保持された設定の下書きを再保存してください。", + "notesAccountServerAddress": "Nextcloud の正規の HTTPS アドレスでログインをやり直してください。アドレスまたはリダイレクトを確認できませんでした。", + "notesAccountUnsupported": "Notes が利用できないか API が未対応です。API メジャーバージョン 1、マイナーバージョン 4 以降の Nextcloud Notes を有効にしてください。", + "notesAccountAuthorizeAgain": "Nextcloud が認証情報を拒否しました。再認証または再接続してください。", + "notesAccountCheckForbidden": "Nextcloud が Notes の機能確認を拒否しました。管理者に許可を依頼して再試行してください。", + "notesAccountMalformed": "Nextcloud の応答を検証できませんでした。サーバーの設定を確認し、再度ログインしてください。", + "notesAccountTimedOut": "ログインがタイムアウトしました。やり直してブラウザーで認証を完了してください。", + "notesAccountBrowserUnavailable": "システムのブラウザーを開けませんでした。既定のブラウザーを確認して再度ログインしてください。", + "notesAccountNetwork": "Nextcloud に接続できませんでした。接続とサーバーを確認して再度ログインしてください。", + "notesAccountRejected": "Nextcloud が機能リクエストを拒否しました。管理者とアクセス権を確認して再試行してください。", + "notesAccountFailed": "アカウント設定を完了できませんでした。再度ログインしてください。", + "notesTransferSourceChanged": "確認後に元の内容が変わりました。現在のファイルを再確認するか、新しいスナップショットを作成してください。", + "notesTransferUnreadable": "元の内容を読めませんでした。ファイルやフォルダーのアクセス権を確認して再確認してください。", + "notesTransferInvalidSnapshot": "無効または未対応のスナップショットです。有効な BusyMark Notes スナップショットか Markdown を選んでください。", + "notesTransferMediaLimit": "メディアがインポートのサイズ制限を超えています。該当項目のメディアを小さくして再確認してください。", + "notesTransferUnsafeSource": "元の内容に安全でないパスがあります。元のフォルダー内のファイルを選び、再確認してください。", + "notesTransferDestination": "エクスポートを書き込めませんでした。保存先のアクセス権と空き容量を確認して再実行してください。", + "notesTransferOperationFailed": "転送を完了できませんでした。該当項目を確認して再試行してください。すでにインポートした項目は保存されています。", + "notesExportPublishedWithOmissions": "一部を除いてエクスポート済み", + "notesBatchUnchanged": "要求された値はすでに設定されています。", + "notesBatchReadOnly": "このノートは読み取り専用です。保護されたプロパティは変更できませんが、お気に入りは変更できます。", + "notesBatchReviewChanged": "確認後にノートが変わりました。現在の状態を確認してから操作をやり直してください。", + "notesBatchMissing": "このノートは利用できなくなりました。一覧を更新して対象を再確認してください。", + "notesBatchStorage": "ローカル保存に失敗しました。保存領域を確認してこの項目を再試行してください。成功した項目は保存済みです。", + "notesRemoteReviewChanged": "サーバーのノートが変わりました。マージの下書きは保持されています。更新された比較を確認し、解決方法を再度選んでください。", + "notesReviewRemote": "現在のサーバーバージョンを確認しました", "wsModuleCapsNotPublished": "モジュールの大文字・小文字設定は公開されません。", "wsTitleOverridesAction": "インスタンスと目次のタイトル…", "wsHeadingNotPublished": "このビルダーでは公開されません。", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "日付", "pdfTitlePageTitleRequired": "タイトルページのタイトルを入力してください。", "pdfTocSectionNote": "別のページから始まり、複数ページにわたる場合があります。", - "sourceSearchZeroLengthUnsupported": "長さがゼロの正規表現の一致には対応していません(^、$、位置のみに一致する先読みなど)。", "workspaceSearchIncomplete": "一部の結果のみ表示しています。さらに表示するか、検索を絞り込んでください。", "workspaceSearchShowMore": "さらに結果を表示", "workspaceSearchSkippedFiles": "検索は未完了です。次のファイルは 1 MiB を超えているか、読み取れませんでした:", @@ -420,18 +451,6 @@ "description": "Insert button label." }, "keyboardShortcuts": "キーボードショートカット", - "commandPalette": "コマンドパレット", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "コマンドを入力", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "一致するコマンドはありません", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "現在のエディターのコンテキストでは利用できません", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -715,7 +734,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "このキーボードショートカット一覧を表示", + "shortcutKeyboardShortcutsDescription": "アプリケーションのキーボードショートカットを設定", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1012,7 +1031,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "最近使ったワークスペースを消去", + "clearRecentWorkspaces": "最近の履歴を消去", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1943,18 +1962,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "大文字と小文字を区別", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "単語単位", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "正規表現", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "置換後", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2138,10 +2145,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "無効な正規表現", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "大きなファイル:ハイライトと折りたたみを一時停止しています", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2281,6 +2284,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "最近使った項目を更新できませんでした: {error}。", "workspaceErrorOpenFailed": "開くことができませんでした:{error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4864,14 +4868,12 @@ "exportInvalidCss": "読み取り可能な UTF-8 .css ファイル(最大 256 KiB)を選択してください。インポート、リソース URL、HTML、実行可能な CSS、エスケープを含む CSS は使用できません。", "exportInvalidRange": "{field}: {minimum} から {maximum} の数値を入力してください。", "clipboardHistory": "コピーボード履歴", - "clipboardHistorySearchHint": "クリップボード履歴を検索", "clipboardHistoryDescription": "セッション固有のコピーボード履歴を開く。", "clipboardCurrent": "現在のコピーボード", "clipboardCurrentExternal": "別のアプリケーションからの現在のコピーボード", "clipboardDestination": "{name} に貼り付け", "clipboardSessionOnly": "このBusyMarkセッションのみに保存されます。", "clipboardNoItems": "コピーボード履歴は空です。", - "clipboardNoMatches": "この検索に一致するコピーボードの項目はありません。", "clipboardDisabled": "コピーボード履歴のコレクションは設定で無効になっています。", "clipboardUnavailable": "現在のコピーボードの内容は利用できませんまたはサポートされていません。", "clipboardTooLarge": "このクリップボードの項目は大きすぎて保持できません。", @@ -4888,8 +4890,6 @@ "findLocalHistoryDescription": "ローカル履歴のドキュメントとリビジョンの内容を検索します。", "localHistoryNoDocuments": "まだローカル履歴を持つドキュメントはありません。", "localHistoryNoRevisions": "このドキュメントにはローカル履歴の修正はありません。", - "localHistoryNoMatches": "この検索に一致するローカル履歴の修正はありません。", - "localHistorySearchHint": "ローカル履歴を検索", "localHistorySelectedRevision": "選択されたリビジョン", "localHistoryCurrentEditor": "現在の編集内容", "localHistoryCurrentDisk": "ディスク上の現在の内容", @@ -5149,5 +5149,125 @@ "wsInsertElements": "要素を挿入", "wsAdmonitionsAndQuotes": "注意書きと引用", "wsSemanticText": "セマンティックテキスト", - "wsPropertiesUnavailable": "確認中の要素は利用できません。続行するには要素を確認してください。" + "wsPropertiesUnavailable": "確認中の要素は利用できません。続行するには要素を確認してください。", + "searchDocumentsCategory": "ドキュメント", + "searchTopicsCategory": "トピック", + "searchCurrentDocument": "現在のドキュメント", + "searchCurrentWorkspace": "現在のワークスペース", + "browseNotes": "ノートを参照", + "searchActionsCommand": "アクションを検索", + "searchDocumentsCommand": "ドキュメントを検索", + "searchTopicsCommand": "トピックを検索", + "searchRecentCommand": "最近の項目を検索", + "searchResourceUnavailable": "このリソースは利用できません。", + "notesSync": "ノートを同期", + "searchInWorkspace": "ワークスペース内検索", + "searchCategory": "検索カテゴリ", + "clearHistorySearchScope": "すべてのドキュメントの履歴を検索", + "searchInDocument": "ドキュメント内を検索", + "searchDocumentHint": "このドキュメントを検索…", + "searchWorkspaceHint": "このワークスペースを検索…", + "searchOutlineHint": "見出しを検索…", + "searchHistoryHint": "ローカル履歴を検索…", + "searchEnterText": "検索するテキストを入力", + "searchAccountUnavailable": "アカウントを利用できません", + "searchDocumentsHint": "ドキュメントを検索…", + "searchTopicsHint": "トピックを検索…", + "searchRecentHint": "最近の項目を検索…", + "searchActionsHint": "アクションを検索…", + "searchRequiresNotesWorkspace": "ノートのワークスペースが必要です", + "searchSidebarLabel": "検索:", + "searchDocumentTextCategory": "文書のテキスト", + "searchWorkspaceTextCategory": "ワークスペースのテキスト", + "searchHeadingsCategory": "見出し", + "shortcutUnassigned": "未割り当て", + "shortcutPressKeys": "キーの組み合わせを押してください…", + "shortcutRecord": "ショートカットを記録", + "shortcutRestoreAll": "すべて既定に戻す", + "shortcutRestoreAllQuestion": "すべてのアクションのショートカットを既定に戻しますか?独自の割り当ては削除されます。", + "shortcutSaveFailed": "ショートカットは有効ですが保存できませんでした。再起動後も保持するには保存を再試行してください。", + "shortcutRetry": "保存を再試行", + "shortcutChanged": "割り当てが変更されました。現在のショートカットを確認して再試行してください。", + "shortcutInvalid": "この組み合わせはこのコンテキストでは割り当てられません。", + "shortcutSystemWarning": "この組み合わせはデスクトップで使われている可能性があります。BusyMarkはシステムのショートカットを上書きできません。", + "shortcutConflicts": "このショートカットは次のアクションと競合します:", + "shortcutRemove": "割り当てを削除", + "shortcutRestoreDefault": "既定に戻す", + "shortcutAssign": "割り当て", + "shortcutReassign": "再割り当て", + "shortcutContextApplication": "アプリケーション", + "shortcutContextDocument": "文書の表示", + "shortcutContextEditor": "エディターとソース", + "shortcutContextWritersideSource": "Writersideのソース", + "shortcutContextWorkspace": "ワークスペースの移動", + "shortcutContextTree": "サイドバーのツリー", + "searchHeadingsCommand": "見出しを検索", + "shortcutFixed": "エディターが管理するため、変更できません。", + "shortcutDefaultConflict": "既定のショートカットがカスタム割り当てと競合しています。", + "shortcutCurrentBinding": "現在のショートカット", + "shortcutProposedBinding": "新しいショートカット", + "notesActions": "ノートの操作", + "noteActions": "このノートの操作", + "noteProperties": "ノートのプロパティ", + "notesOfflinePause": "ダウンロードを一時停止", + "notesOfflineResume": "ダウンロードを再開", + "notesEmptyAll": "ノートはまだありません。新しく作成してください。", + "notesEmptyFavorites": "お気に入りのノートはありません。", + "notesEmptyCategory": "このカテゴリにノートはありません。", + "notesEmptyUncategorized": "未分類のノートはありません。", + "notesEmptyRecovery": "復元できる保存済みノートはありません。", + "notesStartingConnection": "Nextcloud への接続を開始中…", + "notesVerifyingConnection": "ノートへのアクセスを確認中…", + "notesCancellingConnection": "サインインをキャンセルして後処理中…", + "notesRemovingConnection": "ローカルアカウントを削除中…", + "notesSettingsLeave": "移動する前に Nextcloud 設定の変更を保存しますか?", + "notesSettingsReload": "サーバー設定を再読み込み…", + "notesScanning": "ファイルをスキャン中…", + "notesCancelling": "キャンセル中… 現在の処理の終了を待っています。", + "notesCancelled": "キャンセル済み", + "notesPartial": "一部完了", + "notesNotStarted": "未開始", + "notesCreated": "作成済み", + "notesCurrentText": "現在保持されているテキスト", + "notesRecovering": "新しいノートとして復元中…", + "notesSort": "ノートの並べ替え", + "notesOfflineRetentionHelp": "この要件を解除しても既存のキャッシュは削除されません。カテゴリやアカウントの要件により保持される場合があります。", + "notesOfflineBroader": "このノートはカテゴリまたはアカウントの要件によっても保持されています。", + "notesOfflineExternal": "外部メディアには接続が必要です。BusyMark が保持するのはノート本文と管理対象の添付ファイルのみです。", + "notesOperationFailed": "処理を完了できませんでした。下書きは保持されています。保存と競合を確認して再試行してください。", + "notesRemovedWarning": "ローカルの Nextcloud アカウントを削除しました。アプリパスワードの失効は確認できませんでした。Nextcloud のセキュリティ設定で BusyMark のパスワードを削除してください。", + "notesBuffersNotSaved": "開いているすべてのノートをローカルに保存できませんでした。ワークスペースで保存、競合、正規化を確認してください。ローカルの変更は Nextcloud との同期がまだ必要な場合があります。", + "notesSettingsReloadHelp": "再読み込みすると下書きが現在の Nextcloud 設定に置き換わります。下書きを破棄して再読み込みしますか?", + "notesSettingsRefreshFailed": "Nextcloud は設定を受理しましたが、ノートの更新に失敗しました。表示値はサーバーが確認した値です。ノートから更新を再試行してください。", + "notesStorageFull": "Nextcloud の空き容量がありません。サーバーの容量を空けて再試行してください。", + "notesRejected": "Nextcloud が変更を拒否しました。ノートを確認してローカルコピーを保存してから再試行してください。", + "notesThrottled": "Nextcloud が待機を要求しました。指定された再試行時刻を過ぎると同期が再開します。", + "notesImportPartialHelp": "以前の項目はすでにローカルに保存されている場合があります。重複を防ぐため、このインポートを再開してください。", + "notesExportAll": "ノートをエクスポート · すべてのノート…", + "notesExportCategory": "ノートをエクスポート · カテゴリ…", + "notesOfflineAll": "オフラインで利用可能にする · すべてのノート", + "notesOfflineCategory": "オフラインで利用可能にする · カテゴリ", + "notesOfflineRemoveAll": "オフライン保持を解除 · すべてのノート", + "notesOfflineRemoveCategory": "オフライン保持を解除 · カテゴリ", + "notesOfflineKeep": "オフラインで利用可能にする", + "notesLoadFailed": "Nextcloud Notes · 失敗. 再試行", + "notesInspectionFailed": "添付ファイル · 失敗. 再試行", + "notesSettingsLeaveHelp": "サーバーのNotes設定 · 未保存の変更", + "notesUnavailable": "オフライン · テキストはローカルで利用可能 · ローカルに保存済み・同期待ち", + "notesRecoveryRequired": "競合 · 比較", + "notesTransferFailed": "失敗. 再試行", + "notesRemoteDeleteUnavailable": "サーバーURL · 未対応 · 削除 · 添付ファイル", + "notesReadOnlyHelp": "読み取り専用 · お気に入り · ローカルコピーを保存…", + "notesPathRelative": "ノートフォルダーのパスは Nextcloud ストレージからの相対パスです。", + "notesSettingsStale": "Nextcloud の設定が変更されました。サーバーの値を再読み込みして確認してから下書きを置き換えてください。", + "notesDownloadsPaused": "ダウンロードは一時停止中です", + "notesExportNotPublished": "エクスポートは公開されませんでした。", + "notesKeyringUnavailable": "デスクトップのキーリングを解除して再試行してください。", + "notesConnectFailed": "Nextcloud Notes に接続できませんでした。サーバーのアドレスを確認して再試行してください。", + "notesAccountMismatch": "同じ Nextcloud アカウントでサインインしてください。別のアカウントを接続するには現在のアカウントを先に削除してください。", + "notesLocalRemovalFailed": "ローカルアカウントの後処理に失敗しました。削除を再試行できるようアカウントを保持しています。", + "notesTransferring": "ノートを転送中…", + "notesTechnicalDetails": "技術情報", + "notesPauseRequirement": "オフライン保持要件を一時停止", + "notesResumeRequirement": "オフライン保持要件を再開" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 9920f6f7..df3c912b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "보관된 텍스트로 새 노트 만들기", + "notesAccountThrottled": "Nextcloud가 일시 중지를 요청했습니다. 기다린 뒤 다시 로그인하세요.", + "notesSettingsRefreshing": "확인된 설정 변경 후 Notes 새로 고치는 중", + "notesExportOperationFailed": "게시 전에 내보내기가 실패했습니다. 원본과 대상을 확인하고 다시 내보내세요.", + "notesRecoveryCreatedNotOpened": "복구한 노트는 저장되었지만 열지 못했습니다. 보류 중인 편집 또는 속성 변경을 해결한 뒤 이 기존 노트를 여세요.", + "notesSettingsPendingNotes": "보류 중인 노트, 충돌 또는 생성 결과가 불확실한 노트가 변경을 막고 있습니다. Notes에서 동기화하거나 검토한 뒤 보존된 설정 초안을 다시 저장하세요.", + "notesSettingsPendingAttachments": "보류 중인 첨부 작업이 변경을 막고 있습니다. Notes에서 첨부를 검토한 뒤 보존된 설정 초안을 다시 저장하세요.", + "notesAccountServerAddress": "Nextcloud의 정식 HTTPS 주소로 다시 로그인하세요. 주소 또는 리디렉션을 확인할 수 없었습니다.", + "notesAccountUnsupported": "Notes를 사용할 수 없거나 API가 지원되지 않습니다. 주 버전 1, 부 버전 4 이상의 API를 제공하는 Nextcloud Notes를 활성화하세요.", + "notesAccountAuthorizeAgain": "Nextcloud가 인증 정보를 거부했습니다. 계정을 다시 인증하거나 연결하세요.", + "notesAccountCheckForbidden": "Nextcloud가 Notes 기능 확인을 거부했습니다. 관리자에게 허용을 요청한 뒤 다시 시도하세요.", + "notesAccountMalformed": "Nextcloud 응답을 검증할 수 없었습니다. 서버 설치를 확인하고 다시 로그인하세요.", + "notesAccountTimedOut": "로그인 시간이 초과되었습니다. 다시 시작하고 브라우저에서 인증을 완료하세요.", + "notesAccountBrowserUnavailable": "시스템 브라우저를 열지 못했습니다. 기본 브라우저를 확인하고 다시 로그인하세요.", + "notesAccountNetwork": "Nextcloud에 연결하지 못했습니다. 연결과 서버를 확인하고 다시 로그인하세요.", + "notesAccountRejected": "Nextcloud가 기능 요청을 거부했습니다. 관리자와 접근 권한을 확인하고 다시 시도하세요.", + "notesAccountFailed": "계정 설정을 완료하지 못했습니다. 다시 로그인하세요.", + "notesTransferSourceChanged": "검토 후 원본이 변경되었습니다. 현재 파일을 다시 검토하거나 새 스냅샷을 만든 뒤 재시도하세요.", + "notesTransferUnreadable": "원본을 읽을 수 없습니다. 파일 또는 폴더 접근 권한을 확인하고 다시 검토하세요.", + "notesTransferInvalidSnapshot": "스냅샷이 올바르지 않거나 지원되지 않습니다. 유효한 BusyMark Notes 스냅샷 또는 Markdown 원본을 선택하세요.", + "notesTransferMediaLimit": "미디어가 가져오기 크기 제한을 초과합니다. 해당 항목의 미디어를 줄이고 다시 검토하세요.", + "notesTransferUnsafeSource": "원본에 안전하지 않은 경로가 있습니다. 원본 폴더 안의 파일을 선택하고 다시 검토하세요.", + "notesTransferDestination": "내보내기를 기록하지 못했습니다. 대상의 접근 권한과 여유 공간을 확인하고 다시 내보내세요.", + "notesTransferOperationFailed": "전송을 완료하지 못했습니다. 해당 항목을 검토하고 다시 시도하세요. 이전에 가져온 항목은 저장되어 있습니다.", + "notesExportPublishedWithOmissions": "누락된 항목이 있는 내보내기 사용 가능", + "notesBatchUnchanged": "요청한 값이 이미 설정되어 있습니다.", + "notesBatchReadOnly": "이 노트는 읽기 전용입니다. 보호된 속성은 바꿀 수 없지만 즐겨찾기는 변경할 수 있습니다.", + "notesBatchReviewChanged": "검토 후 노트가 변경되었습니다. 현재 상태를 검토한 뒤 작업을 다시 적용하세요.", + "notesBatchMissing": "이 노트를 더 이상 사용할 수 없습니다. 목록을 새로 고치고 대상을 다시 검토하세요.", + "notesBatchStorage": "로컬 저장에 실패했습니다. 저장소를 확인하고 이 항목을 다시 시도하세요. 성공한 항목은 저장되어 있습니다.", + "notesRemoteReviewChanged": "서버 노트가 변경되었습니다. 병합 초안은 보존됩니다. 새 비교를 검토하고 해결 방법을 다시 선택하세요.", + "notesReviewRemote": "현재 서버 버전을 검토했습니다", "wsModuleCapsNotPublished": "모듈의 대소문자 설정은 게시되지 않습니다.", "wsTitleOverridesAction": "인스턴스 및 목차 제목…", "wsHeadingNotPublished": "이 빌더에서는 게시되지 않습니다.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "날짜", "pdfTitlePageTitleRequired": "제목 페이지의 제목을 입력하세요.", "pdfTocSectionNote": "별도 페이지에서 시작하며 여러 페이지에 걸칠 수 있습니다.", - "sourceSearchZeroLengthUnsupported": "길이가 0인 정규식 일치는 지원하지 않습니다(예: ^, $, 위치에만 일치하는 전방 탐색).", "workspaceSearchIncomplete": "일부 결과만 표시됩니다. 더 표시하거나 검색 범위를 좁히세요.", "workspaceSearchShowMore": "결과 더 보기", "workspaceSearchSkippedFiles": "검색이 완료되지 않았습니다. 다음 파일은 1 MiB를 초과하거나 읽을 수 없습니다:", @@ -423,18 +454,6 @@ "@keyboardShortcuts": { "description": "Keyboard shortcuts dialog title and menu item." }, - "commandPalette": "명령 팔레트", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "명령을 입력하세요", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "일치하는 명령이 없습니다.", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "현재 편집기 컨텍스트에서는 사용할 수 없습니다.", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -715,7 +734,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "이 키보드 단축키 참조 표시", + "shortcutKeyboardShortcutsDescription": "애플리케이션 키보드 단축키 사용자 지정", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1012,7 +1031,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "최근 작업공간 지우기", + "clearRecentWorkspaces": "최근 기록 지우기", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1943,18 +1962,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "대소문자 구분", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "전체 단어", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "정규식", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "다음으로 바꾸기", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2138,10 +2145,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "잘못된 정규식", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "대용량 파일: 강조 표시 및 접기가 일시 중지됩니다.", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2281,6 +2284,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "최근 항목을 업데이트하지 못했습니다: {error}.", "workspaceErrorOpenFailed": "열기 실패: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4864,14 +4868,12 @@ "exportInvalidCss": "가져오기, 리소스 URL, HTML, 실행 가능하거나 이스케이프된 CSS가 없는 읽기 가능한 UTF-8 .css 파일(최대 256 KiB)을 선택하세요.", "exportInvalidRange": "{field}: {minimum}에서 {maximum} 사이의 숫자를 입력하세요.", "clipboardHistory": "클립보드 기록", - "clipboardHistorySearchHint": "클립보드 기록 검색", "clipboardHistoryDescription": "세션별 클립보드 기록을 엽니다.", "clipboardCurrent": "현재 클립보드", "clipboardCurrentExternal": "다른 애플리케이션의 현재 클립보드", "clipboardDestination": "{name}에 붙여넣기", "clipboardSessionOnly": "이 BusyMark 세션에만 저장됩니다.", "clipboardNoItems": "클립보드 기록이 비어 있습니다.", - "clipboardNoMatches": "이 검색과 일치하는 클립보드 항목이 없습니다.", "clipboardDisabled": "클립보드 기록 컬렉션이 설정에서 비활성화되었습니다.", "clipboardUnavailable": "현재 클립보드 내용은 사용할 수 없거나 지원되지 않습니다.", "clipboardTooLarge": "이 클립보드 항목은 저장할 수 없을 정도로 큽니다.", @@ -4888,8 +4890,6 @@ "findLocalHistoryDescription": "로컬 히스토리 문서 및 수정 내용 검색.", "localHistoryNoDocuments": "아직 로컬 히스토리가 있는 문서는 없습니다.", "localHistoryNoRevisions": "이 문서는 로컬 히스토리 수정 사항이 없습니다.", - "localHistoryNoMatches": "이 검색과 일치하는 로컬 히스토리 수정 사항이 없습니다.", - "localHistorySearchHint": "로컬 기록 검색", "localHistorySelectedRevision": "선택된 수정", "localHistoryCurrentEditor": "현재 편집 내용", "localHistoryCurrentDisk": "디스크에 있는 현재 내용", @@ -5149,5 +5149,125 @@ "wsInsertElements": "요소 삽입", "wsAdmonitionsAndQuotes": "안내 및 인용", "wsSemanticText": "의미 텍스트", - "wsPropertiesUnavailable": "검사 중인 요소를 사용할 수 없습니다. 계속하려면 요소를 검사하세요." + "wsPropertiesUnavailable": "검사 중인 요소를 사용할 수 없습니다. 계속하려면 요소를 검사하세요.", + "searchDocumentsCategory": "문서", + "searchTopicsCategory": "주제", + "searchCurrentDocument": "현재 문서", + "searchCurrentWorkspace": "현재 작업 공간", + "browseNotes": "노트 찾아보기", + "searchActionsCommand": "작업 검색", + "searchDocumentsCommand": "문서 검색", + "searchTopicsCommand": "주제 검색", + "searchRecentCommand": "최근 항목 검색", + "searchResourceUnavailable": "이 리소스를 사용할 수 없습니다.", + "notesSync": "노트 동기화", + "searchInWorkspace": "작업 공간에서 검색", + "searchCategory": "검색 범주", + "clearHistorySearchScope": "문서 기록 전체 검색", + "searchInDocument": "문서에서 검색", + "searchDocumentHint": "이 문서에서 검색…", + "searchWorkspaceHint": "이 작업 공간에서 검색…", + "searchOutlineHint": "제목 검색…", + "searchHistoryHint": "로컬 기록 검색…", + "searchEnterText": "검색할 텍스트 입력", + "searchAccountUnavailable": "계정을 사용할 수 없음", + "searchDocumentsHint": "문서 검색…", + "searchTopicsHint": "주제 검색…", + "searchRecentHint": "최근 항목 검색…", + "searchActionsHint": "작업 검색…", + "searchRequiresNotesWorkspace": "노트 작업 공간 필요", + "searchSidebarLabel": "검색:", + "searchDocumentTextCategory": "문서 텍스트", + "searchWorkspaceTextCategory": "작업 공간 텍스트", + "searchHeadingsCategory": "제목", + "shortcutUnassigned": "할당되지 않음", + "shortcutPressKeys": "키 조합을 누르세요…", + "shortcutRecord": "단축키 기록", + "shortcutRestoreAll": "모든 기본값 복원", + "shortcutRestoreAllQuestion": "모든 동작의 기본 단축키를 복원할까요? 사용자 지정 할당이 제거됩니다.", + "shortcutSaveFailed": "단축키가 활성화되었지만 저장하지 못했습니다. 다시 시작 후에도 유지하려면 저장을 다시 시도하세요.", + "shortcutRetry": "저장 다시 시도", + "shortcutChanged": "할당이 변경되었습니다. 현재 단축키를 확인하고 다시 시도하세요.", + "shortcutInvalid": "이 조합은 이 문맥에서 할당할 수 없습니다.", + "shortcutSystemWarning": "데스크톱에서 이 조합을 사용할 수 있습니다. BusyMark는 시스템 단축키를 재정의할 수 없습니다.", + "shortcutConflicts": "이 단축키는 다음 동작과 충돌합니다:", + "shortcutRemove": "할당 제거", + "shortcutRestoreDefault": "기본값 복원", + "shortcutAssign": "할당", + "shortcutReassign": "다시 할당", + "shortcutContextApplication": "애플리케이션", + "shortcutContextDocument": "문서 보기", + "shortcutContextEditor": "편집기 및 소스", + "shortcutContextWritersideSource": "Writerside 소스", + "shortcutContextWorkspace": "작업 공간 탐색", + "shortcutContextTree": "사이드바 트리", + "searchHeadingsCommand": "제목 검색", + "shortcutFixed": "편집기에서 관리하므로 설정할 수 없습니다.", + "shortcutDefaultConflict": "기본 단축키가 사용자 지정 할당과 충돌합니다.", + "shortcutCurrentBinding": "현재 단축키", + "shortcutProposedBinding": "제안된 단축키", + "notesActions": "노트 작업", + "noteActions": "이 노트의 작업", + "noteProperties": "노트 속성", + "notesOfflinePause": "다운로드 일시 중지", + "notesOfflineResume": "다운로드 재개", + "notesEmptyAll": "아직 노트가 없습니다. 새 노트를 만드세요.", + "notesEmptyFavorites": "즐겨찾는 노트가 없습니다.", + "notesEmptyCategory": "이 범주에 노트가 없습니다.", + "notesEmptyUncategorized": "분류되지 않은 노트가 없습니다.", + "notesEmptyRecovery": "복구할 보관 노트가 없습니다.", + "notesStartingConnection": "Nextcloud 연결 시작 중…", + "notesVerifyingConnection": "노트 접근 확인 중…", + "notesCancellingConnection": "로그인을 취소하고 정리 중…", + "notesRemovingConnection": "로컬 계정 제거 중…", + "notesSettingsLeave": "나가기 전에 Nextcloud 설정 변경 사항을 저장할까요?", + "notesSettingsReload": "서버 설정 다시 불러오기…", + "notesScanning": "파일 검색 중…", + "notesCancelling": "취소 중… 현재 작업이 끝나기를 기다리는 중입니다.", + "notesCancelled": "취소됨", + "notesPartial": "부분 완료", + "notesNotStarted": "시작하지 않음", + "notesCreated": "생성됨", + "notesCurrentText": "현재 보관된 텍스트", + "notesRecovering": "새 노트로 복구 중…", + "notesSort": "노트 정렬", + "notesOfflineRetentionHelp": "이 요구 사항을 제거해도 기존 캐시 내용은 유지됩니다. 범주나 계정 요구 사항에 따라 노트가 계속 보관될 수 있습니다.", + "notesOfflineBroader": "이 노트는 범주 또는 계정 요구 사항에 따라서도 보관됩니다.", + "notesOfflineExternal": "외부 미디어에는 연결이 필요합니다. BusyMark는 노트 텍스트와 관리되는 첨부 파일만 보관합니다.", + "notesOperationFailed": "작업을 완료하지 못했습니다. 초안은 보관됩니다. 저장과 충돌을 검토한 뒤 다시 시도하세요.", + "notesRemovedWarning": "로컬 Nextcloud 계정을 제거했습니다. 앱 암호 해지를 확인하지 못했습니다. Nextcloud 보안 설정에서 BusyMark의 앱 암호를 제거하세요.", + "notesBuffersNotSaved": "열린 노트를 모두 로컬에 저장하지 못했습니다. 작업 공간에서 저장, 충돌 또는 정규화를 검토하세요. 로컬 변경 사항은 아직 Nextcloud와 동기화해야 할 수 있습니다.", + "notesSettingsReloadHelp": "다시 불러오면 초안이 현재 Nextcloud 설정으로 대체됩니다. 초안을 버리고 다시 불러올까요?", + "notesSettingsRefreshFailed": "Nextcloud가 설정을 수락했지만 노트 새로 고침에 실패했습니다. 표시된 값은 서버에서 확인한 값입니다. 노트에서 새로 고침을 다시 시도하세요.", + "notesStorageFull": "Nextcloud 저장 공간이 가득 찼습니다. 서버 공간을 확보한 뒤 다시 시도하세요.", + "notesRejected": "Nextcloud가 이 변경을 거부했습니다. 노트를 검토하고 로컬 사본을 저장한 뒤 다시 시도하세요.", + "notesThrottled": "Nextcloud가 대기를 요청했습니다. 재시도 기한이 지나면 동기화가 재개됩니다.", + "notesImportPartialHelp": "이전 항목은 이미 로컬에 저장되었을 수 있습니다. 중복을 방지하려면 이 가져오기를 재개하세요.", + "notesExportAll": "노트 내보내기 · 모든 노트…", + "notesExportCategory": "노트 내보내기 · 범주…", + "notesOfflineAll": "오프라인 사용 설정 · 모든 노트", + "notesOfflineCategory": "오프라인 사용 설정 · 범주", + "notesOfflineRemoveAll": "오프라인 보존 해제 · 모든 노트", + "notesOfflineRemoveCategory": "오프라인 보존 해제 · 범주", + "notesOfflineKeep": "오프라인 사용 설정", + "notesLoadFailed": "Nextcloud Notes · 실패. 다시 시도", + "notesInspectionFailed": "첨부 파일 · 실패. 다시 시도", + "notesSettingsLeaveHelp": "서버 Notes 설정 · 저장되지 않은 변경 사항", + "notesUnavailable": "오프라인 · 텍스트를 로컬에서 사용 가능 · 로컬에 저장됨 · 동기화 대기", + "notesRecoveryRequired": "충돌 · 비교", + "notesTransferFailed": "실패. 다시 시도", + "notesRemoteDeleteUnavailable": "서버 주소 · 지원되지 않음 · 삭제 · 첨부 파일", + "notesReadOnlyHelp": "읽기 전용 · 즐겨찾기 · 로컬 사본 저장…", + "notesPathRelative": "노트 폴더 경로는 Nextcloud 저장소 기준의 상대 경로입니다.", + "notesSettingsStale": "Nextcloud에서 설정이 변경되었습니다. 초안을 대체하기 전에 서버 값을 다시 불러와 검토하세요.", + "notesDownloadsPaused": "다운로드 일시 중지됨", + "notesExportNotPublished": "내보내기가 게시되지 않았습니다.", + "notesKeyringUnavailable": "데스크톱 키링을 잠금 해제한 뒤 다시 시도하세요.", + "notesConnectFailed": "Nextcloud Notes에 연결하지 못했습니다. 서버 주소를 확인하고 다시 시도하세요.", + "notesAccountMismatch": "같은 Nextcloud 계정으로 로그인하세요. 다른 계정을 연결하려면 이 계정을 먼저 제거하세요.", + "notesLocalRemovalFailed": "로컬 계정 정리에 실패했습니다. 제거를 다시 시도할 수 있도록 계정을 유지합니다.", + "notesTransferring": "노트 전송 중…", + "notesTechnicalDetails": "기술 정보", + "notesPauseRequirement": "오프라인 보관 요구 사항 일시 중지", + "notesResumeRequirement": "오프라인 보관 요구 사항 재개" } diff --git a/lib/l10n/app_nb.arb b/lib/l10n/app_nb.arb index d3fa2c48..cbde5d9b 100644 --- a/lib/l10n/app_nb.arb +++ b/lib/l10n/app_nb.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nytt notat fra bevart tekst", + "notesAccountThrottled": "Nextcloud ba om en pause. Vent før du starter innloggingen på nytt.", + "notesSettingsRefreshing": "Oppdaterer Notes etter bekreftet innstillingsendring", + "notesExportOperationFailed": "Eksporten mislyktes før publisering. Kontroller kilde og mål, og eksporter igjen.", + "notesRecoveryCreatedNotOpened": "Den gjenopprettede notaten er lagret, men kunne ikke åpnes. Avklar ventende redigerings- eller egenskapsendringer, og åpne denne eksisterende notaten.", + "notesSettingsPendingNotes": "Ventende notater, konflikter eller usikre opprettelser blokkerer endringen. Synkroniser eller gjennomgå dem i Notes, og prøv det bevarte innstillingsutkastet igjen.", + "notesSettingsPendingAttachments": "Ventende vedleggsoperasjoner blokkerer endringen. Gjennomgå vedlegg i Notes, og prøv det bevarte innstillingsutkastet igjen.", + "notesAccountServerAddress": "Bruk Nextclouds kanoniske HTTPS-adresse og start innloggingen på nytt. Adressen eller videresendingen kunne ikke bekreftes.", + "notesAccountUnsupported": "Notes er utilgjengelig eller API-et støttes ikke. Aktiver Nextcloud Notes med API-hovedversjon 1, underversjon 4 eller nyere.", + "notesAccountAuthorizeAgain": "Nextcloud avviste påloggingsinformasjonen. Godkjenn kontoen på nytt eller koble til igjen.", + "notesAccountCheckForbidden": "Nextcloud nektet kontroll av Notes-funksjoner. Be administratoren tillate den, og prøv igjen.", + "notesAccountMalformed": "Nextcloud-svaret kunne ikke bekreftes. Kontroller serverinstallasjonen og logg inn igjen.", + "notesAccountTimedOut": "Innloggingen ble tidsavbrutt. Start igjen og fullfør godkjenningen i nettleseren.", + "notesAccountBrowserUnavailable": "Systemnettleseren kunne ikke åpnes. Kontroller standardnettleseren og logg inn igjen.", + "notesAccountNetwork": "Nextcloud kunne ikke nås. Kontroller tilkobling og server, og logg inn igjen.", + "notesAccountRejected": "Nextcloud avviste funksjonsforespørselen. Kontroller tilgang med administratoren og prøv igjen.", + "notesAccountFailed": "Kontooppsettet kunne ikke fullføres. Prøv å logge inn igjen.", + "notesTransferSourceChanged": "Kilden ble endret etter gjennomgang. Gjennomgå gjeldende fil eller lag et nytt øyeblikksbilde før nytt forsøk.", + "notesTransferUnreadable": "Kilden kunne ikke leses. Kontroller tilgang til fil eller mappe, og gjennomgå igjen.", + "notesTransferInvalidSnapshot": "Øyeblikksbildet er ugyldig eller ustøttet. Velg et gyldig BusyMark Notes-bilde eller en Markdown-kilde.", + "notesTransferMediaLimit": "Medier overskrider importgrensen. Reduser mediene for berørt element og gjennomgå igjen.", + "notesTransferUnsafeSource": "Kilden inneholder en utrygg sti. Velg filer i kildemappen og gjennomgå igjen.", + "notesTransferDestination": "Eksporten kunne ikke skrives. Kontroller tilgang og ledig plass på målet, og eksporter igjen.", + "notesTransferOperationFailed": "Overføringen kunne ikke fullføres. Gjennomgå berørte elementer og prøv igjen. Tidligere importerte elementer er lagret.", + "notesExportPublishedWithOmissions": "Eksport tilgjengelig med utelatelser", + "notesBatchUnchanged": "De ønskede verdiene var allerede angitt.", + "notesBatchReadOnly": "Notaten er skrivebeskyttet. Beskyttede egenskaper kan ikke endres; Favoritt er fortsatt tillatt.", + "notesBatchReviewChanged": "Notaten ble endret etter gjennomgang. Gjennomgå gjeldende tilstand før handlingen gjentas.", + "notesBatchMissing": "Notaten er ikke lenger tilgjengelig. Oppdater listen og gjennomgå målene igjen.", + "notesBatchStorage": "Lokal lagring mislyktes. Kontroller lagringsplassen og prøv dette elementet igjen; vellykkede elementer er lagret.", + "notesRemoteReviewChanged": "Servernotaten ble endret. Fletteutkastet er bevart. Gjennomgå den oppdaterte sammenligningen og velg løsning på nytt.", + "notesReviewRemote": "Jeg har gjennomgått gjeldende serverversjon", "wsModuleCapsNotPublished": "Modulens bokstavstil publiseres ikke.", "wsTitleOverridesAction": "Titler for instans og innholdsfortegnelse…", "wsHeadingNotPublished": "Publiseres ikke av denne byggeren.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Dato", "pdfTitlePageTitleRequired": "Skriv inn en tittel for tittelsiden.", "pdfTocSectionNote": "Starter på en egen side og kan gå over flere sider.", - "sourceSearchZeroLengthUnsupported": "Regex-treff med null lengde støttes ikke (for eksempel ^, $ eller fremoversøk som bare samsvarer med en posisjon).", "workspaceSearchIncomplete": "Bare noen av resultatene vises. Vis flere eller avgrens søket.", "workspaceSearchShowMore": "Vis flere resultater", "workspaceSearchSkippedFiles": "Søket er ufullstendig. Disse filene er større enn 1 MiB eller kunne ikke leses:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Vis denne oversikten over tastatursnarveier", + "shortcutKeyboardShortcutsDescription": "Tilpass programmets hurtigtaster", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Tøm nylige arbeidsområder", + "clearRecentWorkspaces": "Tøm nylig historikk", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Skill mellom store og små bokstaver", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Hele ord", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Regulært uttrykk", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Ugyldig regulært uttrykk", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Stor fil: utheving og folding er satt på pause", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Kunne ikke oppdatere nylige elementer: {error}.", "workspaceErrorOpenFailed": "Åpning mislyktes: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "Lenketeksten er tom. Oppgi et tilgjengelig navn som beskriver formålet.", "diagnosticMarkdownLinkReviewText": "Kontroller om lenketeksten «{text}» beskriver formålet i konteksten.", "diagnosticMarkdownTableEmptyHeader": "Tabelloverskrifter må identifisere kolonnene. Fyll ut alle tomme overskrifter.", - "commandPalette": "Kommandopalett", - "commandPaletteHint": "Skriv inn en kommando", - "commandPaletteEmpty": "Ingen samsvarende kommandoer", "tableAlignmentUnspecified": "Justering: ikke angitt", "tableAlignmentLeft": "Justering: venstre", "tableAlignmentCenter": "Justering: midtstilt", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "Velg en lesbar UTF-8 .css-fil opptil 256 KiB uten importer, ressurs-URL-er, HTML eller kjørbar eller escaped CSS.", "exportInvalidRange": "{field}: skriv inn et tall mellom {minimum} og {maximum}.", "clipboardHistory": "Utklippshistorie", - "clipboardHistorySearchHint": "Søk i utklippstavlehistorikken", "clipboardHistoryDescription": "Åpne historien for utklippet som er lokal for sesjonen.", "clipboardCurrent": "Nåværende utklipp", "clipboardCurrentExternal": "Nåværende utklipp fra en annen applikasjon", "clipboardDestination": "Lim inn i {name}", "clipboardSessionOnly": "Lagret kun for denne BusyMark-sesjonen.", "clipboardNoItems": "Utklippshistorien er tom.", - "clipboardNoMatches": "Ingen utklippsposter samsvarer med denne søkingen.", "clipboardDisabled": "Samlingen \"Utklippshistorie\" er deaktivert i Innstillinger.", "clipboardUnavailable": "Innholdet i det nåværende utklippet er ikke tilgjengelig eller støttet.", "clipboardTooLarge": "Denne utklippsteksten er for stor til å lagre.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "Søk etter dokumenter og innhold i Lokal Historikk.", "localHistoryNoDocuments": "Ingen dokumenter har Lokal Historikk ennå.", "localHistoryNoRevisions": "Dette dokumentet har ingen revisjoner i Lokal Historikk.", - "localHistoryNoMatches": "Ingen revisjoner matcher denne søken.", - "localHistorySearchHint": "Søk i lokal historikk", "localHistorySelectedRevision": "Valgt revisjon", "localHistoryCurrentEditor": "Nåværende innhold i redigereren", "localHistoryCurrentDisk": "Nåværende innhold på disken", @@ -3610,5 +3619,125 @@ "wsInsertElements": "Sett inn elementer", "wsAdmonitionsAndQuotes": "Merknader og sitater", "wsSemanticText": "Semantisk tekst", - "wsPropertiesUnavailable": "Det inspiserte elementet er utilgjengelig. Inspiser et element for å fortsette." + "wsPropertiesUnavailable": "Det inspiserte elementet er utilgjengelig. Inspiser et element for å fortsette.", + "searchDocumentsCategory": "Dokumenter", + "searchTopicsCategory": "Emner", + "searchCurrentDocument": "Gjeldende dokument", + "searchCurrentWorkspace": "Gjeldende arbeidsområde", + "browseNotes": "Bla gjennom notater", + "searchActionsCommand": "Søk etter handlinger", + "searchDocumentsCommand": "Søk etter dokumenter", + "searchTopicsCommand": "Søk etter emner", + "searchRecentCommand": "Søk i nylige", + "searchResourceUnavailable": "Denne ressursen er utilgjengelig.", + "notesSync": "Synkroniser notater", + "searchInWorkspace": "Søk i arbeidsområde", + "searchCategory": "Søkekategori", + "clearHistorySearchScope": "Søk i dokumenthistorikken", + "searchInDocument": "Søk i dokument", + "searchDocumentHint": "Søk i dette dokumentet…", + "searchWorkspaceHint": "Søk i dette arbeidsområdet…", + "searchOutlineHint": "Søk i overskrifter…", + "searchHistoryHint": "Søk i lokal historikk…", + "searchEnterText": "Skriv inn tekst for å søke", + "searchAccountUnavailable": "Konto utilgjengelig", + "searchDocumentsHint": "Søk etter dokumenter…", + "searchTopicsHint": "Søk etter emner…", + "searchRecentHint": "Søk i nylige…", + "searchActionsHint": "Søk etter handlinger…", + "searchRequiresNotesWorkspace": "Krever et notatarbeidsområde", + "searchSidebarLabel": "Søk:", + "searchDocumentTextCategory": "Dokumenttekst", + "searchWorkspaceTextCategory": "Arbeidsområdetekst", + "searchHeadingsCategory": "Overskrifter", + "shortcutUnassigned": "Ikke tilordnet", + "shortcutPressKeys": "Trykk en tastekombinasjon…", + "shortcutRecord": "Registrer hurtigtast", + "shortcutRestoreAll": "Gjenopprett alle standarder", + "shortcutRestoreAllQuestion": "Gjenopprette standardhurtigtaster for alle handlinger? Egne tilordninger fjernes.", + "shortcutSaveFailed": "Hurtigtastene er aktive, men kunne ikke lagres. Prøv igjen for å beholde dem etter omstart.", + "shortcutRetry": "Prøv lagring igjen", + "shortcutChanged": "Tilordningene er endret. Se gjennom gjeldende hurtigtaster og prøv igjen.", + "shortcutInvalid": "Denne kombinasjonen kan ikke tilordnes i denne sammenhengen.", + "shortcutSystemWarning": "Skrivebordet kan bruke denne kombinasjonen. BusyMark kan ikke overstyre systemhurtigtaster.", + "shortcutConflicts": "Denne hurtigtasten overlapper disse handlingene:", + "shortcutRemove": "Fjern tilordning", + "shortcutRestoreDefault": "Gjenopprett standard", + "shortcutAssign": "Tilordne", + "shortcutReassign": "Tilordne på nytt", + "shortcutContextApplication": "Program", + "shortcutContextDocument": "Dokumentvisninger", + "shortcutContextEditor": "Redigering og kilde", + "shortcutContextWritersideSource": "Writerside-kilde", + "shortcutContextWorkspace": "Arbeidsområdenavigasjon", + "shortcutContextTree": "Sidefelttrær", + "searchHeadingsCommand": "Søk i overskrifter", + "shortcutFixed": "Håndteres av redigereren; kan ikke tilpasses.", + "shortcutDefaultConflict": "Standardhurtigtasten er i konflikt med en egendefinert tilordning.", + "shortcutCurrentBinding": "Gjeldende hurtigtast", + "shortcutProposedBinding": "Foreslått hurtigtast", + "notesActions": "Notathandlinger", + "noteActions": "Handlinger for notatet", + "noteProperties": "Notategenskaper", + "notesOfflinePause": "Sett nedlastinger på pause", + "notesOfflineResume": "Fortsett nedlastinger", + "notesEmptyAll": "Ingen notater ennå. Opprett et notat for å begynne.", + "notesEmptyFavorites": "Ingen favorittnotater.", + "notesEmptyCategory": "Ingen notater i denne kategorien.", + "notesEmptyUncategorized": "Ingen ukategoriserte notater.", + "notesEmptyRecovery": "Ingen beholdte notater å gjenopprette.", + "notesStartingConnection": "Starter Nextcloud-tilkoblingen…", + "notesVerifyingConnection": "Kontrollerer tilgang til notater…", + "notesCancellingConnection": "Avbryter innlogging og rydder opp…", + "notesRemovingConnection": "Fjerner den lokale kontoen…", + "notesSettingsLeave": "Lagre endringer i Nextcloud-innstillinger før du går videre?", + "notesSettingsReload": "Last inn serverinnstillinger på nytt…", + "notesScanning": "Skanner filer…", + "notesCancelling": "Avbryter… Venter på at gjeldende handling avsluttes.", + "notesCancelled": "Avbrutt", + "notesPartial": "Delvis fullført", + "notesNotStarted": "Ikke startet", + "notesCreated": "Opprettet", + "notesCurrentText": "Gjeldende beholdte tekst", + "notesRecovering": "Gjenoppretter som et nytt notat…", + "notesSort": "Sorter notater", + "notesOfflineRetentionHelp": "Å fjerne dette kravet beholder eksisterende hurtigbuffer. Et kategori- eller kontokrav kan fortsatt beholde notatet.", + "notesOfflineBroader": "Notatet beholdes også av et kategori- eller kontokrav.", + "notesOfflineExternal": "Eksterne medier krever en tilkobling. BusyMark beholder bare notattekst og administrerte vedlegg.", + "notesOperationFailed": "Handlingen kunne ikke fullføres. Utkastet er beholdt. Kontroller lagring og konflikter, og prøv igjen.", + "notesRemovedWarning": "Den lokale Nextcloud-kontoen er fjernet. Tilbakekalling av app-passordet er ikke bekreftet. Fjern BusyMarks passord i sikkerhetsinnstillingene i Nextcloud.", + "notesBuffersNotSaved": "Ikke alle åpne notater kunne lagres lokalt. Kontroller lagring, konflikter eller normalisering i arbeidsområdet. Lokale endringer må kanskje fortsatt synkroniseres med Nextcloud.", + "notesSettingsReloadHelp": "Ny innlasting erstatter utkastet med gjeldende Nextcloud-innstillinger. Forkaste utkastet og laste inn på nytt?", + "notesSettingsRefreshFailed": "Nextcloud godtok innstillingene, men oppdatering av notatene mislyktes. Verdiene som vises er bekreftet av serveren. Prøv å oppdatere fra Notater igjen.", + "notesStorageFull": "Nextcloud-lagringen er full. Frigjør plass på serveren og prøv igjen.", + "notesRejected": "Nextcloud avviste endringen. Kontroller notatet og lagre en lokal kopi før du prøver igjen.", + "notesThrottled": "Nextcloud ba om en pause. Synkroniseringen fortsetter etter ventefristen.", + "notesImportPartialHelp": "Tidligere elementer kan allerede være lagret lokalt. Fortsett denne importen for å unngå duplikater.", + "notesExportAll": "Eksporter notater · Alle notater…", + "notesExportCategory": "Eksporter notater · Kategori…", + "notesOfflineAll": "Gjør tilgjengelig frakoblet · Alle notater", + "notesOfflineCategory": "Gjør tilgjengelig frakoblet · Kategori", + "notesOfflineRemoveAll": "Fjern frakoblet oppbevaring · Alle notater", + "notesOfflineRemoveCategory": "Fjern frakoblet oppbevaring · Kategori", + "notesOfflineKeep": "Gjør tilgjengelig frakoblet", + "notesLoadFailed": "Nextcloud Notes · Mislyktes. Prøv igjen", + "notesInspectionFailed": "Vedlegg · Mislyktes. Prøv igjen", + "notesSettingsLeaveHelp": "Notes-innstillinger på serveren · Ulagrede endringer", + "notesUnavailable": "Frakoblet · Tekst tilgjengelig lokalt · Lagret lokalt · venter på synkronisering", + "notesRecoveryRequired": "Konflikter · Sammenlign", + "notesTransferFailed": "Mislyktes. Prøv igjen", + "notesRemoteDeleteUnavailable": "Serveradresse · Ikke støttet · Slett · Vedlegg", + "notesReadOnlyHelp": "Skrivebeskyttet · Favoritter · Lagre lokal kopi…", + "notesPathRelative": "Notatmappens sti er relativ til Nextcloud-lagringen din.", + "notesSettingsStale": "Innstillingene er endret i Nextcloud. Last inn og kontroller serververdiene før du erstatter utkastet.", + "notesDownloadsPaused": "Nedlastinger satt på pause", + "notesExportNotPublished": "Ingen eksport ble publisert.", + "notesKeyringUnavailable": "Lås opp skrivebordets nøkkelring og prøv igjen.", + "notesConnectFailed": "Kunne ikke koble til Nextcloud Notes. Kontroller serveradressen og prøv igjen.", + "notesAccountMismatch": "Logg inn på samme Nextcloud-konto. Fjern denne kontoen først for å koble til en annen.", + "notesLocalRemovalFailed": "Lokal kontoopprydding mislyktes. Kontoen beholdes slik at du kan prøve å fjerne den igjen.", + "notesTransferring": "Overfører notater…", + "notesTechnicalDetails": "Tekniske detaljer", + "notesPauseRequirement": "Sett frakoblet krav på pause", + "notesResumeRequirement": "Fortsett frakoblet krav" } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 5729425b..e339a7c0 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nieuwe notitie van bewaarde tekst", + "notesAccountThrottled": "Nextcloud vraagt om een pauze. Wacht voordat u opnieuw aanmeldt.", + "notesSettingsRefreshing": "Notes vernieuwen na de bevestigde instellingenwijziging", + "notesExportOperationFailed": "De export is vóór publicatie mislukt. Controleer bron en bestemming en exporteer opnieuw.", + "notesRecoveryCreatedNotOpened": "De herstelde notitie is opgeslagen maar kon niet worden geopend. Los openstaande editor- of eigenschapswijzigingen op en open deze bestaande notitie.", + "notesSettingsPendingNotes": "Openstaande notities, conflicten of onzekere aanmaak blokkeren deze wijziging. Synchroniseer of beoordeel ze in Notes en probeer dit bewaarde concept opnieuw.", + "notesSettingsPendingAttachments": "Openstaande bijlagebewerkingen blokkeren deze wijziging. Beoordeel ze in Notes en probeer dit bewaarde instellingenconcept opnieuw.", + "notesAccountServerAddress": "Gebruik het canonieke HTTPS-adres van Nextcloud en meld opnieuw aan. Het adres of de omleiding kon niet worden geverifieerd.", + "notesAccountUnsupported": "Notes is niet beschikbaar of de API wordt niet ondersteund. Schakel Nextcloud Notes in met API-hoofdversie 1, subversie 4 of hoger.", + "notesAccountAuthorizeAgain": "Nextcloud heeft de aanmeldgegevens geweigerd. Autoriseer dit account opnieuw of verbind opnieuw.", + "notesAccountCheckForbidden": "Nextcloud heeft de controle van Notes-mogelijkheden geweigerd. Vraag toestemming aan de beheerder en probeer opnieuw.", + "notesAccountMalformed": "Het Nextcloud-antwoord kon niet worden geverifieerd. Controleer de serverinstallatie en meld opnieuw aan.", + "notesAccountTimedOut": "De aanmelding is verlopen. Begin opnieuw en voltooi de toestemming in de browser.", + "notesAccountBrowserUnavailable": "De systeembrowser kon niet worden geopend. Controleer de standaardbrowser en meld opnieuw aan.", + "notesAccountNetwork": "Nextcloud is onbereikbaar. Controleer de verbinding en server en meld opnieuw aan.", + "notesAccountRejected": "Nextcloud heeft het mogelijkhedenverzoek geweigerd. Controleer toegang met de beheerder en probeer opnieuw.", + "notesAccountFailed": "Accountinstelling is mislukt. Probeer opnieuw aan te melden.", + "notesTransferSourceChanged": "De bron is na beoordeling gewijzigd. Beoordeel het huidige bestand of maak een nieuwe momentopname.", + "notesTransferUnreadable": "De bron kon niet worden gelezen. Controleer toegang tot het bestand of de map en beoordeel opnieuw.", + "notesTransferInvalidSnapshot": "Ongeldige of niet-ondersteunde momentopname. Kies een geldige BusyMark Notes-momentopname of Markdown-bron.", + "notesTransferMediaLimit": "Media overschrijden de importlimiet. Verklein de media van het betreffende item en beoordeel opnieuw.", + "notesTransferUnsafeSource": "De bron bevat een onveilig pad. Kies bestanden binnen de bronmap en beoordeel opnieuw.", + "notesTransferDestination": "De export kon niet worden geschreven. Controleer toegang en vrije ruimte bij de bestemming en exporteer opnieuw.", + "notesTransferOperationFailed": "De overdracht is mislukt. Beoordeel de betreffende items en probeer opnieuw. Eerder geïmporteerde items blijven opgeslagen.", + "notesExportPublishedWithOmissions": "Export met weglatingen beschikbaar", + "notesBatchUnchanged": "De gevraagde waarden waren al ingesteld.", + "notesBatchReadOnly": "Deze notitie is alleen-lezen. Beveiligde eigenschappen kunnen niet wijzigen; Favoriet blijft toegestaan.", + "notesBatchReviewChanged": "Deze notitie is na beoordeling gewijzigd. Beoordeel de huidige staat voordat u de actie herhaalt.", + "notesBatchMissing": "Deze notitie is niet meer beschikbaar. Vernieuw de lijst en beoordeel de doelen opnieuw.", + "notesBatchStorage": "Lokaal opslaan is mislukt. Controleer de opslag en probeer dit item opnieuw; geslaagde items blijven opgeslagen.", + "notesRemoteReviewChanged": "De servernotitie is gewijzigd. Uw samenvoegconcept blijft bewaard. Beoordeel de nieuwe vergelijking en kies opnieuw een oplossing.", + "notesReviewRemote": "Ik heb de huidige serverversie beoordeeld", "wsModuleCapsNotPublished": "Hoofdlettergebruik van de module wordt niet gepubliceerd.", "wsTitleOverridesAction": "Instantie- en inhoudsopgavetitels…", "wsHeadingNotPublished": "Wordt niet gepubliceerd door deze bouwer.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Datum", "pdfTitlePageTitleRequired": "Voer een titel voor de titelpagina in.", "pdfTocSectionNote": "Begint op een aparte pagina en kan meerdere pagina’s beslaan.", - "sourceSearchZeroLengthUnsupported": "Regex-overeenkomsten met lengte nul worden niet ondersteund (zoals ^, $ of lookaheads die alleen een positie vinden).", "workspaceSearchIncomplete": "Slechts een deel van de resultaten wordt getoond. Toon meer of verfijn de zoekopdracht.", "workspaceSearchShowMore": "Meer resultaten tonen", "workspaceSearchSkippedFiles": "De zoekopdracht is onvolledig. Deze bestanden zijn groter dan 1 MiB of konden niet worden gelezen:", @@ -423,18 +454,6 @@ "@keyboardShortcuts": { "description": "Keyboard shortcuts dialog title and menu item." }, - "commandPalette": "Commandopalet", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "Typ een opdracht", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "Geen overeenkomende opdrachten", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "Niet beschikbaar in de huidige editorcontext", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -715,7 +734,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Toon deze sneltoetsreferentie", + "shortcutKeyboardShortcutsDescription": "Sneltoetsen van de toepassing aanpassen", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1012,7 +1031,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Wis recente werkruimten", + "clearRecentWorkspaces": "Recente geschiedenis wissen", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1943,18 +1962,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Hoofdlettergevoelig", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Heel woord", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Regex", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "Vervangen door", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2138,10 +2145,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "Ongeldige reguliere expressie", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Groot bestand: markeren en vouwen worden gepauzeerd", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2281,6 +2284,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Recente items konden niet worden bijgewerkt: {error}.", "workspaceErrorOpenFailed": "Openen mislukt: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4864,14 +4868,12 @@ "exportInvalidCss": "Kies een leesbaar UTF-8 .css-bestand tot 256 KiB zonder imports, bron-URL’s, HTML of uitvoerbare of ontsnapte CSS.", "exportInvalidRange": "{field}: voer een getal tussen {minimum} en {maximum} in.", "clipboardHistory": "Historie van de klembord", - "clipboardHistorySearchHint": "Klembordgeschiedenis doorzoeken", "clipboardHistoryDescription": "Open de klembordgeschiedenis per sessie.", "clipboardCurrent": "Huidig klembord", "clipboardCurrentExternal": "Huidig klembord van een ander programma", "clipboardDestination": "Plakken in {name}", "clipboardSessionOnly": "Alleen opgeslagen voor deze BusyMark sessie.", "clipboardNoItems": "De klembordgeschiedenis is leeg.", - "clipboardNoMatches": "Geen klemborditems komen overeen met deze zoekopdracht.", "clipboardDisabled": "De collectie \"Historie van het klembord\" is uitgeschakeld in de instellingen.", "clipboardUnavailable": "De inhoud van het huidige klembord is niet beschikbaar of niet ondersteund.", "clipboardTooLarge": "Dit item op het klembord is te groot om te bewaren.", @@ -4888,8 +4890,6 @@ "findLocalHistoryDescription": "Zoek naar documenten en inhoud van revisies in de lokale geschiedenis.", "localHistoryNoDocuments": "Er zijn nog geen documenten met Lokale Geschiedenis.", "localHistoryNoRevisions": "Dit document heeft geen revisies van de Lokale Geschiedenis.", - "localHistoryNoMatches": "Geen revisies van de Lokale Geschiedenis komen overeen met deze zoekopdracht.", - "localHistorySearchHint": "Lokale geschiedenis doorzoeken", "localHistorySelectedRevision": "Geselecteerde revisie", "localHistoryCurrentEditor": "Huidige inhoud van de editor", "localHistoryCurrentDisk": "Huidige inhoud op schijf", @@ -5149,5 +5149,125 @@ "wsInsertElements": "Elementen invoegen", "wsAdmonitionsAndQuotes": "Opmerkingen en citaten", "wsSemanticText": "Semantische tekst", - "wsPropertiesUnavailable": "Het geïnspecteerde element is niet beschikbaar. Inspecteer een element om door te gaan." + "wsPropertiesUnavailable": "Het geïnspecteerde element is niet beschikbaar. Inspecteer een element om door te gaan.", + "searchDocumentsCategory": "Documenten", + "searchTopicsCategory": "Onderwerpen", + "searchCurrentDocument": "Huidig document", + "searchCurrentWorkspace": "Huidige werkruimte", + "browseNotes": "Notities bekijken", + "searchActionsCommand": "Acties zoeken", + "searchDocumentsCommand": "Documenten zoeken", + "searchTopicsCommand": "Onderwerpen zoeken", + "searchRecentCommand": "Recente items zoeken", + "searchResourceUnavailable": "Deze bron is niet beschikbaar.", + "notesSync": "Notities synchroniseren", + "searchInWorkspace": "Zoeken in werkruimte", + "searchCategory": "Zoekcategorie", + "clearHistorySearchScope": "Zoeken in documentgeschiedenis", + "searchInDocument": "Zoeken in document", + "searchDocumentHint": "Dit document doorzoeken…", + "searchWorkspaceHint": "Deze werkruimte doorzoeken…", + "searchOutlineHint": "Koppen zoeken…", + "searchHistoryHint": "Lokale geschiedenis doorzoeken…", + "searchEnterText": "Voer tekst in om te zoeken", + "searchAccountUnavailable": "Account niet beschikbaar", + "searchDocumentsHint": "Documenten zoeken…", + "searchTopicsHint": "Onderwerpen zoeken…", + "searchRecentHint": "Recente items zoeken…", + "searchActionsHint": "Acties zoeken…", + "searchRequiresNotesWorkspace": "Vereist een notitiewerkruimte", + "searchSidebarLabel": "Zoeken:", + "searchDocumentTextCategory": "Documenttekst", + "searchWorkspaceTextCategory": "Werkruimtetekst", + "searchHeadingsCategory": "Koppen", + "shortcutUnassigned": "Niet toegewezen", + "shortcutPressKeys": "Druk op een toetscombinatie…", + "shortcutRecord": "Sneltoets opnemen", + "shortcutRestoreAll": "Alle standaardwaarden herstellen", + "shortcutRestoreAllQuestion": "Standaardsneltoetsen van alle acties herstellen? Aangepaste toewijzingen worden verwijderd.", + "shortcutSaveFailed": "Sneltoetsen zijn actief, maar konden niet worden opgeslagen. Probeer opnieuw om ze na herstart te behouden.", + "shortcutRetry": "Opslaan opnieuw proberen", + "shortcutChanged": "Toewijzingen zijn gewijzigd. Controleer de huidige sneltoetsen en probeer opnieuw.", + "shortcutInvalid": "Deze combinatie kan in deze context niet worden toegewezen.", + "shortcutSystemWarning": "Deze combinatie wordt mogelijk door uw bureaublad gebruikt. BusyMark kan systeemsneltoetsen niet overschrijven.", + "shortcutConflicts": "Deze sneltoets overlapt met deze acties:", + "shortcutRemove": "Toewijzing verwijderen", + "shortcutRestoreDefault": "Standaard herstellen", + "shortcutAssign": "Toewijzen", + "shortcutReassign": "Opnieuw toewijzen", + "shortcutContextApplication": "Toepassing", + "shortcutContextDocument": "Documentweergaven", + "shortcutContextEditor": "Editor en bron", + "shortcutContextWritersideSource": "Writerside-bron", + "shortcutContextWorkspace": "Werkruimtenavigatie", + "shortcutContextTree": "Zijbalkbomen", + "searchHeadingsCommand": "Koppen zoeken", + "shortcutFixed": "Beheerd door de editor; niet instelbaar.", + "shortcutDefaultConflict": "De standaardsneltoets conflicteert met een aangepaste toewijzing.", + "shortcutCurrentBinding": "Huidige sneltoets", + "shortcutProposedBinding": "Voorgestelde sneltoets", + "notesActions": "Notitieacties", + "noteActions": "Acties voor deze notitie", + "noteProperties": "Notitie-eigenschappen", + "notesOfflinePause": "Downloads pauzeren", + "notesOfflineResume": "Downloads hervatten", + "notesEmptyAll": "Nog geen notities. Maak een notitie om te beginnen.", + "notesEmptyFavorites": "Geen favoriete notities.", + "notesEmptyCategory": "Geen notities in deze categorie.", + "notesEmptyUncategorized": "Geen notities zonder categorie.", + "notesEmptyRecovery": "Geen bewaarde notities om te herstellen.", + "notesStartingConnection": "Nextcloud-verbinding starten…", + "notesVerifyingConnection": "Toegang tot Notities controleren…", + "notesCancellingConnection": "Aanmelden annuleren en opruimen…", + "notesRemovingConnection": "Lokaal account verwijderen…", + "notesSettingsLeave": "Wijzigingen in Nextcloud-instellingen opslaan voor vertrek?", + "notesSettingsReload": "Serverinstellingen opnieuw laden…", + "notesScanning": "Bestanden scannen…", + "notesCancelling": "Annuleren… Wachten tot de huidige bewerking is afgerond.", + "notesCancelled": "Geannuleerd", + "notesPartial": "Gedeeltelijk voltooid", + "notesNotStarted": "Niet gestart", + "notesCreated": "Gemaakt", + "notesCurrentText": "Huidige bewaarde tekst", + "notesRecovering": "Herstellen als nieuwe notitie…", + "notesSort": "Notities sorteren", + "notesOfflineRetentionHelp": "Het verwijderen van deze vereiste behoudt de cache. Een categorie- of accountvereiste kan de notitie nog steeds bewaren.", + "notesOfflineBroader": "Deze notitie wordt ook bewaard door een categorie- of accountvereiste.", + "notesOfflineExternal": "Externe media vereisen een verbinding. BusyMark bewaart alleen notitietekst en beheerde bijlagen.", + "notesOperationFailed": "De bewerking kon niet worden voltooid. Uw concept blijft bewaard. Controleer opslaan en conflicten en probeer opnieuw.", + "notesRemovedWarning": "Het lokale Nextcloud-account is verwijderd. Het intrekken van het app-wachtwoord is niet bevestigd. Verwijder het wachtwoord van BusyMark in de beveiligingsinstellingen van Nextcloud.", + "notesBuffersNotSaved": "Niet alle open notities konden lokaal worden opgeslagen. Controleer opslaan, conflicten of normalisatie in de werkruimte. Lokale wijzigingen moeten mogelijk nog met Nextcloud synchroniseren.", + "notesSettingsReloadHelp": "Opnieuw laden vervangt uw concept door de huidige Nextcloud-instellingen. Concept verwerpen en opnieuw laden?", + "notesSettingsRefreshFailed": "Nextcloud heeft de instellingen geaccepteerd, maar het vernieuwen van notities is mislukt. De getoonde waarden zijn door de server bevestigd. Vernieuw opnieuw vanuit Notities.", + "notesStorageFull": "De Nextcloud-opslag is vol. Maak ruimte vrij op de server en probeer opnieuw.", + "notesRejected": "Nextcloud heeft deze wijziging geweigerd. Controleer de notitie en sla een lokale kopie op voordat u opnieuw probeert.", + "notesThrottled": "Nextcloud heeft gevraagd te wachten. Synchronisatie gaat verder na de wachttijd.", + "notesImportPartialHelp": "Eerdere items kunnen al lokaal opgeslagen zijn. Hervat deze import om zonder duplicaten verder te gaan.", + "notesExportAll": "Notities exporteren · Alle notities…", + "notesExportCategory": "Notities exporteren · Categorie…", + "notesOfflineAll": "Offline beschikbaar maken · Alle notities", + "notesOfflineCategory": "Offline beschikbaar maken · Categorie", + "notesOfflineRemoveAll": "Offlinebewaring verwijderen · Alle notities", + "notesOfflineRemoveCategory": "Offlinebewaring verwijderen · Categorie", + "notesOfflineKeep": "Offline beschikbaar maken", + "notesLoadFailed": "Nextcloud Notes · Mislukt. Opnieuw proberen", + "notesInspectionFailed": "Bijlagen · Mislukt. Opnieuw proberen", + "notesSettingsLeaveHelp": "Notes-instellingen op de server · Niet-opgeslagen wijzigingen", + "notesUnavailable": "Geen verbinding · Tekst lokaal beschikbaar · Lokaal opgeslagen · synchronisatie in behandeling", + "notesRecoveryRequired": "Conflicten · Vergelijken", + "notesTransferFailed": "Mislukt. Opnieuw proberen", + "notesRemoteDeleteUnavailable": "Server-URL · Niet ondersteund · Verwijderen · Bijlagen", + "notesReadOnlyHelp": "Alleen lezen · Favorieten · Lokale kopie opslaan…", + "notesPathRelative": "Het pad van de notitiemap is relatief aan uw Nextcloud-opslag.", + "notesSettingsStale": "De instellingen zijn gewijzigd op Nextcloud. Laad en controleer de serverwaarden voordat u het concept vervangt.", + "notesDownloadsPaused": "Downloads gepauzeerd", + "notesExportNotPublished": "Er is geen export gepubliceerd.", + "notesKeyringUnavailable": "Ontgrendel de bureaubladsleutelbos en probeer opnieuw.", + "notesConnectFailed": "Kan niet verbinden met Nextcloud Notes. Controleer het serveradres en probeer opnieuw.", + "notesAccountMismatch": "Meld u aan bij hetzelfde Nextcloud-account. Verwijder dit account eerst om een ander te verbinden.", + "notesLocalRemovalFailed": "Het lokaal opruimen is mislukt. Het account blijft bewaard zodat u de verwijdering opnieuw kunt proberen.", + "notesTransferring": "Notities overdragen…", + "notesTechnicalDetails": "Technische details", + "notesPauseRequirement": "Offlinevereiste pauzeren", + "notesResumeRequirement": "Offlinevereiste hervatten" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d481fa81..db3a6941 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nowa notatka z zachowanego tekstu", + "notesAccountThrottled": "Nextcloud prosi o przerwę. Zaczekaj przed ponownym logowaniem.", + "notesSettingsRefreshing": "Odświeżanie Notes po potwierdzeniu ustawień", + "notesExportOperationFailed": "Eksport nie został opublikowany. Sprawdź źródło i miejsce docelowe, a następnie ponów.", + "notesRecoveryCreatedNotOpened": "Odzyskana notatka została zapisana, ale nie otwarta. Rozwiąż oczekujące zmiany edytora lub właściwości i otwórz tę istniejącą notatkę.", + "notesSettingsPendingNotes": "Oczekujące notatki, konflikty lub niepewne utworzenia blokują zmianę. Zsynchronizuj lub sprawdź je w Notes, potem ponów zapis zachowanej wersji roboczej ustawień.", + "notesSettingsPendingAttachments": "Operacje załączników blokują zmianę. Sprawdź załączniki w Notes, potem ponów zapis zachowanej wersji roboczej ustawień.", + "notesAccountServerAddress": "Użyj kanonicznego adresu HTTPS Nextcloud i zaloguj się ponownie. Nie udało się zweryfikować adresu lub przekierowania.", + "notesAccountUnsupported": "Notes jest niedostępne lub API nieobsługiwane. Włącz Nextcloud Notes z główną wersją API 1 i podrzędną co najmniej 4.", + "notesAccountAuthorizeAgain": "Nextcloud odrzucił dane logowania. Ponownie autoryzuj konto lub połącz je.", + "notesAccountCheckForbidden": "Nextcloud zabronił sprawdzenia możliwości Notes. Poproś administratora o zgodę i ponów.", + "notesAccountMalformed": "Nie udało się zweryfikować odpowiedzi Nextcloud. Sprawdź instalację serwera i zaloguj się ponownie.", + "notesAccountTimedOut": "Czas logowania upłynął. Rozpocznij ponownie i dokończ autoryzację w przeglądarce.", + "notesAccountBrowserUnavailable": "Nie udało się otworzyć przeglądarki systemowej. Sprawdź domyślną przeglądarkę i ponów logowanie.", + "notesAccountNetwork": "Nextcloud jest nieosiągalny. Sprawdź połączenie i serwer, a następnie ponów logowanie.", + "notesAccountRejected": "Nextcloud odrzucił żądanie możliwości. Sprawdź dostęp z administratorem i ponów.", + "notesAccountFailed": "Nie udało się skonfigurować konta. Zaloguj się ponownie.", + "notesTransferSourceChanged": "Źródło zmieniło się po przeglądzie. Sprawdź bieżący plik lub utwórz nową migawkę przed ponowieniem.", + "notesTransferUnreadable": "Nie można odczytać źródła. Sprawdź dostęp do pliku lub katalogu i przejrzyj ponownie.", + "notesTransferInvalidSnapshot": "Migawka jest nieprawidłowa lub nieobsługiwana. Wybierz poprawną migawkę BusyMark Notes lub źródło Markdown.", + "notesTransferMediaLimit": "Media przekraczają limit importu. Zmniejsz media danego elementu i przejrzyj ponownie.", + "notesTransferUnsafeSource": "Źródło zawiera niebezpieczną ścieżkę. Wybierz pliki wewnątrz katalogu źródłowego i przejrzyj ponownie.", + "notesTransferDestination": "Nie można zapisać eksportu. Sprawdź dostęp i wolne miejsce w miejscu docelowym i ponów.", + "notesTransferOperationFailed": "Transfer nie został ukończony. Sprawdź elementy i ponów. Wcześniej zaimportowane elementy pozostają zapisane.", + "notesExportPublishedWithOmissions": "Eksport dostępny z pominięciami", + "notesBatchUnchanged": "Żądane wartości były już ustawione.", + "notesBatchReadOnly": "Notatka jest tylko do odczytu. Nie można zmieniać chronionych właściwości; Ulubione pozostaje dozwolone.", + "notesBatchReviewChanged": "Notatka zmieniła się po przeglądzie. Sprawdź jej aktualny stan przed ponowieniem działania.", + "notesBatchMissing": "Notatka nie jest już dostępna. Odśwież listę i sprawdź wybrane elementy.", + "notesBatchStorage": "Zapis lokalny nie powiódł się. Sprawdź pamięć i ponów ten element; udane elementy są zapisane.", + "notesRemoteReviewChanged": "Notatka na serwerze się zmieniła. Wersja robocza scalania jest zachowana. Sprawdź nowe porównanie i ponownie wybierz rozwiązanie.", + "notesReviewRemote": "Sprawdzono aktualną wersję na serwerze", "wsModuleCapsNotPublished": "Reguła wielkości liter modułu nie jest publikowana.", "wsTitleOverridesAction": "Tytuły instancji i spisu treści…", "wsHeadingNotPublished": "Ten generator tego nie publikuje.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Data", "pdfTitlePageTitleRequired": "Wpisz tytuł strony tytułowej.", "pdfTocSectionNote": "Zaczyna się na osobnej stronie i może zajmować wiele stron.", - "sourceSearchZeroLengthUnsupported": "Dopasowania wyrażeń regularnych o zerowej długości nie są obsługiwane (np. ^, $ lub asercje dopasowujące tylko pozycję).", "workspaceSearchIncomplete": "Wyświetlono tylko część wyników. Pokaż więcej lub zawęź wyszukiwanie.", "workspaceSearchShowMore": "Pokaż więcej wyników", "workspaceSearchSkippedFiles": "Wyszukiwanie jest niepełne. Te pliki przekraczają 1 MiB lub nie można ich odczytać:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Pokaż ten wykaz skrótów klawiaturowych", + "shortcutKeyboardShortcutsDescription": "Dostosuj skróty klawiaturowe aplikacji", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Wyczyść listę ostatnich obszarów roboczych", + "clearRecentWorkspaces": "Wyczyść ostatnią historię", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Uwzględniaj wielkość liter", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Całe słowo", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Wyrażenie regularne", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Nieprawidłowe wyrażenie regularne", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Duży plik: podświetlanie i zwijanie są wstrzymane", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Nie udało się zaktualizować ostatnich elementów: {error}.", "workspaceErrorOpenFailed": "Otwieranie nie powiodło się: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3234,9 +3250,6 @@ "diagnosticMarkdownLinkEmptyText": "Tekst odnośnika jest pusty; podaj dostępną nazwę opisującą jego cel.", "diagnosticMarkdownLinkReviewText": "Sprawdź, czy tekst odnośnika „{text}” opisuje jego cel w kontekście.", "diagnosticMarkdownTableEmptyHeader": "Nagłówki tabeli muszą identyfikować kolumny; uzupełnij każdy pusty nagłówek.", - "commandPalette": "Paleta poleceń", - "commandPaletteHint": "Wpisz polecenie", - "commandPaletteEmpty": "Brak pasujących poleceń", "tableAlignmentUnspecified": "Wyrównanie: nieokreślone", "tableAlignmentLeft": "Wyrównanie: do lewej", "tableAlignmentCenter": "Wyrównanie: do środka", @@ -3351,14 +3364,12 @@ "exportInvalidCss": "Wybierz czytelny plik .css UTF-8 do 256 KiB, bez importów, adresów zasobów, HTML oraz wykonywalnego CSS i sekwencji ucieczki.", "exportInvalidRange": "{field}: wpisz liczbę między {minimum} a {maximum}.", "clipboardHistory": "Historia w Koszyku", - "clipboardHistorySearchHint": "Przeszukaj historię schowka", "clipboardHistoryDescription": "Otwórz historię w koszyku lokalną dla sesji.", "clipboardCurrent": "Aktualny koszyk", "clipboardCurrentExternal": "Aktualny koszyk z innego programu", "clipboardDestination": "Wklej do {name}", "clipboardSessionOnly": "Przechowywane tylko dla tej sesji BusyMark.", "clipboardNoItems": "Historia w koszyku jest pusta.", - "clipboardNoMatches": "Żadne elementy w koszyku nie pasują do tego wyszukiwania.", "clipboardDisabled": "Zbiór \"Historia w koszyku\" wyłączony w Ustawieniach.", "clipboardUnavailable": "Aktualny zawartość koszyka niedostępna lub nieobsługiwana.", "clipboardTooLarge": "Ten element schowka jest zbyt duży, aby go zapisać.", @@ -3375,8 +3386,6 @@ "findLocalHistoryDescription": "Wyszukaj dokumenty i zawartość wersji w Lokalnej Historii.", "localHistoryNoDocuments": "Żaden dokument nie ma Lokalnej Historii.", "localHistoryNoRevisions": "Ten dokument nie ma wersji w Lokalnej Historii.", - "localHistoryNoMatches": "Żadne wersje nie pasują do tego wyszukiwania.", - "localHistorySearchHint": "Przeszukaj historię lokalną", "localHistorySelectedRevision": "Wybrana wersja", "localHistoryCurrentEditor": "Aktualny zawartość edytora", "localHistoryCurrentDisk": "Aktualna zawartość na dysku", @@ -3636,5 +3645,125 @@ "wsInsertElements": "Wstaw elementy", "wsAdmonitionsAndQuotes": "Uwagi i cytaty", "wsSemanticText": "Tekst semantyczny", - "wsPropertiesUnavailable": "Sprawdzany element jest niedostępny. Sprawdź element, aby kontynuować." + "wsPropertiesUnavailable": "Sprawdzany element jest niedostępny. Sprawdź element, aby kontynuować.", + "searchDocumentsCategory": "Dokumenty", + "searchTopicsCategory": "Tematy", + "searchCurrentDocument": "Bieżący dokument", + "searchCurrentWorkspace": "Bieżący obszar roboczy", + "browseNotes": "Przeglądaj notatki", + "searchActionsCommand": "Szukaj działań", + "searchDocumentsCommand": "Szukaj dokumentów", + "searchTopicsCommand": "Szukaj tematów", + "searchRecentCommand": "Szukaj ostatnich", + "searchResourceUnavailable": "Ten zasób jest niedostępny.", + "notesSync": "Synchronizuj notatki", + "searchInWorkspace": "Szukaj w obszarze roboczym", + "searchCategory": "Kategoria wyszukiwania", + "clearHistorySearchScope": "Szukaj w historii dokumentów", + "searchInDocument": "Szukaj w dokumencie", + "searchDocumentHint": "Przeszukaj ten dokument…", + "searchWorkspaceHint": "Przeszukaj ten obszar roboczy…", + "searchOutlineHint": "Szukaj nagłówków…", + "searchHistoryHint": "Przeszukaj historię lokalną…", + "searchEnterText": "Wpisz tekst do wyszukania", + "searchAccountUnavailable": "Konto niedostępne", + "searchDocumentsHint": "Szukaj dokumentów…", + "searchTopicsHint": "Szukaj tematów…", + "searchRecentHint": "Szukaj ostatnich…", + "searchActionsHint": "Szukaj działań…", + "searchRequiresNotesWorkspace": "Wymaga obszaru roboczego Notatek", + "searchSidebarLabel": "Szukaj:", + "searchDocumentTextCategory": "Tekst dokumentu", + "searchWorkspaceTextCategory": "Tekst obszaru roboczego", + "searchHeadingsCategory": "Nagłówki", + "shortcutUnassigned": "Nieprzypisany", + "shortcutPressKeys": "Naciśnij kombinację klawiszy…", + "shortcutRecord": "Zarejestruj skrót", + "shortcutRestoreAll": "Przywróć wszystkie domyślne", + "shortcutRestoreAllQuestion": "Przywrócić domyślne skróty wszystkich działań? Własne przypisania zostaną usunięte.", + "shortcutSaveFailed": "Skróty są aktywne, lecz nie udało się ich zapisać. Spróbuj ponownie, aby zachować je po restarcie.", + "shortcutRetry": "Ponów zapis", + "shortcutChanged": "Przypisania uległy zmianie. Sprawdź bieżące skróty i spróbuj ponownie.", + "shortcutInvalid": "Nie można przypisać tej kombinacji w tym kontekście.", + "shortcutSystemWarning": "Pulpit może używać tej kombinacji. BusyMark nie może zastępować skrótów systemowych.", + "shortcutConflicts": "Ten skrót koliduje z tymi działaniami:", + "shortcutRemove": "Usuń przypisanie", + "shortcutRestoreDefault": "Przywróć domyślny", + "shortcutAssign": "Przypisz", + "shortcutReassign": "Przypisz ponownie", + "shortcutContextApplication": "Aplikacja", + "shortcutContextDocument": "Widoki dokumentu", + "shortcutContextEditor": "Edytor i źródło", + "shortcutContextWritersideSource": "Źródło Writerside", + "shortcutContextWorkspace": "Nawigacja obszaru roboczego", + "shortcutContextTree": "Drzewa paska bocznego", + "searchHeadingsCommand": "Szukaj nagłówków", + "shortcutFixed": "Zarządzane przez edytor; nie można skonfigurować.", + "shortcutDefaultConflict": "Domyślny skrót koliduje z niestandardowym przypisaniem.", + "shortcutCurrentBinding": "Bieżący skrót", + "shortcutProposedBinding": "Proponowany skrót", + "notesActions": "Działania na notatkach", + "noteActions": "Działania na notatce", + "noteProperties": "Właściwości notatki", + "notesOfflinePause": "Wstrzymaj pobieranie", + "notesOfflineResume": "Wznów pobieranie", + "notesEmptyAll": "Nie ma jeszcze notatek. Utwórz pierwszą.", + "notesEmptyFavorites": "Brak ulubionych notatek.", + "notesEmptyCategory": "Brak notatek w tej kategorii.", + "notesEmptyUncategorized": "Brak notatek bez kategorii.", + "notesEmptyRecovery": "Brak zachowanych notatek do odzyskania.", + "notesStartingConnection": "Rozpoczynanie połączenia z Nextcloud…", + "notesVerifyingConnection": "Weryfikowanie dostępu do notatek…", + "notesCancellingConnection": "Anulowanie logowania i sprzątanie…", + "notesRemovingConnection": "Usuwanie konta lokalnego…", + "notesSettingsLeave": "Zapisać zmiany ustawień Nextcloud przed wyjściem?", + "notesSettingsReload": "Wczytaj ponownie ustawienia serwera…", + "notesScanning": "Skanowanie plików…", + "notesCancelling": "Anulowanie… Oczekiwanie na zakończenie bieżącej operacji.", + "notesCancelled": "Anulowano", + "notesPartial": "Częściowo ukończono", + "notesNotStarted": "Nie rozpoczęto", + "notesCreated": "Utworzono", + "notesCurrentText": "Aktualnie zachowany tekst", + "notesRecovering": "Odzyskiwanie jako nowa notatka…", + "notesSort": "Sortuj notatki", + "notesOfflineRetentionHelp": "Usunięcie tego wymagania zachowuje zawartość pamięci podręcznej. Wymaganie kategorii lub konta może nadal zachowywać notatkę.", + "notesOfflineBroader": "Ta notatka jest też zachowywana przez wymaganie kategorii lub konta.", + "notesOfflineExternal": "Zewnętrzne multimedia wymagają połączenia. BusyMark zachowuje tylko tekst i zarządzane załączniki.", + "notesOperationFailed": "Operacja nie została ukończona. Szkic jest zachowany. Sprawdź zapisywanie i konflikty, a potem spróbuj ponownie.", + "notesRemovedWarning": "Lokalne konto Nextcloud usunięto. Nie potwierdzono unieważnienia hasła aplikacji. Usuń hasło BusyMark w ustawieniach bezpieczeństwa Nextcloud.", + "notesBuffersNotSaved": "Nie wszystkie otwarte notatki zapisano lokalnie. Sprawdź zapisywanie, konflikty lub normalizację w obszarze roboczym. Lokalne zmiany mogą nadal wymagać synchronizacji z Nextcloud.", + "notesSettingsReloadHelp": "Ponowne wczytanie zastąpi szkic aktualnymi ustawieniami Nextcloud. Odrzucić szkic i wczytać ponownie?", + "notesSettingsRefreshFailed": "Nextcloud zaakceptował ustawienia, lecz odświeżenie notatek nie powiodło się. Pokazano wartości potwierdzone przez serwer. Ponów odświeżenie z Notatek.", + "notesStorageFull": "Pamięć Nextcloud jest pełna. Zwolnij miejsce na serwerze i spróbuj ponownie.", + "notesRejected": "Nextcloud odrzucił tę zmianę. Sprawdź notatkę i zapisz lokalną kopię przed ponowieniem.", + "notesThrottled": "Nextcloud zażądał oczekiwania. Synchronizacja wznowi się po upływie wyznaczonego czasu.", + "notesImportPartialHelp": "Poprzednie elementy mogą już być zapisane lokalnie. Wznów ten import, aby kontynuować bez duplikatów.", + "notesExportAll": "Eksportuj notatki · Wszystkie notatki…", + "notesExportCategory": "Eksportuj notatki · Kategoria…", + "notesOfflineAll": "Udostępnij offline · Wszystkie notatki", + "notesOfflineCategory": "Udostępnij offline · Kategoria", + "notesOfflineRemoveAll": "Usuń przechowywanie offline · Wszystkie notatki", + "notesOfflineRemoveCategory": "Usuń przechowywanie offline · Kategoria", + "notesOfflineKeep": "Udostępnij offline", + "notesLoadFailed": "Nextcloud Notes · Niepowodzenie. Spróbuj ponownie", + "notesInspectionFailed": "Załączniki · Niepowodzenie. Spróbuj ponownie", + "notesSettingsLeaveHelp": "Ustawienia Notes na serwerze · Niezapisane zmiany", + "notesUnavailable": "Bez połączenia · Tekst dostępny lokalnie · Zapisano lokalnie · oczekuje na synchronizację", + "notesRecoveryRequired": "Konflikty · Porównaj", + "notesTransferFailed": "Niepowodzenie. Spróbuj ponownie", + "notesRemoteDeleteUnavailable": "Adres serwera · Nieobsługiwane · Usuń · Załączniki", + "notesReadOnlyHelp": "Tylko do odczytu · Ulubione · Zapisz kopię lokalną…", + "notesPathRelative": "Ścieżka folderu notatek jest względna wobec Twojego magazynu Nextcloud.", + "notesSettingsStale": "Ustawienia zmieniły się w Nextcloud. Wczytaj i sprawdź wartości serwera przed zastąpieniem szkicu.", + "notesDownloadsPaused": "Pobieranie wstrzymane", + "notesExportNotPublished": "Nie opublikowano eksportu.", + "notesKeyringUnavailable": "Odblokuj systemowy pęk kluczy i spróbuj ponownie.", + "notesConnectFailed": "Nie można połączyć się z Nextcloud Notes. Sprawdź adres serwera i spróbuj ponownie.", + "notesAccountMismatch": "Zaloguj się na to samo konto Nextcloud. Najpierw usuń to konto, aby połączyć inne.", + "notesLocalRemovalFailed": "Lokalne czyszczenie konta nie powiodło się. Konto zachowano, aby ponowić usuwanie.", + "notesTransferring": "Przenoszenie notatek…", + "notesTechnicalDetails": "Szczegóły techniczne", + "notesPauseRequirement": "Wstrzymaj wymaganie offline", + "notesResumeRequirement": "Wznów wymaganie offline" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8e5c7ee5..df6d18c4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nova nota a partir do texto preservado", + "notesAccountThrottled": "Nextcloud pediu uma pausa. Aguarde antes de voltar a autenticar-se.", + "notesSettingsRefreshing": "A atualizar Notes após a confirmação das definições", + "notesExportOperationFailed": "A exportação falhou antes da publicação. Verifique a origem e o destino e volte a exportar.", + "notesRecoveryCreatedNotOpened": "A nota recuperada foi guardada, mas não abriu. Resolva as alterações pendentes no editor ou nas propriedades e abra esta nota existente.", + "notesSettingsPendingNotes": "Notas pendentes, conflitos ou criações incertas impedem esta alteração. Sincronize ou reveja em Notes e volte a guardar este rascunho preservado.", + "notesSettingsPendingAttachments": "Operações de anexos impedem esta alteração. Reveja os anexos em Notes e volte a guardar este rascunho preservado.", + "notesAccountServerAddress": "Use o endereço HTTPS canónico do Nextcloud e reinicie a autenticação. Não foi possível verificar o endereço ou o redirecionamento.", + "notesAccountUnsupported": "Notes está indisponível ou a API não é suportada. Ative Nextcloud Notes com API principal 1 e secundária 4 ou posterior.", + "notesAccountAuthorizeAgain": "Nextcloud rejeitou as credenciais. Autorize novamente a conta ou volte a ligá-la.", + "notesAccountCheckForbidden": "Nextcloud recusou a verificação de funcionalidades de Notes. Peça autorização ao administrador e tente novamente.", + "notesAccountMalformed": "Não foi possível verificar a resposta do Nextcloud. Verifique a instalação do servidor e reinicie a autenticação.", + "notesAccountTimedOut": "A autenticação expirou. Reinicie-a e conclua a autorização no navegador.", + "notesAccountBrowserUnavailable": "Não foi possível abrir o navegador do sistema. Verifique o navegador predefinido e reinicie a autenticação.", + "notesAccountNetwork": "Nextcloud está inacessível. Verifique a ligação e o servidor e tente autenticar-se novamente.", + "notesAccountRejected": "Nextcloud rejeitou o pedido de funcionalidades. Verifique o acesso com o administrador e tente novamente.", + "notesAccountFailed": "Não foi possível configurar a conta. Tente autenticar-se novamente.", + "notesTransferSourceChanged": "A origem mudou desde a revisão. Reveja o ficheiro atual ou capture uma nova imagem antes de tentar novamente.", + "notesTransferUnreadable": "Não foi possível ler a origem. Verifique o acesso ao ficheiro ou pasta e reveja.", + "notesTransferInvalidSnapshot": "Imagem inválida ou não suportada. Escolha uma imagem BusyMark Notes válida ou uma origem Markdown.", + "notesTransferMediaLimit": "Os conteúdos multimédia excedem o limite de importação. Reduza os do item afetado e reveja-o.", + "notesTransferUnsafeSource": "A origem contém um caminho inseguro. Escolha ficheiros na pasta de origem e reveja.", + "notesTransferDestination": "Não foi possível escrever a exportação. Verifique o acesso e espaço livre no destino e tente novamente.", + "notesTransferOperationFailed": "A transferência falhou. Reveja os itens afetados e tente novamente. Os já importados continuam guardados.", + "notesExportPublishedWithOmissions": "Exportação disponível com omissões", + "notesBatchUnchanged": "Os valores pedidos já estavam definidos.", + "notesBatchReadOnly": "Esta nota é só de leitura. As propriedades protegidas não podem mudar; Favorito continua permitido.", + "notesBatchReviewChanged": "Esta nota mudou desde a revisão. Reveja o estado atual antes de repetir a ação.", + "notesBatchMissing": "Esta nota já não está disponível. Atualize a lista e reveja os destinos.", + "notesBatchStorage": "Falha ao guardar localmente. Verifique o armazenamento e repita este item; os bem-sucedidos continuam guardados.", + "notesRemoteReviewChanged": "A nota no servidor mudou. O rascunho de união foi preservado. Reveja a comparação atualizada e escolha novamente a resolução.", + "notesReviewRemote": "Revi a versão atual do servidor", "wsModuleCapsNotPublished": "A capitalização do módulo não é publicada.", "wsTitleOverridesAction": "Títulos da instância e do índice…", "wsHeadingNotPublished": "Não é publicado por este gerador.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Data", "pdfTitlePageTitleRequired": "Introduza um título para a página de título.", "pdfTocSectionNote": "Começa numa página separada e pode ocupar várias páginas.", - "sourceSearchZeroLengthUnsupported": "Não são suportadas correspondências regex de comprimento zero (por exemplo, ^, $ ou antecipações que correspondem apenas a uma posição).", "workspaceSearchIncomplete": "Só são apresentados alguns resultados. Mostre mais ou restrinja a pesquisa.", "workspaceSearchShowMore": "Mostrar mais resultados", "workspaceSearchSkippedFiles": "A pesquisa está incompleta. Estes ficheiros excedem 1 MiB ou não foi possível lê-los:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Mostrar esta referência de atalhos de teclado", + "shortcutKeyboardShortcutsDescription": "Personalizar os atalhos de teclado da aplicação", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Limpar espaços de trabalho recentes", + "clearRecentWorkspaces": "Limpar histórico recente", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Diferenciar maiúsculas de minúsculas", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Palavra inteira", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Expressão regular", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Expressão regular inválida", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Ficheiro grande: o realce e o recolhimento estão pausados", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Não foi possível atualizar os itens recentes: {error}.", "workspaceErrorOpenFailed": "Falha ao abrir: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "O texto da ligação está vazio; forneça um nome acessível que descreva a respetiva finalidade.", "diagnosticMarkdownLinkReviewText": "Verifique se o texto da ligação “{text}” descreve a respetiva finalidade no contexto.", "diagnosticMarkdownTableEmptyHeader": "Os cabeçalhos da tabela devem identificar suas colunas; preencha cada cabeçalho vazio.", - "commandPalette": "Paleta de comandos", - "commandPaletteHint": "Introduza um comando", - "commandPaletteEmpty": "Nenhum comando correspondente", "tableAlignmentUnspecified": "Alinhamento: não especificado", "tableAlignmentLeft": "Alinhamento: esquerda", "tableAlignmentCenter": "Alinhamento: centro", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "Escolha um ficheiro .css UTF-8 legível até 256 KiB, sem importações, URL de recursos, HTML ou CSS executável ou com escapes.", "exportInvalidRange": "{field}: introduza um número entre {minimum} e {maximum}.", "clipboardHistory": "Histórico da Área de Transferência", - "clipboardHistorySearchHint": "Pesquisar no histórico da área de transferência", "clipboardHistoryDescription": "Abrir o histórico da área de transferência local da sessão.", "clipboardCurrent": "Área de transferência atual", "clipboardCurrentExternal": "Área de transferência de outro aplicativo", "clipboardDestination": "Colar em {name}", "clipboardSessionOnly": "Armazenado apenas para esta sessão BusyMark.", "clipboardNoItems": "O histórico da área de transferência está vazio.", - "clipboardNoMatches": "Nenhum item da área de transferência corresponde a esta pesquisa.", "clipboardDisabled": "A coleção de Histórico da Área de Transferência está desativada nas Configurações.", "clipboardUnavailable": "O conteúdo da área de transferência atual não está disponível ou não é suportado.", "clipboardTooLarge": "Este item na área de transferência é demasiado grande para manter.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "Procurar documentos e conteúdos de revisão no Histórico Local.", "localHistoryNoDocuments": "Nenhum documento tem Histórico Local.", "localHistoryNoRevisions": "Este documento não tem revisões no Histórico Local.", - "localHistoryNoMatches": "Nenhuma revisão corresponde à pesquisa.", - "localHistorySearchHint": "Pesquisar no histórico local", "localHistorySelectedRevision": "Revisão selecionada", "localHistoryCurrentEditor": "Conteúdo atual do editor", "localHistoryCurrentDisk": "Conteúdo atual no disco", @@ -3610,5 +3619,125 @@ "wsInsertElements": "Inserir elementos", "wsAdmonitionsAndQuotes": "Avisos e citações", "wsSemanticText": "Texto semântico", - "wsPropertiesUnavailable": "O elemento inspecionado está indisponível. Inspecione um elemento para continuar." + "wsPropertiesUnavailable": "O elemento inspecionado está indisponível. Inspecione um elemento para continuar.", + "searchDocumentsCategory": "Documentos", + "searchTopicsCategory": "Tópicos", + "searchCurrentDocument": "Documento atual", + "searchCurrentWorkspace": "Área de trabalho atual", + "browseNotes": "Explorar notas", + "searchActionsCommand": "Pesquisar ações", + "searchDocumentsCommand": "Pesquisar documentos", + "searchTopicsCommand": "Pesquisar tópicos", + "searchRecentCommand": "Pesquisar recentes", + "searchResourceUnavailable": "Este recurso não está disponível.", + "notesSync": "Sincronizar notas", + "searchInWorkspace": "Pesquisar no espaço de trabalho", + "searchCategory": "Categoria de pesquisa", + "clearHistorySearchScope": "Pesquisar no histórico de documentos", + "searchInDocument": "Pesquisar no documento", + "searchDocumentHint": "Pesquisar neste documento…", + "searchWorkspaceHint": "Pesquisar neste espaço de trabalho…", + "searchOutlineHint": "Pesquisar títulos…", + "searchHistoryHint": "Pesquisar no histórico local…", + "searchEnterText": "Introduza texto para pesquisar", + "searchAccountUnavailable": "Conta indisponível", + "searchDocumentsHint": "Pesquisar documentos…", + "searchTopicsHint": "Pesquisar tópicos…", + "searchRecentHint": "Pesquisar recentes…", + "searchActionsHint": "Pesquisar ações…", + "searchRequiresNotesWorkspace": "Requer um espaço de trabalho de Notas", + "searchSidebarLabel": "Pesquisar:", + "searchDocumentTextCategory": "Texto do documento", + "searchWorkspaceTextCategory": "Texto do espaço de trabalho", + "searchHeadingsCategory": "Cabeçalhos", + "shortcutUnassigned": "Não atribuído", + "shortcutPressKeys": "Prima uma combinação de teclas…", + "shortcutRecord": "Capturar atalho", + "shortcutRestoreAll": "Repor todas as predefinições", + "shortcutRestoreAllQuestion": "Repor os atalhos predefinidos de todas as ações? As atribuições personalizadas serão removidas.", + "shortcutSaveFailed": "Os atalhos estão ativos, mas não foram guardados. Tente novamente para os manter após reiniciar.", + "shortcutRetry": "Tentar guardar novamente", + "shortcutChanged": "As atribuições mudaram. Reveja os atalhos atuais e tente novamente.", + "shortcutInvalid": "Esta combinação não pode ser atribuída neste contexto.", + "shortcutSystemWarning": "O ambiente de trabalho pode usar esta combinação. O BusyMark não pode substituir atalhos do sistema.", + "shortcutConflicts": "Este atalho entra em conflito com estas ações:", + "shortcutRemove": "Remover atribuição", + "shortcutRestoreDefault": "Repor predefinição", + "shortcutAssign": "Atribuir", + "shortcutReassign": "Reatribuir", + "shortcutContextApplication": "Aplicação", + "shortcutContextDocument": "Vistas do documento", + "shortcutContextEditor": "Editor e código-fonte", + "shortcutContextWritersideSource": "Código-fonte do Writerside", + "shortcutContextWorkspace": "Navegação do espaço de trabalho", + "shortcutContextTree": "Árvores laterais", + "searchHeadingsCommand": "Pesquisar títulos", + "shortcutFixed": "Gerido pelo editor; não é configurável.", + "shortcutDefaultConflict": "O atalho predefinido está em conflito com uma atribuição personalizada.", + "shortcutCurrentBinding": "Atalho atual", + "shortcutProposedBinding": "Atalho proposto", + "notesActions": "Ações das notas", + "noteActions": "Ações da nota", + "noteProperties": "Propriedades da nota", + "notesOfflinePause": "Pausar transferências", + "notesOfflineResume": "Retomar transferências", + "notesEmptyAll": "Ainda não há notas. Crie uma para começar.", + "notesEmptyFavorites": "Não há notas favoritas.", + "notesEmptyCategory": "Não há notas nesta categoria.", + "notesEmptyUncategorized": "Não há notas sem categoria.", + "notesEmptyRecovery": "Não há notas guardadas para recuperar.", + "notesStartingConnection": "A iniciar ligação ao Nextcloud…", + "notesVerifyingConnection": "A verificar o acesso às notas…", + "notesCancellingConnection": "A cancelar o início de sessão e a limpar…", + "notesRemovingConnection": "A remover a conta local…", + "notesSettingsLeave": "Guardar as alterações às definições do Nextcloud antes de sair?", + "notesSettingsReload": "Recarregar definições do servidor…", + "notesScanning": "A analisar ficheiros…", + "notesCancelling": "A cancelar… A aguardar que a operação atual termine.", + "notesCancelled": "Cancelado", + "notesPartial": "Parcialmente concluído", + "notesNotStarted": "Não iniciado", + "notesCreated": "Criado", + "notesCurrentText": "Texto atualmente guardado", + "notesRecovering": "A recuperar como nova nota…", + "notesSort": "Ordenar notas", + "notesOfflineRetentionHelp": "Remover este requisito mantém o conteúdo em cache. Um requisito da categoria ou da conta pode continuar a manter a nota.", + "notesOfflineBroader": "Esta nota também é mantida por um requisito da categoria ou da conta.", + "notesOfflineExternal": "O conteúdo multimédia externo requer ligação. O BusyMark mantém apenas o texto e os anexos geridos.", + "notesOperationFailed": "Não foi possível concluir a operação. O rascunho foi mantido. Reveja a gravação e os conflitos e tente novamente.", + "notesRemovedWarning": "A conta local Nextcloud foi removida. Não foi confirmada a revogação da palavra-passe da aplicação. Remova a palavra-passe do BusyMark nas definições de segurança do Nextcloud.", + "notesBuffersNotSaved": "Não foi possível guardar todas as notas abertas localmente. Reveja a gravação, os conflitos ou a normalização na área de trabalho. As alterações locais podem ainda precisar de sincronização com o Nextcloud.", + "notesSettingsReloadHelp": "Recarregar substitui o rascunho pelas definições atuais do Nextcloud. Descartar o rascunho e recarregar?", + "notesSettingsRefreshFailed": "O Nextcloud aceitou as definições, mas a atualização das notas falhou. São apresentados os valores confirmados pelo servidor. Tente atualizar novamente a partir das Notas.", + "notesStorageFull": "O armazenamento Nextcloud está cheio. Liberte espaço no servidor e tente novamente.", + "notesRejected": "O Nextcloud rejeitou esta alteração. Reveja a nota e guarde uma cópia local antes de tentar novamente.", + "notesThrottled": "O Nextcloud pediu para aguardar. A sincronização será retomada após o prazo de espera.", + "notesImportPartialHelp": "Os itens anteriores podem já estar guardados localmente. Retome esta importação para continuar sem os duplicar.", + "notesExportAll": "Exportar notas · Todas as notas…", + "notesExportCategory": "Exportar notas · Categoria…", + "notesOfflineAll": "Disponibilizar offline · Todas as notas", + "notesOfflineCategory": "Disponibilizar offline · Categoria", + "notesOfflineRemoveAll": "Remover retenção offline · Todas as notas", + "notesOfflineRemoveCategory": "Remover retenção offline · Categoria", + "notesOfflineKeep": "Disponibilizar offline", + "notesLoadFailed": "Nextcloud Notes · Falhou. Tentar novamente", + "notesInspectionFailed": "Anexos · Falhou. Tentar novamente", + "notesSettingsLeaveHelp": "Definições de Notes do servidor · Alterações não guardadas", + "notesUnavailable": "Sem ligação · Texto disponível localmente · Guardado localmente · sincronização pendente", + "notesRecoveryRequired": "Conflitos · Comparar", + "notesTransferFailed": "Falhou. Tentar novamente", + "notesRemoteDeleteUnavailable": "URL do servidor · Não suportada · Eliminar · Anexos", + "notesReadOnlyHelp": "Só de leitura · Favoritos · Guardar cópia local…", + "notesPathRelative": "O caminho da pasta de notas é relativo ao seu armazenamento Nextcloud.", + "notesSettingsStale": "As definições mudaram no Nextcloud. Recarregue e reveja os valores do servidor antes de substituir o rascunho.", + "notesDownloadsPaused": "Transferências pausadas", + "notesExportNotPublished": "Não foi publicada nenhuma exportação.", + "notesKeyringUnavailable": "Desbloqueie o porta-chaves do ambiente de trabalho e tente novamente.", + "notesConnectFailed": "Não foi possível ligar ao Nextcloud Notes. Verifique o endereço do servidor e tente novamente.", + "notesAccountMismatch": "Inicie sessão na mesma conta Nextcloud. Remova esta conta primeiro para ligar outra.", + "notesLocalRemovalFailed": "A limpeza local da conta falhou. A conta é mantida para poder tentar removê-la novamente.", + "notesTransferring": "A transferir notas…", + "notesTechnicalDetails": "Detalhes técnicos", + "notesPauseRequirement": "Pausar requisito offline", + "notesResumeRequirement": "Retomar requisito offline" } diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index c95e7ec6..1d6533ad 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Nova nota a partir do texto preservado", + "notesAccountThrottled": "Nextcloud pediu uma pausa. Aguarde antes de entrar novamente.", + "notesSettingsRefreshing": "A atualizar Notes após a confirmação das configurações", + "notesExportOperationFailed": "A exportação falhou antes da publicação. Verifique a origem e o destino e volte a exportar.", + "notesRecoveryCreatedNotOpened": "A nota recuperada foi salva, mas não abriu. Resolva as alterações pendentes no editor ou nas propriedades e abra esta nota existente.", + "notesSettingsPendingNotes": "Notas pendentes, conflitos ou criações incertas impedem esta alteração. Sincronize ou reveja em Notes e volte a salvar este rascunho preservado.", + "notesSettingsPendingAttachments": "Operações de anexos impedem esta alteração. Reveja os anexos em Notes e volte a salvar este rascunho preservado.", + "notesAccountServerAddress": "Use o endereço HTTPS canônico do Nextcloud e reinicie a autenticação. Não foi possível verificar o endereço ou o redirecionamento.", + "notesAccountUnsupported": "Notes está indisponível ou a API não é suportada. Ative Nextcloud Notes com API principal 1 e secundária 4 ou posterior.", + "notesAccountAuthorizeAgain": "Nextcloud rejeitou as credenciais. Autorize novamente a conta ou volte a ligá-la.", + "notesAccountCheckForbidden": "Nextcloud recusou a verificação de funcionalidades de Notes. Peça autorização ao administrador e tente novamente.", + "notesAccountMalformed": "Não foi possível verificar a resposta do Nextcloud. Verifique a instalação do servidor e reinicie a autenticação.", + "notesAccountTimedOut": "A autenticação expirou. Reinicie-a e conclua a autorização no navegador.", + "notesAccountBrowserUnavailable": "Não foi possível abrir o navegador do sistema. Verifique o navegador padrão e reinicie a autenticação.", + "notesAccountNetwork": "Nextcloud está inacessível. Verifique a conexão e o servidor e tente autenticar-se novamente.", + "notesAccountRejected": "Nextcloud rejeitou o pedido de funcionalidades. Verifique o acesso com o administrador e tente novamente.", + "notesAccountFailed": "Não foi possível configurar a conta. Tente autenticar-se novamente.", + "notesTransferSourceChanged": "A origem mudou desde a revisão. Reveja o arquivo atual ou capture uma nova imagem antes de tentar novamente.", + "notesTransferUnreadable": "Não foi possível ler a origem. Verifique o acesso ao arquivo ou pasta e reveja.", + "notesTransferInvalidSnapshot": "Imagem inválida ou não suportada. Escolha uma imagem BusyMark Notes válida ou uma origem Markdown.", + "notesTransferMediaLimit": "Os conteúdos multimédia excedem o limite de importação. Reduza os do item afetado e reveja-o.", + "notesTransferUnsafeSource": "A origem contém um caminho inseguro. Escolha arquivos na pasta de origem e reveja.", + "notesTransferDestination": "Não foi possível escrever a exportação. Verifique o acesso e espaço livre no destino e tente novamente.", + "notesTransferOperationFailed": "A transferência falhou. Reveja os itens afetados e tente novamente. Os já importados continuam salvos.", + "notesExportPublishedWithOmissions": "Exportação disponível com omissões", + "notesBatchUnchanged": "Os valores pedidos já estavam definidos.", + "notesBatchReadOnly": "Esta nota é só de leitura. As propriedades protegidas não podem mudar; Favorito continua permitido.", + "notesBatchReviewChanged": "Esta nota mudou desde a revisão. Reveja o estado atual antes de repetir a ação.", + "notesBatchMissing": "Esta nota já não está disponível. Atualize a lista e reveja os destinos.", + "notesBatchStorage": "Falha ao salvar localmente. Verifique o armazenamento e repita este item; os bem-sucedidos continuam salvos.", + "notesRemoteReviewChanged": "A nota no servidor mudou. O rascunho de união foi preservado. Reveja a comparação atualizada e escolha novamente a resolução.", + "notesReviewRemote": "Revi a versão atual do servidor", "wsModuleCapsNotPublished": "A capitalização do módulo não é publicada.", "wsTitleOverridesAction": "Títulos da instância e do índice…", "wsHeadingNotPublished": "Não é publicado por este gerador.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Data", "pdfTitlePageTitleRequired": "Insira um título para a página de título.", "pdfTocSectionNote": "Começa em uma página separada e pode ocupar várias páginas.", - "sourceSearchZeroLengthUnsupported": "Não há suporte para correspondências regex de comprimento zero (por exemplo, ^, $ ou antecipações que correspondem apenas a uma posição).", "workspaceSearchIncomplete": "Apenas alguns resultados são exibidos. Mostre mais ou restrinja a pesquisa.", "workspaceSearchShowMore": "Mostrar mais resultados", "workspaceSearchSkippedFiles": "A pesquisa está incompleta. Estes arquivos excedem 1 MiB ou não puderam ser lidos:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Mostrar esta referência de atalhos de teclado", + "shortcutKeyboardShortcutsDescription": "Personalizar os atalhos de teclado do aplicativo", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Limpar espaços de trabalho recentes", + "clearRecentWorkspaces": "Limpar histórico recente", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Diferenciar maiúsculas de minúsculas", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Palavra inteira", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Expressão regular", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Expressão regular inválida", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Arquivo grande: o realce e o recolhimento estão pausados", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Não foi possível atualizar os itens recentes: {error}.", "workspaceErrorOpenFailed": "Falha ao abrir: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3208,9 +3224,6 @@ "diagnosticMarkdownLinkEmptyText": "O texto do link está vazio; forneça um nome acessível que descreva sua finalidade.", "diagnosticMarkdownLinkReviewText": "Verifique se o texto do link “{text}” descreve sua finalidade no contexto.", "diagnosticMarkdownTableEmptyHeader": "Os cabeçalhos da tabela devem identificar suas colunas; preencha cada cabeçalho vazio.", - "commandPalette": "Paleta de comandos", - "commandPaletteHint": "Digite um comando", - "commandPaletteEmpty": "Nenhum comando correspondente", "tableAlignmentUnspecified": "Alinhamento: não especificado", "tableAlignmentLeft": "Alinhamento: esquerda", "tableAlignmentCenter": "Alinhamento: centro", @@ -3325,14 +3338,12 @@ "exportInvalidCss": "Escolha um arquivo .css UTF-8 legível até 256 KiB, sem importações, URL de recursos, HTML ou CSS executável ou com escapes.", "exportInvalidRange": "{field}: insira um número entre {minimum} e {maximum}.", "clipboardHistory": "Histórico da Área de Transferência", - "clipboardHistorySearchHint": "Pesquisar no histórico da área de transferência", "clipboardHistoryDescription": "Abrir o histórico da área de transferência local da sessão.", "clipboardCurrent": "Área de transferência atual", "clipboardCurrentExternal": "Área de transferência de outro aplicativo", "clipboardDestination": "Colar em {name}", "clipboardSessionOnly": "Armazenado apenas para esta sessão BusyMark.", "clipboardNoItems": "O histórico da área de transferência está vazio.", - "clipboardNoMatches": "Nenhum item da área de transferência corresponde a esta pesquisa.", "clipboardDisabled": "A coleção de Histórico da Área de Transferência está desativada nas Configurações.", "clipboardUnavailable": "O conteúdo da área de transferência atual não está disponível ou não é suportado.", "clipboardTooLarge": "Este item da área de transferência é muito grande para ser armazenado.", @@ -3349,8 +3360,6 @@ "findLocalHistoryDescription": "Pesquisar documentos e conteúdo de revisões no Histórico Local.", "localHistoryNoDocuments": "Nenhum documento possui Histórico Local.", "localHistoryNoRevisions": "Este documento não possui revisões no Histórico Local.", - "localHistoryNoMatches": "Nenhuma revisão no Histórico Local corresponde a esta pesquisa.", - "localHistorySearchHint": "Pesquisar no histórico local", "localHistorySelectedRevision": "Revisão selecionada", "localHistoryCurrentEditor": "Conteúdo atual do editor", "localHistoryCurrentDisk": "Conteúdo atual no disco", @@ -3610,5 +3619,125 @@ "wsInsertElements": "Inserir elementos", "wsAdmonitionsAndQuotes": "Avisos e citações", "wsSemanticText": "Texto semântico", - "wsPropertiesUnavailable": "O elemento inspecionado está indisponível. Inspecione um elemento para continuar." + "wsPropertiesUnavailable": "O elemento inspecionado está indisponível. Inspecione um elemento para continuar.", + "searchDocumentsCategory": "Documentos", + "searchTopicsCategory": "Tópicos", + "searchCurrentDocument": "Documento atual", + "searchCurrentWorkspace": "Área de trabalho atual", + "browseNotes": "Explorar notas", + "searchActionsCommand": "Pesquisar ações", + "searchDocumentsCommand": "Pesquisar documentos", + "searchTopicsCommand": "Pesquisar tópicos", + "searchRecentCommand": "Pesquisar recentes", + "searchResourceUnavailable": "Este recurso não está disponível.", + "notesSync": "Sincronizar notas", + "searchInWorkspace": "Pesquisar no espaço de trabalho", + "searchCategory": "Categoria de pesquisa", + "clearHistorySearchScope": "Pesquisar no histórico de documentos", + "searchInDocument": "Pesquisar no documento", + "searchDocumentHint": "Pesquisar neste documento…", + "searchWorkspaceHint": "Pesquisar neste espaço de trabalho…", + "searchOutlineHint": "Pesquisar títulos…", + "searchHistoryHint": "Pesquisar no histórico local…", + "searchEnterText": "Digite texto para pesquisar", + "searchAccountUnavailable": "Conta indisponível", + "searchDocumentsHint": "Pesquisar documentos…", + "searchTopicsHint": "Pesquisar tópicos…", + "searchRecentHint": "Pesquisar recentes…", + "searchActionsHint": "Pesquisar ações…", + "searchRequiresNotesWorkspace": "Requer um espaço de trabalho de Notas", + "searchSidebarLabel": "Pesquisar:", + "searchDocumentTextCategory": "Texto do documento", + "searchWorkspaceTextCategory": "Texto do espaço de trabalho", + "searchHeadingsCategory": "Títulos", + "shortcutUnassigned": "Não atribuído", + "shortcutPressKeys": "Pressione uma combinação de teclas…", + "shortcutRecord": "Capturar atalho", + "shortcutRestoreAll": "Restaurar todos os padrões", + "shortcutRestoreAllQuestion": "Restaurar os atalhos padrão de todas as ações? As atribuições personalizadas serão removidas.", + "shortcutSaveFailed": "Os atalhos estão ativos, mas não foram salvos. Tente novamente para mantê-los após reiniciar.", + "shortcutRetry": "Tentar salvar novamente", + "shortcutChanged": "As atribuições mudaram. Confira os atalhos atuais e tente novamente.", + "shortcutInvalid": "Esta combinação não pode ser atribuída neste contexto.", + "shortcutSystemWarning": "O ambiente de trabalho pode usar esta combinação. O BusyMark não pode substituir atalhos do sistema.", + "shortcutConflicts": "Este atalho entra em conflito com estas ações:", + "shortcutRemove": "Remover atribuição", + "shortcutRestoreDefault": "Restaurar padrão", + "shortcutAssign": "Atribuir", + "shortcutReassign": "Reatribuir", + "shortcutContextApplication": "Aplicativo", + "shortcutContextDocument": "Visualizações do documento", + "shortcutContextEditor": "Editor e código-fonte", + "shortcutContextWritersideSource": "Código-fonte do Writerside", + "shortcutContextWorkspace": "Navegação do espaço de trabalho", + "shortcutContextTree": "Árvores laterais", + "searchHeadingsCommand": "Pesquisar títulos", + "shortcutFixed": "Gerenciado pelo editor; não é configurável.", + "shortcutDefaultConflict": "O atalho padrão está em conflito com uma atribuição personalizada.", + "shortcutCurrentBinding": "Atalho atual", + "shortcutProposedBinding": "Atalho proposto", + "notesActions": "Ações das notas", + "noteActions": "Ações da nota", + "noteProperties": "Propriedades da nota", + "notesOfflinePause": "Pausar downloads", + "notesOfflineResume": "Retomar downloads", + "notesEmptyAll": "Ainda não há notas. Crie uma para começar.", + "notesEmptyFavorites": "Não há notas favoritas.", + "notesEmptyCategory": "Não há notas nesta categoria.", + "notesEmptyUncategorized": "Não há notas sem categoria.", + "notesEmptyRecovery": "Não há notas retidas para recuperar.", + "notesStartingConnection": "Iniciando conexão com o Nextcloud…", + "notesVerifyingConnection": "Verificando o acesso às notas…", + "notesCancellingConnection": "Cancelando a entrada e limpando…", + "notesRemovingConnection": "Removendo a conta local…", + "notesSettingsLeave": "Salvar alterações nas configurações do Nextcloud antes de sair?", + "notesSettingsReload": "Recarregar configurações do servidor…", + "notesScanning": "Verificando arquivos…", + "notesCancelling": "Cancelando… Aguardando a conclusão da operação atual.", + "notesCancelled": "Cancelado", + "notesPartial": "Parcialmente concluído", + "notesNotStarted": "Não iniciado", + "notesCreated": "Criado", + "notesCurrentText": "Texto retido atualmente", + "notesRecovering": "Recuperando como nova nota…", + "notesSort": "Ordenar notas", + "notesOfflineRetentionHelp": "Remover este requisito mantém o conteúdo em cache. Um requisito da categoria ou da conta pode continuar retendo a nota.", + "notesOfflineBroader": "Esta nota também é retida por um requisito da categoria ou da conta.", + "notesOfflineExternal": "Mídia externa exige conexão. O BusyMark mantém apenas texto e anexos gerenciados.", + "notesOperationFailed": "Não foi possível concluir a operação. Seu rascunho foi mantido. Revise o salvamento e os conflitos e tente novamente.", + "notesRemovedWarning": "A conta local do Nextcloud foi removida. A revogação da senha de aplicativo não foi confirmada. Remova a senha do BusyMark nas configurações de segurança do Nextcloud.", + "notesBuffersNotSaved": "Não foi possível salvar todas as notas abertas localmente. Revise o salvamento, os conflitos ou a normalização na área de trabalho. As alterações locais ainda podem precisar de sincronização com o Nextcloud.", + "notesSettingsReloadHelp": "Recarregar substitui o rascunho pelas configurações atuais do Nextcloud. Descartar o rascunho e recarregar?", + "notesSettingsRefreshFailed": "O Nextcloud aceitou as configurações, mas a atualização das notas falhou. Os valores exibidos foram confirmados pelo servidor. Tente atualizar novamente pelas Notas.", + "notesStorageFull": "O armazenamento do Nextcloud está cheio. Libere espaço no servidor e tente novamente.", + "notesRejected": "O Nextcloud rejeitou esta alteração. Revise a nota e salve uma cópia local antes de tentar novamente.", + "notesThrottled": "O Nextcloud pediu para aguardar. A sincronização será retomada após o prazo de espera.", + "notesImportPartialHelp": "Itens anteriores podem já estar salvos localmente. Retome esta importação para continuar sem duplicá-los.", + "notesExportAll": "Exportar notas · Todas as notas…", + "notesExportCategory": "Exportar notas · Categoria…", + "notesOfflineAll": "Disponibilizar offline · Todas as notas", + "notesOfflineCategory": "Disponibilizar offline · Categoria", + "notesOfflineRemoveAll": "Remover retenção offline · Todas as notas", + "notesOfflineRemoveCategory": "Remover retenção offline · Categoria", + "notesOfflineKeep": "Disponibilizar offline", + "notesLoadFailed": "Nextcloud Notes · Falhou. Tentar novamente", + "notesInspectionFailed": "Anexos · Falhou. Tentar novamente", + "notesSettingsLeaveHelp": "Configurações de Notes do servidor · Alterações não salvas", + "notesUnavailable": "Sem conexão · Texto disponível localmente · Salvo localmente · sincronização pendente", + "notesRecoveryRequired": "Conflitos · Comparar", + "notesTransferFailed": "Falhou. Tentar novamente", + "notesRemoteDeleteUnavailable": "URL do servidor · Não compatível · Excluir · Anexos", + "notesReadOnlyHelp": "Somente leitura · Favoritos · Salvar cópia local…", + "notesPathRelative": "O caminho da pasta de notas é relativo ao seu armazenamento Nextcloud.", + "notesSettingsStale": "As configurações mudaram no Nextcloud. Recarregue e revise os valores do servidor antes de substituir o rascunho.", + "notesDownloadsPaused": "Downloads pausados", + "notesExportNotPublished": "Nenhuma exportação foi publicada.", + "notesKeyringUnavailable": "Desbloqueie o chaveiro da área de trabalho e tente novamente.", + "notesConnectFailed": "Não foi possível conectar ao Nextcloud Notes. Verifique o endereço do servidor e tente novamente.", + "notesAccountMismatch": "Entre na mesma conta Nextcloud. Remova esta conta primeiro para conectar outra.", + "notesLocalRemovalFailed": "A limpeza local da conta falhou. A conta foi mantida para que você possa tentar removê-la novamente.", + "notesTransferring": "Transferindo notas…", + "notesTechnicalDetails": "Detalhes técnicos", + "notesPauseRequirement": "Pausar requisito offline", + "notesResumeRequirement": "Retomar requisito offline" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 6de1a317..26d67c29 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Новая заметка из сохранённого текста", + "notesAccountThrottled": "Nextcloud запросил паузу. Подождите перед повторным входом.", + "notesSettingsRefreshing": "Обновление Notes после подтверждения настроек", + "notesExportOperationFailed": "Экспорт не опубликован. Проверьте источник и место назначения, затем повторите.", + "notesRecoveryCreatedNotOpened": "Восстановленная заметка сохранена, но не открылась. Завершите правки редактора или свойств и откройте эту существующую заметку.", + "notesSettingsPendingNotes": "Изменению мешают ожидающие заметки, конфликты или неопределённые создания. Синхронизируйте или проверьте их в Notes, затем повторите сохранение черновика настроек.", + "notesSettingsPendingAttachments": "Изменению мешают операции с вложениями. Проверьте их в Notes, затем повторите сохранение сохранённого черновика настроек.", + "notesAccountServerAddress": "Используйте канонический HTTPS-адрес Nextcloud и войдите снова. Адрес или перенаправление не удалось проверить.", + "notesAccountUnsupported": "Notes недоступен или API не поддерживается. Включите Nextcloud Notes с API основной версии 1, дополнительной 4 или новее.", + "notesAccountAuthorizeAgain": "Nextcloud отклонил учётные данные. Повторно авторизуйте или подключите аккаунт.", + "notesAccountCheckForbidden": "Nextcloud запретил проверку возможностей Notes. Попросите администратора разрешить её и повторите.", + "notesAccountMalformed": "Не удалось проверить ответ Nextcloud. Проверьте установку сервера и войдите снова.", + "notesAccountTimedOut": "Время входа истекло. Начните заново и завершите авторизацию в браузере.", + "notesAccountBrowserUnavailable": "Системный браузер не открылся. Проверьте браузер по умолчанию и войдите снова.", + "notesAccountNetwork": "Nextcloud недоступен. Проверьте соединение и сервер и войдите снова.", + "notesAccountRejected": "Nextcloud отклонил запрос возможностей. Проверьте доступ с администратором и повторите.", + "notesAccountFailed": "Настройка аккаунта не завершена. Повторите вход.", + "notesTransferSourceChanged": "Источник изменился после проверки. Проверьте текущий файл или создайте новый снимок перед повтором.", + "notesTransferUnreadable": "Источник не удалось прочитать. Проверьте доступ к файлу или папке и выполните проверку снова.", + "notesTransferInvalidSnapshot": "Снимок недействителен или не поддерживается. Выберите корректный снимок BusyMark Notes или источник Markdown.", + "notesTransferMediaLimit": "Медиа превышает предел импорта. Уменьшите медиа затронутого элемента и проверьте снова.", + "notesTransferUnsafeSource": "Источник содержит небезопасный путь. Выберите файлы внутри исходной папки и проверьте снова.", + "notesTransferDestination": "Не удалось записать экспорт. Проверьте доступ и свободное место в назначении, затем повторите.", + "notesTransferOperationFailed": "Передача не завершена. Проверьте затронутые элементы и повторите. Ранее импортированные элементы сохранены.", + "notesExportPublishedWithOmissions": "Экспорт доступен с пропусками", + "notesBatchUnchanged": "Запрошенные значения уже установлены.", + "notesBatchReadOnly": "Заметка доступна только для чтения. Защищённые свойства менять нельзя; избранное менять можно.", + "notesBatchReviewChanged": "Заметка изменилась после проверки. Проверьте её текущее состояние перед повтором действия.", + "notesBatchMissing": "Заметка больше недоступна. Обновите список и проверьте выбранные элементы.", + "notesBatchStorage": "Локальное сохранение не удалось. Проверьте хранилище и повторите этот элемент; успешные элементы сохранены.", + "notesRemoteReviewChanged": "Заметка на сервере изменилась. Черновик слияния сохранён. Проверьте новое сравнение и заново выберите решение.", + "notesReviewRemote": "Я проверил текущую версию на сервере", "wsModuleCapsNotPublished": "Правило регистра модуля не публикуется.", "wsTitleOverridesAction": "Заголовки экземпляра и оглавления…", "wsHeadingNotPublished": "Не публикуется этим сборщиком.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Дата", "pdfTitlePageTitleRequired": "Введите заголовок титульной страницы.", "pdfTocSectionNote": "Начинается с отдельной страницы и может занимать несколько страниц.", - "sourceSearchZeroLengthUnsupported": "Совпадения регулярных выражений нулевой длины не поддерживаются (например, ^, $ или проверки вперёд, совпадающие только с позицией).", "workspaceSearchIncomplete": "Показана только часть результатов. Покажите больше или уточните поиск.", "workspaceSearchShowMore": "Показать больше результатов", "workspaceSearchSkippedFiles": "Поиск неполный. Эти файлы превышают 1 MiB или не могут быть прочитаны:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Показать справку по сочетаниям клавиш", + "shortcutKeyboardShortcutsDescription": "Настроить сочетания клавиш приложения", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Очистить список недавних рабочих областей", + "clearRecentWorkspaces": "Очистить недавнюю историю", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "С учётом регистра", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Слово целиком", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Регулярное выражение", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Некорректное регулярное выражение", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Большой файл: подсветка и сворачивание приостановлены", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Не удалось обновить недавние элементы: {error}.", "workspaceErrorOpenFailed": "Не удалось открыть: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3234,9 +3250,6 @@ "diagnosticMarkdownLinkEmptyText": "Текст ссылки пуст; укажите доступное имя, описывающее её назначение.", "diagnosticMarkdownLinkReviewText": "Проверьте, описывает ли текст ссылки «{text}» её назначение в контексте.", "diagnosticMarkdownTableEmptyHeader": "Заголовки таблицы должны обозначать столбцы; заполните каждый пустой заголовок.", - "commandPalette": "Палитра команд", - "commandPaletteHint": "Введите команду", - "commandPaletteEmpty": "Нет подходящих команд", "tableAlignmentUnspecified": "Выравнивание: не задано", "tableAlignmentLeft": "Выравнивание: по левому краю", "tableAlignmentCenter": "Выравнивание: по центру", @@ -3351,14 +3364,12 @@ "exportInvalidCss": "Выберите читаемый файл .css в UTF-8 до 256 КиБ без импортов, URL ресурсов, HTML, исполняемого CSS и escape-последовательностей.", "exportInvalidRange": "{field}: введите число от {minimum} до {maximum}.", "clipboardHistory": "История буфера обмена", - "clipboardHistorySearchHint": "Поиск в истории буфера обмена", "clipboardHistoryDescription": "Открыть историю буфера обмена, специфичную для сеанса.", "clipboardCurrent": "Текущий буфер обмена", "clipboardCurrentExternal": "Текущий буфер обмена из другого приложения", "clipboardDestination": "Вставить в {name}", "clipboardSessionOnly": "Сохраняется только для текущего сеанса BusyMark.", "clipboardNoItems": "История буфера обмена пуста.", - "clipboardNoMatches": "Нет элементов буфера обмена, соответствующих этому поиску.", "clipboardDisabled": "Коллекция \"История буфера обмена\" отключена в настройках.", "clipboardUnavailable": "Текущее содержимое буфера обмена недоступно или не поддерживается.", "clipboardTooLarge": "Этот элемент буфера обмена слишком велик для хранения.", @@ -3375,8 +3386,6 @@ "findLocalHistoryDescription": "Поиск документов и содержимого версий в локальной истории.", "localHistoryNoDocuments": "Ни один документ не имеет локальной истории.", "localHistoryNoRevisions": "Этот документ не имеет версий в локальной истории.", - "localHistoryNoMatches": "Ни одна версия не соответствует этому поиску.", - "localHistorySearchHint": "Поиск в локальной истории", "localHistorySelectedRevision": "Выбранная версия", "localHistoryCurrentEditor": "Текущее содержимое редактора", "localHistoryCurrentDisk": "Текущее содержимое на диске", @@ -3636,5 +3645,125 @@ "wsInsertElements": "Вставить элементы", "wsAdmonitionsAndQuotes": "Примечания и цитаты", "wsSemanticText": "Семантический текст", - "wsPropertiesUnavailable": "Проверяемый элемент недоступен. Выберите элемент для продолжения." + "wsPropertiesUnavailable": "Проверяемый элемент недоступен. Выберите элемент для продолжения.", + "searchDocumentsCategory": "Документы", + "searchTopicsCategory": "Темы", + "searchCurrentDocument": "Текущий документ", + "searchCurrentWorkspace": "Текущая рабочая область", + "browseNotes": "Обзор заметок", + "searchActionsCommand": "Поиск действий", + "searchDocumentsCommand": "Поиск документов", + "searchTopicsCommand": "Поиск тем", + "searchRecentCommand": "Поиск недавних", + "searchResourceUnavailable": "Этот ресурс недоступен.", + "notesSync": "Синхронизировать заметки", + "searchInWorkspace": "Поиск в рабочей области", + "searchCategory": "Категория поиска", + "clearHistorySearchScope": "Поиск в истории документов", + "searchInDocument": "Поиск в документе", + "searchDocumentHint": "Поиск в этом документе…", + "searchWorkspaceHint": "Поиск в этой рабочей области…", + "searchOutlineHint": "Поиск заголовков…", + "searchHistoryHint": "Поиск в локальной истории…", + "searchEnterText": "Введите текст для поиска", + "searchAccountUnavailable": "Аккаунт недоступен", + "searchDocumentsHint": "Поиск документов…", + "searchTopicsHint": "Поиск тем…", + "searchRecentHint": "Поиск недавних…", + "searchActionsHint": "Поиск действий…", + "searchRequiresNotesWorkspace": "Требуется рабочая область заметок", + "searchSidebarLabel": "Поиск:", + "searchDocumentTextCategory": "Текст документа", + "searchWorkspaceTextCategory": "Текст рабочей области", + "searchHeadingsCategory": "Заголовки", + "shortcutUnassigned": "Не назначено", + "shortcutPressKeys": "Нажмите сочетание клавиш…", + "shortcutRecord": "Записать сочетание клавиш", + "shortcutRestoreAll": "Восстановить все значения по умолчанию", + "shortcutRestoreAllQuestion": "Восстановить стандартные сочетания всех действий? Пользовательские назначения будут удалены.", + "shortcutSaveFailed": "Сочетания действуют, но не сохранены. Повторите сохранение, чтобы они сохранились после перезапуска.", + "shortcutRetry": "Повторить сохранение", + "shortcutChanged": "Назначения изменились. Проверьте текущие сочетания и повторите попытку.", + "shortcutInvalid": "Это сочетание нельзя назначить в данном контексте.", + "shortcutSystemWarning": "Это сочетание может использоваться рабочим столом. BusyMark не может переопределять системные сочетания.", + "shortcutConflicts": "Это сочетание конфликтует со следующими действиями:", + "shortcutRemove": "Удалить назначение", + "shortcutRestoreDefault": "Восстановить стандартное", + "shortcutAssign": "Назначить", + "shortcutReassign": "Переназначить", + "shortcutContextApplication": "Приложение", + "shortcutContextDocument": "Представления документа", + "shortcutContextEditor": "Редактор и исходный код", + "shortcutContextWritersideSource": "Исходный код Writerside", + "shortcutContextWorkspace": "Навигация рабочей области", + "shortcutContextTree": "Деревья боковой панели", + "searchHeadingsCommand": "Поиск заголовков", + "shortcutFixed": "Управляется редактором; настройка недоступна.", + "shortcutDefaultConflict": "Сочетание по умолчанию конфликтует с пользовательским назначением.", + "shortcutCurrentBinding": "Текущее сочетание", + "shortcutProposedBinding": "Предлагаемое сочетание", + "notesActions": "Действия с заметками", + "noteActions": "Действия с заметкой", + "noteProperties": "Свойства заметки", + "notesOfflinePause": "Приостановить загрузки", + "notesOfflineResume": "Возобновить загрузки", + "notesEmptyAll": "Заметок пока нет. Создайте первую.", + "notesEmptyFavorites": "Нет избранных заметок.", + "notesEmptyCategory": "В этой категории нет заметок.", + "notesEmptyUncategorized": "Нет заметок без категории.", + "notesEmptyRecovery": "Нет сохранённых заметок для восстановления.", + "notesStartingConnection": "Начало подключения к Nextcloud…", + "notesVerifyingConnection": "Проверка доступа к заметкам…", + "notesCancellingConnection": "Отмена входа и очистка…", + "notesRemovingConnection": "Удаление локальной учётной записи…", + "notesSettingsLeave": "Сохранить изменения настроек Nextcloud перед выходом?", + "notesSettingsReload": "Перезагрузить настройки сервера…", + "notesScanning": "Сканирование файлов…", + "notesCancelling": "Отмена… Ожидание завершения текущей операции.", + "notesCancelled": "Отменено", + "notesPartial": "Частично завершено", + "notesNotStarted": "Не начато", + "notesCreated": "Создано", + "notesCurrentText": "Текущий сохранённый текст", + "notesRecovering": "Восстановление в новую заметку…", + "notesSort": "Сортировка заметок", + "notesOfflineRetentionHelp": "Удаление требования не удаляет содержимое кэша. Требование категории или учётной записи может по-прежнему сохранять заметку.", + "notesOfflineBroader": "Заметка также сохраняется по требованию категории или учётной записи.", + "notesOfflineExternal": "Внешние медиа требуют подключения. BusyMark сохраняет только текст и управляемые вложения.", + "notesOperationFailed": "Не удалось завершить операцию. Черновик сохранён. Проверьте сохранение и конфликты, затем повторите попытку.", + "notesRemovedWarning": "Локальная учётная запись Nextcloud удалена. Отзыв пароля приложения не подтверждён. Удалите пароль BusyMark в настройках безопасности Nextcloud.", + "notesBuffersNotSaved": "Не удалось сохранить все открытые заметки локально. Проверьте сохранение, конфликты или нормализацию в рабочей области. Локальные изменения могут ещё требовать синхронизации с Nextcloud.", + "notesSettingsReloadHelp": "Перезагрузка заменит черновик текущими настройками Nextcloud. Отбросить черновик и перезагрузить?", + "notesSettingsRefreshFailed": "Nextcloud принял настройки, но обновить заметки не удалось. Показаны значения, подтверждённые сервером. Повторите обновление из Заметок.", + "notesStorageFull": "Хранилище Nextcloud заполнено. Освободите место на сервере и повторите попытку.", + "notesRejected": "Nextcloud отклонил это изменение. Проверьте заметку и сохраните локальную копию перед повторной попыткой.", + "notesThrottled": "Nextcloud попросил подождать. Синхронизация продолжится после указанного срока.", + "notesImportPartialHelp": "Предыдущие элементы могут быть уже сохранены локально. Возобновите этот импорт, чтобы избежать дубликатов.", + "notesExportAll": "Экспорт заметок · Все заметки…", + "notesExportCategory": "Экспорт заметок · Категория…", + "notesOfflineAll": "Сохранить для работы без сети · Все заметки", + "notesOfflineCategory": "Сохранить для работы без сети · Категория", + "notesOfflineRemoveAll": "Убрать сохранение без сети · Все заметки", + "notesOfflineRemoveCategory": "Убрать сохранение без сети · Категория", + "notesOfflineKeep": "Сохранить для работы без сети", + "notesLoadFailed": "Nextcloud Notes · Ошибка. Повторить", + "notesInspectionFailed": "Вложения · Ошибка. Повторить", + "notesSettingsLeaveHelp": "Настройки Notes на сервере · Несохранённые изменения", + "notesUnavailable": "Нет сети · Текст доступен локально · Сохранено локально · ожидает синхронизации", + "notesRecoveryRequired": "Конфликты · Сравнить", + "notesTransferFailed": "Ошибка. Повторить", + "notesRemoteDeleteUnavailable": "Адрес сервера · Не поддерживается · Удалить · Вложения", + "notesReadOnlyHelp": "Только чтение · Избранное · Сохранить локальную копию…", + "notesPathRelative": "Путь папки заметок задаётся относительно вашего хранилища Nextcloud.", + "notesSettingsStale": "Настройки на Nextcloud изменились. Перезагрузите и проверьте значения сервера перед заменой черновика.", + "notesDownloadsPaused": "Загрузки приостановлены", + "notesExportNotPublished": "Экспорт не опубликован.", + "notesKeyringUnavailable": "Разблокируйте системную связку ключей и повторите попытку.", + "notesConnectFailed": "Не удалось подключиться к Nextcloud Notes. Проверьте адрес сервера и повторите попытку.", + "notesAccountMismatch": "Войдите в ту же учётную запись Nextcloud. Для подключения другой сначала удалите эту.", + "notesLocalRemovalFailed": "Не удалось очистить локальные данные. Учётная запись сохранена для повторной попытки удаления.", + "notesTransferring": "Передача заметок…", + "notesTechnicalDetails": "Технические сведения", + "notesPauseRequirement": "Приостановить требование автономного хранения", + "notesResumeRequirement": "Возобновить требование автономного хранения" } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 6d2251ae..4e59cc13 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Saklanan metinden yeni not", + "notesAccountThrottled": "Nextcloud beklemenizi istedi. Yeniden oturum açmadan önce bekleyin.", + "notesSettingsRefreshing": "Onaylanan ayar güncellemesinden sonra Notes yenileniyor", + "notesExportOperationFailed": "Dışa aktarma yayımlanmadan başarısız oldu. Kaynağı ve hedefi denetleyip yeniden deneyin.", + "notesRecoveryCreatedNotOpened": "Kurtarılan not kaydedildi ancak açılamadı. Bekleyen düzenleyici veya özellik değişikliklerini çözün ve bu mevcut notu açın.", + "notesSettingsPendingNotes": "Bekleyen notlar, çakışmalar veya belirsiz oluşturmalar bu değişikliği engelliyor. Notes içinde eşitleyin ya da inceleyin, ardından korunan ayar taslağını yeniden kaydedin.", + "notesSettingsPendingAttachments": "Bekleyen ek işlemleri değişikliği engelliyor. Notes içinde ekleri inceleyip korunan ayar taslağını yeniden kaydedin.", + "notesAccountServerAddress": "Nextcloud’un kurallı HTTPS adresini kullanıp yeniden oturum açın. Adres veya yönlendirme doğrulanamadı.", + "notesAccountUnsupported": "Notes kullanılamıyor veya API desteklenmiyor. Ana API sürümü 1, alt sürümü en az 4 olan Nextcloud Notes’u etkinleştirin.", + "notesAccountAuthorizeAgain": "Nextcloud kimlik bilgilerini reddetti. Hesabı yeniden yetkilendirin veya bağlayın.", + "notesAccountCheckForbidden": "Nextcloud, Notes yetenek denetimini engelledi. Yöneticiden izin isteyip yeniden deneyin.", + "notesAccountMalformed": "Nextcloud yanıtı doğrulanamadı. Sunucu kurulumunu denetleyip yeniden oturum açın.", + "notesAccountTimedOut": "Oturum açma zaman aşımına uğradı. Yeniden başlatıp tarayıcıda yetkilendirmeyi tamamlayın.", + "notesAccountBrowserUnavailable": "Sistem tarayıcısı açılamadı. Varsayılan tarayıcıyı denetleyip yeniden oturum açın.", + "notesAccountNetwork": "Nextcloud’a ulaşılamadı. Bağlantıyı ve sunucuyu denetleyip yeniden oturum açın.", + "notesAccountRejected": "Nextcloud yetenek isteğini reddetti. Erişimi yöneticiyle denetleyip yeniden deneyin.", + "notesAccountFailed": "Hesap kurulumu tamamlanamadı. Yeniden oturum açmayı deneyin.", + "notesTransferSourceChanged": "Kaynak incelemeden sonra değişti. Yeniden denemeden önce güncel dosyayı inceleyin veya yeni bir anlık görüntü alın.", + "notesTransferUnreadable": "Kaynak okunamadı. Dosya veya klasör erişimini denetleyip yeniden inceleyin.", + "notesTransferInvalidSnapshot": "Anlık görüntü geçersiz veya desteklenmiyor. Geçerli bir BusyMark Notes görüntüsü veya Markdown kaynağı seçin.", + "notesTransferMediaLimit": "Medya içe aktarma boyut sınırını aşıyor. İlgili öğenin medyasını küçültüp yeniden inceleyin.", + "notesTransferUnsafeSource": "Kaynak güvensiz bir yol içeriyor. Kaynak klasörü içindeki dosyaları seçip yeniden inceleyin.", + "notesTransferDestination": "Dışa aktarma yazılamadı. Hedef erişimini ve boş alanı denetleyip yeniden deneyin.", + "notesTransferOperationFailed": "Aktarım tamamlanamadı. İlgili öğeleri inceleyip yeniden deneyin. Önceden içe aktarılan öğeler kayıtlı kalır.", + "notesExportPublishedWithOmissions": "Eksikleri olan dışa aktarma kullanılabilir", + "notesBatchUnchanged": "İstenen değerler zaten ayarlanmıştı.", + "notesBatchReadOnly": "Bu not salt okunurdur. Korunan özellikler değiştirilemez; Favori yine değiştirilebilir.", + "notesBatchReviewChanged": "Bu not incelemeden sonra değişti. İşlemi yeniden uygulamadan önce güncel durumunu inceleyin.", + "notesBatchMissing": "Bu not artık kullanılamıyor. Listeyi yenileyip hedefleri yeniden inceleyin.", + "notesBatchStorage": "Yerel kayıt başarısız oldu. Depolamayı denetleyip bu öğeyi yeniden deneyin; başarılı öğeler kayıtlı kalır.", + "notesRemoteReviewChanged": "Sunucu notu değişti. Birleştirme taslağınız korunuyor. Güncel karşılaştırmayı inceleyip çözümü yeniden seçin.", + "notesReviewRemote": "Güncel sunucu sürümünü inceledim", "wsModuleCapsNotPublished": "Modülün harf biçimi yayımlanmaz.", "wsTitleOverridesAction": "Örnek ve içindekiler başlıkları…", "wsHeadingNotPublished": "Bu oluşturucu tarafından yayımlanmaz.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Tarih", "pdfTitlePageTitleRequired": "Başlık sayfası için bir başlık girin.", "pdfTocSectionNote": "Ayrı bir sayfada başlar ve birden fazla sayfa sürebilir.", - "sourceSearchZeroLengthUnsupported": "Sıfır uzunluklu düzenli ifade eşleşmeleri desteklenmez (örneğin ^, $ veya yalnızca bir konumla eşleşen ileri bakışlar).", "workspaceSearchIncomplete": "Sonuçların yalnızca bir kısmı gösteriliyor. Daha fazlasını gösterin veya aramayı daraltın.", "workspaceSearchShowMore": "Daha fazla sonuç göster", "workspaceSearchSkippedFiles": "Arama eksik. Bu dosyalar 1 MiB boyutunu aşıyor veya okunamadı:", @@ -423,18 +454,6 @@ "@keyboardShortcuts": { "description": "Keyboard shortcuts dialog title and menu item." }, - "commandPalette": "Komut Paleti", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "Bir komut yazın", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "Eşleşen komut yok", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "Mevcut düzenleyici bağlamında kullanılamıyor", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -715,7 +734,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Bu klavye kısayolu referansını göster", + "shortcutKeyboardShortcutsDescription": "Uygulama klavye kısayollarını özelleştir", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1012,7 +1031,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Son çalışma alanlarını temizle", + "clearRecentWorkspaces": "Son geçmişi temizle", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1943,18 +1962,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Büyük-küçük harf duyarlı", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Tam sözcük", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Düzenli ifade", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "Şununla değiştir:", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2138,10 +2145,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "Geçersiz düzenli ifade", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Büyük dosya: vurgulama ve katlama duraklatıldı", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2281,6 +2284,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Son öğeler güncellenemedi: {error}.", "workspaceErrorOpenFailed": "Açma başarısız oldu: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4864,14 +4868,12 @@ "exportInvalidCss": "İçe aktarım, kaynak URL’si, HTML, çalıştırılabilir veya kaçışlı CSS içermeyen, okunabilir UTF-8 .css dosyası seçin (en fazla 256 KiB).", "exportInvalidRange": "{field}: {minimum} ile {maximum} arasında bir sayı girin.", "clipboardHistory": "Kopyalama Geçmişi", - "clipboardHistorySearchHint": "Pano geçmişinde ara", "clipboardHistoryDescription": "Oturuma özgü kopyalama geçmişini aç.", "clipboardCurrent": "Mevcut kopyalama", "clipboardCurrentExternal": "Başka bir uygulamadan mevcut kopyalama", "clipboardDestination": "{name}'e yapıştır", "clipboardSessionOnly": "Sadece bu BusyMark oturumu için saklanır.", "clipboardNoItems": "Kopyalama geçmişi boş.", - "clipboardNoMatches": "Bu arama ile eşleşen kopyalama öğesi bulunamadı.", "clipboardDisabled": "Kopyalama Geçmişi koleksiyonu Ayarlar'da devre dışıdır.", "clipboardUnavailable": "Mevcut kopyalama içeriği mevcut değil veya desteklenmiyor.", "clipboardTooLarge": "Bu kopyalanmış öğe, saklamak için çok büyüktür.", @@ -4888,8 +4890,6 @@ "findLocalHistoryDescription": "Yerel Geçmişte belgeleri ve revizyon içeriğini arayın.", "localHistoryNoDocuments": "Henüz herhangi bir belge Yerel Geçmişe sahip değil.", "localHistoryNoRevisions": "Bu belge Yerel Geçmiş revizyonlarına sahip değil.", - "localHistoryNoMatches": "Bu arama ile eşleşen Yerel Geçmiş revizyonu yok.", - "localHistorySearchHint": "Yerel geçmişte ara", "localHistorySelectedRevision": "Seçilen revizyon", "localHistoryCurrentEditor": "Mevcut düzenleme içeriği", "localHistoryCurrentDisk": "Disk üzerinde mevcut içerik", @@ -5149,5 +5149,125 @@ "wsInsertElements": "Öğe ekle", "wsAdmonitionsAndQuotes": "Uyarılar ve alıntılar", "wsSemanticText": "Anlamsal metin", - "wsPropertiesUnavailable": "İncelenen öğe kullanılamıyor. Devam etmek için bir öğeyi inceleyin." + "wsPropertiesUnavailable": "İncelenen öğe kullanılamıyor. Devam etmek için bir öğeyi inceleyin.", + "searchDocumentsCategory": "Belgeler", + "searchTopicsCategory": "Konular", + "searchCurrentDocument": "Geçerli belge", + "searchCurrentWorkspace": "Geçerli çalışma alanı", + "browseNotes": "Notlara göz at", + "searchActionsCommand": "Eylemleri ara", + "searchDocumentsCommand": "Belgeleri ara", + "searchTopicsCommand": "Konuları ara", + "searchRecentCommand": "Son kullanılanları ara", + "searchResourceUnavailable": "Bu kaynak kullanılamıyor.", + "notesSync": "Notları eşitle", + "searchInWorkspace": "Çalışma Alanında Ara", + "searchCategory": "Arama kategorisi", + "clearHistorySearchScope": "Belgelerin geçmişinde ara", + "searchInDocument": "Belgede ara", + "searchDocumentHint": "Bu belgede ara…", + "searchWorkspaceHint": "Bu çalışma alanında ara…", + "searchOutlineHint": "Başlıklarda ara…", + "searchHistoryHint": "Yerel geçmişte ara…", + "searchEnterText": "Aranacak metni girin", + "searchAccountUnavailable": "Hesap kullanılamıyor", + "searchDocumentsHint": "Belgeleri ara…", + "searchTopicsHint": "Konuları ara…", + "searchRecentHint": "Son kullanılanları ara…", + "searchActionsHint": "Eylemleri ara…", + "searchRequiresNotesWorkspace": "Notlar çalışma alanı gerektirir", + "searchSidebarLabel": "Ara:", + "searchDocumentTextCategory": "Belge metni", + "searchWorkspaceTextCategory": "Çalışma alanı metni", + "searchHeadingsCategory": "Başlıklar", + "shortcutUnassigned": "Atanmamış", + "shortcutPressKeys": "Bir tuş birleşimine basın…", + "shortcutRecord": "Kısayol kaydet", + "shortcutRestoreAll": "Tüm varsayılanları geri yükle", + "shortcutRestoreAllQuestion": "Tüm eylemlerin varsayılan kısayolları geri yüklensin mi? Özel atamalar kaldırılacak.", + "shortcutSaveFailed": "Kısayollar etkin ancak kaydedilemedi. Yeniden başlatmadan sonra korumak için tekrar kaydedin.", + "shortcutRetry": "Kaydetmeyi yeniden dene", + "shortcutChanged": "Atamalar değişti. Geçerli kısayolları gözden geçirip yeniden deneyin.", + "shortcutInvalid": "Bu birleşim bu bağlamda atanamaz.", + "shortcutSystemWarning": "Masaüstünüz bu birleşimi kullanıyor olabilir. BusyMark sistem kısayollarını geçersiz kılamaz.", + "shortcutConflicts": "Bu kısayol şu eylemlerle çakışıyor:", + "shortcutRemove": "Atamayı kaldır", + "shortcutRestoreDefault": "Varsayılanı geri yükle", + "shortcutAssign": "Ata", + "shortcutReassign": "Yeniden ata", + "shortcutContextApplication": "Uygulama", + "shortcutContextDocument": "Belge görünümleri", + "shortcutContextEditor": "Düzenleyici ve kaynak", + "shortcutContextWritersideSource": "Writerside kaynağı", + "shortcutContextWorkspace": "Çalışma alanı gezintisi", + "shortcutContextTree": "Kenar çubuğu ağaçları", + "searchHeadingsCommand": "Başlıklarda Ara", + "shortcutFixed": "Düzenleyici tarafından yönetilir; yapılandırılamaz.", + "shortcutDefaultConflict": "Varsayılan kısayol özel bir atamayla çakışıyor.", + "shortcutCurrentBinding": "Geçerli kısayol", + "shortcutProposedBinding": "Önerilen kısayol", + "notesActions": "Not işlemleri", + "noteActions": "Bu notun işlemleri", + "noteProperties": "Not Özellikleri", + "notesOfflinePause": "İndirmeleri Duraklat", + "notesOfflineResume": "İndirmeleri Sürdür", + "notesEmptyAll": "Henüz not yok. Başlamak için bir not oluşturun.", + "notesEmptyFavorites": "Favori not yok.", + "notesEmptyCategory": "Bu kategoride not yok.", + "notesEmptyUncategorized": "Kategorisiz not yok.", + "notesEmptyRecovery": "Kurtarılacak saklanmış not yok.", + "notesStartingConnection": "Nextcloud bağlantısı başlatılıyor…", + "notesVerifyingConnection": "Notlar erişimi doğrulanıyor…", + "notesCancellingConnection": "Oturum açma iptal ediliyor ve temizleniyor…", + "notesRemovingConnection": "Yerel hesap kaldırılıyor…", + "notesSettingsLeave": "Ayrılmadan önce Nextcloud ayarlarındaki değişiklikler kaydedilsin mi?", + "notesSettingsReload": "Sunucu Ayarlarını Yeniden Yükle…", + "notesScanning": "Dosyalar taranıyor…", + "notesCancelling": "İptal ediliyor… Geçerli işlemin bitmesi bekleniyor.", + "notesCancelled": "İptal edildi", + "notesPartial": "Kısmen tamamlandı", + "notesNotStarted": "Başlatılmadı", + "notesCreated": "Oluşturuldu", + "notesCurrentText": "Şu anda saklanan metin", + "notesRecovering": "Yeni bir not olarak kurtarılıyor…", + "notesSort": "Notları sırala", + "notesOfflineRetentionHelp": "Bu gereksinimin kaldırılması mevcut önbelleği silmez. Kategori veya hesap gereksinimi notu tutmaya devam edebilir.", + "notesOfflineBroader": "Bu not bir kategori veya hesap gereksinimi nedeniyle de tutuluyor.", + "notesOfflineExternal": "Harici medya bağlantı gerektirir. BusyMark yalnızca not metnini ve yönetilen ekleri tutar.", + "notesOperationFailed": "İşlem tamamlanamadı. Taslağınız korunuyor. Kaydetmeyi ve çakışmaları inceleyip yeniden deneyin.", + "notesRemovedWarning": "Yerel Nextcloud hesabı kaldırıldı. Uygulama parolasının iptali doğrulanamadı. Nextcloud Güvenlik ayarlarından BusyMark uygulama parolasını kaldırın.", + "notesBuffersNotSaved": "Tüm açık notlar yerel olarak kaydedilemedi. Çalışma alanında kaydetmeyi, çakışmaları veya normalleştirmeyi inceleyin. Yerel değişikliklerin hâlâ Nextcloud ile eşitlenmesi gerekebilir.", + "notesSettingsReloadHelp": "Yeniden yükleme, taslağınızı geçerli Nextcloud ayarlarıyla değiştirir. Taslak atılıp yeniden yüklensin mi?", + "notesSettingsRefreshFailed": "Nextcloud ayarları kabul etti ancak notları yenileme başarısız oldu. Gösterilen değerler sunucunun onayladığı değerlerdir. Notlar üzerinden yenilemeyi yeniden deneyin.", + "notesStorageFull": "Nextcloud depolaması dolu. Sunucuda yer açıp yeniden deneyin.", + "notesRejected": "Nextcloud bu değişikliği reddetti. Yeniden denemeden önce notu inceleyin ve yerel bir kopya kaydedin.", + "notesThrottled": "Nextcloud beklemenizi istedi. Eşitleme, yeniden deneme süresi dolunca devam eder.", + "notesImportPartialHelp": "Önceki öğeler zaten yerel olarak kaydedilmiş olabilir. Kopya oluşturmadan devam etmek için bu içe aktarmayı sürdürün.", + "notesExportAll": "Notları dışa aktar · Tüm notlar…", + "notesExportCategory": "Notları dışa aktar · Kategori…", + "notesOfflineAll": "Çevrimdışı kullanılabilir yap · Tüm notlar", + "notesOfflineCategory": "Çevrimdışı kullanılabilir yap · Kategori", + "notesOfflineRemoveAll": "Çevrimdışı saklamayı kaldır · Tüm notlar", + "notesOfflineRemoveCategory": "Çevrimdışı saklamayı kaldır · Kategori", + "notesOfflineKeep": "Çevrimdışı kullanılabilir yap", + "notesLoadFailed": "Nextcloud Notes · Başarısız. Yeniden dene", + "notesInspectionFailed": "Ekler · Başarısız. Yeniden dene", + "notesSettingsLeaveHelp": "Sunucu Notes ayarları · Kaydedilmemiş değişiklikler", + "notesUnavailable": "Çevrimdışı · Metin yerel olarak kullanılabilir · Yerel kaydedildi · eşitleme bekliyor", + "notesRecoveryRequired": "Çatışmalar · Karşılaştır", + "notesTransferFailed": "Başarısız. Yeniden dene", + "notesRemoteDeleteUnavailable": "Sunucu adresi · Desteklenmiyor · Sil · Ekler", + "notesReadOnlyHelp": "Salt okunur · Sık kullanılanlar · Yerel kopyayı kaydet…", + "notesPathRelative": "Not klasörü yolu Nextcloud depolamanıza görelidir.", + "notesSettingsStale": "Nextcloud ayarları değişti. Taslağı değiştirmeden önce sunucu değerlerini yeniden yükleyip inceleyin.", + "notesDownloadsPaused": "İndirmeler duraklatıldı", + "notesExportNotPublished": "Hiçbir dışa aktarım yayımlanmadı.", + "notesKeyringUnavailable": "Masaüstü anahtarlığının kilidini açıp yeniden deneyin.", + "notesConnectFailed": "Nextcloud Notes bağlantısı kurulamadı. Sunucu adresini denetleyip yeniden deneyin.", + "notesAccountMismatch": "Aynı Nextcloud hesabında oturum açın. Başka bir hesap bağlamak için önce bunu kaldırın.", + "notesLocalRemovalFailed": "Yerel hesap temizliği başarısız oldu. Kaldırmayı yeniden deneyebilmeniz için hesap korundu.", + "notesTransferring": "Notlar aktarılıyor…", + "notesTechnicalDetails": "Teknik ayrıntılar", + "notesPauseRequirement": "Çevrimdışı Gereksinimini Duraklat", + "notesResumeRequirement": "Çevrimdışı Gereksinimini Sürdür" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3331cd25..3256b7c7 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Нова нотатка зі збереженого тексту", + "notesAccountThrottled": "Nextcloud попросив зачекати. Зачекайте перед повторним входом.", + "notesSettingsRefreshing": "Оновлення Notes після підтвердження налаштувань", + "notesExportOperationFailed": "Експорт не опубліковано. Перевірте джерело та призначення й повторіть.", + "notesRecoveryCreatedNotOpened": "Відновлену нотатку збережено, але не відкрито. Завершіть зміни редактора або властивостей і відкрийте цю наявну нотатку.", + "notesSettingsPendingNotes": "Зміні заважають очікувані нотатки, конфлікти або непевні створення. Синхронізуйте чи перевірте їх у Notes і повторіть збереження чернетки налаштувань.", + "notesSettingsPendingAttachments": "Зміні заважають операції з вкладеннями. Перевірте їх у Notes і повторіть збереження збереженої чернетки.", + "notesAccountServerAddress": "Використайте канонічну HTTPS-адресу Nextcloud та ввійдіть знову. Адресу або перенаправлення не вдалося перевірити.", + "notesAccountUnsupported": "Notes недоступний або API не підтримується. Увімкніть Nextcloud Notes з основною версією API 1, додатковою 4 або новішою.", + "notesAccountAuthorizeAgain": "Nextcloud відхилив облікові дані. Повторно авторизуйте або підключіть обліковий запис.", + "notesAccountCheckForbidden": "Nextcloud заборонив перевірку можливостей Notes. Попросіть адміністратора дозволити її й повторіть.", + "notesAccountMalformed": "Не вдалося перевірити відповідь Nextcloud. Перевірте встановлення сервера та ввійдіть знову.", + "notesAccountTimedOut": "Час входу минув. Почніть знову та завершіть авторизацію в браузері.", + "notesAccountBrowserUnavailable": "Системний браузер не відкрився. Перевірте типовий браузер і ввійдіть знову.", + "notesAccountNetwork": "Nextcloud недоступний. Перевірте з’єднання та сервер і ввійдіть знову.", + "notesAccountRejected": "Nextcloud відхилив запит можливостей. Перевірте доступ з адміністратором і повторіть.", + "notesAccountFailed": "Налаштування облікового запису не завершено. Повторіть вхід.", + "notesTransferSourceChanged": "Джерело змінилося після перевірки. Перевірте поточний файл або створіть новий знімок.", + "notesTransferUnreadable": "Не вдалося прочитати джерело. Перевірте доступ до файлу чи теки й перевірте знову.", + "notesTransferInvalidSnapshot": "Знімок недійсний або не підтримується. Виберіть коректний знімок BusyMark Notes або джерело Markdown.", + "notesTransferMediaLimit": "Медіа перевищують ліміт імпорту. Зменште медіа відповідного елемента й перевірте знову.", + "notesTransferUnsafeSource": "Джерело містить небезпечний шлях. Виберіть файли в межах вихідної теки та перевірте знову.", + "notesTransferDestination": "Не вдалося записати експорт. Перевірте доступ і вільне місце в призначенні й повторіть.", + "notesTransferOperationFailed": "Передавання не завершено. Перевірте відповідні елементи й повторіть. Раніше імпортовані елементи збережено.", + "notesExportPublishedWithOmissions": "Експорт доступний із пропусками", + "notesBatchUnchanged": "Запитані значення вже встановлено.", + "notesBatchReadOnly": "Нотатка лише для читання. Захищені властивості змінювати не можна; обране змінювати можна.", + "notesBatchReviewChanged": "Нотатка змінилася після перевірки. Перевірте поточний стан перед повторенням дії.", + "notesBatchMissing": "Нотатка більше недоступна. Оновіть список і перевірте вибрані елементи.", + "notesBatchStorage": "Локальне збереження не вдалося. Перевірте сховище й повторіть цей елемент; успішні елементи збережено.", + "notesRemoteReviewChanged": "Нотатка сервера змінилася. Чернетку злиття збережено. Перевірте нове порівняння й виберіть рішення знову.", + "notesReviewRemote": "Я перевірив поточну версію сервера", "wsModuleCapsNotPublished": "Правило регістру модуля не публікується.", "wsTitleOverridesAction": "Заголовки екземпляра та змісту…", "wsHeadingNotPublished": "Не публікується цим збирачем.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Дата", "pdfTitlePageTitleRequired": "Введіть заголовок титульної сторінки.", "pdfTocSectionNote": "Починається з окремої сторінки й може займати кілька сторінок.", - "sourceSearchZeroLengthUnsupported": "Збіги регулярних виразів нульової довжини не підтримуються (наприклад, ^, $ або перевірки вперед, що збігаються лише з позицією).", "workspaceSearchIncomplete": "Показано лише частину результатів. Покажіть більше або звузьте пошук.", "workspaceSearchShowMore": "Показати більше результатів", "workspaceSearchSkippedFiles": "Пошук неповний. Ці файли перевищують 1 MiB або їх не вдалося прочитати:", @@ -585,7 +616,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Показати довідку з комбінацій клавіш", + "shortcutKeyboardShortcutsDescription": "Налаштувати сполучення клавіш застосунку", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -882,7 +913,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Очистити список останніх робочих областей", + "clearRecentWorkspaces": "Очистити недавню історію", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1562,22 +1593,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "З урахуванням регістру", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Ціле слово", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Регулярний вираз", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, - "sourceSearchInvalidRegex": "Некоректний регулярний вираз", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Великий файл: підсвічування та згортання призупинено", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -1711,6 +1726,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Не вдалося оновити нещодавні елементи: {error}.", "workspaceErrorOpenFailed": "Не вдалося відкрити: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -3234,9 +3250,6 @@ "diagnosticMarkdownLinkEmptyText": "Текст посилання порожній; укажіть доступну назву, що описує його призначення.", "diagnosticMarkdownLinkReviewText": "Перевірте, чи описує текст посилання «{text}» його призначення в контексті.", "diagnosticMarkdownTableEmptyHeader": "Заголовки таблиці мають позначати стовпці; заповніть кожен порожній заголовок.", - "commandPalette": "Палітра команд", - "commandPaletteHint": "Введіть команду", - "commandPaletteEmpty": "Немає відповідних команд", "tableAlignmentUnspecified": "Вирівнювання: не вказано", "tableAlignmentLeft": "Вирівнювання: ліворуч", "tableAlignmentCenter": "Вирівнювання: по центру", @@ -3351,14 +3364,12 @@ "exportInvalidCss": "Виберіть читабельний файл .css у UTF-8 до 256 КіБ без імпортів, URL ресурсів, HTML, виконуваного CSS і escape-послідовностей.", "exportInvalidRange": "{field}: введіть число від {minimum} до {maximum}.", "clipboardHistory": "Історія Вирізання", - "clipboardHistorySearchHint": "Пошук в історії буфера обміну", "clipboardHistoryDescription": "Відкрити історію вирізання, прив'язану до сесії.", "clipboardCurrent": "Поточне вирізання", "clipboardCurrentExternal": "Поточне вирізання з іншого додатку", "clipboardDestination": "Вставити в {name}", "clipboardSessionOnly": "Зберігається лише для цієї сесії BusyMark.", "clipboardNoItems": "Історія вирізання порожня.", - "clipboardNoMatches": "Жоден елемент вирізання не відповідає цьому пошуку.", "clipboardDisabled": "Збірка \"Історія Вирізання\" вимкнена в Налаштуваннях.", "clipboardUnavailable": "Поточний вміст вирізання недоступний або не підтримується.", "clipboardTooLarge": "Цей елемент буфера обміну занадто великий для зберігання.", @@ -3375,8 +3386,6 @@ "findLocalHistoryDescription": "Пошук документів та вмісту версій у локальній історії.", "localHistoryNoDocuments": "Жоден документ поки не має локальної історії.", "localHistoryNoRevisions": "Цей документ не має версій локальної історії.", - "localHistoryNoMatches": "Жодна версія локальної історії не відповідає цьому пошуку.", - "localHistorySearchHint": "Пошук у локальній історії", "localHistorySelectedRevision": "Обрана версія", "localHistoryCurrentEditor": "Поточний вміст редактора", "localHistoryCurrentDisk": "Поточний вміст на диску", @@ -3636,5 +3645,125 @@ "wsInsertElements": "Вставити елементи", "wsAdmonitionsAndQuotes": "Примітки та цитати", "wsSemanticText": "Семантичний текст", - "wsPropertiesUnavailable": "Елемент, що перевіряється, недоступний. Виберіть елемент для продовження." + "wsPropertiesUnavailable": "Елемент, що перевіряється, недоступний. Виберіть елемент для продовження.", + "searchDocumentsCategory": "Документи", + "searchTopicsCategory": "Теми", + "searchCurrentDocument": "Поточний документ", + "searchCurrentWorkspace": "Поточна робоча область", + "browseNotes": "Переглянути нотатки", + "searchActionsCommand": "Пошук дій", + "searchDocumentsCommand": "Пошук документів", + "searchTopicsCommand": "Пошук тем", + "searchRecentCommand": "Пошук нещодавніх", + "searchResourceUnavailable": "Цей ресурс недоступний.", + "notesSync": "Синхронізувати нотатки", + "searchInWorkspace": "Пошук у робочій області", + "searchCategory": "Категорія пошуку", + "clearHistorySearchScope": "Пошук в історії документів", + "searchInDocument": "Пошук у документі", + "searchDocumentHint": "Пошук у цьому документі…", + "searchWorkspaceHint": "Пошук у цьому робочому просторі…", + "searchOutlineHint": "Пошук заголовків…", + "searchHistoryHint": "Пошук у локальній історії…", + "searchEnterText": "Введіть текст для пошуку", + "searchAccountUnavailable": "Обліковий запис недоступний", + "searchDocumentsHint": "Пошук документів…", + "searchTopicsHint": "Пошук тем…", + "searchRecentHint": "Пошук нещодавніх…", + "searchActionsHint": "Пошук дій…", + "searchRequiresNotesWorkspace": "Потрібен робочий простір нотаток", + "searchSidebarLabel": "Пошук:", + "searchDocumentTextCategory": "Текст документа", + "searchWorkspaceTextCategory": "Текст робочої області", + "searchHeadingsCategory": "Заголовки", + "shortcutUnassigned": "Не призначено", + "shortcutPressKeys": "Натисніть сполучення клавіш…", + "shortcutRecord": "Записати сполучення клавіш", + "shortcutRestoreAll": "Відновити всі типові значення", + "shortcutRestoreAllQuestion": "Відновити типові сполучення всіх дій? Власні призначення буде видалено.", + "shortcutSaveFailed": "Сполучення діють, але їх не збережено. Повторіть збереження, щоб залишити їх після перезапуску.", + "shortcutRetry": "Повторити збереження", + "shortcutChanged": "Призначення змінилися. Перевірте поточні сполучення та повторіть спробу.", + "shortcutInvalid": "Це сполучення не можна призначити в цьому контексті.", + "shortcutSystemWarning": "Це сполучення може використовуватися стільницею. BusyMark не може перевизначати системні сполучення.", + "shortcutConflicts": "Це сполучення конфліктує з такими діями:", + "shortcutRemove": "Вилучити призначення", + "shortcutRestoreDefault": "Відновити типове", + "shortcutAssign": "Призначити", + "shortcutReassign": "Перепризначити", + "shortcutContextApplication": "Застосунок", + "shortcutContextDocument": "Подання документа", + "shortcutContextEditor": "Редактор і джерело", + "shortcutContextWritersideSource": "Джерело Writerside", + "shortcutContextWorkspace": "Навігація робочою областю", + "shortcutContextTree": "Дерева бічної панелі", + "searchHeadingsCommand": "Пошук заголовків", + "shortcutFixed": "Керується редактором; налаштування недоступне.", + "shortcutDefaultConflict": "Типове сполучення конфліктує з користувацьким призначенням.", + "shortcutCurrentBinding": "Поточне сполучення", + "shortcutProposedBinding": "Запропоноване сполучення", + "notesActions": "Дії з нотатками", + "noteActions": "Дії з нотаткою", + "noteProperties": "Властивості нотатки", + "notesOfflinePause": "Призупинити завантаження", + "notesOfflineResume": "Відновити завантаження", + "notesEmptyAll": "Нотаток ще немає. Створіть першу.", + "notesEmptyFavorites": "Немає улюблених нотаток.", + "notesEmptyCategory": "У цій категорії немає нотаток.", + "notesEmptyUncategorized": "Немає нотаток без категорії.", + "notesEmptyRecovery": "Немає збережених нотаток для відновлення.", + "notesStartingConnection": "Початок підключення до Nextcloud…", + "notesVerifyingConnection": "Перевірка доступу до нотаток…", + "notesCancellingConnection": "Скасування входу та очищення…", + "notesRemovingConnection": "Видалення локального облікового запису…", + "notesSettingsLeave": "Зберегти зміни налаштувань Nextcloud перед виходом?", + "notesSettingsReload": "Перезавантажити налаштування сервера…", + "notesScanning": "Сканування файлів…", + "notesCancelling": "Скасування… Очікування завершення поточної операції.", + "notesCancelled": "Скасовано", + "notesPartial": "Частково завершено", + "notesNotStarted": "Не розпочато", + "notesCreated": "Створено", + "notesCurrentText": "Поточний збережений текст", + "notesRecovering": "Відновлення як нової нотатки…", + "notesSort": "Сортування нотаток", + "notesOfflineRetentionHelp": "Видалення вимоги не видаляє вміст кешу. Вимога категорії чи облікового запису може й надалі зберігати нотатку.", + "notesOfflineBroader": "Ця нотатка також зберігається за вимогою категорії чи облікового запису.", + "notesOfflineExternal": "Зовнішні медіа потребують з’єднання. BusyMark зберігає лише текст і керовані вкладення.", + "notesOperationFailed": "Не вдалося завершити операцію. Чернетку збережено. Перевірте збереження та конфлікти й повторіть спробу.", + "notesRemovedWarning": "Локальний обліковий запис Nextcloud видалено. Відкликання пароля застосунку не підтверджено. Видаліть пароль BusyMark у налаштуваннях безпеки Nextcloud.", + "notesBuffersNotSaved": "Не вдалося зберегти всі відкриті нотатки локально. Перевірте збереження, конфлікти чи нормалізацію в робочому просторі. Локальні зміни можуть ще потребувати синхронізації з Nextcloud.", + "notesSettingsReloadHelp": "Перезавантаження замінить чернетку поточними налаштуваннями Nextcloud. Відкинути чернетку й перезавантажити?", + "notesSettingsRefreshFailed": "Nextcloud прийняв налаштування, але оновлення нотаток не вдалося. Показано значення, підтверджені сервером. Повторіть оновлення з Нотаток.", + "notesStorageFull": "Сховище Nextcloud заповнене. Звільніть місце на сервері та повторіть спробу.", + "notesRejected": "Nextcloud відхилив цю зміну. Перевірте нотатку та збережіть локальну копію перед повторенням.", + "notesThrottled": "Nextcloud попросив зачекати. Синхронізація продовжиться після зазначеного терміну.", + "notesImportPartialHelp": "Попередні елементи можуть уже бути збережені локально. Відновіть цей імпорт, щоб уникнути дублікатів.", + "notesExportAll": "Експорт нотаток · Усі нотатки…", + "notesExportCategory": "Експорт нотаток · Категорія…", + "notesOfflineAll": "Зберегти для роботи без мережі · Усі нотатки", + "notesOfflineCategory": "Зберегти для роботи без мережі · Категорія", + "notesOfflineRemoveAll": "Вилучити збереження без мережі · Усі нотатки", + "notesOfflineRemoveCategory": "Вилучити збереження без мережі · Категорія", + "notesOfflineKeep": "Зберегти для роботи без мережі", + "notesLoadFailed": "Nextcloud Notes · Помилка. Повторити", + "notesInspectionFailed": "Вкладення · Помилка. Повторити", + "notesSettingsLeaveHelp": "Налаштування Notes на сервері · Незбережені зміни", + "notesUnavailable": "Немає мережі · Текст доступний локально · Збережено локально · очікує синхронізації", + "notesRecoveryRequired": "Конфлікти · Порівняти", + "notesTransferFailed": "Помилка. Повторити", + "notesRemoteDeleteUnavailable": "Адреса сервера · Не підтримується · Видалити · Вкладення", + "notesReadOnlyHelp": "Лише читання · Обране · Зберегти локальну копію…", + "notesPathRelative": "Шлях папки нотаток задається відносно вашого сховища Nextcloud.", + "notesSettingsStale": "Налаштування на Nextcloud змінилися. Перезавантажте й перевірте значення сервера перед заміною чернетки.", + "notesDownloadsPaused": "Завантаження призупинено", + "notesExportNotPublished": "Експорт не опубліковано.", + "notesKeyringUnavailable": "Розблокуйте системну в’язку ключів і повторіть спробу.", + "notesConnectFailed": "Не вдалося підключитися до Nextcloud Notes. Перевірте адресу сервера та повторіть спробу.", + "notesAccountMismatch": "Увійдіть до того самого облікового запису Nextcloud. Щоб підключити інший, спершу видаліть цей.", + "notesLocalRemovalFailed": "Не вдалося очистити локальні дані. Обліковий запис збережено для повторного видалення.", + "notesTransferring": "Передавання нотаток…", + "notesTechnicalDetails": "Технічні відомості", + "notesPauseRequirement": "Призупинити вимогу автономного зберігання", + "notesResumeRequirement": "Відновити вимогу автономного зберігання" } diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 81f61b10..f5a92a4f 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "Ghi chú mới từ văn bản được giữ lại", + "notesAccountThrottled": "Nextcloud yêu cầu tạm dừng. Hãy đợi trước khi đăng nhập lại.", + "notesSettingsRefreshing": "Đang làm mới Notes sau khi xác nhận cài đặt", + "notesExportOperationFailed": "Xuất thất bại trước khi công bố. Kiểm tra nguồn và đích rồi xuất lại.", + "notesRecoveryCreatedNotOpened": "Ghi chú khôi phục đã được lưu nhưng không mở được. Giải quyết thay đổi đang chờ trong trình soạn thảo hoặc thuộc tính rồi mở ghi chú hiện có này.", + "notesSettingsPendingNotes": "Ghi chú đang chờ, xung đột hoặc lần tạo chưa rõ kết quả đang chặn thay đổi. Đồng bộ hoặc xem xét trong Notes rồi lưu lại bản nháp cài đặt được giữ.", + "notesSettingsPendingAttachments": "Thao tác tệp đính kèm đang chờ đang chặn thay đổi. Xem xét tệp đính kèm trong Notes rồi lưu lại bản nháp cài đặt được giữ.", + "notesAccountServerAddress": "Dùng địa chỉ HTTPS chuẩn của Nextcloud rồi đăng nhập lại. Không thể xác minh địa chỉ hoặc chuyển hướng.", + "notesAccountUnsupported": "Notes không khả dụng hoặc API không được hỗ trợ. Bật Nextcloud Notes với API phiên bản chính 1, phụ từ 4 trở lên.", + "notesAccountAuthorizeAgain": "Nextcloud từ chối thông tin xác thực. Cấp quyền lại hoặc kết nối lại tài khoản.", + "notesAccountCheckForbidden": "Nextcloud cấm kiểm tra khả năng Notes. Nhờ quản trị viên cho phép rồi thử lại.", + "notesAccountMalformed": "Không thể xác minh phản hồi Nextcloud. Kiểm tra cài đặt máy chủ rồi đăng nhập lại.", + "notesAccountTimedOut": "Đăng nhập hết thời gian. Bắt đầu lại và hoàn tất cấp quyền trong trình duyệt.", + "notesAccountBrowserUnavailable": "Không mở được trình duyệt hệ thống. Kiểm tra trình duyệt mặc định rồi đăng nhập lại.", + "notesAccountNetwork": "Không thể kết nối Nextcloud. Kiểm tra kết nối và máy chủ rồi đăng nhập lại.", + "notesAccountRejected": "Nextcloud từ chối yêu cầu khả năng. Kiểm tra quyền truy cập với quản trị viên rồi thử lại.", + "notesAccountFailed": "Không hoàn tất thiết lập tài khoản. Hãy đăng nhập lại.", + "notesTransferSourceChanged": "Nguồn đã đổi sau khi xem xét. Xem xét tệp hiện tại hoặc tạo ảnh chụp mới trước khi thử lại.", + "notesTransferUnreadable": "Không thể đọc nguồn. Kiểm tra quyền truy cập tệp hoặc thư mục rồi xem xét lại.", + "notesTransferInvalidSnapshot": "Ảnh chụp không hợp lệ hoặc không được hỗ trợ. Chọn ảnh chụp BusyMark Notes hợp lệ hoặc nguồn Markdown.", + "notesTransferMediaLimit": "Nội dung đa phương tiện vượt giới hạn nhập. Giảm kích thước của mục liên quan rồi xem xét lại.", + "notesTransferUnsafeSource": "Nguồn chứa đường dẫn không an toàn. Chọn tệp trong thư mục nguồn rồi xem xét lại.", + "notesTransferDestination": "Không thể ghi bản xuất. Kiểm tra quyền truy cập và chỗ trống ở đích rồi xuất lại.", + "notesTransferOperationFailed": "Không hoàn tất truyền. Xem xét các mục liên quan rồi thử lại. Các mục đã nhập vẫn được lưu.", + "notesExportPublishedWithOmissions": "Bản xuất có sẵn nhưng có mục bị bỏ qua", + "notesBatchUnchanged": "Các giá trị yêu cầu đã được đặt.", + "notesBatchReadOnly": "Ghi chú này chỉ đọc. Không thể sửa thuộc tính được bảo vệ; vẫn có thể đổi Yêu thích.", + "notesBatchReviewChanged": "Ghi chú đã đổi sau khi xem xét. Xem xét trạng thái hiện tại trước khi thực hiện lại.", + "notesBatchMissing": "Ghi chú không còn khả dụng. Làm mới danh sách rồi xem xét lại các mục đích.", + "notesBatchStorage": "Lưu cục bộ thất bại. Kiểm tra bộ nhớ rồi thử lại mục này; các mục thành công vẫn được lưu.", + "notesRemoteReviewChanged": "Ghi chú trên máy chủ đã đổi. Bản nháp hợp nhất được giữ. Xem xét so sánh mới rồi chọn lại cách giải quyết.", + "notesReviewRemote": "Tôi đã xem xét phiên bản máy chủ hiện tại", "wsModuleCapsNotPublished": "Quy tắc viết hoa của mô-đun không được xuất bản.", "wsTitleOverridesAction": "Tiêu đề phiên bản và mục lục…", "wsHeadingNotPublished": "Trình dựng này không xuất bản.", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "Ngày", "pdfTitlePageTitleRequired": "Nhập tiêu đề cho trang tiêu đề.", "pdfTocSectionNote": "Bắt đầu trên trang riêng và có thể kéo dài nhiều trang.", - "sourceSearchZeroLengthUnsupported": "Không hỗ trợ kết quả khớp regex có độ dài bằng 0 (ví dụ ^, $ hoặc lookahead chỉ khớp với một vị trí).", "workspaceSearchIncomplete": "Chỉ hiển thị một phần kết quả. Hiển thị thêm hoặc thu hẹp tìm kiếm.", "workspaceSearchShowMore": "Hiển thị thêm kết quả", "workspaceSearchSkippedFiles": "Tìm kiếm chưa đầy đủ. Các tệp này vượt quá 1 MiB hoặc không thể đọc được:", @@ -420,18 +451,6 @@ "description": "Insert button label." }, "keyboardShortcuts": "Phím tắt", - "commandPalette": "Bảng lệnh", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "Nhập lệnh", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "Không có lệnh phù hợp", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "Lệnh không khả dụng trong ngữ cảnh trình biên tập hiện tại", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -715,7 +734,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "Hiển thị tham chiếu phím tắt này", + "shortcutKeyboardShortcutsDescription": "Tùy chỉnh phím tắt của ứng dụng", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1012,7 +1031,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "Xóa không gian làm việc gần đây", + "clearRecentWorkspaces": "Xóa lịch sử gần đây", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1943,18 +1962,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "Phân biệt chữ hoa, chữ thường", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "Toàn bộ từ", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "Biểu thức chính quy", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "Thay thế bằng", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2138,10 +2145,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "Biểu thức chính quy không hợp lệ", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "Tệp lớn: đã tạm dừng tô sáng và thu gọn", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2281,6 +2284,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "Không thể cập nhật các mục gần đây: {error}.", "workspaceErrorOpenFailed": "Mở không thành công: {error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4864,14 +4868,12 @@ "exportInvalidCss": "Chọn tệp .css UTF-8 đọc được tối đa 256 KiB, không có nhập tài nguyên, URL tài nguyên, HTML, CSS thực thi hoặc ký tự thoát.", "exportInvalidRange": "{field}: nhập số từ {minimum} đến {maximum}.", "clipboardHistory": "Lịch sử sao chép", - "clipboardHistorySearchHint": "Tìm kiếm lịch sử bảng nhớ tạm", "clipboardHistoryDescription": "Mở lịch sử sao chép cục bộ phiên.", "clipboardCurrent": "Lịch sử sao chép hiện tại", "clipboardCurrentExternal": "Lịch sử sao chép từ một ứng dụng khác", "clipboardDestination": "Dán vào {name}", "clipboardSessionOnly": "Chỉ lưu trong phiên BusyMark này.", "clipboardNoItems": "Lịch sử sao chép trống.", - "clipboardNoMatches": "Không có mục sao chép nào khớp với tìm kiếm này.", "clipboardDisabled": "Bộ sưu tập Lịch sử sao chép bị vô hiệu hóa trong Cài đặt.", "clipboardUnavailable": "Nội dung sao chép hiện tại không khả dụng hoặc không được hỗ trợ.", "clipboardTooLarge": "Mục này trong clipboard quá lớn để lưu.", @@ -4888,8 +4890,6 @@ "findLocalHistoryDescription": "Tìm kiếm các tài liệu và nội dung phiên bản trong Lịch sử cục bộ.", "localHistoryNoDocuments": "Chưa có tài liệu nào có Lịch sử cục bộ.", "localHistoryNoRevisions": "Tài liệu này không có các phiên bản Lịch sử cục bộ.", - "localHistoryNoMatches": "Không có phiên bản nào trong Lịch sử cục bộ khớp với tìm kiếm này.", - "localHistorySearchHint": "Tìm kiếm lịch sử cục bộ", "localHistorySelectedRevision": "Phiên bản đã chọn", "localHistoryCurrentEditor": "Nội dung trình soạn thảo hiện tại", "localHistoryCurrentDisk": "Nội dung hiện tại trên ổ đĩa", @@ -5149,5 +5149,125 @@ "wsInsertElements": "Chèn phần tử", "wsAdmonitionsAndQuotes": "Lưu ý và trích dẫn", "wsSemanticText": "Văn bản ngữ nghĩa", - "wsPropertiesUnavailable": "Phần tử đang kiểm tra không khả dụng. Kiểm tra một phần tử để tiếp tục." + "wsPropertiesUnavailable": "Phần tử đang kiểm tra không khả dụng. Kiểm tra một phần tử để tiếp tục.", + "searchDocumentsCategory": "Tài liệu", + "searchTopicsCategory": "Chủ đề", + "searchCurrentDocument": "Tài liệu hiện tại", + "searchCurrentWorkspace": "Không gian làm việc hiện tại", + "browseNotes": "Duyệt ghi chú", + "searchActionsCommand": "Tìm thao tác", + "searchDocumentsCommand": "Tìm tài liệu", + "searchTopicsCommand": "Tìm chủ đề", + "searchRecentCommand": "Tìm mục gần đây", + "searchResourceUnavailable": "Tài nguyên này không khả dụng.", + "notesSync": "Đồng bộ ghi chú", + "searchInWorkspace": "Tìm trong không gian làm việc", + "searchCategory": "Danh mục tìm kiếm", + "clearHistorySearchScope": "Tìm trong lịch sử tài liệu", + "searchInDocument": "Tìm trong tài liệu", + "searchDocumentHint": "Tìm trong tài liệu này…", + "searchWorkspaceHint": "Tìm trong không gian làm việc này…", + "searchOutlineHint": "Tìm tiêu đề…", + "searchHistoryHint": "Tìm trong lịch sử cục bộ…", + "searchEnterText": "Nhập văn bản để tìm kiếm", + "searchAccountUnavailable": "Tài khoản không khả dụng", + "searchDocumentsHint": "Tìm tài liệu…", + "searchTopicsHint": "Tìm chủ đề…", + "searchRecentHint": "Tìm mục gần đây…", + "searchActionsHint": "Tìm thao tác…", + "searchRequiresNotesWorkspace": "Yêu cầu không gian làm việc Ghi chú", + "searchSidebarLabel": "Tìm kiếm:", + "searchDocumentTextCategory": "Văn bản tài liệu", + "searchWorkspaceTextCategory": "Văn bản không gian làm việc", + "searchHeadingsCategory": "Tiêu đề", + "shortcutUnassigned": "Chưa gán", + "shortcutPressKeys": "Nhấn tổ hợp phím…", + "shortcutRecord": "Ghi phím tắt", + "shortcutRestoreAll": "Khôi phục tất cả mặc định", + "shortcutRestoreAllQuestion": "Khôi phục phím tắt mặc định của mọi thao tác? Các gán tùy chỉnh sẽ bị xóa.", + "shortcutSaveFailed": "Phím tắt đang có hiệu lực nhưng chưa lưu được. Thử lưu lại để giữ chúng sau khi khởi động lại.", + "shortcutRetry": "Thử lưu lại", + "shortcutChanged": "Các gán đã thay đổi. Xem lại phím tắt hiện tại và thử lại.", + "shortcutInvalid": "Không thể gán tổ hợp này trong ngữ cảnh này.", + "shortcutSystemWarning": "Môi trường màn hình có thể dùng tổ hợp này. BusyMark không thể ghi đè phím tắt hệ thống.", + "shortcutConflicts": "Phím tắt này xung đột với các thao tác sau:", + "shortcutRemove": "Xóa gán", + "shortcutRestoreDefault": "Khôi phục mặc định", + "shortcutAssign": "Gán", + "shortcutReassign": "Gán lại", + "shortcutContextApplication": "Ứng dụng", + "shortcutContextDocument": "Chế độ xem tài liệu", + "shortcutContextEditor": "Trình sửa và mã nguồn", + "shortcutContextWritersideSource": "Mã nguồn Writerside", + "shortcutContextWorkspace": "Điều hướng không gian làm việc", + "shortcutContextTree": "Cây thanh bên", + "searchHeadingsCommand": "Tìm tiêu đề", + "shortcutFixed": "Do trình soạn thảo quản lý; không thể cấu hình.", + "shortcutDefaultConflict": "Phím tắt mặc định xung đột với gán tùy chỉnh.", + "shortcutCurrentBinding": "Phím tắt hiện tại", + "shortcutProposedBinding": "Phím tắt đề xuất", + "notesActions": "Thao tác ghi chú", + "noteActions": "Thao tác với ghi chú", + "noteProperties": "Thuộc tính ghi chú", + "notesOfflinePause": "Tạm dừng tải xuống", + "notesOfflineResume": "Tiếp tục tải xuống", + "notesEmptyAll": "Chưa có ghi chú. Hãy tạo ghi chú đầu tiên.", + "notesEmptyFavorites": "Không có ghi chú yêu thích.", + "notesEmptyCategory": "Không có ghi chú trong danh mục này.", + "notesEmptyUncategorized": "Không có ghi chú chưa phân loại.", + "notesEmptyRecovery": "Không có ghi chú được giữ lại để khôi phục.", + "notesStartingConnection": "Đang bắt đầu kết nối Nextcloud…", + "notesVerifyingConnection": "Đang xác minh quyền truy cập ghi chú…", + "notesCancellingConnection": "Đang hủy đăng nhập và dọn dẹp…", + "notesRemovingConnection": "Đang xóa tài khoản cục bộ…", + "notesSettingsLeave": "Lưu thay đổi cài đặt Nextcloud trước khi rời đi?", + "notesSettingsReload": "Tải lại cài đặt máy chủ…", + "notesScanning": "Đang quét tệp…", + "notesCancelling": "Đang hủy… Đợi thao tác hiện tại kết thúc.", + "notesCancelled": "Đã hủy", + "notesPartial": "Hoàn tất một phần", + "notesNotStarted": "Chưa bắt đầu", + "notesCreated": "Đã tạo", + "notesCurrentText": "Văn bản hiện đang được giữ lại", + "notesRecovering": "Đang khôi phục thành ghi chú mới…", + "notesSort": "Sắp xếp ghi chú", + "notesOfflineRetentionHelp": "Bỏ yêu cầu này vẫn giữ nội dung đã lưu đệm. Yêu cầu của danh mục hoặc tài khoản có thể vẫn giữ ghi chú này.", + "notesOfflineBroader": "Ghi chú này cũng được giữ theo yêu cầu của danh mục hoặc tài khoản.", + "notesOfflineExternal": "Nội dung đa phương tiện bên ngoài cần kết nối. BusyMark chỉ giữ văn bản ghi chú và tệp đính kèm được quản lý.", + "notesOperationFailed": "Không thể hoàn tất thao tác. Bản nháp vẫn được giữ. Kiểm tra việc lưu và xung đột rồi thử lại.", + "notesRemovedWarning": "Đã xóa tài khoản Nextcloud cục bộ. Chưa xác nhận thu hồi mật khẩu ứng dụng. Hãy xóa mật khẩu BusyMark trong cài đặt Bảo mật Nextcloud.", + "notesBuffersNotSaved": "Không thể lưu cục bộ tất cả ghi chú đang mở. Kiểm tra việc lưu, xung đột hoặc chuẩn hóa trong không gian làm việc. Thay đổi cục bộ có thể vẫn cần đồng bộ với Nextcloud.", + "notesSettingsReloadHelp": "Tải lại sẽ thay bản nháp bằng cài đặt Nextcloud hiện tại. Bỏ bản nháp và tải lại?", + "notesSettingsRefreshFailed": "Nextcloud đã nhận cài đặt nhưng không làm mới được ghi chú. Giá trị hiển thị đã được máy chủ xác nhận. Hãy thử làm mới từ Ghi chú.", + "notesStorageFull": "Bộ nhớ Nextcloud đã đầy. Giải phóng dung lượng máy chủ rồi thử lại.", + "notesRejected": "Nextcloud từ chối thay đổi này. Kiểm tra ghi chú và lưu bản sao cục bộ trước khi thử lại.", + "notesThrottled": "Nextcloud yêu cầu chờ. Đồng bộ sẽ tiếp tục sau thời hạn thử lại.", + "notesImportPartialHelp": "Các mục trước có thể đã được lưu cục bộ. Tiếp tục lần nhập này để tránh tạo bản trùng.", + "notesExportAll": "Xuất ghi chú · Tất cả ghi chú…", + "notesExportCategory": "Xuất ghi chú · Danh mục…", + "notesOfflineAll": "Cho phép dùng ngoại tuyến · Tất cả ghi chú", + "notesOfflineCategory": "Cho phép dùng ngoại tuyến · Danh mục", + "notesOfflineRemoveAll": "Bỏ yêu cầu lưu ngoại tuyến · Tất cả ghi chú", + "notesOfflineRemoveCategory": "Bỏ yêu cầu lưu ngoại tuyến · Danh mục", + "notesOfflineKeep": "Cho phép dùng ngoại tuyến", + "notesLoadFailed": "Nextcloud Notes · Thất bại. Thử lại", + "notesInspectionFailed": "Tệp đính kèm · Thất bại. Thử lại", + "notesSettingsLeaveHelp": "Cài đặt Notes trên máy chủ · Thay đổi chưa lưu", + "notesUnavailable": "Ngoại tuyến · Văn bản có sẵn cục bộ · Đã lưu cục bộ · chờ đồng bộ", + "notesRecoveryRequired": "Xung đột · So sánh", + "notesTransferFailed": "Thất bại. Thử lại", + "notesRemoteDeleteUnavailable": "Địa chỉ máy chủ · Không được hỗ trợ · Xóa · Tệp đính kèm", + "notesReadOnlyHelp": "Chỉ đọc · Yêu thích · Lưu bản sao cục bộ…", + "notesPathRelative": "Đường dẫn thư mục ghi chú tương đối với bộ nhớ Nextcloud của bạn.", + "notesSettingsStale": "Cài đặt trên Nextcloud đã thay đổi. Tải lại và kiểm tra giá trị máy chủ trước khi thay bản nháp.", + "notesDownloadsPaused": "Đã tạm dừng tải xuống", + "notesExportNotPublished": "Không có bản xuất nào được công bố.", + "notesKeyringUnavailable": "Mở khóa kho khóa của máy tính rồi thử lại.", + "notesConnectFailed": "Không thể kết nối Nextcloud Notes. Kiểm tra địa chỉ máy chủ rồi thử lại.", + "notesAccountMismatch": "Đăng nhập cùng tài khoản Nextcloud. Xóa tài khoản này trước để kết nối tài khoản khác.", + "notesLocalRemovalFailed": "Dọn dẹp tài khoản cục bộ thất bại. Tài khoản được giữ để bạn thử xóa lại.", + "notesTransferring": "Đang chuyển ghi chú…", + "notesTechnicalDetails": "Chi tiết kỹ thuật", + "notesPauseRequirement": "Tạm dừng yêu cầu giữ ngoại tuyến", + "notesResumeRequirement": "Tiếp tục yêu cầu giữ ngoại tuyến" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 40ecc186..534cb475 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "从保留的文本新建笔记", + "notesAccountThrottled": "Nextcloud 要求暂停。请等待后再重新登录。", + "notesSettingsRefreshing": "正在按已确认的设置更新笔记", + "notesExportOperationFailed": "导出在发布前失败。请检查来源和目标后重新导出。", + "notesRecoveryCreatedNotOpened": "恢复的笔记已保存,但无法打开。请处理待保存的编辑或属性,再打开这篇已有笔记。", + "notesSettingsPendingNotes": "待处理的笔记、冲突或结果不明的创建阻止了此更改。请在笔记中同步或检查,再重试保留的设置草稿。", + "notesSettingsPendingAttachments": "待处理的附件操作阻止了此更改。请在笔记中检查附件,再重试保留的设置草稿。", + "notesAccountServerAddress": "请使用 Nextcloud 的规范 HTTPS 地址并重新登录。无法验证地址或重定向。", + "notesAccountUnsupported": "笔记不可用或 API 不受支持。请启用 API 主版本为 1、次版本不低于 4 的 Nextcloud Notes。", + "notesAccountAuthorizeAgain": "Nextcloud 拒绝了凭据。请重新授权或重新连接账户。", + "notesAccountCheckForbidden": "Nextcloud 拒绝了笔记功能检查。请联系管理员允许此检查后重试。", + "notesAccountMalformed": "无法验证 Nextcloud 的响应。请检查服务器安装并重新登录。", + "notesAccountTimedOut": "登录超时。请重新登录并在浏览器中完成授权。", + "notesAccountBrowserUnavailable": "无法打开系统浏览器。请检查默认浏览器并重新登录。", + "notesAccountNetwork": "无法连接 Nextcloud。请检查网络和服务器后重新登录。", + "notesAccountRejected": "Nextcloud 拒绝了功能请求。请与管理员确认访问权限后重试。", + "notesAccountFailed": "无法完成账户设置。请重新登录。", + "notesTransferSourceChanged": "来源在审核后已更改。请重新审核当前文件或创建新快照后重试。", + "notesTransferUnreadable": "无法读取来源。请检查文件或文件夹的访问权限,再重新审核。", + "notesTransferInvalidSnapshot": "快照无效或不受支持。请选择有效的 BusyMark Notes 快照或 Markdown 来源。", + "notesTransferMediaLimit": "媒体超出导入大小限制。请缩小受影响项目的媒体,再重新审核。", + "notesTransferUnsafeSource": "来源包含不安全的路径。请选择来源文件夹内的文件并重新审核。", + "notesTransferDestination": "无法写入导出内容。请检查目标的访问权限和可用空间后重新导出。", + "notesTransferOperationFailed": "传输未能完成。请检查受影响的项目并重试。之前导入的项目仍已保存。", + "notesExportPublishedWithOmissions": "导出已发布,但有遗漏", + "notesBatchUnchanged": "请求的值已经设置。", + "notesBatchReadOnly": "此笔记为只读。受保护的属性无法更改,但仍可修改收藏状态。", + "notesBatchReviewChanged": "此笔记在审核后已更改。请审核当前状态后重新执行操作。", + "notesBatchMissing": "此笔记已不可用。请刷新列表并重新检查操作目标。", + "notesBatchStorage": "本地保存失败。请检查存储并重试此项目;成功的项目仍已保存。", + "notesRemoteReviewChanged": "服务器笔记已更改。合并草稿已保留。请审核更新的比较并重新选择解决方式。", + "notesReviewRemote": "我已审核当前服务器版本", "wsModuleCapsNotPublished": "模块的大小写规则不会发布。", "wsTitleOverridesAction": "实例与目录标题…", "wsHeadingNotPublished": "此构建器不会发布此属性。", @@ -110,7 +142,6 @@ "pdfTitlePageDate": "日期", "pdfTitlePageTitleRequired": "请输入标题页的标题。", "pdfTocSectionNote": "从单独一页开始,可以跨多页。", - "sourceSearchZeroLengthUnsupported": "不支持零长度的正则表达式匹配(例如 ^、$ 或仅匹配位置的前瞻断言)。", "workspaceSearchIncomplete": "仅显示了部分结果。请显示更多结果或缩小搜索范围。", "workspaceSearchShowMore": "显示更多结果", "workspaceSearchSkippedFiles": "搜索不完整。以下文件超过 1 MiB 或无法读取:", @@ -422,18 +453,6 @@ "description": "Insert button label." }, "keyboardShortcuts": "键盘快捷键", - "commandPalette": "命令面板", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "输入命令", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "没有匹配的命令", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "当前编辑器上下文中无法使用此命令", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -717,7 +736,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "显示此键盘快捷键参考", + "shortcutKeyboardShortcutsDescription": "自定义应用程序键盘快捷键", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1014,7 +1033,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "清除最近使用的工作区", + "clearRecentWorkspaces": "清除最近记录", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1945,18 +1964,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "区分大小写", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "全字匹配", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "正则表达式", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "替换为", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2140,10 +2147,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "无效的正则表达式", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "大文件:已暂停语法突出显示和折叠", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2283,6 +2286,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "无法更新最近使用的项目:{error}。", "workspaceErrorOpenFailed": "打开失败:{error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4872,14 +4876,12 @@ "exportInvalidCss": "请选择可读取的 UTF-8 .css 文件(最大 256 KiB),不得包含导入、资源 URL、HTML、可执行或转义 CSS。", "exportInvalidRange": "{field}:请输入 {minimum} 到 {maximum} 之间的数字。", "clipboardHistory": "剪贴板历史", - "clipboardHistorySearchHint": "搜索剪贴板历史记录", "clipboardHistoryDescription": "打开会话本地剪贴板历史。", "clipboardCurrent": "当前剪贴板", "clipboardCurrentExternal": "来自其他应用程序的当前剪贴板", "clipboardDestination": "粘贴到 {name}", "clipboardSessionOnly": "仅存储在本次 BusyMark 会话中。", "clipboardNoItems": "剪贴板历史为空。", - "clipboardNoMatches": "没有匹配此搜索的剪贴板项目。", "clipboardDisabled": "剪贴板历史集合在设置中已禁用。", "clipboardUnavailable": "当前剪贴板内容不可用或不受支持。", "clipboardTooLarge": "此剪贴板内容过大,无法保留。", @@ -4896,8 +4898,6 @@ "findLocalHistoryDescription": "搜索本地历史文档和修订内容。", "localHistoryNoDocuments": "尚无文档具有本地历史。", "localHistoryNoRevisions": "此文档没有本地历史修订。", - "localHistoryNoMatches": "没有本地历史修订与此搜索匹配。", - "localHistorySearchHint": "搜索本地历史记录", "localHistorySelectedRevision": "已选择的修订", "localHistoryCurrentEditor": "当前编辑器内容", "localHistoryCurrentDisk": "磁盘上的当前内容", @@ -5161,5 +5161,125 @@ "wsInsertElements": "插入元素", "wsAdmonitionsAndQuotes": "提示和引用", "wsSemanticText": "语义文本", - "wsPropertiesUnavailable": "所检查的元素不可用。请检查一个元素以继续。" + "wsPropertiesUnavailable": "所检查的元素不可用。请检查一个元素以继续。", + "searchDocumentsCategory": "文档", + "searchTopicsCategory": "主题", + "searchCurrentDocument": "当前文档", + "searchCurrentWorkspace": "当前工作区", + "browseNotes": "浏览笔记", + "searchActionsCommand": "搜索操作", + "searchDocumentsCommand": "搜索文档", + "searchTopicsCommand": "搜索主题", + "searchRecentCommand": "搜索最近项目", + "searchResourceUnavailable": "此资源不可用。", + "notesSync": "同步笔记", + "searchInWorkspace": "在工作区中搜索", + "searchCategory": "搜索类别", + "clearHistorySearchScope": "搜索文档历史记录", + "searchInDocument": "在文档中搜索", + "searchDocumentHint": "搜索此文档…", + "searchWorkspaceHint": "搜索此工作区…", + "searchOutlineHint": "搜索标题…", + "searchHistoryHint": "搜索本地历史记录…", + "searchEnterText": "输入要搜索的文本", + "searchAccountUnavailable": "账户不可用", + "searchDocumentsHint": "搜索文档…", + "searchTopicsHint": "搜索主题…", + "searchRecentHint": "搜索最近项目…", + "searchActionsHint": "搜索操作…", + "searchRequiresNotesWorkspace": "需要笔记工作区", + "searchSidebarLabel": "搜索:", + "searchDocumentTextCategory": "文档文本", + "searchWorkspaceTextCategory": "工作区文本", + "searchHeadingsCategory": "标题", + "shortcutUnassigned": "未分配", + "shortcutPressKeys": "请按下组合键…", + "shortcutRecord": "录制快捷键", + "shortcutRestoreAll": "恢复所有默认设置", + "shortcutRestoreAllQuestion": "恢复所有操作的默认快捷键?自定义分配将被移除。", + "shortcutSaveFailed": "快捷键已生效,但无法保存。请重试保存以便重启后保留。", + "shortcutRetry": "重试保存", + "shortcutChanged": "分配已更改。请检查当前快捷键并重试。", + "shortcutInvalid": "无法在此上下文中分配此组合键。", + "shortcutSystemWarning": "桌面可能正在使用此组合键。BusyMark 无法覆盖系统快捷键。", + "shortcutConflicts": "此快捷键与以下操作冲突:", + "shortcutRemove": "移除分配", + "shortcutRestoreDefault": "恢复默认设置", + "shortcutAssign": "分配", + "shortcutReassign": "重新分配", + "shortcutContextApplication": "应用程序", + "shortcutContextDocument": "文档视图", + "shortcutContextEditor": "编辑器和源码", + "shortcutContextWritersideSource": "Writerside 源码", + "shortcutContextWorkspace": "工作区导航", + "shortcutContextTree": "侧栏树", + "searchHeadingsCommand": "搜索标题", + "shortcutFixed": "由编辑器管理,无法配置。", + "shortcutDefaultConflict": "默认快捷键与自定义分配冲突。", + "shortcutCurrentBinding": "当前快捷键", + "shortcutProposedBinding": "拟用快捷键", + "notesActions": "笔记操作", + "noteActions": "此笔记的操作", + "noteProperties": "笔记属性", + "notesOfflinePause": "暂停下载", + "notesOfflineResume": "继续下载", + "notesEmptyAll": "还没有笔记。请新建一条笔记。", + "notesEmptyFavorites": "没有收藏的笔记。", + "notesEmptyCategory": "此分类中没有笔记。", + "notesEmptyUncategorized": "没有未分类笔记。", + "notesEmptyRecovery": "没有可恢复的本地保留笔记。", + "notesStartingConnection": "正在连接 Nextcloud…", + "notesVerifyingConnection": "正在验证笔记访问权限…", + "notesCancellingConnection": "正在取消登录并清理…", + "notesRemovingConnection": "正在移除本地账户…", + "notesSettingsLeave": "离开前保存 Nextcloud 设置更改吗?", + "notesSettingsReload": "重新加载服务器设置…", + "notesScanning": "正在扫描文件…", + "notesCancelling": "正在取消…等待当前操作结束。", + "notesCancelled": "已取消", + "notesPartial": "部分完成", + "notesNotStarted": "尚未开始", + "notesCreated": "已创建", + "notesCurrentText": "当前保留的文本", + "notesRecovering": "正在恢复为新笔记…", + "notesSort": "笔记排序", + "notesOfflineRetentionHelp": "移除此保留要求不会删除现有缓存。分类或账户的要求仍可能保留此笔记。", + "notesOfflineBroader": "此笔记也因分类或账户的要求而被保留。", + "notesOfflineExternal": "外部媒体需要网络连接。BusyMark 只保留笔记文本和托管附件。", + "notesOperationFailed": "无法完成操作。草稿已保留。请检查保存状态和冲突后重试。", + "notesRemovedWarning": "本地 Nextcloud 账户已移除,但无法确认应用密码已撤销。请在 Nextcloud 安全设置中移除 BusyMark 的应用密码。", + "notesBuffersNotSaved": "无法将所有打开的笔记保存到本地。请在工作区检查保存、冲突或规范化处理。待处理的本地更改可能仍需与 Nextcloud 同步。", + "notesSettingsReloadHelp": "重新加载会用 Nextcloud 当前设置替换草稿。放弃草稿并重新加载吗?", + "notesSettingsRefreshFailed": "Nextcloud 已接受设置,但刷新笔记失败。显示的是服务器确认的设置值。请从笔记界面重试刷新。", + "notesStorageFull": "Nextcloud 存储空间已满。请释放服务器空间后重试。", + "notesRejected": "Nextcloud 拒绝了此更改。请检查笔记并保存本地副本后重试。", + "notesThrottled": "Nextcloud 要求稍候。同步将在重试期限后继续。", + "notesImportPartialHelp": "之前的项目可能已保存在本地。请继续本次导入,以免创建重复笔记。", + "notesExportAll": "导出笔记 · 所有笔记…", + "notesExportCategory": "导出笔记 · 类别…", + "notesOfflineAll": "设为离线可用 · 所有笔记", + "notesOfflineCategory": "设为离线可用 · 类别", + "notesOfflineRemoveAll": "移除离线保留要求 · 所有笔记", + "notesOfflineRemoveCategory": "移除离线保留要求 · 类别", + "notesOfflineKeep": "设为离线可用", + "notesLoadFailed": "Nextcloud Notes · 失败. 重试", + "notesInspectionFailed": "附件 · 失败. 重试", + "notesSettingsLeaveHelp": "服务器 Notes 设置 · 未保存的更改", + "notesUnavailable": "离线 · 文本已在本地可用 · 已保存到本地 · 等待同步", + "notesRecoveryRequired": "冲突 · 比较", + "notesTransferFailed": "失败. 重试", + "notesRemoteDeleteUnavailable": "服务器地址 · 不支持 · 删除 · 附件", + "notesReadOnlyHelp": "只读 · 收藏 · 保存本地副本…", + "notesPathRelative": "笔记文件夹路径相对于您的 Nextcloud 存储空间。", + "notesSettingsStale": "Nextcloud 上的设置已更改。请重新加载并检查服务器值,再替换草稿。", + "notesDownloadsPaused": "下载已暂停", + "notesExportNotPublished": "未发布任何导出。", + "notesKeyringUnavailable": "请解锁桌面密钥环后重试。", + "notesConnectFailed": "无法连接 Nextcloud Notes。请检查服务器地址后重试。", + "notesAccountMismatch": "请登录同一 Nextcloud 账户。要连接其他账户,请先移除此账户。", + "notesLocalRemovalFailed": "本地账户清理失败。账户已保留,以便重试移除。", + "notesTransferring": "正在传输笔记…", + "notesTechnicalDetails": "技术详情", + "notesPauseRequirement": "暂停离线保留要求", + "notesResumeRequirement": "继续离线保留要求" } diff --git a/lib/l10n/app_zh_CN.arb b/lib/l10n/app_zh_CN.arb index 8cb2aaa8..e7ad036d 100644 --- a/lib/l10n/app_zh_CN.arb +++ b/lib/l10n/app_zh_CN.arb @@ -1,4 +1,36 @@ { + "notesNewFromRetainedText": "从保留的文本新建笔记", + "notesAccountThrottled": "Nextcloud 要求暂停。请等待后再重新登录。", + "notesSettingsRefreshing": "正在按已确认的设置更新笔记", + "notesExportOperationFailed": "导出在发布前失败。请检查来源和目标后重新导出。", + "notesRecoveryCreatedNotOpened": "恢复的笔记已保存,但无法打开。请处理待保存的编辑或属性,再打开这篇已有笔记。", + "notesSettingsPendingNotes": "待处理的笔记、冲突或结果不明的创建阻止了此更改。请在笔记中同步或检查,再重试保留的设置草稿。", + "notesSettingsPendingAttachments": "待处理的附件操作阻止了此更改。请在笔记中检查附件,再重试保留的设置草稿。", + "notesAccountServerAddress": "请使用 Nextcloud 的规范 HTTPS 地址并重新登录。无法验证地址或重定向。", + "notesAccountUnsupported": "笔记不可用或 API 不受支持。请启用 API 主版本为 1、次版本不低于 4 的 Nextcloud Notes。", + "notesAccountAuthorizeAgain": "Nextcloud 拒绝了凭据。请重新授权或重新连接账户。", + "notesAccountCheckForbidden": "Nextcloud 拒绝了笔记功能检查。请联系管理员允许此检查后重试。", + "notesAccountMalformed": "无法验证 Nextcloud 的响应。请检查服务器安装并重新登录。", + "notesAccountTimedOut": "登录超时。请重新登录并在浏览器中完成授权。", + "notesAccountBrowserUnavailable": "无法打开系统浏览器。请检查默认浏览器并重新登录。", + "notesAccountNetwork": "无法连接 Nextcloud。请检查网络和服务器后重新登录。", + "notesAccountRejected": "Nextcloud 拒绝了功能请求。请与管理员确认访问权限后重试。", + "notesAccountFailed": "无法完成账户设置。请重新登录。", + "notesTransferSourceChanged": "来源在审核后已更改。请重新审核当前文件或创建新快照后重试。", + "notesTransferUnreadable": "无法读取来源。请检查文件或文件夹的访问权限,再重新审核。", + "notesTransferInvalidSnapshot": "快照无效或不受支持。请选择有效的 BusyMark Notes 快照或 Markdown 来源。", + "notesTransferMediaLimit": "媒体超出导入大小限制。请缩小受影响项目的媒体,再重新审核。", + "notesTransferUnsafeSource": "来源包含不安全的路径。请选择来源文件夹内的文件并重新审核。", + "notesTransferDestination": "无法写入导出内容。请检查目标的访问权限和可用空间后重新导出。", + "notesTransferOperationFailed": "传输未能完成。请检查受影响的项目并重试。之前导入的项目仍已保存。", + "notesExportPublishedWithOmissions": "导出已发布,但有遗漏", + "notesBatchUnchanged": "请求的值已经设置。", + "notesBatchReadOnly": "此笔记为只读。受保护的属性无法更改,但仍可修改收藏状态。", + "notesBatchReviewChanged": "此笔记在审核后已更改。请审核当前状态后重新执行操作。", + "notesBatchMissing": "此笔记已不可用。请刷新列表并重新检查操作目标。", + "notesBatchStorage": "本地保存失败。请检查存储并重试此项目;成功的项目仍已保存。", + "notesRemoteReviewChanged": "服务器笔记已更改。合并草稿已保留。请审核更新的比较并重新选择解决方式。", + "notesReviewRemote": "我已审核当前服务器版本", "wsModuleCapsNotPublished": "模块的大小写规则不会发布。", "wsTitleOverridesAction": "实例与目录标题…", "wsHeadingNotPublished": "此构建器不会发布此属性。", @@ -108,7 +140,6 @@ "pdfTitlePageDate": "日期", "pdfTitlePageTitleRequired": "请输入标题页的标题。", "pdfTocSectionNote": "从单独一页开始,可以跨多页。", - "sourceSearchZeroLengthUnsupported": "不支持零长度的正则表达式匹配(例如 ^、$ 或仅匹配位置的前瞻断言)。", "workspaceSearchIncomplete": "仅显示了部分结果。请显示更多结果或缩小搜索范围。", "workspaceSearchShowMore": "显示更多结果", "workspaceSearchSkippedFiles": "搜索不完整。以下文件超过 1 MiB 或无法读取:", @@ -420,18 +451,6 @@ "description": "Insert button label." }, "keyboardShortcuts": "键盘快捷键", - "commandPalette": "命令面板", - "@commandPalette": { - "description": "Title and command label for the searchable command palette." - }, - "commandPaletteHint": "输入命令", - "@commandPaletteHint": { - "description": "Search hint in the command palette." - }, - "commandPaletteEmpty": "没有匹配的命令", - "@commandPaletteEmpty": { - "description": "Empty state in the command palette." - }, "commandUnavailableInContext": "当前编辑器上下文中无法使用此命令", "@commandUnavailableInContext": { "description": "Reason shown for a disabled contextual command." @@ -715,7 +734,7 @@ "@shortcutSearchDescription": { "description": "Keyboard shortcut description for search." }, - "shortcutKeyboardShortcutsDescription": "显示此键盘快捷键参考", + "shortcutKeyboardShortcutsDescription": "自定义应用程序键盘快捷键", "@shortcutKeyboardShortcutsDescription": { "description": "Keyboard shortcut description for opening shortcuts." }, @@ -1012,7 +1031,7 @@ "@validateOnEdit": { "description": "Validation setting label." }, - "clearRecentWorkspaces": "清除最近使用的工作区", + "clearRecentWorkspaces": "清除最近记录", "@clearRecentWorkspaces": { "description": "Settings action label for clearing recent workspaces." }, @@ -1943,18 +1962,6 @@ "@sourceSearchNextMatch": { "description": "Tooltip for moving to the next source search match." }, - "sourceSearchCaseSensitive": "区分大小写", - "@sourceSearchCaseSensitive": { - "description": "Tooltip for toggling case-sensitive source search." - }, - "sourceSearchWholeWord": "全字匹配", - "@sourceSearchWholeWord": { - "description": "Tooltip for toggling whole-word source search." - }, - "sourceSearchRegex": "正则表达式", - "@sourceSearchRegex": { - "description": "Tooltip for toggling regular-expression source search." - }, "sourceSearchReplacement": "替换为", "@sourceSearchReplacement": { "description": "Placeholder for the active-document replacement field." @@ -2138,10 +2145,6 @@ "@saveAs": { "description": "Action that saves the current document to another path." }, - "sourceSearchInvalidRegex": "无效的正则表达式", - "@sourceSearchInvalidRegex": { - "description": "Source search status shown when the regular expression is invalid." - }, "sourceLargeFileFeaturesPaused": "大文件:已暂停语法突出显示和折叠", "@sourceLargeFileFeaturesPaused": { "description": "Status banner shown when source highlighting and folding are disabled for a large file." @@ -2281,6 +2284,7 @@ "@documentKindResourceFile": { "description": "Search result document kind label." }, + "workspaceErrorRecentWorkspaceUpdateFailed": "无法更新最近使用的项目:{error}。", "workspaceErrorOpenFailed": "打开失败:{error}", "@workspaceErrorOpenFailed": { "description": "Workspace error message shown when opening a path fails.", @@ -4864,14 +4868,12 @@ "exportInvalidCss": "请选择可读取的 UTF-8 .css 文件(最大 256 KiB),不得包含导入、资源 URL、HTML、可执行或转义 CSS。", "exportInvalidRange": "{field}:请输入 {minimum} 到 {maximum} 之间的数字。", "clipboardHistory": "剪贴板历史", - "clipboardHistorySearchHint": "搜索剪贴板历史记录", "clipboardHistoryDescription": "打开会话本地剪贴板历史。", "clipboardCurrent": "当前剪贴板", "clipboardCurrentExternal": "来自其他应用程序的当前剪贴板", "clipboardDestination": "粘贴到 {name}", "clipboardSessionOnly": "仅存储在本次 BusyMark 会话中。", "clipboardNoItems": "剪贴板历史为空。", - "clipboardNoMatches": "没有匹配此搜索的剪贴板项目。", "clipboardDisabled": "剪贴板历史集合在设置中已禁用。", "clipboardUnavailable": "当前剪贴板内容不可用或不受支持。", "clipboardTooLarge": "此剪贴板内容过大,无法保留。", @@ -4888,8 +4890,6 @@ "findLocalHistoryDescription": "搜索本地历史文档和修订内容。", "localHistoryNoDocuments": "尚无文档具有本地历史。", "localHistoryNoRevisions": "此文档没有本地历史修订。", - "localHistoryNoMatches": "没有本地历史修订与此搜索匹配。", - "localHistorySearchHint": "搜索本地历史记录", "localHistorySelectedRevision": "已选择的修订", "localHistoryCurrentEditor": "当前编辑器内容", "localHistoryCurrentDisk": "磁盘上的当前内容", @@ -5149,5 +5149,125 @@ "wsInsertElements": "插入元素", "wsAdmonitionsAndQuotes": "提示和引用", "wsSemanticText": "语义文本", - "wsPropertiesUnavailable": "所检查的元素不可用。请检查一个元素以继续。" + "wsPropertiesUnavailable": "所检查的元素不可用。请检查一个元素以继续。", + "searchDocumentsCategory": "文档", + "searchTopicsCategory": "主题", + "searchCurrentDocument": "当前文档", + "searchCurrentWorkspace": "当前工作区", + "browseNotes": "浏览笔记", + "searchActionsCommand": "搜索操作", + "searchDocumentsCommand": "搜索文档", + "searchTopicsCommand": "搜索主题", + "searchRecentCommand": "搜索最近项目", + "searchResourceUnavailable": "此资源不可用。", + "notesSync": "同步笔记", + "searchInWorkspace": "在工作区中搜索", + "searchCategory": "搜索类别", + "clearHistorySearchScope": "搜索文档历史记录", + "searchInDocument": "在文档中搜索", + "searchDocumentHint": "搜索此文档…", + "searchWorkspaceHint": "搜索此工作区…", + "searchOutlineHint": "搜索标题…", + "searchHistoryHint": "搜索本地历史记录…", + "searchEnterText": "输入要搜索的文本", + "searchAccountUnavailable": "账户不可用", + "searchDocumentsHint": "搜索文档…", + "searchTopicsHint": "搜索主题…", + "searchRecentHint": "搜索最近项目…", + "searchActionsHint": "搜索操作…", + "searchRequiresNotesWorkspace": "需要笔记工作区", + "searchSidebarLabel": "搜索:", + "searchDocumentTextCategory": "文档文本", + "searchWorkspaceTextCategory": "工作区文本", + "searchHeadingsCategory": "标题", + "shortcutUnassigned": "未分配", + "shortcutPressKeys": "请按下组合键…", + "shortcutRecord": "录制快捷键", + "shortcutRestoreAll": "恢复所有默认设置", + "shortcutRestoreAllQuestion": "恢复所有操作的默认快捷键?自定义分配将被移除。", + "shortcutSaveFailed": "快捷键已生效,但无法保存。请重试保存以便重启后保留。", + "shortcutRetry": "重试保存", + "shortcutChanged": "分配已更改。请检查当前快捷键并重试。", + "shortcutInvalid": "无法在此上下文中分配此组合键。", + "shortcutSystemWarning": "桌面可能正在使用此组合键。BusyMark 无法覆盖系统快捷键。", + "shortcutConflicts": "此快捷键与以下操作冲突:", + "shortcutRemove": "移除分配", + "shortcutRestoreDefault": "恢复默认设置", + "shortcutAssign": "分配", + "shortcutReassign": "重新分配", + "shortcutContextApplication": "应用程序", + "shortcutContextDocument": "文档视图", + "shortcutContextEditor": "编辑器和源码", + "shortcutContextWritersideSource": "Writerside 源码", + "shortcutContextWorkspace": "工作区导航", + "shortcutContextTree": "侧栏树", + "searchHeadingsCommand": "搜索标题", + "shortcutFixed": "由编辑器管理,无法配置。", + "shortcutDefaultConflict": "默认快捷键与自定义分配冲突。", + "shortcutCurrentBinding": "当前快捷键", + "shortcutProposedBinding": "拟用快捷键", + "notesActions": "笔记操作", + "noteActions": "此笔记的操作", + "noteProperties": "笔记属性", + "notesOfflinePause": "暂停下载", + "notesOfflineResume": "继续下载", + "notesEmptyAll": "还没有笔记。请新建一条笔记。", + "notesEmptyFavorites": "没有收藏的笔记。", + "notesEmptyCategory": "此分类中没有笔记。", + "notesEmptyUncategorized": "没有未分类笔记。", + "notesEmptyRecovery": "没有可恢复的本地保留笔记。", + "notesStartingConnection": "正在连接 Nextcloud…", + "notesVerifyingConnection": "正在验证笔记访问权限…", + "notesCancellingConnection": "正在取消登录并清理…", + "notesRemovingConnection": "正在移除本地账户…", + "notesSettingsLeave": "离开前保存 Nextcloud 设置更改吗?", + "notesSettingsReload": "重新加载服务器设置…", + "notesScanning": "正在扫描文件…", + "notesCancelling": "正在取消…等待当前操作结束。", + "notesCancelled": "已取消", + "notesPartial": "部分完成", + "notesNotStarted": "尚未开始", + "notesCreated": "已创建", + "notesCurrentText": "当前保留的文本", + "notesRecovering": "正在恢复为新笔记…", + "notesSort": "笔记排序", + "notesOfflineRetentionHelp": "移除此保留要求不会删除现有缓存。分类或账户的要求仍可能保留此笔记。", + "notesOfflineBroader": "此笔记也因分类或账户的要求而被保留。", + "notesOfflineExternal": "外部媒体需要网络连接。BusyMark 只保留笔记文本和托管附件。", + "notesOperationFailed": "无法完成操作。草稿已保留。请检查保存状态和冲突后重试。", + "notesRemovedWarning": "本地 Nextcloud 账户已移除,但无法确认应用密码已撤销。请在 Nextcloud 安全设置中移除 BusyMark 的应用密码。", + "notesBuffersNotSaved": "无法将所有打开的笔记保存到本地。请在工作区检查保存、冲突或规范化处理。待处理的本地更改可能仍需与 Nextcloud 同步。", + "notesSettingsReloadHelp": "重新加载会用 Nextcloud 当前设置替换草稿。放弃草稿并重新加载吗?", + "notesSettingsRefreshFailed": "Nextcloud 已接受设置,但刷新笔记失败。显示的是服务器确认的设置值。请从笔记界面重试刷新。", + "notesStorageFull": "Nextcloud 存储空间已满。请释放服务器空间后重试。", + "notesRejected": "Nextcloud 拒绝了此更改。请检查笔记并保存本地副本后重试。", + "notesThrottled": "Nextcloud 要求稍候。同步将在重试期限后继续。", + "notesImportPartialHelp": "之前的项目可能已保存在本地。请继续本次导入,以免创建重复笔记。", + "notesExportAll": "导出笔记 · 所有笔记…", + "notesExportCategory": "导出笔记 · 类别…", + "notesOfflineAll": "设为离线可用 · 所有笔记", + "notesOfflineCategory": "设为离线可用 · 类别", + "notesOfflineRemoveAll": "移除离线保留要求 · 所有笔记", + "notesOfflineRemoveCategory": "移除离线保留要求 · 类别", + "notesOfflineKeep": "设为离线可用", + "notesLoadFailed": "Nextcloud Notes · 失败. 重试", + "notesInspectionFailed": "附件 · 失败. 重试", + "notesSettingsLeaveHelp": "服务器 Notes 设置 · 未保存的更改", + "notesUnavailable": "离线 · 文本已在本地可用 · 已保存到本地 · 等待同步", + "notesRecoveryRequired": "冲突 · 比较", + "notesTransferFailed": "失败. 重试", + "notesRemoteDeleteUnavailable": "服务器地址 · 不支持 · 删除 · 附件", + "notesReadOnlyHelp": "只读 · 收藏 · 保存本地副本…", + "notesPathRelative": "笔记文件夹路径相对于您的 Nextcloud 存储空间。", + "notesSettingsStale": "Nextcloud 上的设置已更改。请重新加载并检查服务器值,再替换草稿。", + "notesDownloadsPaused": "下载已暂停", + "notesExportNotPublished": "未发布任何导出。", + "notesKeyringUnavailable": "请解锁桌面密钥环后重试。", + "notesConnectFailed": "无法连接 Nextcloud Notes。请检查服务器地址后重试。", + "notesAccountMismatch": "请登录同一 Nextcloud 账户。要连接其他账户,请先移除此账户。", + "notesLocalRemovalFailed": "本地账户清理失败。账户已保留,以便重试移除。", + "notesTransferring": "正在传输笔记…", + "notesTechnicalDetails": "技术详情", + "notesPauseRequirement": "暂停离线保留要求", + "notesResumeRequirement": "继续离线保留要求" } diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 3150b175..fa7563be 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -139,6 +139,198 @@ abstract class AppLocalizations { Locale('zh', 'CN'), ]; + /// No description provided for @notesNewFromRetainedText. + /// + /// In en, this message translates to: + /// **'New note from retained text'** + String get notesNewFromRetainedText; + + /// No description provided for @notesAccountThrottled. + /// + /// In en, this message translates to: + /// **'Nextcloud requested a pause. Wait before starting sign-in again.'** + String get notesAccountThrottled; + + /// No description provided for @notesSettingsRefreshing. + /// + /// In en, this message translates to: + /// **'Refreshing Notes after the acknowledged settings update'** + String get notesSettingsRefreshing; + + /// No description provided for @notesExportOperationFailed. + /// + /// In en, this message translates to: + /// **'The export failed before publication. Check the source and destination, then export again.'** + String get notesExportOperationFailed; + + /// No description provided for @notesRecoveryCreatedNotOpened. + /// + /// In en, this message translates to: + /// **'The recovered note was saved. Opening it failed. Resolve pending editor or property changes, then open this existing note.'** + String get notesRecoveryCreatedNotOpened; + + /// No description provided for @notesSettingsPendingNotes. + /// + /// In en, this message translates to: + /// **'Pending notes, conflicts, or uncertain creations block this collection change. Open Notes and synchronize or review them, then retry this retained settings draft.'** + String get notesSettingsPendingNotes; + + /// No description provided for @notesSettingsPendingAttachments. + /// + /// In en, this message translates to: + /// **'Pending attachment operations block this collection change. Open Notes and review the attachments, then retry this retained settings draft.'** + String get notesSettingsPendingAttachments; + + /// No description provided for @notesAccountServerAddress. + /// + /// In en, this message translates to: + /// **'Use the canonical HTTPS address of your Nextcloud server, then start sign-in again. The address or redirect could not be verified.'** + String get notesAccountServerAddress; + + /// No description provided for @notesAccountUnsupported. + /// + /// In en, this message translates to: + /// **'Notes is unavailable or its API is unsupported. Enable Nextcloud Notes with API major version 1, minor version 4 or later.'** + String get notesAccountUnsupported; + + /// No description provided for @notesAccountAuthorizeAgain. + /// + /// In en, this message translates to: + /// **'Nextcloud rejected the credentials. Authorize this account again or reconnect.'** + String get notesAccountAuthorizeAgain; + + /// No description provided for @notesAccountCheckForbidden. + /// + /// In en, this message translates to: + /// **'Nextcloud denied the Notes capability check. Ask the server administrator to allow it, then try again.'** + String get notesAccountCheckForbidden; + + /// No description provided for @notesAccountMalformed. + /// + /// In en, this message translates to: + /// **'The Nextcloud response could not be verified. Check the server installation, then start sign-in again.'** + String get notesAccountMalformed; + + /// No description provided for @notesAccountTimedOut. + /// + /// In en, this message translates to: + /// **'Sign-in timed out. Start sign-in again and complete authorization in your browser.'** + String get notesAccountTimedOut; + + /// No description provided for @notesAccountBrowserUnavailable. + /// + /// In en, this message translates to: + /// **'The system browser could not open. Check your default browser, then start sign-in again.'** + String get notesAccountBrowserUnavailable; + + /// No description provided for @notesAccountNetwork. + /// + /// In en, this message translates to: + /// **'BusyMark could not reach Nextcloud. Check your connection and the server, then try signing in again.'** + String get notesAccountNetwork; + + /// No description provided for @notesAccountRejected. + /// + /// In en, this message translates to: + /// **'Nextcloud rejected the capability request. Check server access with your administrator, then try again.'** + String get notesAccountRejected; + + /// No description provided for @notesAccountFailed. + /// + /// In en, this message translates to: + /// **'BusyMark could not complete account setup. Try signing in again.'** + String get notesAccountFailed; + + /// No description provided for @notesTransferSourceChanged. + /// + /// In en, this message translates to: + /// **'The source changed after review. Review the current file or capture a new snapshot before retrying.'** + String get notesTransferSourceChanged; + + /// No description provided for @notesTransferUnreadable. + /// + /// In en, this message translates to: + /// **'The source could not be read. Check access to the file or folder, then review it again.'** + String get notesTransferUnreadable; + + /// No description provided for @notesTransferInvalidSnapshot. + /// + /// In en, this message translates to: + /// **'The snapshot is invalid or unsupported. Choose a valid BusyMark Notes snapshot or Markdown source.'** + String get notesTransferInvalidSnapshot; + + /// No description provided for @notesTransferMediaLimit. + /// + /// In en, this message translates to: + /// **'Media exceeds the import size limit. Reduce the affected item\'s media before reviewing it again.'** + String get notesTransferMediaLimit; + + /// No description provided for @notesTransferUnsafeSource. + /// + /// In en, this message translates to: + /// **'The source contains an unsafe path. Choose files within the selected source folder and review again.'** + String get notesTransferUnsafeSource; + + /// No description provided for @notesTransferDestination. + /// + /// In en, this message translates to: + /// **'The export could not be written. Check access and free space at the destination, then export again.'** + String get notesTransferDestination; + + /// No description provided for @notesTransferOperationFailed. + /// + /// In en, this message translates to: + /// **'The transfer could not finish. Review the affected items and try again. Earlier imported items remain saved.'** + String get notesTransferOperationFailed; + + /// No description provided for @notesExportPublishedWithOmissions. + /// + /// In en, this message translates to: + /// **'Export available with omissions'** + String get notesExportPublishedWithOmissions; + + /// No description provided for @notesBatchUnchanged. + /// + /// In en, this message translates to: + /// **'The requested values were already set.'** + String get notesBatchUnchanged; + + /// No description provided for @notesBatchReadOnly. + /// + /// In en, this message translates to: + /// **'This note is read-only. Protected properties cannot change; Favorite is still permitted.'** + String get notesBatchReadOnly; + + /// No description provided for @notesBatchReviewChanged. + /// + /// In en, this message translates to: + /// **'This note changed after review. Review its current state before applying this action again.'** + String get notesBatchReviewChanged; + + /// No description provided for @notesBatchMissing. + /// + /// In en, this message translates to: + /// **'This note is no longer available. Refresh the list and review the targets again.'** + String get notesBatchMissing; + + /// No description provided for @notesBatchStorage. + /// + /// In en, this message translates to: + /// **'The local save failed. Check local storage and retry this item; successful items remain saved.'** + String get notesBatchStorage; + + /// No description provided for @notesRemoteReviewChanged. + /// + /// In en, this message translates to: + /// **'The server note changed. Your merge draft is retained. Review the updated comparison and choose how to resolve it again.'** + String get notesRemoteReviewChanged; + + /// No description provided for @notesReviewRemote. + /// + /// In en, this message translates to: + /// **'I have reviewed the current server version'** + String get notesReviewRemote; + /// No description provided for @wsModuleCapsNotPublished. /// /// In en, this message translates to: @@ -1189,24 +1381,6 @@ abstract class AppLocalizations { /// **'Keyboard Shortcuts'** String get keyboardShortcuts; - /// Title and command label for the searchable command palette. - /// - /// In en, this message translates to: - /// **'Command Palette'** - String get commandPalette; - - /// Search hint in the command palette. - /// - /// In en, this message translates to: - /// **'Type a command'** - String get commandPaletteHint; - - /// Empty state in the command palette. - /// - /// In en, this message translates to: - /// **'No matching commands'** - String get commandPaletteEmpty; - /// Reason shown for a disabled contextual command. /// /// In en, this message translates to: @@ -1630,7 +1804,7 @@ abstract class AppLocalizations { /// Keyboard shortcut description for opening shortcuts. /// /// In en, this message translates to: - /// **'Show this keyboard shortcut reference'** + /// **'Customize application keyboard shortcuts'** String get shortcutKeyboardShortcutsDescription; /// Keyboard shortcut description for opening the Syntax Reference. @@ -2077,10 +2251,10 @@ abstract class AppLocalizations { /// **'Validate on edit'** String get validateOnEdit; - /// Settings action label for clearing recent workspaces. + /// Settings action for clearing Recent navigation metadata in both document and workspace sections. /// /// In en, this message translates to: - /// **'Clear recent workspaces'** + /// **'Clear recent history'** String get clearRecentWorkspaces; /// Settings label for WYSIWYG editing button position. @@ -3391,24 +3565,6 @@ abstract class AppLocalizations { /// **'Next match'** String get sourceSearchNextMatch; - /// Tooltip for toggling case-sensitive source search. - /// - /// In en, this message translates to: - /// **'Case sensitive'** - String get sourceSearchCaseSensitive; - - /// Tooltip for toggling whole-word source search. - /// - /// In en, this message translates to: - /// **'Whole word'** - String get sourceSearchWholeWord; - - /// Tooltip for toggling regular-expression source search. - /// - /// In en, this message translates to: - /// **'Regex'** - String get sourceSearchRegex; - /// Placeholder for the active-document replacement field. /// /// In en, this message translates to: @@ -3613,18 +3769,6 @@ abstract class AppLocalizations { /// **'Save As'** String get saveAs; - /// Source search status shown when the regular expression is invalid. - /// - /// In en, this message translates to: - /// **'Invalid regular expression'** - String get sourceSearchInvalidRegex; - - /// No description provided for @sourceSearchZeroLengthUnsupported. - /// - /// In en, this message translates to: - /// **'Zero-length regex matches are not supported (for example ^, \$, or lookaheads that match only a position).'** - String get sourceSearchZeroLengthUnsupported; - /// No description provided for @workspaceSearchIncomplete. /// /// In en, this message translates to: @@ -3817,6 +3961,12 @@ abstract class AppLocalizations { /// **'Resource file'** String get documentKindResourceFile; + /// Warning when updating Recent items fails after a successful open or save. + /// + /// In en, this message translates to: + /// **'Could not update Recent items: {error}.'** + String workspaceErrorRecentWorkspaceUpdateFailed(String error); + /// Workspace error message shown when opening a path fails. /// /// In en, this message translates to: @@ -7368,12 +7518,6 @@ abstract class AppLocalizations { /// **'Clipboard History'** String get clipboardHistory; - /// No description provided for @clipboardHistorySearchHint. - /// - /// In en, this message translates to: - /// **'Search clipboard history'** - String get clipboardHistorySearchHint; - /// No description provided for @clipboardHistoryDescription. /// /// In en, this message translates to: @@ -7410,12 +7554,6 @@ abstract class AppLocalizations { /// **'Clipboard history is empty.'** String get clipboardNoItems; - /// No description provided for @clipboardNoMatches. - /// - /// In en, this message translates to: - /// **'No clipboard items match this search.'** - String get clipboardNoMatches; - /// No description provided for @clipboardDisabled. /// /// In en, this message translates to: @@ -7512,18 +7650,6 @@ abstract class AppLocalizations { /// **'This document has no Local History revisions.'** String get localHistoryNoRevisions; - /// No description provided for @localHistoryNoMatches. - /// - /// In en, this message translates to: - /// **'No Local History revisions match this search.'** - String get localHistoryNoMatches; - - /// No description provided for @localHistorySearchHint. - /// - /// In en, this message translates to: - /// **'Search local history'** - String get localHistorySearchHint; - /// No description provided for @localHistorySelectedRevision. /// /// In en, this message translates to: @@ -9251,6 +9377,726 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'The inspected element is unavailable. Inspect an element to continue.'** String get wsPropertiesUnavailable; + + /// No description provided for @searchDocumentsCategory. + /// + /// In en, this message translates to: + /// **'Documents'** + String get searchDocumentsCategory; + + /// No description provided for @searchTopicsCategory. + /// + /// In en, this message translates to: + /// **'Topics'** + String get searchTopicsCategory; + + /// No description provided for @searchCurrentDocument. + /// + /// In en, this message translates to: + /// **'Current document'** + String get searchCurrentDocument; + + /// No description provided for @searchCurrentWorkspace. + /// + /// In en, this message translates to: + /// **'Current workspace'** + String get searchCurrentWorkspace; + + /// No description provided for @browseNotes. + /// + /// In en, this message translates to: + /// **'Browse Notes'** + String get browseNotes; + + /// No description provided for @searchActionsCommand. + /// + /// In en, this message translates to: + /// **'Search Actions'** + String get searchActionsCommand; + + /// No description provided for @searchDocumentsCommand. + /// + /// In en, this message translates to: + /// **'Search Documents'** + String get searchDocumentsCommand; + + /// No description provided for @searchTopicsCommand. + /// + /// In en, this message translates to: + /// **'Search Topics'** + String get searchTopicsCommand; + + /// No description provided for @searchRecentCommand. + /// + /// In en, this message translates to: + /// **'Search Recent'** + String get searchRecentCommand; + + /// No description provided for @searchResourceUnavailable. + /// + /// In en, this message translates to: + /// **'This resource is unavailable.'** + String get searchResourceUnavailable; + + /// Command to synchronize the active Nextcloud Notes account. + /// + /// In en, this message translates to: + /// **'Sync notes'** + String get notesSync; + + /// No description provided for @searchInWorkspace. + /// + /// In en, this message translates to: + /// **'Search in Workspace'** + String get searchInWorkspace; + + /// No description provided for @searchCategory. + /// + /// In en, this message translates to: + /// **'Search category'** + String get searchCategory; + + /// No description provided for @clearHistorySearchScope. + /// + /// In en, this message translates to: + /// **'Search history across documents'** + String get clearHistorySearchScope; + + /// No description provided for @searchInDocument. + /// + /// In en, this message translates to: + /// **'Search in Document'** + String get searchInDocument; + + /// No description provided for @searchDocumentHint. + /// + /// In en, this message translates to: + /// **'Search this document…'** + String get searchDocumentHint; + + /// No description provided for @searchWorkspaceHint. + /// + /// In en, this message translates to: + /// **'Search this workspace…'** + String get searchWorkspaceHint; + + /// No description provided for @searchOutlineHint. + /// + /// In en, this message translates to: + /// **'Search headings…'** + String get searchOutlineHint; + + /// No description provided for @searchHistoryHint. + /// + /// In en, this message translates to: + /// **'Search Local History…'** + String get searchHistoryHint; + + /// No description provided for @searchEnterText. + /// + /// In en, this message translates to: + /// **'Enter text to search'** + String get searchEnterText; + + /// No description provided for @searchAccountUnavailable. + /// + /// In en, this message translates to: + /// **'Account unavailable'** + String get searchAccountUnavailable; + + /// No description provided for @searchDocumentsHint. + /// + /// In en, this message translates to: + /// **'Search documents…'** + String get searchDocumentsHint; + + /// No description provided for @searchTopicsHint. + /// + /// In en, this message translates to: + /// **'Search topics…'** + String get searchTopicsHint; + + /// No description provided for @searchRecentHint. + /// + /// In en, this message translates to: + /// **'Search recent resources…'** + String get searchRecentHint; + + /// No description provided for @searchActionsHint. + /// + /// In en, this message translates to: + /// **'Search actions…'** + String get searchActionsHint; + + /// No description provided for @searchRequiresNotesWorkspace. + /// + /// In en, this message translates to: + /// **'Requires a Notes workspace'** + String get searchRequiresNotesWorkspace; + + /// Search and keyboard shortcut settings: searchSidebarLabel + /// + /// In en, this message translates to: + /// **'Search:'** + String get searchSidebarLabel; + + /// Search and keyboard shortcut settings: searchDocumentTextCategory + /// + /// In en, this message translates to: + /// **'Document Text'** + String get searchDocumentTextCategory; + + /// Search and keyboard shortcut settings: searchWorkspaceTextCategory + /// + /// In en, this message translates to: + /// **'Workspace Text'** + String get searchWorkspaceTextCategory; + + /// Search and keyboard shortcut settings: searchHeadingsCategory + /// + /// In en, this message translates to: + /// **'Headings'** + String get searchHeadingsCategory; + + /// Search and keyboard shortcut settings: shortcutUnassigned + /// + /// In en, this message translates to: + /// **'Unassigned'** + String get shortcutUnassigned; + + /// Search and keyboard shortcut settings: shortcutPressKeys + /// + /// In en, this message translates to: + /// **'Press a key combination…'** + String get shortcutPressKeys; + + /// Search and keyboard shortcut settings: shortcutRecord + /// + /// In en, this message translates to: + /// **'Record shortcut'** + String get shortcutRecord; + + /// Search and keyboard shortcut settings: shortcutRestoreAll + /// + /// In en, this message translates to: + /// **'Restore all defaults'** + String get shortcutRestoreAll; + + /// Search and keyboard shortcut settings: shortcutRestoreAllQuestion + /// + /// In en, this message translates to: + /// **'Restore every action’s default shortcut? Custom assignments will be removed.'** + String get shortcutRestoreAllQuestion; + + /// Search and keyboard shortcut settings: shortcutSaveFailed + /// + /// In en, this message translates to: + /// **'Shortcuts are active but could not be saved. Retry to keep them after restart.'** + String get shortcutSaveFailed; + + /// Search and keyboard shortcut settings: shortcutRetry + /// + /// In en, this message translates to: + /// **'Retry saving'** + String get shortcutRetry; + + /// Search and keyboard shortcut settings: shortcutChanged + /// + /// In en, this message translates to: + /// **'Assignments changed. Review the current shortcuts and try again.'** + String get shortcutChanged; + + /// Search and keyboard shortcut settings: shortcutInvalid + /// + /// In en, this message translates to: + /// **'This combination cannot be assigned in this context.'** + String get shortcutInvalid; + + /// Search and keyboard shortcut settings: shortcutSystemWarning + /// + /// In en, this message translates to: + /// **'This combination may be used by your desktop. BusyMark cannot override system shortcuts.'** + String get shortcutSystemWarning; + + /// Search and keyboard shortcut settings: shortcutConflicts + /// + /// In en, this message translates to: + /// **'This shortcut overlaps these actions:'** + String get shortcutConflicts; + + /// Search and keyboard shortcut settings: shortcutRemove + /// + /// In en, this message translates to: + /// **'Remove assignment'** + String get shortcutRemove; + + /// Search and keyboard shortcut settings: shortcutRestoreDefault + /// + /// In en, this message translates to: + /// **'Restore default'** + String get shortcutRestoreDefault; + + /// Search and keyboard shortcut settings: shortcutAssign + /// + /// In en, this message translates to: + /// **'Assign'** + String get shortcutAssign; + + /// Search and keyboard shortcut settings: shortcutReassign + /// + /// In en, this message translates to: + /// **'Reassign'** + String get shortcutReassign; + + /// Search and keyboard shortcut settings: shortcutContextApplication + /// + /// In en, this message translates to: + /// **'Application'** + String get shortcutContextApplication; + + /// Search and keyboard shortcut settings: shortcutContextDocument + /// + /// In en, this message translates to: + /// **'Document views'** + String get shortcutContextDocument; + + /// Search and keyboard shortcut settings: shortcutContextEditor + /// + /// In en, this message translates to: + /// **'Editor and Source'** + String get shortcutContextEditor; + + /// Search and keyboard shortcut settings: shortcutContextWritersideSource + /// + /// In en, this message translates to: + /// **'Writerside Source'** + String get shortcutContextWritersideSource; + + /// Search and keyboard shortcut settings: shortcutContextWorkspace + /// + /// In en, this message translates to: + /// **'Workspace navigation'** + String get shortcutContextWorkspace; + + /// Search and keyboard shortcut settings: shortcutContextTree + /// + /// In en, this message translates to: + /// **'Sidebar trees'** + String get shortcutContextTree; + + /// No description provided for @searchHeadingsCommand. + /// + /// In en, this message translates to: + /// **'Search Headings'** + String get searchHeadingsCommand; + + /// No description provided for @shortcutFixed. + /// + /// In en, this message translates to: + /// **'Handled by the editor; not configurable.'** + String get shortcutFixed; + + /// No description provided for @shortcutDefaultConflict. + /// + /// In en, this message translates to: + /// **'Default shortcut conflicts with a custom assignment.'** + String get shortcutDefaultConflict; + + /// No description provided for @shortcutCurrentBinding. + /// + /// In en, this message translates to: + /// **'Current shortcut'** + String get shortcutCurrentBinding; + + /// No description provided for @shortcutProposedBinding. + /// + /// In en, this message translates to: + /// **'Proposed shortcut'** + String get shortcutProposedBinding; + + /// No description provided for @notesActions. + /// + /// In en, this message translates to: + /// **'Notes actions'** + String get notesActions; + + /// No description provided for @noteActions. + /// + /// In en, this message translates to: + /// **'Note actions'** + String get noteActions; + + /// No description provided for @noteProperties. + /// + /// In en, this message translates to: + /// **'Note Properties'** + String get noteProperties; + + /// No description provided for @notesExportAll. + /// + /// In en, this message translates to: + /// **'Export All Notes…'** + String get notesExportAll; + + /// No description provided for @notesExportCategory. + /// + /// In en, this message translates to: + /// **'Export Category…'** + String get notesExportCategory; + + /// No description provided for @notesOfflineAll. + /// + /// In en, this message translates to: + /// **'Keep All Notes Available Offline'** + String get notesOfflineAll; + + /// No description provided for @notesOfflineCategory. + /// + /// In en, this message translates to: + /// **'Keep Category Available Offline'** + String get notesOfflineCategory; + + /// No description provided for @notesOfflineRemoveAll. + /// + /// In en, this message translates to: + /// **'Remove All Notes Offline Requirement'** + String get notesOfflineRemoveAll; + + /// No description provided for @notesOfflineRemoveCategory. + /// + /// In en, this message translates to: + /// **'Remove Category Offline Requirement'** + String get notesOfflineRemoveCategory; + + /// No description provided for @notesOfflinePause. + /// + /// In en, this message translates to: + /// **'Pause Downloads'** + String get notesOfflinePause; + + /// No description provided for @notesOfflineResume. + /// + /// In en, this message translates to: + /// **'Resume Downloads'** + String get notesOfflineResume; + + /// No description provided for @notesOfflineKeep. + /// + /// In en, this message translates to: + /// **'Keep this note available offline'** + String get notesOfflineKeep; + + /// No description provided for @notesOfflineRetentionHelp. + /// + /// In en, this message translates to: + /// **'Removing this requirement keeps existing cached content. A category or account requirement may still retain this note.'** + String get notesOfflineRetentionHelp; + + /// No description provided for @notesOfflineBroader. + /// + /// In en, this message translates to: + /// **'This note is also retained by a category or account requirement.'** + String get notesOfflineBroader; + + /// No description provided for @notesOfflineExternal. + /// + /// In en, this message translates to: + /// **'External media requires a connection. BusyMark only retains note text and managed attachments.'** + String get notesOfflineExternal; + + /// No description provided for @notesEmptyAll. + /// + /// In en, this message translates to: + /// **'No notes yet. Create a note to get started.'** + String get notesEmptyAll; + + /// No description provided for @notesEmptyFavorites. + /// + /// In en, this message translates to: + /// **'No favorite notes.'** + String get notesEmptyFavorites; + + /// No description provided for @notesEmptyCategory. + /// + /// In en, this message translates to: + /// **'No notes in this category.'** + String get notesEmptyCategory; + + /// No description provided for @notesEmptyUncategorized. + /// + /// In en, this message translates to: + /// **'No uncategorized notes.'** + String get notesEmptyUncategorized; + + /// No description provided for @notesEmptyRecovery. + /// + /// In en, this message translates to: + /// **'No retained notes to recover.'** + String get notesEmptyRecovery; + + /// No description provided for @notesLoadFailed. + /// + /// In en, this message translates to: + /// **'BusyMark could not load the local Notes store. Retry to open your cached notes.'** + String get notesLoadFailed; + + /// No description provided for @notesOperationFailed. + /// + /// In en, this message translates to: + /// **'The operation could not finish. Your draft is retained. Review Save and any conflicts, then try again.'** + String get notesOperationFailed; + + /// No description provided for @notesInspectionFailed. + /// + /// In en, this message translates to: + /// **'BusyMark could not inspect attachments. Retry to review availability.'** + String get notesInspectionFailed; + + /// No description provided for @notesStartingConnection. + /// + /// In en, this message translates to: + /// **'Starting Nextcloud connection…'** + String get notesStartingConnection; + + /// No description provided for @notesVerifyingConnection. + /// + /// In en, this message translates to: + /// **'Verifying Notes access…'** + String get notesVerifyingConnection; + + /// No description provided for @notesCancellingConnection. + /// + /// In en, this message translates to: + /// **'Cancelling login and cleaning up…'** + String get notesCancellingConnection; + + /// No description provided for @notesRemovingConnection. + /// + /// In en, this message translates to: + /// **'Removing the local account…'** + String get notesRemovingConnection; + + /// No description provided for @notesRemovedWarning. + /// + /// In en, this message translates to: + /// **'The local Nextcloud account was removed. App-password revocation was not confirmed. Remove BusyMark’s app password in Nextcloud Security settings.'** + String get notesRemovedWarning; + + /// No description provided for @notesBuffersNotSaved. + /// + /// In en, this message translates to: + /// **'BusyMark could not save all open notes locally. Review Save, conflicts, or normalization in the workspace before trying again. Pending local changes may still need to synchronize with Nextcloud.'** + String get notesBuffersNotSaved; + + /// No description provided for @notesSettingsLeave. + /// + /// In en, this message translates to: + /// **'Save changes to Nextcloud settings before leaving?'** + String get notesSettingsLeave; + + /// No description provided for @notesSettingsLeaveHelp. + /// + /// In en, this message translates to: + /// **'Your Notes folder and suffix changes have not been sent to Nextcloud.'** + String get notesSettingsLeaveHelp; + + /// No description provided for @notesSettingsReload. + /// + /// In en, this message translates to: + /// **'Reload Server Settings…'** + String get notesSettingsReload; + + /// No description provided for @notesSettingsReloadHelp. + /// + /// In en, this message translates to: + /// **'Reloading replaces your draft with the current settings on Nextcloud. Discard this draft and reload?'** + String get notesSettingsReloadHelp; + + /// No description provided for @notesSettingsRefreshFailed. + /// + /// In en, this message translates to: + /// **'Nextcloud accepted the settings update, but refreshing notes failed. The displayed settings are the server’s acknowledged values. Retry the refresh from Notes.'** + String get notesSettingsRefreshFailed; + + /// No description provided for @notesUnavailable. + /// + /// In en, this message translates to: + /// **'Nextcloud is unavailable. Cached notes remain available; local changes are waiting to synchronize.'** + String get notesUnavailable; + + /// No description provided for @notesStorageFull. + /// + /// In en, this message translates to: + /// **'Nextcloud storage is full. Free space on the server, then retry.'** + String get notesStorageFull; + + /// No description provided for @notesRejected. + /// + /// In en, this message translates to: + /// **'Nextcloud rejected this change. Review the note and save a local copy before retrying.'** + String get notesRejected; + + /// No description provided for @notesThrottled. + /// + /// In en, this message translates to: + /// **'Nextcloud asked BusyMark to wait. Synchronization resumes after the retry deadline.'** + String get notesThrottled; + + /// No description provided for @notesRecoveryRequired. + /// + /// In en, this message translates to: + /// **'This change requires review before synchronization can continue.'** + String get notesRecoveryRequired; + + /// No description provided for @notesScanning. + /// + /// In en, this message translates to: + /// **'Scanning files…'** + String get notesScanning; + + /// No description provided for @notesCancelling. + /// + /// In en, this message translates to: + /// **'Cancelling… Waiting for the current operation to settle.'** + String get notesCancelling; + + /// No description provided for @notesCancelled. + /// + /// In en, this message translates to: + /// **'Cancelled'** + String get notesCancelled; + + /// No description provided for @notesPartial. + /// + /// In en, this message translates to: + /// **'Partially completed'** + String get notesPartial; + + /// No description provided for @notesTransferFailed. + /// + /// In en, this message translates to: + /// **'Transfer failed. Review the outcome before retrying.'** + String get notesTransferFailed; + + /// No description provided for @notesImportPartialHelp. + /// + /// In en, this message translates to: + /// **'Earlier items may already be saved locally. Resume this import to continue without duplicating them.'** + String get notesImportPartialHelp; + + /// No description provided for @notesNotStarted. + /// + /// In en, this message translates to: + /// **'Not started'** + String get notesNotStarted; + + /// No description provided for @notesCreated. + /// + /// In en, this message translates to: + /// **'Created'** + String get notesCreated; + + /// No description provided for @notesRemoteDeleteUnavailable. + /// + /// In en, this message translates to: + /// **'This server does not support deleting remote attachments.'** + String get notesRemoteDeleteUnavailable; + + /// No description provided for @notesCurrentText. + /// + /// In en, this message translates to: + /// **'Current retained text'** + String get notesCurrentText; + + /// No description provided for @notesRecovering. + /// + /// In en, this message translates to: + /// **'Recovering as a new note…'** + String get notesRecovering; + + /// No description provided for @notesReadOnlyHelp. + /// + /// In en, this message translates to: + /// **'This note is read-only. You can change its favorite status or save a local copy.'** + String get notesReadOnlyHelp; + + /// No description provided for @notesSort. + /// + /// In en, this message translates to: + /// **'Sort notes'** + String get notesSort; + + /// No description provided for @notesPathRelative. + /// + /// In en, this message translates to: + /// **'The Notes folder path is relative to your Nextcloud storage.'** + String get notesPathRelative; + + /// No description provided for @notesSettingsStale. + /// + /// In en, this message translates to: + /// **'The settings changed on Nextcloud. Reload and review the server values before replacing your draft.'** + String get notesSettingsStale; + + /// No description provided for @notesDownloadsPaused. + /// + /// In en, this message translates to: + /// **'Downloads paused'** + String get notesDownloadsPaused; + + /// No description provided for @notesExportNotPublished. + /// + /// In en, this message translates to: + /// **'No export was published.'** + String get notesExportNotPublished; + + /// No description provided for @notesKeyringUnavailable. + /// + /// In en, this message translates to: + /// **'Unlock the desktop keyring and try again.'** + String get notesKeyringUnavailable; + + /// No description provided for @notesConnectFailed. + /// + /// In en, this message translates to: + /// **'Could not connect to Nextcloud Notes. Check the server address and try again.'** + String get notesConnectFailed; + + /// No description provided for @notesAccountMismatch. + /// + /// In en, this message translates to: + /// **'Sign in to the same Nextcloud account. Remove this account first to connect a different one.'** + String get notesAccountMismatch; + + /// No description provided for @notesLocalRemovalFailed. + /// + /// In en, this message translates to: + /// **'Local account cleanup failed. The account is retained so you can retry removal.'** + String get notesLocalRemovalFailed; + + /// No description provided for @notesTransferring. + /// + /// In en, this message translates to: + /// **'Transferring notes…'** + String get notesTransferring; + + /// No description provided for @notesTechnicalDetails. + /// + /// In en, this message translates to: + /// **'Technical details'** + String get notesTechnicalDetails; + + /// No description provided for @notesPauseRequirement. + /// + /// In en, this message translates to: + /// **'Pause Offline Requirement'** + String get notesPauseRequirement; + + /// No description provided for @notesResumeRequirement. + /// + /// In en, this message translates to: + /// **'Resume Offline Requirement'** + String get notesResumeRequirement; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 97db9500..42c665e3 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -10,6 +10,130 @@ import 'app_localizations.dart'; class AppLocalizationsAr extends AppLocalizations { AppLocalizationsAr([String locale = 'ar']) : super(locale); + @override + String get notesNewFromRetainedText => 'ملاحظة جديدة من النص المحتفَظ به'; + + @override + String get notesAccountThrottled => + 'طلب Nextcloud التوقف مؤقتًا. انتظر قبل بدء تسجيل الدخول مجددًا.'; + + @override + String get notesSettingsRefreshing => 'جارٍ تحديث Notes بعد تأكيد الإعدادات'; + + @override + String get notesExportOperationFailed => + 'فشل التصدير قبل النشر. تحقق من المصدر والوجهة ثم أعد التصدير.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'حُفظت الملاحظة المستردة لكن تعذر فتحها. عالج تعديلات المحرر أو الخصائص المعلقة ثم افتح هذه الملاحظة الموجودة.'; + + @override + String get notesSettingsPendingNotes => + 'تمنع الملاحظات المعلقة أو التعارضات أو عمليات الإنشاء غير المؤكدة هذا التغيير. زامنها أو راجعها في Notes ثم أعد حفظ مسودة الإعدادات المحفوظة.'; + + @override + String get notesSettingsPendingAttachments => + 'تمنع عمليات المرفقات المعلقة هذا التغيير. راجع المرفقات في Notes ثم أعد حفظ مسودة الإعدادات المحفوظة.'; + + @override + String get notesAccountServerAddress => + 'استخدم عنوان HTTPS الأساسي لخادم Nextcloud وأعد تسجيل الدخول. تعذر التحقق من العنوان أو إعادة التوجيه.'; + + @override + String get notesAccountUnsupported => + 'Notes غير متاح أو واجهته غير مدعومة. فعّل Nextcloud Notes بواجهة API ذات الإصدار الرئيسي 1 والفرعي 4 أو أحدث.'; + + @override + String get notesAccountAuthorizeAgain => + 'رفض Nextcloud بيانات الاعتماد. أعد تفويض الحساب أو الاتصال به.'; + + @override + String get notesAccountCheckForbidden => + 'رفض Nextcloud فحص إمكانات Notes. اطلب من مسؤول الخادم السماح به ثم أعد المحاولة.'; + + @override + String get notesAccountMalformed => + 'تعذر التحقق من استجابة Nextcloud. تحقق من تثبيت الخادم ثم أعد تسجيل الدخول.'; + + @override + String get notesAccountTimedOut => + 'انتهت مهلة تسجيل الدخول. ابدأ مجددًا وأكمل التفويض في المتصفح.'; + + @override + String get notesAccountBrowserUnavailable => + 'تعذر فتح متصفح النظام. تحقق من المتصفح الافتراضي ثم أعد تسجيل الدخول.'; + + @override + String get notesAccountNetwork => + 'تعذر الوصول إلى Nextcloud. تحقق من الاتصال والخادم ثم أعد تسجيل الدخول.'; + + @override + String get notesAccountRejected => + 'رفض Nextcloud طلب الإمكانات. تحقق من صلاحية الوصول مع المسؤول ثم أعد المحاولة.'; + + @override + String get notesAccountFailed => + 'تعذر إكمال إعداد الحساب. أعد محاولة تسجيل الدخول.'; + + @override + String get notesTransferSourceChanged => + 'تغير المصدر بعد المراجعة. راجع الملف الحالي أو التقط نسخة جديدة قبل إعادة المحاولة.'; + + @override + String get notesTransferUnreadable => + 'تعذرت قراءة المصدر. تحقق من الوصول إلى الملف أو المجلد ثم راجعه مجددًا.'; + + @override + String get notesTransferInvalidSnapshot => + 'النسخة غير صالحة أو غير مدعومة. اختر نسخة BusyMark Notes صالحة أو مصدر Markdown.'; + + @override + String get notesTransferMediaLimit => + 'تتجاوز الوسائط حد الاستيراد. قلل وسائط العنصر المتأثر ثم راجعه مجددًا.'; + + @override + String get notesTransferUnsafeSource => + 'يحتوي المصدر على مسار غير آمن. اختر ملفات داخل مجلد المصدر ثم راجعها مجددًا.'; + + @override + String get notesTransferDestination => + 'تعذرت كتابة التصدير. تحقق من الوصول والمساحة المتاحة في الوجهة ثم أعد التصدير.'; + + @override + String get notesTransferOperationFailed => + 'تعذر إكمال النقل. راجع العناصر المتأثرة وأعد المحاولة. تبقى العناصر المستوردة سابقًا محفوظة.'; + + @override + String get notesExportPublishedWithOmissions => + 'التصدير متاح مع عناصر مستثناة'; + + @override + String get notesBatchUnchanged => 'القيم المطلوبة مضبوطة بالفعل.'; + + @override + String get notesBatchReadOnly => + 'هذه الملاحظة للقراءة فقط. لا يمكن تغيير الخصائص المحمية؛ ما زال تغيير المفضلة مسموحًا.'; + + @override + String get notesBatchReviewChanged => + 'تغيرت هذه الملاحظة بعد المراجعة. راجع حالتها الحالية قبل تطبيق الإجراء مجددًا.'; + + @override + String get notesBatchMissing => + 'لم تعد هذه الملاحظة متاحة. حدّث القائمة وراجع العناصر المستهدفة مجددًا.'; + + @override + String get notesBatchStorage => + 'فشل الحفظ المحلي. تحقق من التخزين وأعد محاولة هذا العنصر؛ تبقى العناصر الناجحة محفوظة.'; + + @override + String get notesRemoteReviewChanged => + 'تغيرت ملاحظة الخادم. مسودة الدمج محفوظة. راجع المقارنة المحدثة واختر طريقة الحل مجددًا.'; + + @override + String get notesReviewRemote => 'راجعت إصدار الخادم الحالي'; + @override String get wsModuleCapsNotPublished => 'لا يُنشر تنسيق أحرف الوحدة.'; @@ -577,15 +701,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get keyboardShortcuts => 'اختصارات لوحة المفاتيح'; - @override - String get commandPalette => 'لوحة الأوامر'; - - @override - String get commandPaletteHint => 'اكتب أمرًا'; - - @override - String get commandPaletteEmpty => 'لا توجد أوامر مطابقة'; - @override String get commandUnavailableInContext => 'هذا الأمر غير متاح في السياق الحالي.'; @@ -803,7 +918,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'إظهار مرجع اختصارات لوحة المفاتيح'; + 'تخصيص اختصارات لوحة مفاتيح التطبيق'; @override String get shortcutSyntaxReferenceDescription => 'فتح مرجع بناء الجملة'; @@ -1045,7 +1160,7 @@ class AppLocalizationsAr extends AppLocalizations { String get validateOnEdit => 'التحقق أثناء التحرير'; @override - String get clearRecentWorkspaces => 'مسح مساحات العمل الأخيرة'; + String get clearRecentWorkspaces => 'مسح السجل الأخير'; @override String get editingButtonsPosition => 'موضع أزرار التحرير'; @@ -1847,15 +1962,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get sourceSearchNextMatch => 'التطابق التالي'; - @override - String get sourceSearchCaseSensitive => 'حساس لحالة الأحرف'; - - @override - String get sourceSearchWholeWord => 'الكلمة بالكامل'; - - @override - String get sourceSearchRegex => 'تعبير نمطي'; - @override String get sourceSearchReplacement => 'استبدال بـ'; @@ -1981,13 +2087,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get saveAs => 'حفظ باسم'; - @override - String get sourceSearchInvalidRegex => 'تعبير نمطي غير صالح'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'لا تُدعم مطابقات التعبير النمطي ذات الطول الصفري (مثل ^ أو \$ أو الاستباق الذي يطابق موضعًا فقط).'; - @override String get workspaceSearchIncomplete => 'تظهر بعض النتائج فقط. اعرض المزيد أو ضيّق نطاق البحث.'; @@ -2096,6 +2195,11 @@ class AppLocalizationsAr extends AppLocalizations { @override String get documentKindResourceFile => 'ملف الموارد'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'تعذر تحديث العناصر الأخيرة: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'فشل الفتح: ⁨$error⁩'; @@ -4387,9 +4491,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get clipboardHistory => 'سجل النسخ واللصق'; - @override - String get clipboardHistorySearchHint => 'البحث في سجل الحافظة'; - @override String get clipboardHistoryDescription => 'افتح سجل النسخ واللصق الخاص بالجلسة.'; @@ -4411,10 +4512,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get clipboardNoItems => 'سجل النسخ واللصق فارغ.'; - @override - String get clipboardNoMatches => - 'لا توجد عناصر في سجل النسخ واللصق تطابق هذا البحث.'; - @override String get clipboardDisabled => 'مجموعة سجل النسخ واللصق معطلة في الإعدادات.'; @@ -4470,13 +4567,6 @@ class AppLocalizationsAr extends AppLocalizations { String get localHistoryNoRevisions => 'هذا المستند ليس لديه إصدارات في التاريخ المحلي.'; - @override - String get localHistoryNoMatches => - 'لا تتطابق أي إصدارات في التاريخ المحلي مع هذا البحث.'; - - @override - String get localHistorySearchHint => 'البحث في السجل المحلي'; - @override String get localHistorySelectedRevision => 'الإصدار المحدد'; @@ -5423,4 +5513,392 @@ class AppLocalizationsAr extends AppLocalizations { @override String get wsPropertiesUnavailable => 'العنصر الذي يتم فحصه غير متاح. افحص عنصرًا للمتابعة.'; + + @override + String get searchDocumentsCategory => 'المستندات'; + + @override + String get searchTopicsCategory => 'المواضيع'; + + @override + String get searchCurrentDocument => 'المستند الحالي'; + + @override + String get searchCurrentWorkspace => 'مساحة العمل الحالية'; + + @override + String get browseNotes => 'تصفح الملاحظات'; + + @override + String get searchActionsCommand => 'البحث عن إجراءات'; + + @override + String get searchDocumentsCommand => 'البحث عن مستندات'; + + @override + String get searchTopicsCommand => 'البحث عن مواضيع'; + + @override + String get searchRecentCommand => 'البحث في الأخيرة'; + + @override + String get searchResourceUnavailable => 'هذا المورد غير متاح.'; + + @override + String get notesSync => 'مزامنة الملاحظات'; + + @override + String get searchInWorkspace => 'البحث في مساحة العمل'; + + @override + String get searchCategory => 'فئة البحث'; + + @override + String get clearHistorySearchScope => 'البحث في سجل المستندات'; + + @override + String get searchInDocument => 'البحث في المستند'; + + @override + String get searchDocumentHint => 'ابحث في هذا المستند…'; + + @override + String get searchWorkspaceHint => 'ابحث في مساحة العمل هذه…'; + + @override + String get searchOutlineHint => 'ابحث في العناوين…'; + + @override + String get searchHistoryHint => 'ابحث في السجل المحلي…'; + + @override + String get searchEnterText => 'أدخل نصًا للبحث'; + + @override + String get searchAccountUnavailable => 'الحساب غير متاح'; + + @override + String get searchDocumentsHint => 'البحث عن مستندات…'; + + @override + String get searchTopicsHint => 'البحث عن مواضيع…'; + + @override + String get searchRecentHint => 'البحث في الأخيرة…'; + + @override + String get searchActionsHint => 'البحث عن إجراءات…'; + + @override + String get searchRequiresNotesWorkspace => 'يتطلب مساحة عمل للملاحظات'; + + @override + String get searchSidebarLabel => 'بحث:'; + + @override + String get searchDocumentTextCategory => 'نص المستند'; + + @override + String get searchWorkspaceTextCategory => 'نص مساحة العمل'; + + @override + String get searchHeadingsCategory => 'العناوين'; + + @override + String get shortcutUnassigned => 'غير معيّن'; + + @override + String get shortcutPressKeys => 'اضغط مجموعة مفاتيح…'; + + @override + String get shortcutRecord => 'تسجيل اختصار'; + + @override + String get shortcutRestoreAll => 'استعادة جميع الإعدادات الافتراضية'; + + @override + String get shortcutRestoreAllQuestion => + 'هل تريد استعادة الاختصارات الافتراضية لجميع الإجراءات؟ ستُزال التعيينات المخصصة.'; + + @override + String get shortcutSaveFailed => + 'الاختصارات مفعّلة ولكن تعذر حفظها. أعد المحاولة للاحتفاظ بها بعد إعادة التشغيل.'; + + @override + String get shortcutRetry => 'إعادة محاولة الحفظ'; + + @override + String get shortcutChanged => + 'تغيرت التعيينات. راجع الاختصارات الحالية وحاول مجددًا.'; + + @override + String get shortcutInvalid => 'لا يمكن تعيين هذه المجموعة في هذا السياق.'; + + @override + String get shortcutSystemWarning => + 'قد يستخدم سطح المكتب هذه المجموعة. لا يستطيع BusyMark تجاوز اختصارات النظام.'; + + @override + String get shortcutConflicts => 'يتعارض هذا الاختصار مع الإجراءات التالية:'; + + @override + String get shortcutRemove => 'إزالة التعيين'; + + @override + String get shortcutRestoreDefault => 'استعادة الافتراضي'; + + @override + String get shortcutAssign => 'تعيين'; + + @override + String get shortcutReassign => 'إعادة التعيين'; + + @override + String get shortcutContextApplication => 'التطبيق'; + + @override + String get shortcutContextDocument => 'طرق عرض المستند'; + + @override + String get shortcutContextEditor => 'المحرر والمصدر'; + + @override + String get shortcutContextWritersideSource => 'مصدر Writerside'; + + @override + String get shortcutContextWorkspace => 'التنقل في مساحة العمل'; + + @override + String get shortcutContextTree => 'أشجار الشريط الجانبي'; + + @override + String get searchHeadingsCommand => 'البحث عن العناوين'; + + @override + String get shortcutFixed => 'يتولاه المحرر؛ غير قابل للتخصيص.'; + + @override + String get shortcutDefaultConflict => + 'يتعارض الاختصار الافتراضي مع تعيين مخصص.'; + + @override + String get shortcutCurrentBinding => 'الاختصار الحالي'; + + @override + String get shortcutProposedBinding => 'الاختصار المقترح'; + + @override + String get notesActions => 'إجراءات الملاحظات'; + + @override + String get noteActions => 'إجراءات الملاحظة'; + + @override + String get noteProperties => 'خصائص الملاحظة'; + + @override + String get notesExportAll => 'تصدير ملاحظات · كل الملاحظات…'; + + @override + String get notesExportCategory => 'تصدير ملاحظات · الفئة…'; + + @override + String get notesOfflineAll => 'إتاحة دون اتصال · كل الملاحظات'; + + @override + String get notesOfflineCategory => 'إتاحة دون اتصال · الفئة'; + + @override + String get notesOfflineRemoveAll => 'إزالة الاحتفاظ دون اتصال · كل الملاحظات'; + + @override + String get notesOfflineRemoveCategory => 'إزالة الاحتفاظ دون اتصال · الفئة'; + + @override + String get notesOfflinePause => 'إيقاف التنزيلات مؤقتًا'; + + @override + String get notesOfflineResume => 'استئناف التنزيلات'; + + @override + String get notesOfflineKeep => 'إتاحة دون اتصال'; + + @override + String get notesOfflineRetentionHelp => + 'إزالة هذا المتطلب تبقي المحتوى المخزن مؤقتًا. قد يظل متطلب الفئة أو الحساب يحتفظ بالملاحظة.'; + + @override + String get notesOfflineBroader => + 'تُحتفظ بهذه الملاحظة أيضًا بسبب متطلب الفئة أو الحساب.'; + + @override + String get notesOfflineExternal => + 'تتطلب الوسائط الخارجية اتصالًا. يحتفظ BusyMark بنص الملاحظة والمرفقات المُدارة فقط.'; + + @override + String get notesEmptyAll => 'لا توجد ملاحظات بعد. أنشئ ملاحظة للبدء.'; + + @override + String get notesEmptyFavorites => 'لا توجد ملاحظات مفضلة.'; + + @override + String get notesEmptyCategory => 'لا توجد ملاحظات في هذه الفئة.'; + + @override + String get notesEmptyUncategorized => 'لا توجد ملاحظات غير مصنفة.'; + + @override + String get notesEmptyRecovery => 'لا توجد ملاحظات محفوظة لاستعادتها.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · فشل. إعادة المحاولة'; + + @override + String get notesOperationFailed => + 'تعذر إكمال العملية. المسودة محفوظة. راجع الحفظ والتعارضات ثم أعد المحاولة.'; + + @override + String get notesInspectionFailed => 'المرفقات · فشل. إعادة المحاولة'; + + @override + String get notesStartingConnection => 'جارٍ بدء الاتصال بـ Nextcloud…'; + + @override + String get notesVerifyingConnection => 'جارٍ التحقق من الوصول إلى الملاحظات…'; + + @override + String get notesCancellingConnection => 'جارٍ إلغاء تسجيل الدخول والتنظيف…'; + + @override + String get notesRemovingConnection => 'جارٍ إزالة الحساب المحلي…'; + + @override + String get notesRemovedWarning => + 'أُزيل حساب Nextcloud المحلي. لم يُؤكد إبطال كلمة مرور التطبيق. احذف كلمة مرور BusyMark من إعدادات أمان Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'تعذر حفظ جميع الملاحظات المفتوحة محليًا. راجع الحفظ والتعارضات أو التسوية في مساحة العمل. قد تظل التغييرات المحلية بحاجة إلى المزامنة مع Nextcloud.'; + + @override + String get notesSettingsLeave => + 'حفظ تغييرات إعدادات Nextcloud قبل المغادرة؟'; + + @override + String get notesSettingsLeaveHelp => + 'إعدادات Notes على الخادم · تغييرات غير محفوظة'; + + @override + String get notesSettingsReload => 'إعادة تحميل إعدادات الخادم…'; + + @override + String get notesSettingsReloadHelp => + 'تحل إعدادات Nextcloud الحالية محل المسودة عند إعادة التحميل. أتريد تجاهل المسودة وإعادة التحميل؟'; + + @override + String get notesSettingsRefreshFailed => + 'قَبِل Nextcloud الإعدادات، لكن تعذر تحديث الملاحظات. القيم المعروضة هي ما أكده الخادم. أعد التحديث من الملاحظات.'; + + @override + String get notesUnavailable => + 'غير متصل · النص متاح محليًا · محفوظ محليًا · المزامنة معلقة'; + + @override + String get notesStorageFull => + 'مساحة Nextcloud ممتلئة. أفرغ مساحة على الخادم ثم أعد المحاولة.'; + + @override + String get notesRejected => + 'رفض Nextcloud هذا التغيير. راجع الملاحظة واحفظ نسخة محلية قبل إعادة المحاولة.'; + + @override + String get notesThrottled => + 'طلب Nextcloud الانتظار. تُستأنف المزامنة بعد انتهاء مهلة إعادة المحاولة.'; + + @override + String get notesRecoveryRequired => 'تعارضات · مقارنة'; + + @override + String get notesScanning => 'جارٍ فحص الملفات…'; + + @override + String get notesCancelling => + 'جارٍ الإلغاء… في انتظار انتهاء العملية الحالية.'; + + @override + String get notesCancelled => 'أُلغي'; + + @override + String get notesPartial => 'اكتمل جزئيًا'; + + @override + String get notesTransferFailed => 'فشل. إعادة المحاولة'; + + @override + String get notesImportPartialHelp => + 'قد تكون العناصر السابقة محفوظة محليًا بالفعل. استأنف هذا الاستيراد للمتابعة دون تكرارها.'; + + @override + String get notesNotStarted => 'لم يبدأ'; + + @override + String get notesCreated => 'أُنشئ'; + + @override + String get notesRemoteDeleteUnavailable => + 'عنوان الخادم · غير مدعوم · حذف · المرفقات'; + + @override + String get notesCurrentText => 'النص المحلي المحفوظ حاليًا'; + + @override + String get notesRecovering => 'جارٍ الاستعادة كملاحظة جديدة…'; + + @override + String get notesReadOnlyHelp => 'للقراءة فقط · المفضلة · حفظ نسخة محلية…'; + + @override + String get notesSort => 'ترتيب الملاحظات'; + + @override + String get notesPathRelative => + 'مسار مجلد الملاحظات نسبي إلى مساحة تخزين Nextcloud الخاصة بك.'; + + @override + String get notesSettingsStale => + 'تغيرت الإعدادات في Nextcloud. أعد تحميل قيم الخادم وراجعها قبل استبدال المسودة.'; + + @override + String get notesDownloadsPaused => 'التنزيلات متوقفة مؤقتًا'; + + @override + String get notesExportNotPublished => 'لم يُنشر أي تصدير.'; + + @override + String get notesKeyringUnavailable => + 'افتح قفل سلسلة مفاتيح سطح المكتب وأعد المحاولة.'; + + @override + String get notesConnectFailed => + 'تعذر الاتصال بملاحظات Nextcloud. تحقق من عنوان الخادم وأعد المحاولة.'; + + @override + String get notesAccountMismatch => + 'سجل الدخول إلى حساب Nextcloud نفسه. أزل هذا الحساب أولًا للاتصال بحساب مختلف.'; + + @override + String get notesLocalRemovalFailed => + 'تعذر تنظيف الحساب المحلي. احتُفظ بالحساب لتتمكن من إعادة محاولة إزالته.'; + + @override + String get notesTransferring => 'جارٍ نقل الملاحظات…'; + + @override + String get notesTechnicalDetails => 'تفاصيل تقنية'; + + @override + String get notesPauseRequirement => 'إيقاف متطلب الإتاحة دون اتصال مؤقتًا'; + + @override + String get notesResumeRequirement => 'استئناف متطلب الإتاحة دون اتصال'; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 721df893..cdffd46e 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -10,6 +10,132 @@ import 'app_localizations.dart'; class AppLocalizationsDe extends AppLocalizations { AppLocalizationsDe([String locale = 'de']) : super(locale); + @override + String get notesNewFromRetainedText => 'Neue Notiz aus aufbewahrtem Text'; + + @override + String get notesAccountThrottled => + 'Nextcloud hat um eine Pause gebeten. Warte, bevor du die Anmeldung erneut startest.'; + + @override + String get notesSettingsRefreshing => + 'Notes wird nach der bestätigten Einstellungsänderung aktualisiert'; + + @override + String get notesExportOperationFailed => + 'Der Export wurde nicht veröffentlicht. Quelle und Ziel prüfen und erneut exportieren.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Die wiederhergestellte Notiz wurde gespeichert, konnte aber nicht geöffnet werden. Ausstehende Editor- oder Eigenschaftsänderungen klären und diese Notiz öffnen.'; + + @override + String get notesSettingsPendingNotes => + 'Ausstehende Notizen, Konflikte oder ungewisse Erstellungen blockieren diese Änderung. In Notes synchronisieren oder prüfen, dann den erhaltenen Einstellungsentwurf erneut speichern.'; + + @override + String get notesSettingsPendingAttachments => + 'Ausstehende Anhangsvorgänge blockieren diese Änderung. Anhänge in Notes prüfen, dann den erhaltenen Einstellungsentwurf erneut speichern.'; + + @override + String get notesAccountServerAddress => + 'Die kanonische HTTPS-Adresse des Nextcloud-Servers verwenden und die Anmeldung neu starten. Adresse oder Weiterleitung konnten nicht verifiziert werden.'; + + @override + String get notesAccountUnsupported => + 'Notes ist nicht verfügbar oder die API wird nicht unterstützt. Nextcloud Notes mit API-Hauptversion 1 ab Nebenversion 4 aktivieren.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud hat die Zugangsdaten abgelehnt. Das Konto erneut autorisieren oder verbinden.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud hat die Notes-Funktionsprüfung verweigert. Die Serveradministration um Freigabe bitten und erneut versuchen.'; + + @override + String get notesAccountMalformed => + 'Die Nextcloud-Antwort konnte nicht verifiziert werden. Die Serverinstallation prüfen und die Anmeldung neu starten.'; + + @override + String get notesAccountTimedOut => + 'Die Anmeldung ist abgelaufen. Erneut starten und im Browser autorisieren.'; + + @override + String get notesAccountBrowserUnavailable => + 'Der Systembrowser konnte nicht geöffnet werden. Den Standardbrowser prüfen und die Anmeldung neu starten.'; + + @override + String get notesAccountNetwork => + 'Nextcloud ist nicht erreichbar. Verbindung und Server prüfen und die Anmeldung erneut versuchen.'; + + @override + String get notesAccountRejected => + 'Nextcloud hat die Funktionsanfrage abgelehnt. Den Serverzugriff mit der Administration prüfen und erneut versuchen.'; + + @override + String get notesAccountFailed => + 'Die Kontoeinrichtung konnte nicht abgeschlossen werden. Die Anmeldung erneut versuchen.'; + + @override + String get notesTransferSourceChanged => + 'Die Quelle wurde nach der Prüfung geändert. Die aktuelle Datei erneut prüfen oder einen neuen Schnappschuss erstellen.'; + + @override + String get notesTransferUnreadable => + 'Die Quelle konnte nicht gelesen werden. Den Datei- oder Ordnerzugriff prüfen und erneut einlesen.'; + + @override + String get notesTransferInvalidSnapshot => + 'Der Schnappschuss ist ungültig oder wird nicht unterstützt. Einen gültigen BusyMark-Notes-Schnappschuss oder eine Markdown-Quelle wählen.'; + + @override + String get notesTransferMediaLimit => + 'Medien überschreiten das Importlimit. Die Medien des betroffenen Eintrags verkleinern und erneut prüfen.'; + + @override + String get notesTransferUnsafeSource => + 'Die Quelle enthält einen unsicheren Pfad. Dateien innerhalb des Quellordners wählen und erneut prüfen.'; + + @override + String get notesTransferDestination => + 'Der Export konnte nicht geschrieben werden. Zugriff und freien Speicher am Ziel prüfen und erneut exportieren.'; + + @override + String get notesTransferOperationFailed => + 'Die Übertragung ist fehlgeschlagen. Betroffene Einträge prüfen und erneut versuchen. Bereits importierte Einträge bleiben gespeichert.'; + + @override + String get notesExportPublishedWithOmissions => + 'Export mit Auslassungen verfügbar'; + + @override + String get notesBatchUnchanged => + 'Die angeforderten Werte waren bereits gesetzt.'; + + @override + String get notesBatchReadOnly => + 'Diese Notiz ist schreibgeschützt. Geschützte Eigenschaften können nicht geändert werden; Favorit bleibt erlaubt.'; + + @override + String get notesBatchReviewChanged => + 'Diese Notiz wurde nach der Prüfung geändert. Vor einem erneuten Versuch den aktuellen Stand prüfen.'; + + @override + String get notesBatchMissing => + 'Diese Notiz ist nicht mehr verfügbar. Die Liste aktualisieren und die Ziele erneut prüfen.'; + + @override + String get notesBatchStorage => + 'Lokales Speichern fehlgeschlagen. Speicher prüfen und diesen Eintrag erneut versuchen; erfolgreiche Einträge bleiben gespeichert.'; + + @override + String get notesRemoteReviewChanged => + 'Die Servernotiz wurde geändert. Der Zusammenführungsentwurf bleibt erhalten. Den neuen Vergleich prüfen und die Auflösung erneut wählen.'; + + @override + String get notesReviewRemote => 'Ich habe die aktuelle Serverversion geprüft'; + @override String get wsModuleCapsNotPublished => 'Modul-Großschreibung wird nicht veröffentlicht.'; @@ -590,15 +716,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get keyboardShortcuts => 'Tastaturkürzel'; - @override - String get commandPalette => 'Befehlspalette'; - - @override - String get commandPaletteHint => 'Befehl eingeben'; - - @override - String get commandPaletteEmpty => 'Keine passenden Befehle'; - @override String get commandUnavailableInContext => 'Dieser Befehl ist im aktuellen Kontext nicht verfügbar.'; @@ -818,7 +935,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Diese Tastaturkürzelübersicht anzeigen'; + 'Tastenkürzel der Anwendung anpassen'; @override String get shortcutSyntaxReferenceDescription => 'Syntaxreferenz öffnen'; @@ -1075,8 +1192,7 @@ class AppLocalizationsDe extends AppLocalizations { String get validateOnEdit => 'Bei Bearbeitung validieren'; @override - String get clearRecentWorkspaces => - 'Zuletzt verwendete Arbeitsbereiche löschen'; + String get clearRecentWorkspaces => 'Zuletzt verwendete Einträge löschen'; @override String get editingButtonsPosition => 'Position der Bearbeitungsschaltflächen'; @@ -1866,15 +1982,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get sourceSearchNextMatch => 'Nächster Treffer'; - @override - String get sourceSearchCaseSensitive => 'Groß-/Kleinschreibung beachten'; - - @override - String get sourceSearchWholeWord => 'Nur ganze Wörter'; - - @override - String get sourceSearchRegex => 'Regulärer Ausdruck'; - @override String get sourceSearchReplacement => 'Ersetzen durch'; @@ -2003,13 +2110,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get saveAs => 'Speichern unter'; - @override - String get sourceSearchInvalidRegex => 'Ungültiger regulärer Ausdruck'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Regex-Treffer ohne Zeichen werden nicht unterstützt (z. B. ^, \$ oder Lookaheads, die nur eine Position treffen).'; - @override String get workspaceSearchIncomplete => 'Es wird nur ein Teil der Ergebnisse angezeigt. Zeigen Sie mehr an oder grenzen Sie die Suche ein.'; @@ -2118,6 +2218,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get documentKindResourceFile => 'Ressourcendatei'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Zuletzt verwendete Elemente konnten nicht aktualisiert werden: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Öffnen fehlgeschlagen: $error'; @@ -4389,9 +4494,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get clipboardHistory => 'Zwischenablage-Historie'; - @override - String get clipboardHistorySearchHint => 'Zwischenablageverlauf durchsuchen'; - @override String get clipboardHistoryDescription => 'Öffne die Sitzungs-spezifische Zwischenablage-Historie.'; @@ -4415,10 +4517,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get clipboardNoItems => 'Zwischenablage-Historie ist leer.'; - @override - String get clipboardNoMatches => - 'Keine Zwischenablagen-Elemente passen zu dieser Suche.'; - @override String get clipboardDisabled => 'Die Sammlung \"Zwischenablage-Historie\" ist in den Einstellungen deaktiviert.'; @@ -4476,13 +4574,6 @@ class AppLocalizationsDe extends AppLocalizations { String get localHistoryNoRevisions => 'Dieses Dokument hat keine Lokalen Historie Revisionen.'; - @override - String get localHistoryNoMatches => - 'Keine Lokalen Historie Revisionen passen zu dieser Suche.'; - - @override - String get localHistorySearchHint => 'Lokalen Verlauf durchsuchen'; - @override String get localHistorySelectedRevision => 'Ausgewählte Revision'; @@ -5445,4 +5536,403 @@ class AppLocalizationsDe extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Das betrachtete Element ist nicht verfügbar. Wählen Sie ein Element zum Fortfahren.'; + + @override + String get searchDocumentsCategory => 'Dokumente'; + + @override + String get searchTopicsCategory => 'Themen'; + + @override + String get searchCurrentDocument => 'Aktuelles Dokument'; + + @override + String get searchCurrentWorkspace => 'Aktueller Arbeitsbereich'; + + @override + String get browseNotes => 'Notizen durchsuchen'; + + @override + String get searchActionsCommand => 'Aktionen suchen'; + + @override + String get searchDocumentsCommand => 'Dokumente suchen'; + + @override + String get searchTopicsCommand => 'Themen suchen'; + + @override + String get searchRecentCommand => 'Zuletzt geöffnet suchen'; + + @override + String get searchResourceUnavailable => + 'Diese Ressource ist nicht verfügbar.'; + + @override + String get notesSync => 'Notizen synchronisieren'; + + @override + String get searchInWorkspace => 'Im Arbeitsbereich suchen'; + + @override + String get searchCategory => 'Suchkategorie'; + + @override + String get clearHistorySearchScope => 'Verlauf aller Dokumente durchsuchen'; + + @override + String get searchInDocument => 'Im Dokument suchen'; + + @override + String get searchDocumentHint => 'Dieses Dokument durchsuchen…'; + + @override + String get searchWorkspaceHint => 'Diesen Arbeitsbereich durchsuchen…'; + + @override + String get searchOutlineHint => 'Überschriften suchen…'; + + @override + String get searchHistoryHint => 'Lokalen Verlauf durchsuchen…'; + + @override + String get searchEnterText => 'Suchtext eingeben'; + + @override + String get searchAccountUnavailable => 'Konto nicht verfügbar'; + + @override + String get searchDocumentsHint => 'Dokumente suchen…'; + + @override + String get searchTopicsHint => 'Themen suchen…'; + + @override + String get searchRecentHint => 'Zuletzt geöffnet suchen…'; + + @override + String get searchActionsHint => 'Aktionen suchen…'; + + @override + String get searchRequiresNotesWorkspace => + 'Erfordert einen Notizen-Arbeitsbereich'; + + @override + String get searchSidebarLabel => 'Suchen:'; + + @override + String get searchDocumentTextCategory => 'Dokumenttext'; + + @override + String get searchWorkspaceTextCategory => 'Arbeitsbereichstext'; + + @override + String get searchHeadingsCategory => 'Überschriften'; + + @override + String get shortcutUnassigned => 'Nicht zugewiesen'; + + @override + String get shortcutPressKeys => 'Tastenkombination drücken…'; + + @override + String get shortcutRecord => 'Tastenkürzel aufnehmen'; + + @override + String get shortcutRestoreAll => 'Alle Vorgaben wiederherstellen'; + + @override + String get shortcutRestoreAllQuestion => + 'Standardkürzel aller Aktionen wiederherstellen? Eigene Zuweisungen werden entfernt.'; + + @override + String get shortcutSaveFailed => + 'Tastenkürzel sind aktiv, konnten aber nicht gespeichert werden. Zum Beibehalten nach Neustart erneut speichern.'; + + @override + String get shortcutRetry => 'Speichern wiederholen'; + + @override + String get shortcutChanged => + 'Zuweisungen geändert. Aktuelle Tastenkürzel prüfen und erneut versuchen.'; + + @override + String get shortcutInvalid => + 'Diese Kombination kann hier nicht zugewiesen werden.'; + + @override + String get shortcutSystemWarning => + 'Diese Kombination wird möglicherweise vom Desktop verwendet. BusyMark kann Systemkürzel nicht überschreiben.'; + + @override + String get shortcutConflicts => + 'Dieses Tastenkürzel überschneidet sich mit diesen Aktionen:'; + + @override + String get shortcutRemove => 'Zuweisung entfernen'; + + @override + String get shortcutRestoreDefault => 'Vorgabe wiederherstellen'; + + @override + String get shortcutAssign => 'Zuweisen'; + + @override + String get shortcutReassign => 'Neu zuweisen'; + + @override + String get shortcutContextApplication => 'Anwendung'; + + @override + String get shortcutContextDocument => 'Dokumentansichten'; + + @override + String get shortcutContextEditor => 'Editor und Quelltext'; + + @override + String get shortcutContextWritersideSource => 'Writerside-Quelltext'; + + @override + String get shortcutContextWorkspace => 'Arbeitsbereichsnavigation'; + + @override + String get shortcutContextTree => 'Seitenleistenbäume'; + + @override + String get searchHeadingsCommand => 'Überschriften suchen'; + + @override + String get shortcutFixed => 'Vom Editor verwaltet; nicht konfigurierbar.'; + + @override + String get shortcutDefaultConflict => + 'Das Standardkürzel steht mit einer eigenen Zuweisung in Konflikt.'; + + @override + String get shortcutCurrentBinding => 'Aktuelles Tastenkürzel'; + + @override + String get shortcutProposedBinding => 'Vorgeschlagenes Tastenkürzel'; + + @override + String get notesActions => 'Notizaktionen'; + + @override + String get noteActions => 'Aktionen für diese Notiz'; + + @override + String get noteProperties => 'Notizeigenschaften'; + + @override + String get notesExportAll => 'Notizen exportieren · Alle Notizen…'; + + @override + String get notesExportCategory => 'Notizen exportieren · Kategorie…'; + + @override + String get notesOfflineAll => 'Offline verfügbar machen · Alle Notizen'; + + @override + String get notesOfflineCategory => 'Offline verfügbar machen · Kategorie'; + + @override + String get notesOfflineRemoveAll => + 'Offline-Aufbewahrung entfernen · Alle Notizen'; + + @override + String get notesOfflineRemoveCategory => + 'Offline-Aufbewahrung entfernen · Kategorie'; + + @override + String get notesOfflinePause => 'Downloads pausieren'; + + @override + String get notesOfflineResume => 'Downloads fortsetzen'; + + @override + String get notesOfflineKeep => 'Offline verfügbar machen'; + + @override + String get notesOfflineRetentionHelp => + 'Das Entfernen dieser Anforderung löscht keine zwischengespeicherten Inhalte. Eine Kategorie- oder Kontoanforderung kann die Notiz weiter aufbewahren.'; + + @override + String get notesOfflineBroader => + 'Diese Notiz wird auch durch eine Kategorie- oder Kontoanforderung aufbewahrt.'; + + @override + String get notesOfflineExternal => + 'Externe Medien benötigen eine Verbindung. BusyMark bewahrt nur Notiztext und verwaltete Anhänge auf.'; + + @override + String get notesEmptyAll => 'Noch keine Notizen. Erstellen Sie eine Notiz.'; + + @override + String get notesEmptyFavorites => 'Keine favorisierten Notizen.'; + + @override + String get notesEmptyCategory => 'Keine Notizen in dieser Kategorie.'; + + @override + String get notesEmptyUncategorized => 'Keine nicht kategorisierten Notizen.'; + + @override + String get notesEmptyRecovery => + 'Keine aufbewahrten Notizen zur Wiederherstellung.'; + + @override + String get notesLoadFailed => + 'Nextcloud Notes · Fehlgeschlagen. Erneut versuchen'; + + @override + String get notesOperationFailed => + 'Der Vorgang ist fehlgeschlagen. Ihr Entwurf bleibt erhalten. Prüfen Sie Speichern und Konflikte und versuchen Sie es erneut.'; + + @override + String get notesInspectionFailed => + 'Anhänge · Fehlgeschlagen. Erneut versuchen'; + + @override + String get notesStartingConnection => 'Nextcloud-Verbindung wird gestartet…'; + + @override + String get notesVerifyingConnection => 'Zugriff auf Notizen wird geprüft…'; + + @override + String get notesCancellingConnection => + 'Anmeldung wird abgebrochen und bereinigt…'; + + @override + String get notesRemovingConnection => 'Lokales Konto wird entfernt…'; + + @override + String get notesRemovedWarning => + 'Das lokale Nextcloud-Konto wurde entfernt. Der Widerruf des App-Passworts wurde nicht bestätigt. Entfernen Sie BusyMarks App-Passwort in den Nextcloud-Sicherheitseinstellungen.'; + + @override + String get notesBuffersNotSaved => + 'Nicht alle offenen Notizen konnten lokal gespeichert werden. Prüfen Sie Speichern, Konflikte oder Normalisierung im Arbeitsbereich. Lokale Änderungen müssen eventuell noch mit Nextcloud synchronisiert werden.'; + + @override + String get notesSettingsLeave => + 'Änderungen an den Nextcloud-Einstellungen vor dem Verlassen speichern?'; + + @override + String get notesSettingsLeaveHelp => + 'Notes-Einstellungen auf dem Server · Ungespeicherte Änderungen'; + + @override + String get notesSettingsReload => 'Servereinstellungen neu laden…'; + + @override + String get notesSettingsReloadHelp => + 'Neu laden ersetzt Ihren Entwurf durch die aktuellen Nextcloud-Einstellungen. Entwurf verwerfen und neu laden?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud hat die Einstellungen angenommen, aber die Notizen konnten nicht aktualisiert werden. Angezeigt werden die bestätigten Serverwerte. Aktualisieren Sie die Notizen erneut.'; + + @override + String get notesUnavailable => + 'Nicht verbunden · Text lokal verfügbar · Lokal gespeichert · Synchronisierung ausstehend'; + + @override + String get notesStorageFull => + 'Der Nextcloud-Speicher ist voll. Geben Sie auf dem Server Speicherplatz frei und versuchen Sie es erneut.'; + + @override + String get notesRejected => + 'Nextcloud hat diese Änderung abgelehnt. Prüfen Sie die Notiz und speichern Sie vor einem neuen Versuch eine lokale Kopie.'; + + @override + String get notesThrottled => + 'Nextcloud hat eine Wartezeit angefordert. Die Synchronisierung wird nach Ablauf dieser Frist fortgesetzt.'; + + @override + String get notesRecoveryRequired => 'Konflikte · Vergleichen'; + + @override + String get notesScanning => 'Dateien werden durchsucht…'; + + @override + String get notesCancelling => + 'Abbruch läuft… Der aktuelle Vorgang wird noch beendet.'; + + @override + String get notesCancelled => 'Abgebrochen'; + + @override + String get notesPartial => 'Teilweise abgeschlossen'; + + @override + String get notesTransferFailed => 'Fehlgeschlagen. Erneut versuchen'; + + @override + String get notesImportPartialHelp => + 'Frühere Einträge können bereits lokal gespeichert sein. Setzen Sie diesen Import fort, um Duplikate zu vermeiden.'; + + @override + String get notesNotStarted => 'Nicht gestartet'; + + @override + String get notesCreated => 'Erstellt'; + + @override + String get notesRemoteDeleteUnavailable => + 'Server-URL · Nicht unterstützt · Löschen · Anhänge'; + + @override + String get notesCurrentText => 'Aktuell aufbewahrter Text'; + + @override + String get notesRecovering => 'Wiederherstellung als neue Notiz…'; + + @override + String get notesReadOnlyHelp => + 'Schreibgeschützt · Favoriten · Lokale Kopie speichern…'; + + @override + String get notesSort => 'Notizen sortieren'; + + @override + String get notesPathRelative => + 'Der Notizordnerpfad ist relativ zu Ihrem Nextcloud-Speicher.'; + + @override + String get notesSettingsStale => + 'Die Einstellungen haben sich auf Nextcloud geändert. Laden und prüfen Sie die Serverwerte, bevor Sie den Entwurf ersetzen.'; + + @override + String get notesDownloadsPaused => 'Downloads pausiert'; + + @override + String get notesExportNotPublished => 'Es wurde kein Export veröffentlicht.'; + + @override + String get notesKeyringUnavailable => + 'Entsperren Sie den Desktop-Schlüsselbund und versuchen Sie es erneut.'; + + @override + String get notesConnectFailed => + 'Keine Verbindung zu Nextcloud Notes. Prüfen Sie die Serveradresse und versuchen Sie es erneut.'; + + @override + String get notesAccountMismatch => + 'Melden Sie sich beim selben Nextcloud-Konto an. Entfernen Sie dieses Konto zuerst, um ein anderes zu verbinden.'; + + @override + String get notesLocalRemovalFailed => + 'Die lokale Kontobereinigung ist fehlgeschlagen. Das Konto bleibt für einen erneuten Entfernungsversuch erhalten.'; + + @override + String get notesTransferring => 'Notizen werden übertragen…'; + + @override + String get notesTechnicalDetails => 'Technische Details'; + + @override + String get notesPauseRequirement => 'Offline-Anforderung pausieren'; + + @override + String get notesResumeRequirement => 'Offline-Anforderung fortsetzen'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index f07b1c4f..0cc861ca 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); + @override + String get notesNewFromRetainedText => 'New note from retained text'; + + @override + String get notesAccountThrottled => + 'Nextcloud requested a pause. Wait before starting sign-in again.'; + + @override + String get notesSettingsRefreshing => + 'Refreshing Notes after the acknowledged settings update'; + + @override + String get notesExportOperationFailed => + 'The export failed before publication. Check the source and destination, then export again.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'The recovered note was saved. Opening it failed. Resolve pending editor or property changes, then open this existing note.'; + + @override + String get notesSettingsPendingNotes => + 'Pending notes, conflicts, or uncertain creations block this collection change. Open Notes and synchronize or review them, then retry this retained settings draft.'; + + @override + String get notesSettingsPendingAttachments => + 'Pending attachment operations block this collection change. Open Notes and review the attachments, then retry this retained settings draft.'; + + @override + String get notesAccountServerAddress => + 'Use the canonical HTTPS address of your Nextcloud server, then start sign-in again. The address or redirect could not be verified.'; + + @override + String get notesAccountUnsupported => + 'Notes is unavailable or its API is unsupported. Enable Nextcloud Notes with API major version 1, minor version 4 or later.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud rejected the credentials. Authorize this account again or reconnect.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud denied the Notes capability check. Ask the server administrator to allow it, then try again.'; + + @override + String get notesAccountMalformed => + 'The Nextcloud response could not be verified. Check the server installation, then start sign-in again.'; + + @override + String get notesAccountTimedOut => + 'Sign-in timed out. Start sign-in again and complete authorization in your browser.'; + + @override + String get notesAccountBrowserUnavailable => + 'The system browser could not open. Check your default browser, then start sign-in again.'; + + @override + String get notesAccountNetwork => + 'BusyMark could not reach Nextcloud. Check your connection and the server, then try signing in again.'; + + @override + String get notesAccountRejected => + 'Nextcloud rejected the capability request. Check server access with your administrator, then try again.'; + + @override + String get notesAccountFailed => + 'BusyMark could not complete account setup. Try signing in again.'; + + @override + String get notesTransferSourceChanged => + 'The source changed after review. Review the current file or capture a new snapshot before retrying.'; + + @override + String get notesTransferUnreadable => + 'The source could not be read. Check access to the file or folder, then review it again.'; + + @override + String get notesTransferInvalidSnapshot => + 'The snapshot is invalid or unsupported. Choose a valid BusyMark Notes snapshot or Markdown source.'; + + @override + String get notesTransferMediaLimit => + 'Media exceeds the import size limit. Reduce the affected item\'s media before reviewing it again.'; + + @override + String get notesTransferUnsafeSource => + 'The source contains an unsafe path. Choose files within the selected source folder and review again.'; + + @override + String get notesTransferDestination => + 'The export could not be written. Check access and free space at the destination, then export again.'; + + @override + String get notesTransferOperationFailed => + 'The transfer could not finish. Review the affected items and try again. Earlier imported items remain saved.'; + + @override + String get notesExportPublishedWithOmissions => + 'Export available with omissions'; + + @override + String get notesBatchUnchanged => 'The requested values were already set.'; + + @override + String get notesBatchReadOnly => + 'This note is read-only. Protected properties cannot change; Favorite is still permitted.'; + + @override + String get notesBatchReviewChanged => + 'This note changed after review. Review its current state before applying this action again.'; + + @override + String get notesBatchMissing => + 'This note is no longer available. Refresh the list and review the targets again.'; + + @override + String get notesBatchStorage => + 'The local save failed. Check local storage and retry this item; successful items remain saved.'; + + @override + String get notesRemoteReviewChanged => + 'The server note changed. Your merge draft is retained. Review the updated comparison and choose how to resolve it again.'; + + @override + String get notesReviewRemote => 'I have reviewed the current server version'; + @override String get wsModuleCapsNotPublished => 'Module capitalization is not published.'; @@ -583,15 +708,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get keyboardShortcuts => 'Keyboard Shortcuts'; - @override - String get commandPalette => 'Command Palette'; - - @override - String get commandPaletteHint => 'Type a command'; - - @override - String get commandPaletteEmpty => 'No matching commands'; - @override String get commandUnavailableInContext => 'Unavailable in the current editor context'; @@ -810,7 +926,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Show this keyboard shortcut reference'; + 'Customize application keyboard shortcuts'; @override String get shortcutSyntaxReferenceDescription => 'Open the Syntax Reference'; @@ -1058,7 +1174,7 @@ class AppLocalizationsEn extends AppLocalizations { String get validateOnEdit => 'Validate on edit'; @override - String get clearRecentWorkspaces => 'Clear recent workspaces'; + String get clearRecentWorkspaces => 'Clear recent history'; @override String get editingButtonsPosition => 'Editing buttons position'; @@ -1838,15 +1954,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sourceSearchNextMatch => 'Next match'; - @override - String get sourceSearchCaseSensitive => 'Case sensitive'; - - @override - String get sourceSearchWholeWord => 'Whole word'; - - @override - String get sourceSearchRegex => 'Regex'; - @override String get sourceSearchReplacement => 'Replace with'; @@ -1979,13 +2086,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get saveAs => 'Save As'; - @override - String get sourceSearchInvalidRegex => 'Invalid regular expression'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Zero-length regex matches are not supported (for example ^, \$, or lookaheads that match only a position).'; - @override String get workspaceSearchIncomplete => 'Only some results are shown. Show more or narrow your search.'; @@ -2094,6 +2194,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get documentKindResourceFile => 'Resource file'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Could not update Recent items: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Open failed: $error'; @@ -4374,9 +4479,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get clipboardHistory => 'Clipboard History'; - @override - String get clipboardHistorySearchHint => 'Search clipboard history'; - @override String get clipboardHistoryDescription => 'Open the session-local clipboard history.'; @@ -4399,9 +4501,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get clipboardNoItems => 'Clipboard history is empty.'; - @override - String get clipboardNoMatches => 'No clipboard items match this search.'; - @override String get clipboardDisabled => 'Clipboard History collection is disabled in Settings.'; @@ -4457,13 +4556,6 @@ class AppLocalizationsEn extends AppLocalizations { String get localHistoryNoRevisions => 'This document has no Local History revisions.'; - @override - String get localHistoryNoMatches => - 'No Local History revisions match this search.'; - - @override - String get localHistorySearchHint => 'Search local history'; - @override String get localHistorySelectedRevision => 'Selected revision'; @@ -5413,4 +5505,399 @@ class AppLocalizationsEn extends AppLocalizations { @override String get wsPropertiesUnavailable => 'The inspected element is unavailable. Inspect an element to continue.'; + + @override + String get searchDocumentsCategory => 'Documents'; + + @override + String get searchTopicsCategory => 'Topics'; + + @override + String get searchCurrentDocument => 'Current document'; + + @override + String get searchCurrentWorkspace => 'Current workspace'; + + @override + String get browseNotes => 'Browse Notes'; + + @override + String get searchActionsCommand => 'Search Actions'; + + @override + String get searchDocumentsCommand => 'Search Documents'; + + @override + String get searchTopicsCommand => 'Search Topics'; + + @override + String get searchRecentCommand => 'Search Recent'; + + @override + String get searchResourceUnavailable => 'This resource is unavailable.'; + + @override + String get notesSync => 'Sync notes'; + + @override + String get searchInWorkspace => 'Search in Workspace'; + + @override + String get searchCategory => 'Search category'; + + @override + String get clearHistorySearchScope => 'Search history across documents'; + + @override + String get searchInDocument => 'Search in Document'; + + @override + String get searchDocumentHint => 'Search this document…'; + + @override + String get searchWorkspaceHint => 'Search this workspace…'; + + @override + String get searchOutlineHint => 'Search headings…'; + + @override + String get searchHistoryHint => 'Search Local History…'; + + @override + String get searchEnterText => 'Enter text to search'; + + @override + String get searchAccountUnavailable => 'Account unavailable'; + + @override + String get searchDocumentsHint => 'Search documents…'; + + @override + String get searchTopicsHint => 'Search topics…'; + + @override + String get searchRecentHint => 'Search recent resources…'; + + @override + String get searchActionsHint => 'Search actions…'; + + @override + String get searchRequiresNotesWorkspace => 'Requires a Notes workspace'; + + @override + String get searchSidebarLabel => 'Search:'; + + @override + String get searchDocumentTextCategory => 'Document Text'; + + @override + String get searchWorkspaceTextCategory => 'Workspace Text'; + + @override + String get searchHeadingsCategory => 'Headings'; + + @override + String get shortcutUnassigned => 'Unassigned'; + + @override + String get shortcutPressKeys => 'Press a key combination…'; + + @override + String get shortcutRecord => 'Record shortcut'; + + @override + String get shortcutRestoreAll => 'Restore all defaults'; + + @override + String get shortcutRestoreAllQuestion => + 'Restore every action’s default shortcut? Custom assignments will be removed.'; + + @override + String get shortcutSaveFailed => + 'Shortcuts are active but could not be saved. Retry to keep them after restart.'; + + @override + String get shortcutRetry => 'Retry saving'; + + @override + String get shortcutChanged => + 'Assignments changed. Review the current shortcuts and try again.'; + + @override + String get shortcutInvalid => + 'This combination cannot be assigned in this context.'; + + @override + String get shortcutSystemWarning => + 'This combination may be used by your desktop. BusyMark cannot override system shortcuts.'; + + @override + String get shortcutConflicts => 'This shortcut overlaps these actions:'; + + @override + String get shortcutRemove => 'Remove assignment'; + + @override + String get shortcutRestoreDefault => 'Restore default'; + + @override + String get shortcutAssign => 'Assign'; + + @override + String get shortcutReassign => 'Reassign'; + + @override + String get shortcutContextApplication => 'Application'; + + @override + String get shortcutContextDocument => 'Document views'; + + @override + String get shortcutContextEditor => 'Editor and Source'; + + @override + String get shortcutContextWritersideSource => 'Writerside Source'; + + @override + String get shortcutContextWorkspace => 'Workspace navigation'; + + @override + String get shortcutContextTree => 'Sidebar trees'; + + @override + String get searchHeadingsCommand => 'Search Headings'; + + @override + String get shortcutFixed => 'Handled by the editor; not configurable.'; + + @override + String get shortcutDefaultConflict => + 'Default shortcut conflicts with a custom assignment.'; + + @override + String get shortcutCurrentBinding => 'Current shortcut'; + + @override + String get shortcutProposedBinding => 'Proposed shortcut'; + + @override + String get notesActions => 'Notes actions'; + + @override + String get noteActions => 'Note actions'; + + @override + String get noteProperties => 'Note Properties'; + + @override + String get notesExportAll => 'Export All Notes…'; + + @override + String get notesExportCategory => 'Export Category…'; + + @override + String get notesOfflineAll => 'Keep All Notes Available Offline'; + + @override + String get notesOfflineCategory => 'Keep Category Available Offline'; + + @override + String get notesOfflineRemoveAll => 'Remove All Notes Offline Requirement'; + + @override + String get notesOfflineRemoveCategory => + 'Remove Category Offline Requirement'; + + @override + String get notesOfflinePause => 'Pause Downloads'; + + @override + String get notesOfflineResume => 'Resume Downloads'; + + @override + String get notesOfflineKeep => 'Keep this note available offline'; + + @override + String get notesOfflineRetentionHelp => + 'Removing this requirement keeps existing cached content. A category or account requirement may still retain this note.'; + + @override + String get notesOfflineBroader => + 'This note is also retained by a category or account requirement.'; + + @override + String get notesOfflineExternal => + 'External media requires a connection. BusyMark only retains note text and managed attachments.'; + + @override + String get notesEmptyAll => 'No notes yet. Create a note to get started.'; + + @override + String get notesEmptyFavorites => 'No favorite notes.'; + + @override + String get notesEmptyCategory => 'No notes in this category.'; + + @override + String get notesEmptyUncategorized => 'No uncategorized notes.'; + + @override + String get notesEmptyRecovery => 'No retained notes to recover.'; + + @override + String get notesLoadFailed => + 'BusyMark could not load the local Notes store. Retry to open your cached notes.'; + + @override + String get notesOperationFailed => + 'The operation could not finish. Your draft is retained. Review Save and any conflicts, then try again.'; + + @override + String get notesInspectionFailed => + 'BusyMark could not inspect attachments. Retry to review availability.'; + + @override + String get notesStartingConnection => 'Starting Nextcloud connection…'; + + @override + String get notesVerifyingConnection => 'Verifying Notes access…'; + + @override + String get notesCancellingConnection => 'Cancelling login and cleaning up…'; + + @override + String get notesRemovingConnection => 'Removing the local account…'; + + @override + String get notesRemovedWarning => + 'The local Nextcloud account was removed. App-password revocation was not confirmed. Remove BusyMark’s app password in Nextcloud Security settings.'; + + @override + String get notesBuffersNotSaved => + 'BusyMark could not save all open notes locally. Review Save, conflicts, or normalization in the workspace before trying again. Pending local changes may still need to synchronize with Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Save changes to Nextcloud settings before leaving?'; + + @override + String get notesSettingsLeaveHelp => + 'Your Notes folder and suffix changes have not been sent to Nextcloud.'; + + @override + String get notesSettingsReload => 'Reload Server Settings…'; + + @override + String get notesSettingsReloadHelp => + 'Reloading replaces your draft with the current settings on Nextcloud. Discard this draft and reload?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud accepted the settings update, but refreshing notes failed. The displayed settings are the server’s acknowledged values. Retry the refresh from Notes.'; + + @override + String get notesUnavailable => + 'Nextcloud is unavailable. Cached notes remain available; local changes are waiting to synchronize.'; + + @override + String get notesStorageFull => + 'Nextcloud storage is full. Free space on the server, then retry.'; + + @override + String get notesRejected => + 'Nextcloud rejected this change. Review the note and save a local copy before retrying.'; + + @override + String get notesThrottled => + 'Nextcloud asked BusyMark to wait. Synchronization resumes after the retry deadline.'; + + @override + String get notesRecoveryRequired => + 'This change requires review before synchronization can continue.'; + + @override + String get notesScanning => 'Scanning files…'; + + @override + String get notesCancelling => + 'Cancelling… Waiting for the current operation to settle.'; + + @override + String get notesCancelled => 'Cancelled'; + + @override + String get notesPartial => 'Partially completed'; + + @override + String get notesTransferFailed => + 'Transfer failed. Review the outcome before retrying.'; + + @override + String get notesImportPartialHelp => + 'Earlier items may already be saved locally. Resume this import to continue without duplicating them.'; + + @override + String get notesNotStarted => 'Not started'; + + @override + String get notesCreated => 'Created'; + + @override + String get notesRemoteDeleteUnavailable => + 'This server does not support deleting remote attachments.'; + + @override + String get notesCurrentText => 'Current retained text'; + + @override + String get notesRecovering => 'Recovering as a new note…'; + + @override + String get notesReadOnlyHelp => + 'This note is read-only. You can change its favorite status or save a local copy.'; + + @override + String get notesSort => 'Sort notes'; + + @override + String get notesPathRelative => + 'The Notes folder path is relative to your Nextcloud storage.'; + + @override + String get notesSettingsStale => + 'The settings changed on Nextcloud. Reload and review the server values before replacing your draft.'; + + @override + String get notesDownloadsPaused => 'Downloads paused'; + + @override + String get notesExportNotPublished => 'No export was published.'; + + @override + String get notesKeyringUnavailable => + 'Unlock the desktop keyring and try again.'; + + @override + String get notesConnectFailed => + 'Could not connect to Nextcloud Notes. Check the server address and try again.'; + + @override + String get notesAccountMismatch => + 'Sign in to the same Nextcloud account. Remove this account first to connect a different one.'; + + @override + String get notesLocalRemovalFailed => + 'Local account cleanup failed. The account is retained so you can retry removal.'; + + @override + String get notesTransferring => 'Transferring notes…'; + + @override + String get notesTechnicalDetails => 'Technical details'; + + @override + String get notesPauseRequirement => 'Pause Offline Requirement'; + + @override + String get notesResumeRequirement => 'Resume Offline Requirement'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 243eae28..1c7f637f 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -10,6 +10,132 @@ import 'app_localizations.dart'; class AppLocalizationsEs extends AppLocalizations { AppLocalizationsEs([String locale = 'es']) : super(locale); + @override + String get notesNewFromRetainedText => 'Nueva nota del texto conservado'; + + @override + String get notesAccountThrottled => + 'Nextcloud solicitó una pausa. Espere antes de iniciar sesión de nuevo.'; + + @override + String get notesSettingsRefreshing => + 'Actualizando Notes tras confirmar los ajustes'; + + @override + String get notesExportOperationFailed => + 'La exportación falló antes de publicarse. Compruebe el origen y el destino y vuelva a exportar.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'La nota recuperada se guardó, pero no pudo abrirse. Resuelva los cambios pendientes del editor o de propiedades y abra esta nota existente.'; + + @override + String get notesSettingsPendingNotes => + 'Hay notas pendientes, conflictos o creaciones inciertas que bloquean este cambio. Sincronícelos o revíselos en Notes y vuelva a guardar este borrador conservado.'; + + @override + String get notesSettingsPendingAttachments => + 'Hay operaciones de adjuntos que bloquean este cambio. Revise los adjuntos en Notes y vuelva a guardar este borrador conservado.'; + + @override + String get notesAccountServerAddress => + 'Use la dirección HTTPS canónica de Nextcloud y vuelva a iniciar sesión. No se pudo verificar la dirección o la redirección.'; + + @override + String get notesAccountUnsupported => + 'Notes no está disponible o su API no es compatible. Active Nextcloud Notes con la API de versión principal 1 y secundaria 4 o posterior.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud rechazó las credenciales. Vuelva a autorizar la cuenta o a conectarla.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud denegó la comprobación de funciones de Notes. Solicite permiso al administrador y reinténtelo.'; + + @override + String get notesAccountMalformed => + 'No se pudo verificar la respuesta de Nextcloud. Compruebe la instalación del servidor y vuelva a iniciar sesión.'; + + @override + String get notesAccountTimedOut => + 'El inicio de sesión caducó. Reinícielo y complete la autorización en el navegador.'; + + @override + String get notesAccountBrowserUnavailable => + 'No se pudo abrir el navegador del sistema. Compruebe el navegador predeterminado y vuelva a iniciar sesión.'; + + @override + String get notesAccountNetwork => + 'No se pudo contactar con Nextcloud. Compruebe la conexión y el servidor y vuelva a iniciar sesión.'; + + @override + String get notesAccountRejected => + 'Nextcloud rechazó la solicitud de funciones. Revise el acceso con el administrador y reinténtelo.'; + + @override + String get notesAccountFailed => + 'No se pudo configurar la cuenta. Vuelva a iniciar sesión.'; + + @override + String get notesTransferSourceChanged => + 'El origen cambió desde la revisión. Revise el archivo actual o capture otra instantánea antes de reintentar.'; + + @override + String get notesTransferUnreadable => + 'No se pudo leer el origen. Compruebe el acceso al archivo o carpeta y vuelva a revisarlo.'; + + @override + String get notesTransferInvalidSnapshot => + 'La instantánea no es válida o compatible. Elija una instantánea válida de BusyMark Notes o un origen Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Los medios superan el límite de importación. Reduzca los del elemento afectado y vuelva a revisarlo.'; + + @override + String get notesTransferUnsafeSource => + 'El origen contiene una ruta insegura. Elija archivos dentro de la carpeta de origen y revíselos.'; + + @override + String get notesTransferDestination => + 'No se pudo escribir la exportación. Compruebe el acceso y el espacio libre en el destino y reinténtelo.'; + + @override + String get notesTransferOperationFailed => + 'La transferencia falló. Revise los elementos afectados y reinténtelo. Los ya importados siguen guardados.'; + + @override + String get notesExportPublishedWithOmissions => + 'Exportación disponible con omisiones'; + + @override + String get notesBatchUnchanged => + 'Los valores solicitados ya estaban establecidos.'; + + @override + String get notesBatchReadOnly => + 'Esta nota es de solo lectura. Las propiedades protegidas no pueden cambiar; Favorito sigue permitido.'; + + @override + String get notesBatchReviewChanged => + 'Esta nota cambió desde la revisión. Revise su estado actual antes de repetir la acción.'; + + @override + String get notesBatchMissing => + 'Esta nota ya no está disponible. Actualice la lista y revise los destinos.'; + + @override + String get notesBatchStorage => + 'Falló el guardado local. Compruebe el almacenamiento y reintente este elemento; los correctos siguen guardados.'; + + @override + String get notesRemoteReviewChanged => + 'La nota del servidor cambió. Se conserva su borrador de combinación. Revise la nueva comparación y decida de nuevo cómo resolverla.'; + + @override + String get notesReviewRemote => 'He revisado la versión actual del servidor'; + @override String get wsModuleCapsNotPublished => 'No se publica la capitalización del módulo.'; @@ -586,15 +712,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get keyboardShortcuts => 'Atajos de teclado'; - @override - String get commandPalette => 'Paleta de comandos'; - - @override - String get commandPaletteHint => 'Escribe un comando'; - - @override - String get commandPaletteEmpty => 'No hay comandos coincidentes'; - @override String get commandUnavailableInContext => 'Este comando no está disponible en el contexto actual.'; @@ -814,7 +931,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Mostrar esta referencia de atajos de teclado'; + 'Personalizar los atajos de teclado de la aplicación'; @override String get shortcutSyntaxReferenceDescription => @@ -1073,7 +1190,7 @@ class AppLocalizationsEs extends AppLocalizations { String get validateOnEdit => 'Validar al editar'; @override - String get clearRecentWorkspaces => 'Borrar espacios de trabajo recientes'; + String get clearRecentWorkspaces => 'Borrar el historial reciente'; @override String get editingButtonsPosition => 'Posición de los botones de edición'; @@ -1861,15 +1978,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get sourceSearchNextMatch => 'Coincidencia siguiente'; - @override - String get sourceSearchCaseSensitive => 'Distinguir mayúsculas y minúsculas'; - - @override - String get sourceSearchWholeWord => 'Palabra completa'; - - @override - String get sourceSearchRegex => 'Expresión regular'; - @override String get sourceSearchReplacement => 'Reemplazar por'; @@ -1997,13 +2105,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get saveAs => 'Guardar como'; - @override - String get sourceSearchInvalidRegex => 'Expresión regular no válida'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'No se admiten coincidencias de longitud cero (por ejemplo, ^, \$ o anticipaciones que solo coinciden con una posición).'; - @override String get workspaceSearchIncomplete => 'Solo se muestran algunos resultados. Muestre más o limite la búsqueda.'; @@ -2113,6 +2214,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get documentKindResourceFile => 'Archivo de recursos'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'No se pudieron actualizar los elementos recientes: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Error al abrir: $error'; @@ -4389,10 +4495,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get clipboardHistory => 'Historial de la Copia'; - @override - String get clipboardHistorySearchHint => - 'Buscar en el historial del portapapeles'; - @override String get clipboardHistoryDescription => 'Abrir el historial de la copia local de la sesión.'; @@ -4415,10 +4517,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get clipboardNoItems => 'El historial de la copia está vacío.'; - @override - String get clipboardNoMatches => - 'Ningún elemento de la copia coincide con esta búsqueda.'; - @override String get clipboardDisabled => 'La colección de Historial de la Copia está deshabilitada en la configuración.'; @@ -4476,13 +4574,6 @@ class AppLocalizationsEs extends AppLocalizations { String get localHistoryNoRevisions => 'Este documento no tiene revisiones en el Historial Local.'; - @override - String get localHistoryNoMatches => - 'Ninguna revisión del Historial Local coincide con esta búsqueda.'; - - @override - String get localHistorySearchHint => 'Buscar en el historial local'; - @override String get localHistorySelectedRevision => 'Revisión seleccionada'; @@ -5448,4 +5539,402 @@ class AppLocalizationsEs extends AppLocalizations { @override String get wsPropertiesUnavailable => 'El elemento inspeccionado no está disponible. Inspeccione otro elemento para continuar.'; + + @override + String get searchDocumentsCategory => 'Documentos'; + + @override + String get searchTopicsCategory => 'Temas'; + + @override + String get searchCurrentDocument => 'Documento actual'; + + @override + String get searchCurrentWorkspace => 'Espacio de trabajo actual'; + + @override + String get browseNotes => 'Explorar notas'; + + @override + String get searchActionsCommand => 'Buscar acciones'; + + @override + String get searchDocumentsCommand => 'Buscar documentos'; + + @override + String get searchTopicsCommand => 'Buscar temas'; + + @override + String get searchRecentCommand => 'Buscar recientes'; + + @override + String get searchResourceUnavailable => 'Este recurso no está disponible.'; + + @override + String get notesSync => 'Sincronizar notas'; + + @override + String get searchInWorkspace => 'Buscar en espacio de trabajo'; + + @override + String get searchCategory => 'Categoría de búsqueda'; + + @override + String get clearHistorySearchScope => 'Buscar en el historial de documentos'; + + @override + String get searchInDocument => 'Buscar en el documento'; + + @override + String get searchDocumentHint => 'Buscar en este documento…'; + + @override + String get searchWorkspaceHint => 'Buscar en este espacio de trabajo…'; + + @override + String get searchOutlineHint => 'Buscar encabezados…'; + + @override + String get searchHistoryHint => 'Buscar en el historial local…'; + + @override + String get searchEnterText => 'Introduce texto para buscar'; + + @override + String get searchAccountUnavailable => 'Cuenta no disponible'; + + @override + String get searchDocumentsHint => 'Buscar documentos…'; + + @override + String get searchTopicsHint => 'Buscar temas…'; + + @override + String get searchRecentHint => 'Buscar recientes…'; + + @override + String get searchActionsHint => 'Buscar acciones…'; + + @override + String get searchRequiresNotesWorkspace => + 'Requiere un espacio de trabajo de Notas'; + + @override + String get searchSidebarLabel => 'Buscar:'; + + @override + String get searchDocumentTextCategory => 'Texto del documento'; + + @override + String get searchWorkspaceTextCategory => 'Texto del espacio de trabajo'; + + @override + String get searchHeadingsCategory => 'Encabezados'; + + @override + String get shortcutUnassigned => 'Sin asignar'; + + @override + String get shortcutPressKeys => 'Pulse una combinación de teclas…'; + + @override + String get shortcutRecord => 'Capturar atajo'; + + @override + String get shortcutRestoreAll => + 'Restaurar todos los valores predeterminados'; + + @override + String get shortcutRestoreAllQuestion => + '¿Restaurar los atajos predeterminados de todas las acciones? Se eliminarán las asignaciones personalizadas.'; + + @override + String get shortcutSaveFailed => + 'Los atajos están activos, pero no se pudieron guardar. Reintente para conservarlos tras reiniciar.'; + + @override + String get shortcutRetry => 'Reintentar guardado'; + + @override + String get shortcutChanged => + 'Las asignaciones cambiaron. Revise los atajos actuales y reintente.'; + + @override + String get shortcutInvalid => + 'Esta combinación no se puede asignar en este contexto.'; + + @override + String get shortcutSystemWarning => + 'El escritorio puede usar esta combinación. BusyMark no puede sustituir atajos del sistema.'; + + @override + String get shortcutConflicts => + 'Este atajo entra en conflicto con estas acciones:'; + + @override + String get shortcutRemove => 'Quitar asignación'; + + @override + String get shortcutRestoreDefault => 'Restaurar valor predeterminado'; + + @override + String get shortcutAssign => 'Asignar'; + + @override + String get shortcutReassign => 'Reasignar'; + + @override + String get shortcutContextApplication => 'Aplicación'; + + @override + String get shortcutContextDocument => 'Vistas del documento'; + + @override + String get shortcutContextEditor => 'Editor y código fuente'; + + @override + String get shortcutContextWritersideSource => 'Código fuente de Writerside'; + + @override + String get shortcutContextWorkspace => 'Navegación del espacio de trabajo'; + + @override + String get shortcutContextTree => 'Árboles laterales'; + + @override + String get searchHeadingsCommand => 'Buscar encabezados'; + + @override + String get shortcutFixed => 'Lo gestiona el editor; no se puede configurar.'; + + @override + String get shortcutDefaultConflict => + 'El atajo predeterminado entra en conflicto con una asignación personalizada.'; + + @override + String get shortcutCurrentBinding => 'Atajo actual'; + + @override + String get shortcutProposedBinding => 'Atajo propuesto'; + + @override + String get notesActions => 'Acciones de notas'; + + @override + String get noteActions => 'Acciones de la nota'; + + @override + String get noteProperties => 'Propiedades de la nota'; + + @override + String get notesExportAll => 'Exportar notas · Todas las notas…'; + + @override + String get notesExportCategory => 'Exportar notas · Categoría…'; + + @override + String get notesOfflineAll => + 'Hacer disponible sin conexión · Todas las notas'; + + @override + String get notesOfflineCategory => + 'Hacer disponible sin conexión · Categoría'; + + @override + String get notesOfflineRemoveAll => + 'Quitar conservación sin conexión · Todas las notas'; + + @override + String get notesOfflineRemoveCategory => + 'Quitar conservación sin conexión · Categoría'; + + @override + String get notesOfflinePause => 'Pausar descargas'; + + @override + String get notesOfflineResume => 'Reanudar descargas'; + + @override + String get notesOfflineKeep => 'Hacer disponible sin conexión'; + + @override + String get notesOfflineRetentionHelp => + 'Quitar este requisito conserva el contenido en caché. Un requisito de categoría o cuenta puede seguir reteniendo la nota.'; + + @override + String get notesOfflineBroader => + 'Esta nota también se conserva por un requisito de categoría o cuenta.'; + + @override + String get notesOfflineExternal => + 'El contenido multimedia externo necesita conexión. BusyMark solo conserva el texto y los adjuntos gestionados.'; + + @override + String get notesEmptyAll => 'Aún no hay notas. Cree una para empezar.'; + + @override + String get notesEmptyFavorites => 'No hay notas favoritas.'; + + @override + String get notesEmptyCategory => 'No hay notas en esta categoría.'; + + @override + String get notesEmptyUncategorized => 'No hay notas sin categoría.'; + + @override + String get notesEmptyRecovery => 'No hay notas conservadas para recuperar.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Fallido. Reintentar'; + + @override + String get notesOperationFailed => + 'La operación no pudo terminar. El borrador se conserva. Revise el guardado y los conflictos antes de reintentarlo.'; + + @override + String get notesInspectionFailed => 'Adjuntos · Fallido. Reintentar'; + + @override + String get notesStartingConnection => 'Iniciando la conexión con Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Verificando el acceso a Notas…'; + + @override + String get notesCancellingConnection => + 'Cancelando el inicio de sesión y limpiando…'; + + @override + String get notesRemovingConnection => 'Eliminando la cuenta local…'; + + @override + String get notesRemovedWarning => + 'Se eliminó la cuenta local de Nextcloud. No se confirmó la revocación de la contraseña de aplicación. Elimine la contraseña de BusyMark en los ajustes de seguridad de Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'No se pudieron guardar todas las notas abiertas localmente. Revise el guardado, los conflictos o la normalización en el espacio de trabajo. Los cambios pendientes aún pueden necesitar sincronización con Nextcloud.'; + + @override + String get notesSettingsLeave => + '¿Guardar los cambios de ajustes de Nextcloud antes de salir?'; + + @override + String get notesSettingsLeaveHelp => + 'Ajustes de Notes del servidor · Cambios sin guardar'; + + @override + String get notesSettingsReload => 'Volver a cargar los ajustes del servidor…'; + + @override + String get notesSettingsReloadHelp => + 'La recarga sustituye el borrador por los ajustes actuales de Nextcloud. ¿Descartar el borrador y recargar?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud aceptó los ajustes, pero falló la actualización de las notas. Se muestran los valores confirmados por el servidor. Reintente actualizar desde Notas.'; + + @override + String get notesUnavailable => + 'Sin conexión · Texto disponible localmente · Guardado localmente · sincronización pendiente'; + + @override + String get notesStorageFull => + 'El almacenamiento de Nextcloud está lleno. Libere espacio en el servidor y reinténtelo.'; + + @override + String get notesRejected => + 'Nextcloud rechazó este cambio. Revise la nota y guarde una copia local antes de reintentarlo.'; + + @override + String get notesThrottled => + 'Nextcloud ha pedido esperar. La sincronización continuará al finalizar el plazo de espera.'; + + @override + String get notesRecoveryRequired => 'Conflictos · Comparar'; + + @override + String get notesScanning => 'Examinando archivos…'; + + @override + String get notesCancelling => + 'Cancelando… Esperando a que termine la operación actual.'; + + @override + String get notesCancelled => 'Cancelado'; + + @override + String get notesPartial => 'Completado parcialmente'; + + @override + String get notesTransferFailed => 'Fallido. Reintentar'; + + @override + String get notesImportPartialHelp => + 'Los elementos anteriores pueden estar guardados localmente. Reanude esta importación para continuar sin duplicarlos.'; + + @override + String get notesNotStarted => 'Sin iniciar'; + + @override + String get notesCreated => 'Creado'; + + @override + String get notesRemoteDeleteUnavailable => + 'URL del servidor · No compatible · Eliminar · Adjuntos'; + + @override + String get notesCurrentText => 'Texto conservado actual'; + + @override + String get notesRecovering => 'Recuperando como nota nueva…'; + + @override + String get notesReadOnlyHelp => + 'Solo lectura · Favoritos · Guardar copia local…'; + + @override + String get notesSort => 'Ordenar notas'; + + @override + String get notesPathRelative => + 'La ruta de la carpeta de notas es relativa a su almacenamiento Nextcloud.'; + + @override + String get notesSettingsStale => + 'Los ajustes cambiaron en Nextcloud. Recargue y revise los valores del servidor antes de sustituir el borrador.'; + + @override + String get notesDownloadsPaused => 'Descargas pausadas'; + + @override + String get notesExportNotPublished => 'No se publicó ninguna exportación.'; + + @override + String get notesKeyringUnavailable => + 'Desbloquee el llavero del escritorio y reinténtelo.'; + + @override + String get notesConnectFailed => + 'No se pudo conectar con Nextcloud Notes. Compruebe la dirección del servidor y reinténtelo.'; + + @override + String get notesAccountMismatch => + 'Inicie sesión con la misma cuenta Nextcloud. Elimínela primero para conectar otra.'; + + @override + String get notesLocalRemovalFailed => + 'Falló la limpieza local. La cuenta se conserva para poder reintentar su eliminación.'; + + @override + String get notesTransferring => 'Transfiriendo notas…'; + + @override + String get notesTechnicalDetails => 'Detalles técnicos'; + + @override + String get notesPauseRequirement => 'Pausar requisito sin conexión'; + + @override + String get notesResumeRequirement => 'Reanudar requisito sin conexión'; } diff --git a/lib/l10n/generated/app_localizations_et.dart b/lib/l10n/generated/app_localizations_et.dart index c8f55cf4..20c63179 100644 --- a/lib/l10n/generated/app_localizations_et.dart +++ b/lib/l10n/generated/app_localizations_et.dart @@ -10,6 +10,132 @@ import 'app_localizations.dart'; class AppLocalizationsEt extends AppLocalizations { AppLocalizationsEt([String locale = 'et']) : super(locale); + @override + String get notesNewFromRetainedText => 'Uus märge säilitatud tekstist'; + + @override + String get notesAccountThrottled => + 'Nextcloud palus pausi. Oota enne uuesti sisselogimist.'; + + @override + String get notesSettingsRefreshing => + 'Notes värskendamine pärast seadete kinnitamist'; + + @override + String get notesExportOperationFailed => + 'Eksport nurjus enne avaldamist. Kontrolli allikat ja sihtkohta ning ekspordi uuesti.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Taastatud märge on salvestatud, kuid ei avanenud. Lahenda ootel redaktori- või omaduste muudatused ning ava see olemasolev märge.'; + + @override + String get notesSettingsPendingNotes => + 'Ootel märkmed, konfliktid või ebakindlad loomised takistavad muudatust. Sünkrooni või vaata need Notes’is üle ning salvesta säilitatud seadete mustand uuesti.'; + + @override + String get notesSettingsPendingAttachments => + 'Ootel manuste toimingud takistavad muudatust. Vaata manused Notes’is üle ning salvesta säilitatud seadete mustand uuesti.'; + + @override + String get notesAccountServerAddress => + 'Kasuta Nextcloudi kanoonilist HTTPS-aadressi ja logi uuesti sisse. Aadressi või ümbersuunamist ei saanud kinnitada.'; + + @override + String get notesAccountUnsupported => + 'Notes pole saadaval või API pole toetatud. Luba Nextcloud Notes API põhiversiooniga 1, alamversiooniga 4 või uuemaga.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud lükkas pääsuandmed tagasi. Volita konto uuesti või ühenda uuesti.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud keelas Notes’i võimaluste kontrolli. Palu administraatoril see lubada ja proovi uuesti.'; + + @override + String get notesAccountMalformed => + 'Nextcloudi vastust ei saanud kinnitada. Kontrolli serveri paigaldust ja logi uuesti sisse.'; + + @override + String get notesAccountTimedOut => + 'Sisselogimise aeg sai läbi. Alusta uuesti ja lõpeta volitamine brauseris.'; + + @override + String get notesAccountBrowserUnavailable => + 'Süsteemi brauser ei avanenud. Kontrolli vaikebrauserit ja logi uuesti sisse.'; + + @override + String get notesAccountNetwork => + 'Nextcloudiga ei saadud ühendust. Kontrolli võrku ja serverit ning logi uuesti sisse.'; + + @override + String get notesAccountRejected => + 'Nextcloud lükkas võimaluste päringu tagasi. Kontrolli administraatoriga ligipääsu ja proovi uuesti.'; + + @override + String get notesAccountFailed => + 'Konto seadistamine nurjus. Proovi uuesti sisse logida.'; + + @override + String get notesTransferSourceChanged => + 'Allikas muutus pärast ülevaatust. Vaata praegune fail üle või loo uus hetktõmmis.'; + + @override + String get notesTransferUnreadable => + 'Allikat ei saanud lugeda. Kontrolli faili või kausta ligipääsu ja vaata uuesti üle.'; + + @override + String get notesTransferInvalidSnapshot => + 'Hetktõmmis on vigane või toetamata. Vali kehtiv BusyMark Notes’i hetktõmmis või Markdown-allikas.'; + + @override + String get notesTransferMediaLimit => + 'Meedia ületab impordi mahupiiri. Vähenda vastava kirje meediat ja vaata uuesti üle.'; + + @override + String get notesTransferUnsafeSource => + 'Allikas sisaldab ohtlikku teed. Vali failid allikakausta seest ja vaata uuesti üle.'; + + @override + String get notesTransferDestination => + 'Eksporti ei saanud kirjutada. Kontrolli sihtkoha ligipääsu ja vaba ruumi ning proovi uuesti.'; + + @override + String get notesTransferOperationFailed => + 'Edastus ei lõppenud. Vaata vastavad kirjed üle ja proovi uuesti. Varem imporditud kirjed on salvestatud.'; + + @override + String get notesExportPublishedWithOmissions => + 'Eksport on saadaval koos väljajättudega'; + + @override + String get notesBatchUnchanged => 'Soovitud väärtused olid juba määratud.'; + + @override + String get notesBatchReadOnly => + 'Märge on kirjutuskaitstud. Kaitstud omadusi muuta ei saa; lemmikuks märkimine on lubatud.'; + + @override + String get notesBatchReviewChanged => + 'Märge muutus pärast ülevaatust. Vaata selle praegune olek enne toimingu kordamist üle.'; + + @override + String get notesBatchMissing => + 'Märge pole enam saadaval. Värskenda loendit ja vaata sihtkirjed uuesti üle.'; + + @override + String get notesBatchStorage => + 'Kohalik salvestamine nurjus. Kontrolli andmeruumi ja proovi seda kirjet uuesti; õnnestunud kirjed on salvestatud.'; + + @override + String get notesRemoteReviewChanged => + 'Serveri märge muutus. Liitmise mustand säilis. Vaata uus võrdlus üle ja vali lahendus uuesti.'; + + @override + String get notesReviewRemote => + 'Olen serveri praeguse versiooni üle vaadanud'; + @override String get wsModuleCapsNotPublished => 'Mooduli suurtähereeglit ei avaldata.'; @@ -578,15 +704,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get keyboardShortcuts => 'Kiirklahvid'; - @override - String get commandPalette => 'Käskude palett'; - - @override - String get commandPaletteHint => 'Sisesta käsk'; - - @override - String get commandPaletteEmpty => 'Sobivaid käske pole'; - @override String get commandUnavailableInContext => 'See käsk pole praeguses kontekstis saadaval.'; @@ -804,7 +921,8 @@ class AppLocalizationsEt extends AppLocalizations { String get shortcutSearchDescription => 'Otsi praegusest tööruumist'; @override - String get shortcutKeyboardShortcutsDescription => 'Kuva kiirklahvide loend'; + String get shortcutKeyboardShortcutsDescription => + 'Kohanda rakenduse kiirklahve'; @override String get shortcutSyntaxReferenceDescription => 'Ava süntaksiviide'; @@ -1056,7 +1174,7 @@ class AppLocalizationsEt extends AppLocalizations { String get validateOnEdit => 'Valideeri muutmisel'; @override - String get clearRecentWorkspaces => 'Tühjenda hiljutiste tööruumide loend'; + String get clearRecentWorkspaces => 'Tühjenda hiljutiste kirjete loend'; @override String get editingButtonsPosition => 'Redigeerimisnuppude asukoht'; @@ -1839,15 +1957,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get sourceSearchNextMatch => 'Järgmine vaste'; - @override - String get sourceSearchCaseSensitive => 'Tõstutundlik'; - - @override - String get sourceSearchWholeWord => 'Terve sõna'; - - @override - String get sourceSearchRegex => 'Regulaaravaldis'; - @override String get sourceSearchReplacement => 'Asenda väärtusega'; @@ -1973,13 +2082,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get saveAs => 'Salvesta nimega'; - @override - String get sourceSearchInvalidRegex => 'Vigane regulaaravaldis'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Nullpikkusega regulaaravaldiste vasteid ei toetata (näiteks ^, \$ või ainult asukohta sobitavad ettevaated).'; - @override String get workspaceSearchIncomplete => 'Kuvatakse ainult osa tulemustest. Kuva rohkem või kitsenda otsingut.'; @@ -2089,6 +2191,11 @@ class AppLocalizationsEt extends AppLocalizations { @override String get documentKindResourceFile => 'Ressursifail'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Hiljutisi üksusi ei saanud uuendada: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Avamine nurjus: $error'; @@ -4348,9 +4455,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get clipboardHistory => 'Kleepimiskredi ajalugu'; - @override - String get clipboardHistorySearchHint => 'Otsi lõikepuhvri ajaloost'; - @override String get clipboardHistoryDescription => 'Ava sessiooni-spetsiifiline kleepimiskredi ajalugu.'; @@ -4374,10 +4478,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get clipboardNoItems => 'Kleepimiskredi ajalugu on tühi.'; - @override - String get clipboardNoMatches => - 'Ei leitud kleepimiskredi elemente, mis vastavad sellele otsingule.'; - @override String get clipboardDisabled => 'Kleepimiskredi ajalugu on peatatud seadetes.'; @@ -4434,13 +4534,6 @@ class AppLocalizationsEt extends AppLocalizations { String get localHistoryNoRevisions => 'Selle dokumendil pole kohalist ajaloos.'; - @override - String get localHistoryNoMatches => - 'Ei leidunud kohalise ajalooga versioone.'; - - @override - String get localHistorySearchHint => 'Otsi kohalikust ajaloost'; - @override String get localHistorySelectedRevision => 'Valitud versioon'; @@ -5394,4 +5487,398 @@ class AppLocalizationsEt extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Vaadeldav element pole saadaval. Jätkamiseks vali element.'; + + @override + String get searchDocumentsCategory => 'Dokumendid'; + + @override + String get searchTopicsCategory => 'Teemad'; + + @override + String get searchCurrentDocument => 'Praegune dokument'; + + @override + String get searchCurrentWorkspace => 'Praegune tööruum'; + + @override + String get browseNotes => 'Sirvi märkmeid'; + + @override + String get searchActionsCommand => 'Otsi toiminguid'; + + @override + String get searchDocumentsCommand => 'Otsi dokumente'; + + @override + String get searchTopicsCommand => 'Otsi teemasid'; + + @override + String get searchRecentCommand => 'Otsi hiljutisi'; + + @override + String get searchResourceUnavailable => 'See ressurss pole saadaval.'; + + @override + String get notesSync => 'Sünkrooni märkmed'; + + @override + String get searchInWorkspace => 'Otsi tööalast'; + + @override + String get searchCategory => 'Otsingu kategooria'; + + @override + String get clearHistorySearchScope => 'Otsi dokumentide ajaloost'; + + @override + String get searchInDocument => 'Otsi dokumendist'; + + @override + String get searchDocumentHint => 'Otsi sellest dokumendist…'; + + @override + String get searchWorkspaceHint => 'Otsi sellest tööruumist…'; + + @override + String get searchOutlineHint => 'Otsi pealkirju…'; + + @override + String get searchHistoryHint => 'Otsi kohalikust ajaloost…'; + + @override + String get searchEnterText => 'Sisesta otsitav tekst'; + + @override + String get searchAccountUnavailable => 'Konto pole saadaval'; + + @override + String get searchDocumentsHint => 'Otsi dokumente…'; + + @override + String get searchTopicsHint => 'Otsi teemasid…'; + + @override + String get searchRecentHint => 'Otsi hiljutisi…'; + + @override + String get searchActionsHint => 'Otsi toiminguid…'; + + @override + String get searchRequiresNotesWorkspace => 'Vajab märkmete tööruumi'; + + @override + String get searchSidebarLabel => 'Otsi:'; + + @override + String get searchDocumentTextCategory => 'Dokumendi tekst'; + + @override + String get searchWorkspaceTextCategory => 'Tööala tekst'; + + @override + String get searchHeadingsCategory => 'Pealkirjad'; + + @override + String get shortcutUnassigned => 'Määramata'; + + @override + String get shortcutPressKeys => 'Vajuta klahvikombinatsiooni…'; + + @override + String get shortcutRecord => 'Salvesta kiirklahv'; + + @override + String get shortcutRestoreAll => 'Taasta kõik vaikeväärtused'; + + @override + String get shortcutRestoreAllQuestion => + 'Kas taastada kõigi toimingute vaikekiirklahvid? Kohandatud määrangud eemaldatakse.'; + + @override + String get shortcutSaveFailed => + 'Kiirklahvid on aktiivsed, kuid neid ei saanud salvestada. Proovi uuesti, et säilitada need pärast taaskäivitamist.'; + + @override + String get shortcutRetry => 'Proovi salvestamist uuesti'; + + @override + String get shortcutChanged => + 'Määrangud on muutunud. Vaata praegused kiirklahvid üle ja proovi uuesti.'; + + @override + String get shortcutInvalid => + 'Seda kombinatsiooni ei saa selles kontekstis määrata.'; + + @override + String get shortcutSystemWarning => + 'Töölaud võib seda kombinatsiooni kasutada. BusyMark ei saa süsteemi kiirklahve alistada.'; + + @override + String get shortcutConflicts => + 'See kiirklahv kattub järgmiste toimingutega:'; + + @override + String get shortcutRemove => 'Eemalda määrang'; + + @override + String get shortcutRestoreDefault => 'Taasta vaikeväärtus'; + + @override + String get shortcutAssign => 'Määra'; + + @override + String get shortcutReassign => 'Määra ümber'; + + @override + String get shortcutContextApplication => 'Rakendus'; + + @override + String get shortcutContextDocument => 'Dokumendivaated'; + + @override + String get shortcutContextEditor => 'Redaktor ja lähtekood'; + + @override + String get shortcutContextWritersideSource => 'Writerside’i lähtekood'; + + @override + String get shortcutContextWorkspace => 'Tööala navigeerimine'; + + @override + String get shortcutContextTree => 'Külgriba puud'; + + @override + String get searchHeadingsCommand => 'Otsi pealkirju'; + + @override + String get shortcutFixed => 'Redaktori hallatav; pole seadistatav.'; + + @override + String get shortcutDefaultConflict => + 'Vaikimisi otsetee on vastuolus kohandatud määranguga.'; + + @override + String get shortcutCurrentBinding => 'Praegune otsetee'; + + @override + String get shortcutProposedBinding => 'Pakutud otsetee'; + + @override + String get notesActions => 'Märkmete toimingud'; + + @override + String get noteActions => 'Märkme toimingud'; + + @override + String get noteProperties => 'Märkme omadused'; + + @override + String get notesExportAll => 'Ekspordi märkmed · Kõik märkmed…'; + + @override + String get notesExportCategory => 'Ekspordi märkmed · Kategooria…'; + + @override + String get notesOfflineAll => 'Tee võrguta kättesaadavaks · Kõik märkmed'; + + @override + String get notesOfflineCategory => 'Tee võrguta kättesaadavaks · Kategooria'; + + @override + String get notesOfflineRemoveAll => + 'Eemalda võrguta säilitamine · Kõik märkmed'; + + @override + String get notesOfflineRemoveCategory => + 'Eemalda võrguta säilitamine · Kategooria'; + + @override + String get notesOfflinePause => 'Peata allalaadimised'; + + @override + String get notesOfflineResume => 'Jätka allalaadimisi'; + + @override + String get notesOfflineKeep => 'Tee võrguta kättesaadavaks'; + + @override + String get notesOfflineRetentionHelp => + 'Selle nõude eemaldamine jätab vahemälu alles. Kategooria või konto nõue võib märget endiselt säilitada.'; + + @override + String get notesOfflineBroader => + 'Seda märget säilitatakse ka kategooria või konto nõude tõttu.'; + + @override + String get notesOfflineExternal => + 'Väline meedia vajab ühendust. BusyMark säilitab vaid märkmete teksti ja hallatavad manused.'; + + @override + String get notesEmptyAll => 'Märkmeid veel pole. Alustamiseks loo märge.'; + + @override + String get notesEmptyFavorites => 'Lemmikmärkmeid pole.'; + + @override + String get notesEmptyCategory => 'Selles kategoorias pole märkmeid.'; + + @override + String get notesEmptyUncategorized => 'Kategooriata märkmeid pole.'; + + @override + String get notesEmptyRecovery => 'Taastamiseks säilitatud märkmeid pole.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Ebaõnnestus. Proovi uuesti'; + + @override + String get notesOperationFailed => + 'Toiming ei lõppenud. Mustand on alles. Kontrolli salvestamist ja konflikte ning proovi uuesti.'; + + @override + String get notesInspectionFailed => 'Manused · Ebaõnnestus. Proovi uuesti'; + + @override + String get notesStartingConnection => 'Nextcloudi ühenduse alustamine…'; + + @override + String get notesVerifyingConnection => 'Märkmete ligipääsu kontrollimine…'; + + @override + String get notesCancellingConnection => + 'Sisselogimise tühistamine ja puhastamine…'; + + @override + String get notesRemovingConnection => 'Kohaliku konto eemaldamine…'; + + @override + String get notesRemovedWarning => + 'Kohalik Nextcloudi konto eemaldati. Rakenduse parooli tühistamist ei kinnitatud. Eemalda BusyMarki parool Nextcloudi turvaseadetest.'; + + @override + String get notesBuffersNotSaved => + 'Kõiki avatud märkmeid ei saanud kohalikult salvestada. Kontrolli tööruumis salvestamist, konflikte või normaliseerimist. Kohalikud muudatused võivad veel vajada Nextcloudiga sünkroonimist.'; + + @override + String get notesSettingsLeave => + 'Kas salvestada Nextcloudi seadete muudatused enne lahkumist?'; + + @override + String get notesSettingsLeaveHelp => + 'Serveri Notesi seaded · Salvestamata muudatused'; + + @override + String get notesSettingsReload => 'Laadi serveri seaded uuesti…'; + + @override + String get notesSettingsReloadHelp => + 'Uuesti laadimine asendab mustandi Nextcloudi praeguste seadetega. Kas loobuda mustandist ja laadida uuesti?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud kinnitas seaded, kuid märkmete värskendamine ebaõnnestus. Kuvatakse serveri kinnitatud väärtused. Proovi märkmete värskendamist uuesti.'; + + @override + String get notesUnavailable => + 'Ühenduseta · Tekst on kohalikult saadaval · Salvestatud kohalikult · sünkroonimine ootel'; + + @override + String get notesStorageFull => + 'Nextcloudi salvestusruum on täis. Vabasta serveris ruumi ja proovi uuesti.'; + + @override + String get notesRejected => + 'Nextcloud lükkas muudatuse tagasi. Kontrolli märget ja salvesta enne uut katset kohalik koopia.'; + + @override + String get notesThrottled => + 'Nextcloud palus oodata. Sünkroonimine jätkub pärast ooteaja lõppu.'; + + @override + String get notesRecoveryRequired => 'Konfliktid · Võrdle'; + + @override + String get notesScanning => 'Failide uurimine…'; + + @override + String get notesCancelling => 'Tühistamine… Praeguse toimingu lõpu ootamine.'; + + @override + String get notesCancelled => 'Tühistatud'; + + @override + String get notesPartial => 'Osaliselt lõpetatud'; + + @override + String get notesTransferFailed => 'Ebaõnnestus. Proovi uuesti'; + + @override + String get notesImportPartialHelp => + 'Varasemad üksused võivad olla juba kohalikult salvestatud. Jätka sama importi, et vältida duplikaate.'; + + @override + String get notesNotStarted => 'Alustamata'; + + @override + String get notesCreated => 'Loodud'; + + @override + String get notesRemoteDeleteUnavailable => + 'Serveri aadress · Pole toetatud · Kustuta · Manused'; + + @override + String get notesCurrentText => 'Praegu säilitatud tekst'; + + @override + String get notesRecovering => 'Taastamine uue märkmena…'; + + @override + String get notesReadOnlyHelp => + 'Kirjutuskaitstud · Lemmikud · Salvesta kohalik koopia…'; + + @override + String get notesSort => 'Sordi märkmeid'; + + @override + String get notesPathRelative => + 'Märkmete kaustatee on suhteline sinu Nextcloudi salvestusruumi suhtes.'; + + @override + String get notesSettingsStale => + 'Seaded muutusid Nextcloudis. Laadi serveri väärtused uuesti ja vaata üle enne mustandi asendamist.'; + + @override + String get notesDownloadsPaused => 'Allalaadimised peatatud'; + + @override + String get notesExportNotPublished => 'Eksporti ei avaldatud.'; + + @override + String get notesKeyringUnavailable => + 'Ava töölaua võtmehoidla ja proovi uuesti.'; + + @override + String get notesConnectFailed => + 'Nextcloudi märkmetega ei saanud ühendust. Kontrolli serveri aadressi ja proovi uuesti.'; + + @override + String get notesAccountMismatch => + 'Logi sisse samale Nextcloudi kontole. Teise konto ühendamiseks eemalda esmalt see konto.'; + + @override + String get notesLocalRemovalFailed => + 'Kohaliku konto puhastamine ebaõnnestus. Konto on alles, et saaksid eemaldamist korrata.'; + + @override + String get notesTransferring => 'Märkmete teisaldamine…'; + + @override + String get notesTechnicalDetails => 'Tehnilised üksikasjad'; + + @override + String get notesPauseRequirement => 'Peata võrguühenduseta säilitamise nõue'; + + @override + String get notesResumeRequirement => + 'Jätka võrguühenduseta säilitamise nõuet'; } diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index 2fa72377..02a5f42c 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsFa extends AppLocalizations { AppLocalizationsFa([String locale = 'fa']) : super(locale); + @override + String get notesNewFromRetainedText => 'یادداشت جدید از متن نگه‌داری‌شده'; + + @override + String get notesAccountThrottled => + 'Nextcloud درخواست مکث کرد. پیش از آغاز دوبارهٔ ورود منتظر بمانید.'; + + @override + String get notesSettingsRefreshing => + 'در حال تازه‌سازی Notes پس از تأیید تنظیمات'; + + @override + String get notesExportOperationFailed => + 'برون‌بری پیش از انتشار شکست خورد. مبدأ و مقصد را بررسی و دوباره برون‌بری کنید.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'یادداشت بازیابی‌شده ذخیره شد اما باز نشد. تغییرات معلق ویرایشگر یا ویژگی‌ها را رفع و همین یادداشت را باز کنید.'; + + @override + String get notesSettingsPendingNotes => + 'یادداشت‌های معلق، تعارض‌ها یا ایجادهای نامطمئن مانع این تغییرند. در Notes همگام‌سازی یا بازبینی کنید، سپس ذخیرهٔ پیش‌نویس حفظ‌شده را تکرار کنید.'; + + @override + String get notesSettingsPendingAttachments => + 'عملیات معلق پیوست‌ها مانع این تغییرند. پیوست‌ها را در Notes بازبینی و ذخیرهٔ پیش‌نویس حفظ‌شده را تکرار کنید.'; + + @override + String get notesAccountServerAddress => + 'از نشانی HTTPS اصلی Nextcloud استفاده و ورود را دوباره آغاز کنید. نشانی یا تغییرمسیر تأیید نشد.'; + + @override + String get notesAccountUnsupported => + 'Notes در دسترس نیست یا API پشتیبانی نمی‌شود. Nextcloud Notes را با نسخهٔ اصلی API برابر 1 و فرعی 4 یا بالاتر فعال کنید.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud اطلاعات ورود را رد کرد. حساب را دوباره مجاز یا متصل کنید.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud بررسی قابلیت‌های Notes را نپذیرفت. از مدیر سرور مجوز بخواهید و دوباره تلاش کنید.'; + + @override + String get notesAccountMalformed => + 'پاسخ Nextcloud تأیید نشد. نصب سرور را بررسی و ورود را دوباره آغاز کنید.'; + + @override + String get notesAccountTimedOut => + 'مهلت ورود تمام شد. دوباره آغاز و مجوز را در مرورگر تکمیل کنید.'; + + @override + String get notesAccountBrowserUnavailable => + 'مرورگر سیستم باز نشد. مرورگر پیش‌فرض را بررسی و ورود را دوباره آغاز کنید.'; + + @override + String get notesAccountNetwork => + 'دسترسی به Nextcloud ممکن نشد. اتصال و سرور را بررسی و دوباره وارد شوید.'; + + @override + String get notesAccountRejected => + 'Nextcloud درخواست قابلیت‌ها را رد کرد. دسترسی را با مدیر بررسی و دوباره تلاش کنید.'; + + @override + String get notesAccountFailed => + 'راه‌اندازی حساب کامل نشد. دوباره وارد شوید.'; + + @override + String get notesTransferSourceChanged => + 'مبدأ پس از بازبینی تغییر کرده است. فایل فعلی را دوباره بازبینی یا تصویر تازه‌ای تهیه کنید.'; + + @override + String get notesTransferUnreadable => + 'مبدأ خوانده نشد. دسترسی فایل یا پوشه را بررسی و دوباره بازبینی کنید.'; + + @override + String get notesTransferInvalidSnapshot => + 'تصویر نامعتبر یا پشتیبانی‌نشده است. یک تصویر معتبر BusyMark Notes یا مبدأ Markdown انتخاب کنید.'; + + @override + String get notesTransferMediaLimit => + 'رسانه از محدودیت اندازهٔ درون‌بری بیشتر است. رسانهٔ مورد مربوط را کوچک‌تر و دوباره بازبینی کنید.'; + + @override + String get notesTransferUnsafeSource => + 'مبدأ مسیر ناامن دارد. فایل‌هایی از داخل پوشهٔ مبدأ انتخاب و دوباره بازبینی کنید.'; + + @override + String get notesTransferDestination => + 'برون‌بری نوشته نشد. دسترسی و فضای آزاد مقصد را بررسی و دوباره برون‌بری کنید.'; + + @override + String get notesTransferOperationFailed => + 'انتقال کامل نشد. موارد مربوط را بازبینی و دوباره تلاش کنید. موارد درون‌برده‌شدهٔ قبلی ذخیره می‌مانند.'; + + @override + String get notesExportPublishedWithOmissions => + 'برون‌بری با موارد حذف‌شده در دسترس است'; + + @override + String get notesBatchUnchanged => 'مقادیر خواسته‌شده از قبل تنظیم بودند.'; + + @override + String get notesBatchReadOnly => + 'این یادداشت فقط‌خواندنی است. ویژگی‌های محافظت‌شده تغییر نمی‌کنند؛ تغییر علاقه‌مندی همچنان مجاز است.'; + + @override + String get notesBatchReviewChanged => + 'یادداشت پس از بازبینی تغییر کرده است. پیش از اجرای دوبارهٔ عمل، وضعیت فعلی را بازبینی کنید.'; + + @override + String get notesBatchMissing => + 'این یادداشت دیگر در دسترس نیست. فهرست و اهداف را دوباره بررسی کنید.'; + + @override + String get notesBatchStorage => + 'ذخیرهٔ محلی شکست خورد. فضای ذخیره‌سازی را بررسی و این مورد را تکرار کنید؛ موارد موفق ذخیره می‌مانند.'; + + @override + String get notesRemoteReviewChanged => + 'یادداشت سرور تغییر کرد. پیش‌نویس ادغام حفظ شده است. مقایسهٔ به‌روز را بازبینی و راه‌حل را دوباره انتخاب کنید.'; + + @override + String get notesReviewRemote => 'نسخهٔ فعلی سرور را بازبینی کردم'; + @override String get wsModuleCapsNotPublished => 'سبک حروف ماژول منتشر نمی‌شود.'; @@ -581,15 +706,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get keyboardShortcuts => 'میانبرهای صفحه‌کلید'; - @override - String get commandPalette => 'پالت فرمان'; - - @override - String get commandPaletteHint => 'یک فرمان وارد کنید'; - - @override - String get commandPaletteEmpty => 'هیچ فرمان منطبقی وجود ندارد'; - @override String get commandUnavailableInContext => 'این فرمان در زمینهٔ فعلی در دسترس نیست.'; @@ -808,7 +924,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'نمایش مرجع میانبرهای صفحه‌کلید'; + 'سفارشی‌سازی میان‌برهای صفحه‌کلید برنامه'; @override String get shortcutSyntaxReferenceDescription => 'باز کردن راهنمای نحو'; @@ -1053,7 +1169,7 @@ class AppLocalizationsFa extends AppLocalizations { String get validateOnEdit => 'اعتبارسنجی هنگام ویرایش'; @override - String get clearRecentWorkspaces => 'پاک کردن فضاهای کاری اخیر'; + String get clearRecentWorkspaces => 'پاک کردن تاریخچهٔ اخیر'; @override String get editingButtonsPosition => 'موقعیت دکمه‌های ویرایش'; @@ -1878,15 +1994,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get sourceSearchNextMatch => 'تطابق بعدی'; - @override - String get sourceSearchCaseSensitive => 'حساس به بزرگی و کوچکی حروف'; - - @override - String get sourceSearchWholeWord => 'تمام کلمه'; - - @override - String get sourceSearchRegex => 'عبارت منظم'; - @override String get sourceSearchReplacement => 'جایگزینی با'; @@ -2013,13 +2120,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get saveAs => 'ذخیره با نام'; - @override - String get sourceSearchInvalidRegex => 'عبارت منظم نامعتبر است'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'تطبیق‌های با طول صفر در عبارت منظم پشتیبانی نمی‌شوند (مانند ^، \$ یا نگاه‌به‌جلو که فقط با یک موقعیت تطبیق دارد).'; - @override String get workspaceSearchIncomplete => 'فقط بخشی از نتایج نمایش داده می‌شود. نتایج بیشتری نمایش دهید یا جستجو را محدود کنید.'; @@ -2133,6 +2233,11 @@ class AppLocalizationsFa extends AppLocalizations { @override String get documentKindResourceFile => 'فایل منبع'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'به‌روزرسانی موارد اخیر ممکن نشد: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'باز کردن ناموفق بود: ⁨$error⁩'; @@ -4396,9 +4501,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get clipboardHistory => 'تاریخچه کپی و پیست'; - @override - String get clipboardHistorySearchHint => 'جستجو در تاریخچه کلیپ‌بورد'; - @override String get clipboardHistoryDescription => 'باز کردن تاریخچه کپی و پیست مخصوص جلسه.'; @@ -4420,10 +4522,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get clipboardNoItems => 'تاریخچه کپی و پیست خالی است.'; - @override - String get clipboardNoMatches => - 'هیچ آیتم کپی و پیستی با این جستجو مطابقت ندارد.'; - @override String get clipboardDisabled => 'مجموعه تاریخچه کپی و پیست در تنظیمات غیرفعال است.'; @@ -4480,13 +4578,6 @@ class AppLocalizationsFa extends AppLocalizations { String get localHistoryNoRevisions => 'این سند دارای هیچ نسخه‌ای از تاریخ محلی نیست.'; - @override - String get localHistoryNoMatches => - 'هیچ نسخه‌ای از تاریخ محلی با این جستجو مطابقت ندارد.'; - - @override - String get localHistorySearchHint => 'جستجو در تاریخچه محلی'; - @override String get localHistorySelectedRevision => 'نسخه انتخاب شده'; @@ -5434,4 +5525,393 @@ class AppLocalizationsFa extends AppLocalizations { @override String get wsPropertiesUnavailable => 'عنصر بررسی‌شده در دسترس نیست. برای ادامه عنصری را بررسی کنید.'; + + @override + String get searchDocumentsCategory => 'سندها'; + + @override + String get searchTopicsCategory => 'موضوع‌ها'; + + @override + String get searchCurrentDocument => 'سند کنونی'; + + @override + String get searchCurrentWorkspace => 'فضای کاری کنونی'; + + @override + String get browseNotes => 'مرور یادداشت‌ها'; + + @override + String get searchActionsCommand => 'جستجوی کنش‌ها'; + + @override + String get searchDocumentsCommand => 'جستجوی سندها'; + + @override + String get searchTopicsCommand => 'جستجوی موضوع‌ها'; + + @override + String get searchRecentCommand => 'جستجوی موارد اخیر'; + + @override + String get searchResourceUnavailable => 'این منبع در دسترس نیست.'; + + @override + String get notesSync => 'همگام‌سازی یادداشت‌ها'; + + @override + String get searchInWorkspace => 'جستجو در فضای کاری'; + + @override + String get searchCategory => 'دستهٔ جستجو'; + + @override + String get clearHistorySearchScope => 'جستجو در تاریخچهٔ سندها'; + + @override + String get searchInDocument => 'جستجو در سند'; + + @override + String get searchDocumentHint => 'جستجو در این سند…'; + + @override + String get searchWorkspaceHint => 'جستجو در این فضای کاری…'; + + @override + String get searchOutlineHint => 'جستجوی عنوان‌ها…'; + + @override + String get searchHistoryHint => 'جستجو در تاریخچهٔ محلی…'; + + @override + String get searchEnterText => 'متن جستجو را وارد کنید'; + + @override + String get searchAccountUnavailable => 'حساب در دسترس نیست'; + + @override + String get searchDocumentsHint => 'جستجوی سندها…'; + + @override + String get searchTopicsHint => 'جستجوی موضوع‌ها…'; + + @override + String get searchRecentHint => 'جستجوی موارد اخیر…'; + + @override + String get searchActionsHint => 'جستجوی کنش‌ها…'; + + @override + String get searchRequiresNotesWorkspace => + 'به فضای کاری یادداشت‌ها نیاز دارد'; + + @override + String get searchSidebarLabel => 'جستجو:'; + + @override + String get searchDocumentTextCategory => 'متن سند'; + + @override + String get searchWorkspaceTextCategory => 'متن فضای کاری'; + + @override + String get searchHeadingsCategory => 'عنوان‌ها'; + + @override + String get shortcutUnassigned => 'تخصیص داده نشده'; + + @override + String get shortcutPressKeys => 'یک ترکیب کلید را فشار دهید…'; + + @override + String get shortcutRecord => 'ثبت میان‌بر'; + + @override + String get shortcutRestoreAll => 'بازیابی همهٔ پیش‌فرض‌ها'; + + @override + String get shortcutRestoreAllQuestion => + 'میان‌برهای پیش‌فرض همهٔ کنش‌ها بازیابی شوند؟ تخصیص‌های سفارشی حذف می‌شوند.'; + + @override + String get shortcutSaveFailed => + 'میان‌برها فعال‌اند اما ذخیره نشدند. برای حفظ آن‌ها پس از راه‌اندازی مجدد، دوباره ذخیره کنید.'; + + @override + String get shortcutRetry => 'تلاش دوباره برای ذخیره'; + + @override + String get shortcutChanged => + 'تخصیص‌ها تغییر کرده‌اند. میان‌برهای فعلی را بررسی و دوباره تلاش کنید.'; + + @override + String get shortcutInvalid => 'این ترکیب را نمی‌توان در این زمینه تخصیص داد.'; + + @override + String get shortcutSystemWarning => + 'ممکن است میزکار از این ترکیب استفاده کند. BusyMark نمی‌تواند میان‌برهای سیستم را بازنویسی کند.'; + + @override + String get shortcutConflicts => 'این میان‌بر با کنش‌های زیر تداخل دارد:'; + + @override + String get shortcutRemove => 'حذف تخصیص'; + + @override + String get shortcutRestoreDefault => 'بازیابی پیش‌فرض'; + + @override + String get shortcutAssign => 'تخصیص'; + + @override + String get shortcutReassign => 'تخصیص مجدد'; + + @override + String get shortcutContextApplication => 'برنامه'; + + @override + String get shortcutContextDocument => 'نماهای سند'; + + @override + String get shortcutContextEditor => 'ویرایشگر و منبع'; + + @override + String get shortcutContextWritersideSource => 'منبع Writerside'; + + @override + String get shortcutContextWorkspace => 'پیمایش فضای کاری'; + + @override + String get shortcutContextTree => 'درخت‌های نوار کناری'; + + @override + String get searchHeadingsCommand => 'جستجوی عنوان‌ها'; + + @override + String get shortcutFixed => 'توسط ویرایشگر مدیریت می‌شود؛ قابل تنظیم نیست.'; + + @override + String get shortcutDefaultConflict => + 'میان‌بر پیش‌فرض با تخصیص سفارشی تداخل دارد.'; + + @override + String get shortcutCurrentBinding => 'میان‌بر فعلی'; + + @override + String get shortcutProposedBinding => 'میان‌بر پیشنهادی'; + + @override + String get notesActions => 'کنش‌های یادداشت‌ها'; + + @override + String get noteActions => 'کنش‌های یادداشت'; + + @override + String get noteProperties => 'ویژگی‌های یادداشت'; + + @override + String get notesExportAll => 'صادر کردن یادداشت‌ها · همه یادداشت‌ها…'; + + @override + String get notesExportCategory => 'صادر کردن یادداشت‌ها · دسته‌بندی…'; + + @override + String get notesOfflineAll => 'در دسترس کردن آفلاین · همه یادداشت‌ها'; + + @override + String get notesOfflineCategory => 'در دسترس کردن آفلاین · دسته‌بندی'; + + @override + String get notesOfflineRemoveAll => 'حذف نگهداری آفلاین · همه یادداشت‌ها'; + + @override + String get notesOfflineRemoveCategory => 'حذف نگهداری آفلاین · دسته‌بندی'; + + @override + String get notesOfflinePause => 'مکث بارگیری‌ها'; + + @override + String get notesOfflineResume => 'ادامهٔ بارگیری‌ها'; + + @override + String get notesOfflineKeep => 'در دسترس کردن آفلاین'; + + @override + String get notesOfflineRetentionHelp => + 'حذف این نیازمندی محتوای نهان‌شده را نگه می‌دارد. نیازمندی دسته یا حساب ممکن است همچنان یادداشت را نگه دارد.'; + + @override + String get notesOfflineBroader => + 'این یادداشت به‌دلیل نیازمندی دسته یا حساب نیز نگهداری می‌شود.'; + + @override + String get notesOfflineExternal => + 'رسانهٔ خارجی به اتصال نیاز دارد. BusyMark فقط متن یادداشت و پیوست‌های مدیریت‌شده را نگه می‌دارد.'; + + @override + String get notesEmptyAll => 'هنوز یادداشتی نیست. یک یادداشت بسازید.'; + + @override + String get notesEmptyFavorites => 'یادداشت دلخواهی نیست.'; + + @override + String get notesEmptyCategory => 'در این دسته یادداشتی نیست.'; + + @override + String get notesEmptyUncategorized => 'یادداشت بدون دسته‌ای نیست.'; + + @override + String get notesEmptyRecovery => 'یادداشت نگهداری‌شده‌ای برای بازیابی نیست.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · ناموفق. تلاش دوباره'; + + @override + String get notesOperationFailed => + 'عملیات کامل نشد. پیش‌نویس نگهداری شده است. ذخیره و تعارض‌ها را بررسی کنید و دوباره تلاش کنید.'; + + @override + String get notesInspectionFailed => 'پیوست‌ها · ناموفق. تلاش دوباره'; + + @override + String get notesStartingConnection => 'در حال آغاز اتصال Nextcloud…'; + + @override + String get notesVerifyingConnection => 'در حال بررسی دسترسی به یادداشت‌ها…'; + + @override + String get notesCancellingConnection => 'در حال لغو ورود و پاک‌سازی…'; + + @override + String get notesRemovingConnection => 'در حال حذف حساب محلی…'; + + @override + String get notesRemovedWarning => + 'حساب محلی Nextcloud حذف شد. لغو گذرواژهٔ برنامه تأیید نشد. گذرواژهٔ BusyMark را در تنظیمات امنیتی Nextcloud حذف کنید.'; + + @override + String get notesBuffersNotSaved => + 'همهٔ یادداشت‌های باز به‌صورت محلی ذخیره نشدند. ذخیره، تعارض‌ها یا نرمال‌سازی را در فضای کار بررسی کنید. تغییرات محلی ممکن است هنوز به همگام‌سازی با Nextcloud نیاز داشته باشند.'; + + @override + String get notesSettingsLeave => + 'پیش از خروج تغییرات تنظیمات Nextcloud ذخیره شوند؟'; + + @override + String get notesSettingsLeaveHelp => + 'تنظیمات Notes سرور · تغییرات ذخیره‌نشده'; + + @override + String get notesSettingsReload => 'بارگیری دوبارهٔ تنظیمات کارساز…'; + + @override + String get notesSettingsReloadHelp => + 'بارگیری دوباره پیش‌نویس را با تنظیمات فعلی Nextcloud جایگزین می‌کند. پیش‌نویس دور ریخته و دوباره بارگیری شود؟'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud تنظیمات را پذیرفت ولی تازه‌سازی یادداشت‌ها ناموفق بود. مقادیر نمایش‌داده‌شده تأییدشدهٔ کارسازند. از یادداشت‌ها دوباره تازه‌سازی کنید.'; + + @override + String get notesUnavailable => + 'بدون اتصال · متن به صورت محلی در دسترس است · ذخیرهٔ محلی · در انتظار همگام‌سازی'; + + @override + String get notesStorageFull => + 'فضای Nextcloud پر است. در کارساز جا باز کنید و دوباره تلاش کنید.'; + + @override + String get notesRejected => + 'Nextcloud این تغییر را نپذیرفت. یادداشت را بررسی کنید و پیش از تلاش دوباره نسخه‌ای محلی ذخیره کنید.'; + + @override + String get notesThrottled => + 'Nextcloud درخواست انتظار کرد. همگام‌سازی پس از پایان مهلت دوباره ادامه می‌یابد.'; + + @override + String get notesRecoveryRequired => 'تداخل‌ها · مقایسه'; + + @override + String get notesScanning => 'در حال بررسی پرونده‌ها…'; + + @override + String get notesCancelling => 'در حال لغو… منتظر پایان عملیات جاری.'; + + @override + String get notesCancelled => 'لغو شد'; + + @override + String get notesPartial => 'تا حدی کامل شد'; + + @override + String get notesTransferFailed => 'ناموفق. تلاش دوباره'; + + @override + String get notesImportPartialHelp => + 'موارد قبلی ممکن است از پیش محلی ذخیره شده باشند. همین درون‌ریزی را ادامه دهید تا تکراری ایجاد نشود.'; + + @override + String get notesNotStarted => 'آغاز نشده'; + + @override + String get notesCreated => 'ایجاد شد'; + + @override + String get notesRemoteDeleteUnavailable => + 'نشانی سرور · پشتیبانی نمی‌شود · حذف · پیوست‌ها'; + + @override + String get notesCurrentText => 'متن محلی نگهداری‌شدهٔ کنونی'; + + @override + String get notesRecovering => 'در حال بازیابی به‌عنوان یادداشت تازه…'; + + @override + String get notesReadOnlyHelp => + 'فقط خواندنی · علاقه‌مندی‌ها · ذخیرهٔ نسخهٔ محلی…'; + + @override + String get notesSort => 'مرتب‌سازی یادداشت‌ها'; + + @override + String get notesPathRelative => + 'مسیر پوشهٔ یادداشت‌ها نسبت به فضای ذخیره‌سازی Nextcloud شماست.'; + + @override + String get notesSettingsStale => + 'تنظیمات در Nextcloud تغییر کرده‌اند. پیش از جایگزینی پیش‌نویس، مقادیر کارساز را دوباره بارگیری و بررسی کنید.'; + + @override + String get notesDownloadsPaused => 'بارگیری‌ها مکث شده‌اند'; + + @override + String get notesExportNotPublished => 'هیچ برون‌بری منتشر نشد.'; + + @override + String get notesKeyringUnavailable => + 'قفل دسته‌کلید میزکار را باز کنید و دوباره تلاش کنید.'; + + @override + String get notesConnectFailed => + 'اتصال به یادداشت‌های Nextcloud ناموفق بود. نشانی کارساز را بررسی و دوباره تلاش کنید.'; + + @override + String get notesAccountMismatch => + 'به همان حساب Nextcloud وارد شوید. برای اتصال به حساب دیگر ابتدا این حساب را حذف کنید.'; + + @override + String get notesLocalRemovalFailed => + 'پاک‌سازی حساب محلی ناموفق بود. حساب برای تلاش دوبارهٔ حذف نگه داشته شده است.'; + + @override + String get notesTransferring => 'در حال انتقال یادداشت‌ها…'; + + @override + String get notesTechnicalDetails => 'جزئیات فنی'; + + @override + String get notesPauseRequirement => 'مکث نیازمندی آفلاین'; + + @override + String get notesResumeRequirement => 'ادامهٔ نیازمندی آفلاین'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 65489800..57850be9 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -10,6 +10,133 @@ import 'app_localizations.dart'; class AppLocalizationsFr extends AppLocalizations { AppLocalizationsFr([String locale = 'fr']) : super(locale); + @override + String get notesNewFromRetainedText => + 'Nouvelle note à partir du texte conservé'; + + @override + String get notesAccountThrottled => + 'Nextcloud demande une pause. Attendez avant de relancer la connexion.'; + + @override + String get notesSettingsRefreshing => + 'Actualisation de Notes après confirmation des paramètres'; + + @override + String get notesExportOperationFailed => + 'L’export a échoué avant publication. Vérifiez la source et la destination, puis réessayez.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'La note récupérée est enregistrée, mais son ouverture a échoué. Réglez les modifications en attente dans l’éditeur ou les propriétés, puis ouvrez cette note.'; + + @override + String get notesSettingsPendingNotes => + 'Des notes en attente, conflits ou créations incertaines bloquent ce changement. Synchronisez ou examinez-les dans Notes, puis réessayez ce brouillon conservé.'; + + @override + String get notesSettingsPendingAttachments => + 'Des opérations de pièces jointes bloquent ce changement. Examinez-les dans Notes, puis réessayez ce brouillon de paramètres conservé.'; + + @override + String get notesAccountServerAddress => + 'Utilisez l’adresse HTTPS canonique de Nextcloud et relancez la connexion. L’adresse ou la redirection n’a pas pu être vérifiée.'; + + @override + String get notesAccountUnsupported => + 'Notes est indisponible ou son API incompatible. Activez Nextcloud Notes avec l’API majeure 1, mineure 4 ou ultérieure.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud a refusé les identifiants. Autorisez à nouveau ce compte ou reconnectez-le.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud a interdit la vérification des fonctionnalités Notes. Demandez son autorisation à l’administrateur, puis réessayez.'; + + @override + String get notesAccountMalformed => + 'La réponse Nextcloud n’a pas pu être vérifiée. Vérifiez l’installation du serveur, puis relancez la connexion.'; + + @override + String get notesAccountTimedOut => + 'La connexion a expiré. Relancez-la et terminez l’autorisation dans le navigateur.'; + + @override + String get notesAccountBrowserUnavailable => + 'Impossible d’ouvrir le navigateur système. Vérifiez le navigateur par défaut, puis relancez la connexion.'; + + @override + String get notesAccountNetwork => + 'Nextcloud est injoignable. Vérifiez la connexion et le serveur, puis réessayez.'; + + @override + String get notesAccountRejected => + 'Nextcloud a refusé la demande de fonctionnalités. Vérifiez l’accès avec l’administrateur, puis réessayez.'; + + @override + String get notesAccountFailed => + 'La configuration du compte a échoué. Réessayez de vous connecter.'; + + @override + String get notesTransferSourceChanged => + 'La source a changé depuis son examen. Examinez le fichier actuel ou créez un nouvel instantané avant de réessayer.'; + + @override + String get notesTransferUnreadable => + 'Impossible de lire la source. Vérifiez l’accès au fichier ou au dossier, puis examinez-la à nouveau.'; + + @override + String get notesTransferInvalidSnapshot => + 'Instantané invalide ou incompatible. Choisissez un instantané BusyMark Notes valide ou une source Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Les médias dépassent la limite d’importation. Réduisez ceux de l’élément concerné avant de le réexaminer.'; + + @override + String get notesTransferUnsafeSource => + 'La source contient un chemin non sûr. Choisissez des fichiers dans le dossier source, puis réexaminez-les.'; + + @override + String get notesTransferDestination => + 'Impossible d’écrire l’export. Vérifiez l’accès et l’espace libre à destination, puis réessayez.'; + + @override + String get notesTransferOperationFailed => + 'Le transfert a échoué. Examinez les éléments concernés et réessayez. Les éléments déjà importés restent enregistrés.'; + + @override + String get notesExportPublishedWithOmissions => + 'Export disponible avec des omissions'; + + @override + String get notesBatchUnchanged => + 'Les valeurs demandées étaient déjà définies.'; + + @override + String get notesBatchReadOnly => + 'Cette note est en lecture seule. Ses propriétés protégées ne peuvent pas changer ; le favori reste modifiable.'; + + @override + String get notesBatchReviewChanged => + 'Cette note a changé depuis son examen. Examinez son état actuel avant de réappliquer l’action.'; + + @override + String get notesBatchMissing => + 'Cette note n’est plus disponible. Actualisez la liste et vérifiez les cibles.'; + + @override + String get notesBatchStorage => + 'Échec de l’enregistrement local. Vérifiez le stockage et réessayez cet élément ; les éléments réussis restent enregistrés.'; + + @override + String get notesRemoteReviewChanged => + 'La note du serveur a changé. Votre brouillon de fusion est conservé. Examinez la comparaison actualisée et choisissez à nouveau la résolution.'; + + @override + String get notesReviewRemote => 'J’ai examiné la version actuelle du serveur'; + @override String get wsModuleCapsNotPublished => 'La casse du module n’est pas publiée.'; @@ -591,15 +718,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get keyboardShortcuts => 'Raccourcis clavier'; - @override - String get commandPalette => 'Palette de commandes'; - - @override - String get commandPaletteHint => 'Saisissez une commande'; - - @override - String get commandPaletteEmpty => 'Aucune commande correspondante'; - @override String get commandUnavailableInContext => 'Cette commande n’est pas disponible dans le contexte actuel.'; @@ -819,7 +937,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Afficher cette liste de raccourcis clavier'; + 'Personnaliser les raccourcis clavier de l’application'; @override String get shortcutSyntaxReferenceDescription => @@ -1077,7 +1195,7 @@ class AppLocalizationsFr extends AppLocalizations { String get validateOnEdit => 'Valider pendant la modification'; @override - String get clearRecentWorkspaces => 'Effacer les espaces de travail récents'; + String get clearRecentWorkspaces => 'Effacer l’historique récent'; @override String get editingButtonsPosition => 'Position des boutons d’édition'; @@ -1866,15 +1984,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get sourceSearchNextMatch => 'Occurrence suivante'; - @override - String get sourceSearchCaseSensitive => 'Respecter la casse'; - - @override - String get sourceSearchWholeWord => 'Mot entier'; - - @override - String get sourceSearchRegex => 'Expression régulière'; - @override String get sourceSearchReplacement => 'Remplacer par'; @@ -2005,13 +2114,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get saveAs => 'Enregistrer sous'; - @override - String get sourceSearchInvalidRegex => 'Expression régulière non valide'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Les correspondances de longueur nulle ne sont pas prises en charge (par exemple ^, \$ ou les assertions qui ne correspondent qu’à une position).'; - @override String get workspaceSearchIncomplete => 'Seule une partie des résultats est affichée. Affichez-en davantage ou affinez la recherche.'; @@ -2120,6 +2222,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get documentKindResourceFile => 'Fichier de ressources'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Impossible de mettre à jour les éléments récents : $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Échec de l’ouverture : $error'; @@ -4391,10 +4498,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get clipboardHistory => 'Historique de la presse-papiers'; - @override - String get clipboardHistorySearchHint => - 'Rechercher dans l’historique du presse-papiers'; - @override String get clipboardHistoryDescription => 'Ouvrir l\'historique de la presse-papiers local à la session.'; @@ -4418,10 +4521,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get clipboardNoItems => 'L\'historique de la presse-papiers est vide.'; - @override - String get clipboardNoMatches => - 'Aucun élément de presse-papiers ne correspond à cette recherche.'; - @override String get clipboardDisabled => 'La collection d\'Historique de la presse-papiers est désactivée dans les paramètres.'; @@ -4478,13 +4577,6 @@ class AppLocalizationsFr extends AppLocalizations { String get localHistoryNoRevisions => 'Ce document n\'a pas de révisions dans l\'Historique Local.'; - @override - String get localHistoryNoMatches => - 'Aucune révision n\'est trouvée dans cette recherche.'; - - @override - String get localHistorySearchHint => 'Rechercher dans l’historique local'; - @override String get localHistorySelectedRevision => 'Révision sélectionnée'; @@ -5451,4 +5543,401 @@ class AppLocalizationsFr extends AppLocalizations { @override String get wsPropertiesUnavailable => 'L’élément inspecté est indisponible. Inspectez un élément pour continuer.'; + + @override + String get searchDocumentsCategory => 'Documents'; + + @override + String get searchTopicsCategory => 'Rubriques'; + + @override + String get searchCurrentDocument => 'Document actuel'; + + @override + String get searchCurrentWorkspace => 'Espace de travail actuel'; + + @override + String get browseNotes => 'Parcourir les notes'; + + @override + String get searchActionsCommand => 'Rechercher des actions'; + + @override + String get searchDocumentsCommand => 'Rechercher des documents'; + + @override + String get searchTopicsCommand => 'Rechercher des rubriques'; + + @override + String get searchRecentCommand => 'Rechercher les éléments récents'; + + @override + String get searchResourceUnavailable => 'Cette ressource est indisponible.'; + + @override + String get notesSync => 'Synchroniser les notes'; + + @override + String get searchInWorkspace => 'Rechercher dans l’espace de travail'; + + @override + String get searchCategory => 'Catégorie de recherche'; + + @override + String get clearHistorySearchScope => + 'Rechercher dans l’historique des documents'; + + @override + String get searchInDocument => 'Rechercher dans le document'; + + @override + String get searchDocumentHint => 'Rechercher dans ce document…'; + + @override + String get searchWorkspaceHint => 'Rechercher dans cet espace de travail…'; + + @override + String get searchOutlineHint => 'Rechercher des titres…'; + + @override + String get searchHistoryHint => 'Rechercher dans l’historique local…'; + + @override + String get searchEnterText => 'Saisissez du texte à rechercher'; + + @override + String get searchAccountUnavailable => 'Compte indisponible'; + + @override + String get searchDocumentsHint => 'Rechercher des documents…'; + + @override + String get searchTopicsHint => 'Rechercher des rubriques…'; + + @override + String get searchRecentHint => 'Rechercher les éléments récents…'; + + @override + String get searchActionsHint => 'Rechercher des actions…'; + + @override + String get searchRequiresNotesWorkspace => + 'Nécessite un espace de travail Notes'; + + @override + String get searchSidebarLabel => 'Rechercher:'; + + @override + String get searchDocumentTextCategory => 'Texte du document'; + + @override + String get searchWorkspaceTextCategory => 'Texte de l’espace de travail'; + + @override + String get searchHeadingsCategory => 'Titres'; + + @override + String get shortcutUnassigned => 'Non attribué'; + + @override + String get shortcutPressKeys => 'Appuyez sur une combinaison de touches…'; + + @override + String get shortcutRecord => 'Enregistrer un raccourci'; + + @override + String get shortcutRestoreAll => 'Rétablir tous les raccourcis'; + + @override + String get shortcutRestoreAllQuestion => + 'Rétablir les raccourcis par défaut de toutes les actions ? Les affectations personnalisées seront supprimées.'; + + @override + String get shortcutSaveFailed => + 'Les raccourcis sont actifs mais n’ont pas été sauvegardés. Réessayez pour les conserver après redémarrage.'; + + @override + String get shortcutRetry => 'Réessayer la sauvegarde'; + + @override + String get shortcutChanged => + 'Les affectations ont changé. Vérifiez les raccourcis actuels et réessayez.'; + + @override + String get shortcutInvalid => + 'Cette combinaison ne peut pas être attribuée dans ce contexte.'; + + @override + String get shortcutSystemWarning => + 'Cette combinaison peut être utilisée par votre bureau. BusyMark ne peut pas remplacer les raccourcis système.'; + + @override + String get shortcutConflicts => + 'Ce raccourci entre en conflit avec ces actions :'; + + @override + String get shortcutRemove => 'Supprimer l’affectation'; + + @override + String get shortcutRestoreDefault => 'Rétablir le raccourci par défaut'; + + @override + String get shortcutAssign => 'Attribuer'; + + @override + String get shortcutReassign => 'Réattribuer'; + + @override + String get shortcutContextApplication => 'Application entière'; + + @override + String get shortcutContextDocument => 'Vues du document'; + + @override + String get shortcutContextEditor => 'Éditeur et source'; + + @override + String get shortcutContextWritersideSource => 'Source Writerside'; + + @override + String get shortcutContextWorkspace => 'Navigation de l’espace de travail'; + + @override + String get shortcutContextTree => 'Arborescences latérales'; + + @override + String get searchHeadingsCommand => 'Rechercher des titres'; + + @override + String get shortcutFixed => 'Géré par l’éditeur ; non configurable.'; + + @override + String get shortcutDefaultConflict => + 'Le raccourci par défaut entre en conflit avec une affectation personnalisée.'; + + @override + String get shortcutCurrentBinding => 'Raccourci actuel'; + + @override + String get shortcutProposedBinding => 'Raccourci proposé'; + + @override + String get notesActions => 'Actions des notes'; + + @override + String get noteActions => 'Actions de la note'; + + @override + String get noteProperties => 'Propriétés de la note'; + + @override + String get notesExportAll => 'Exporter des notes · Toutes les notes…'; + + @override + String get notesExportCategory => 'Exporter des notes · Catégorie…'; + + @override + String get notesOfflineAll => + 'Rendre disponible hors ligne · Toutes les notes'; + + @override + String get notesOfflineCategory => 'Rendre disponible hors ligne · Catégorie'; + + @override + String get notesOfflineRemoveAll => + 'Retirer la conservation hors ligne · Toutes les notes'; + + @override + String get notesOfflineRemoveCategory => + 'Retirer la conservation hors ligne · Catégorie'; + + @override + String get notesOfflinePause => 'Suspendre les téléchargements'; + + @override + String get notesOfflineResume => 'Reprendre les téléchargements'; + + @override + String get notesOfflineKeep => 'Rendre disponible hors ligne'; + + @override + String get notesOfflineRetentionHelp => + 'Retirer cette exigence conserve le contenu en cache. Une exigence de catégorie ou de compte peut encore conserver cette note.'; + + @override + String get notesOfflineBroader => + 'Cette note est aussi conservée par une exigence de catégorie ou de compte.'; + + @override + String get notesOfflineExternal => + 'Les médias externes nécessitent une connexion. BusyMark conserve uniquement le texte et les pièces jointes gérées.'; + + @override + String get notesEmptyAll => 'Aucune note. Créez-en une pour commencer.'; + + @override + String get notesEmptyFavorites => 'Aucune note favorite.'; + + @override + String get notesEmptyCategory => 'Aucune note dans cette catégorie.'; + + @override + String get notesEmptyUncategorized => 'Aucune note sans catégorie.'; + + @override + String get notesEmptyRecovery => 'Aucune note conservée à récupérer.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Échec. Réessayer'; + + @override + String get notesOperationFailed => + 'L’opération a échoué. Votre brouillon est conservé. Vérifiez l’enregistrement et les conflits avant de réessayer.'; + + @override + String get notesInspectionFailed => 'Pièces jointes · Échec. Réessayer'; + + @override + String get notesStartingConnection => 'Démarrage de la connexion Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Vérification de l’accès aux notes…'; + + @override + String get notesCancellingConnection => + 'Annulation de la connexion et nettoyage…'; + + @override + String get notesRemovingConnection => 'Suppression du compte local…'; + + @override + String get notesRemovedWarning => + 'Le compte Nextcloud local a été supprimé. La révocation du mot de passe d’application n’a pas été confirmée. Supprimez le mot de passe de BusyMark dans les paramètres de sécurité Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Toutes les notes ouvertes n’ont pas pu être enregistrées localement. Vérifiez l’enregistrement, les conflits ou la normalisation dans l’espace de travail. Des changements locaux peuvent encore nécessiter une synchronisation Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Enregistrer les modifications des paramètres Nextcloud avant de quitter ?'; + + @override + String get notesSettingsLeaveHelp => + 'Paramètres Notes du serveur · Modifications non enregistrées'; + + @override + String get notesSettingsReload => 'Recharger les paramètres du serveur…'; + + @override + String get notesSettingsReloadHelp => + 'Le rechargement remplace votre brouillon par les paramètres actuels de Nextcloud. Abandonner le brouillon et recharger ?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud a accepté les paramètres, mais l’actualisation des notes a échoué. Les valeurs affichées sont confirmées par le serveur. Réessayez l’actualisation depuis Notes.'; + + @override + String get notesUnavailable => + 'Hors ligne · Texte disponible localement · Enregistré localement · synchronisation en attente'; + + @override + String get notesStorageFull => + 'Le stockage Nextcloud est plein. Libérez de l’espace sur le serveur et réessayez.'; + + @override + String get notesRejected => + 'Nextcloud a rejeté cette modification. Vérifiez la note et enregistrez une copie locale avant de réessayer.'; + + @override + String get notesThrottled => + 'Nextcloud a demandé d’attendre. La synchronisation reprendra à l’expiration du délai.'; + + @override + String get notesRecoveryRequired => 'Conflits · Comparer'; + + @override + String get notesScanning => 'Analyse des fichiers…'; + + @override + String get notesCancelling => + 'Annulation… En attente de la fin de l’opération en cours.'; + + @override + String get notesCancelled => 'Annulé'; + + @override + String get notesPartial => 'Partiellement terminé'; + + @override + String get notesTransferFailed => 'Échec. Réessayer'; + + @override + String get notesImportPartialHelp => + 'Des éléments précédents peuvent déjà être enregistrés localement. Reprenez cette importation pour continuer sans les dupliquer.'; + + @override + String get notesNotStarted => 'Non démarré'; + + @override + String get notesCreated => 'Créé'; + + @override + String get notesRemoteDeleteUnavailable => + 'URL du serveur · Non prise en charge · Supprimer · Pièces jointes'; + + @override + String get notesCurrentText => 'Texte actuellement conservé'; + + @override + String get notesRecovering => 'Récupération dans une nouvelle note…'; + + @override + String get notesReadOnlyHelp => + 'Lecture seule · Favoris · Enregistrer une copie locale…'; + + @override + String get notesSort => 'Trier les notes'; + + @override + String get notesPathRelative => + 'Le chemin du dossier des notes est relatif à votre stockage Nextcloud.'; + + @override + String get notesSettingsStale => + 'Les paramètres ont changé sur Nextcloud. Rechargez et vérifiez les valeurs du serveur avant de remplacer le brouillon.'; + + @override + String get notesDownloadsPaused => 'Téléchargements suspendus'; + + @override + String get notesExportNotPublished => 'Aucune exportation n’a été publiée.'; + + @override + String get notesKeyringUnavailable => + 'Déverrouillez le trousseau du bureau et réessayez.'; + + @override + String get notesConnectFailed => + 'Connexion à Nextcloud Notes impossible. Vérifiez l’adresse du serveur et réessayez.'; + + @override + String get notesAccountMismatch => + 'Connectez-vous au même compte Nextcloud. Supprimez d’abord ce compte pour en connecter un autre.'; + + @override + String get notesLocalRemovalFailed => + 'Le nettoyage local a échoué. Le compte est conservé pour permettre une nouvelle tentative de suppression.'; + + @override + String get notesTransferring => 'Transfert des notes…'; + + @override + String get notesTechnicalDetails => 'Détails techniques'; + + @override + String get notesPauseRequirement => 'Suspendre l’exigence hors ligne'; + + @override + String get notesResumeRequirement => 'Reprendre l’exigence hors ligne'; } diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index a27cb5b2..c0891afd 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -10,6 +10,132 @@ import 'app_localizations.dart'; class AppLocalizationsHi extends AppLocalizations { AppLocalizationsHi([String locale = 'hi']) : super(locale); + @override + String get notesNewFromRetainedText => 'सहेजे गए पाठ से नया नोट'; + + @override + String get notesAccountThrottled => + 'Nextcloud ने विराम माँगा है। फिर साइन इन शुरू करने से पहले प्रतीक्षा करें।'; + + @override + String get notesSettingsRefreshing => + 'पुष्टि किए गए सेटिंग बदलाव के बाद Notes ताज़ा हो रहा है'; + + @override + String get notesExportOperationFailed => + 'प्रकाशन से पहले निर्यात विफल हुआ। स्रोत और गंतव्य जाँचकर फिर निर्यात करें।'; + + @override + String get notesRecoveryCreatedNotOpened => + 'पुनर्प्राप्त नोट सहेजा गया, पर खुल नहीं सका। संपादक या गुणों के लंबित बदलाव सुलझाएँ, फिर इसी मौजूदा नोट को खोलें।'; + + @override + String get notesSettingsPendingNotes => + 'लंबित नोट, विरोध या अनिश्चित निर्माण इस बदलाव को रोक रहे हैं। Notes में समन्वय या समीक्षा करें, फिर सुरक्षित रखा गया सेटिंग मसौदा दोबारा सहेजें।'; + + @override + String get notesSettingsPendingAttachments => + 'लंबित संलग्नक कार्य इस बदलाव को रोक रहे हैं। Notes में संलग्नकों की समीक्षा करें, फिर सुरक्षित रखा गया सेटिंग मसौदा दोबारा सहेजें।'; + + @override + String get notesAccountServerAddress => + 'Nextcloud का मानक HTTPS पता उपयोग करें और फिर साइन इन करें। पते या रीडायरेक्ट की पुष्टि नहीं हो सकी।'; + + @override + String get notesAccountUnsupported => + 'Notes उपलब्ध नहीं है या API समर्थित नहीं है। मुख्य API संस्करण 1 और गौण संस्करण 4 या नए वाला Nextcloud Notes सक्षम करें।'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud ने पहचान जानकारी अस्वीकार की। खाते को फिर अधिकृत करें या दोबारा कनेक्ट करें।'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud ने Notes क्षमता जाँच की अनुमति नहीं दी। व्यवस्थापक से अनुमति लेकर फिर प्रयास करें।'; + + @override + String get notesAccountMalformed => + 'Nextcloud के उत्तर की पुष्टि नहीं हो सकी। सर्वर स्थापना जाँचें और फिर साइन इन करें।'; + + @override + String get notesAccountTimedOut => + 'साइन इन का समय समाप्त हुआ। फिर शुरू करें और ब्राउज़र में अनुमति पूरी करें।'; + + @override + String get notesAccountBrowserUnavailable => + 'सिस्टम ब्राउज़र नहीं खुल सका। डिफ़ॉल्ट ब्राउज़र जाँचें और फिर साइन इन करें।'; + + @override + String get notesAccountNetwork => + 'Nextcloud से संपर्क नहीं हो सका। कनेक्शन और सर्वर जाँचें, फिर साइन इन करें।'; + + @override + String get notesAccountRejected => + 'Nextcloud ने क्षमता अनुरोध अस्वीकार किया। व्यवस्थापक के साथ पहुँच जाँचकर फिर प्रयास करें।'; + + @override + String get notesAccountFailed => + 'खाता सेटअप पूरा नहीं हुआ। फिर साइन इन करें।'; + + @override + String get notesTransferSourceChanged => + 'समीक्षा के बाद स्रोत बदल गया। फिर प्रयास से पहले मौजूदा फ़ाइल की समीक्षा करें या नया स्नैपशॉट लें।'; + + @override + String get notesTransferUnreadable => + 'स्रोत पढ़ा नहीं जा सका। फ़ाइल या फ़ोल्डर की पहुँच जाँचकर फिर समीक्षा करें।'; + + @override + String get notesTransferInvalidSnapshot => + 'स्नैपशॉट अमान्य या असमर्थित है। मान्य BusyMark Notes स्नैपशॉट या Markdown स्रोत चुनें।'; + + @override + String get notesTransferMediaLimit => + 'मीडिया आयात आकार सीमा से बड़ा है। प्रभावित आइटम का मीडिया छोटा करके फिर समीक्षा करें।'; + + @override + String get notesTransferUnsafeSource => + 'स्रोत में असुरक्षित पथ है। स्रोत फ़ोल्डर के अंदर की फ़ाइलें चुनकर फिर समीक्षा करें।'; + + @override + String get notesTransferDestination => + 'निर्यात लिखा नहीं जा सका। गंतव्य की पहुँच और खाली स्थान जाँचकर फिर निर्यात करें।'; + + @override + String get notesTransferOperationFailed => + 'स्थानांतरण पूरा नहीं हुआ। प्रभावित आइटम की समीक्षा करके फिर प्रयास करें। पहले आयात किए आइटम सहेजे हुए हैं।'; + + @override + String get notesExportPublishedWithOmissions => + 'कुछ चीज़ें छूटी हैं, निर्यात उपलब्ध है'; + + @override + String get notesBatchUnchanged => 'माँगे गए मान पहले से तय हैं।'; + + @override + String get notesBatchReadOnly => + 'यह नोट केवल पढ़ने योग्य है। सुरक्षित गुण नहीं बदल सकते; पसंदीदा स्थिति बदलना अनुमत है।'; + + @override + String get notesBatchReviewChanged => + 'समीक्षा के बाद नोट बदल गया। कार्य दोबारा लागू करने से पहले उसकी मौजूदा स्थिति की समीक्षा करें।'; + + @override + String get notesBatchMissing => + 'यह नोट अब उपलब्ध नहीं है। सूची ताज़ा करके लक्ष्यों की समीक्षा करें।'; + + @override + String get notesBatchStorage => + 'स्थानीय सहेजना विफल हुआ। संग्रहण जाँचकर इस आइटम पर फिर प्रयास करें; सफल आइटम सहेजे हुए हैं।'; + + @override + String get notesRemoteReviewChanged => + 'सर्वर का नोट बदल गया। आपका विलय मसौदा सुरक्षित है। नई तुलना की समीक्षा करें और समाधान फिर चुनें।'; + + @override + String get notesReviewRemote => + 'मैंने सर्वर के मौजूदा संस्करण की समीक्षा की है'; + @override String get wsModuleCapsNotPublished => 'मॉड्यूल का अक्षर-शैली नियम प्रकाशित नहीं होता।'; @@ -585,15 +711,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get keyboardShortcuts => 'कीबोर्ड शॉर्टकट'; - @override - String get commandPalette => 'कमांड पैलेट'; - - @override - String get commandPaletteHint => 'कमांड लिखें'; - - @override - String get commandPaletteEmpty => 'कोई मेल खाता कमांड नहीं'; - @override String get commandUnavailableInContext => 'यह आदेश वर्तमान संदर्भ में उपलब्ध नहीं है।'; @@ -812,7 +929,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'यह कीबोर्ड शॉर्टकट संदर्भ दिखाएँ'; + 'अनुप्रयोग के कीबोर्ड शॉर्टकट अनुकूलित करें'; @override String get shortcutSyntaxReferenceDescription => 'सिंटैक्स संदर्भ खोलें'; @@ -1055,7 +1172,7 @@ class AppLocalizationsHi extends AppLocalizations { String get validateOnEdit => 'संपादन के समय सत्यापित करें'; @override - String get clearRecentWorkspaces => 'हालिया कार्यस्थान साफ़ करें'; + String get clearRecentWorkspaces => 'हाल का इतिहास साफ़ करें'; @override String get editingButtonsPosition => 'संपादन बटनों की स्थिति'; @@ -1838,16 +1955,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get sourceSearchNextMatch => 'अगला मिलान'; - @override - String get sourceSearchCaseSensitive => - 'बड़े-छोटे अक्षरों के प्रति संवेदनशील'; - - @override - String get sourceSearchWholeWord => 'पूरा शब्द'; - - @override - String get sourceSearchRegex => 'रेगुलर एक्सप्रेशन'; - @override String get sourceSearchReplacement => 'इससे बदलें'; @@ -1974,13 +2081,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get saveAs => 'इस रूप में सहेजें'; - @override - String get sourceSearchInvalidRegex => 'अमान्य रेगुलर एक्सप्रेशन'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'शून्य लंबाई वाले रेगेक्स मिलान समर्थित नहीं हैं (जैसे ^, \$ या केवल स्थिति से मेल खाने वाले लुकअहेड)।'; - @override String get workspaceSearchIncomplete => 'केवल कुछ परिणाम दिखाए गए हैं। अधिक परिणाम देखें या खोज को सीमित करें।'; @@ -2089,6 +2189,11 @@ class AppLocalizationsHi extends AppLocalizations { @override String get documentKindResourceFile => 'संसाधन फ़ाइल'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'हाल के आइटम अपडेट नहीं किए जा सके: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'खोलना विफल रहा: $error'; @@ -4346,9 +4451,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get clipboardHistory => 'क्लिपबोर्ड इतिहास'; - @override - String get clipboardHistorySearchHint => 'क्लिपबोर्ड इतिहास खोजें'; - @override String get clipboardHistoryDescription => 'सत्र-विशिष्ट क्लिपबोर्ड इतिहास खोलें।'; @@ -4371,10 +4473,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get clipboardNoItems => 'क्लिपबोर्ड इतिहास खाली है।'; - @override - String get clipboardNoMatches => - 'कोई क्लिपबोर्ड आइटम इस खोज से मेल नहीं खाता।'; - @override String get clipboardDisabled => 'क्लिपबोर्ड इतिहास संग्रह सेटिंग्स में अक्षम है।'; @@ -4432,13 +4530,6 @@ class AppLocalizationsHi extends AppLocalizations { String get localHistoryNoRevisions => 'इस दस्तावेज़ में स्थानीय इतिहास में कोई संशोधन नहीं है।'; - @override - String get localHistoryNoMatches => - 'यह खोज स्थानीय इतिहास संशोधनों से मेल नहीं खाती।'; - - @override - String get localHistorySearchHint => 'स्थानीय इतिहास खोजें'; - @override String get localHistorySelectedRevision => 'चयनित संशोधन'; @@ -5388,4 +5479,398 @@ class AppLocalizationsHi extends AppLocalizations { @override String get wsPropertiesUnavailable => 'जाँचा गया तत्व उपलब्ध नहीं है। जारी रखने के लिए कोई तत्व जाँचें।'; + + @override + String get searchDocumentsCategory => 'दस्तावेज़'; + + @override + String get searchTopicsCategory => 'विषय'; + + @override + String get searchCurrentDocument => 'वर्तमान दस्तावेज़'; + + @override + String get searchCurrentWorkspace => 'वर्तमान कार्यक्षेत्र'; + + @override + String get browseNotes => 'नोट ब्राउज़ करें'; + + @override + String get searchActionsCommand => 'क्रियाएँ खोजें'; + + @override + String get searchDocumentsCommand => 'दस्तावेज़ खोजें'; + + @override + String get searchTopicsCommand => 'विषय खोजें'; + + @override + String get searchRecentCommand => 'हाल के आइटम खोजें'; + + @override + String get searchResourceUnavailable => 'यह संसाधन उपलब्ध नहीं है।'; + + @override + String get notesSync => 'नोट सिंक करें'; + + @override + String get searchInWorkspace => 'कार्यस्थान में खोजें'; + + @override + String get searchCategory => 'खोज श्रेणी'; + + @override + String get clearHistorySearchScope => 'दस्तावेज़ों के इतिहास में खोजें'; + + @override + String get searchInDocument => 'दस्तावेज़ में खोजें'; + + @override + String get searchDocumentHint => 'इस दस्तावेज़ में खोजें…'; + + @override + String get searchWorkspaceHint => 'इस कार्यक्षेत्र में खोजें…'; + + @override + String get searchOutlineHint => 'शीर्षक खोजें…'; + + @override + String get searchHistoryHint => 'स्थानीय इतिहास में खोजें…'; + + @override + String get searchEnterText => 'खोजने के लिए पाठ दर्ज करें'; + + @override + String get searchAccountUnavailable => 'खाता उपलब्ध नहीं है'; + + @override + String get searchDocumentsHint => 'दस्तावेज़ खोजें…'; + + @override + String get searchTopicsHint => 'विषय खोजें…'; + + @override + String get searchRecentHint => 'हाल के आइटम खोजें…'; + + @override + String get searchActionsHint => 'क्रियाएँ खोजें…'; + + @override + String get searchRequiresNotesWorkspace => 'नोट्स कार्यक्षेत्र आवश्यक है'; + + @override + String get searchSidebarLabel => 'खोजें:'; + + @override + String get searchDocumentTextCategory => 'दस्तावेज़ पाठ'; + + @override + String get searchWorkspaceTextCategory => 'कार्यस्थान पाठ'; + + @override + String get searchHeadingsCategory => 'शीर्षक'; + + @override + String get shortcutUnassigned => 'असाइन नहीं किया गया'; + + @override + String get shortcutPressKeys => 'कुंजी संयोजन दबाएँ…'; + + @override + String get shortcutRecord => 'शॉर्टकट रिकॉर्ड करें'; + + @override + String get shortcutRestoreAll => 'सभी डिफ़ॉल्ट पुनर्स्थापित करें'; + + @override + String get shortcutRestoreAllQuestion => + 'सभी क्रियाओं के डिफ़ॉल्ट शॉर्टकट पुनर्स्थापित करें? कस्टम असाइनमेंट हटा दिए जाएँगे।'; + + @override + String get shortcutSaveFailed => + 'शॉर्टकट सक्रिय हैं लेकिन सहेजे नहीं जा सके। पुनः प्रारंभ के बाद रखने के लिए दोबारा सहेजें।'; + + @override + String get shortcutRetry => 'सहेजने का पुनः प्रयास करें'; + + @override + String get shortcutChanged => + 'असाइनमेंट बदल गए हैं। वर्तमान शॉर्टकट जाँचें और पुनः प्रयास करें।'; + + @override + String get shortcutInvalid => + 'इस संदर्भ में यह संयोजन असाइन नहीं किया जा सकता।'; + + @override + String get shortcutSystemWarning => + 'आपका डेस्कटॉप इस संयोजन का उपयोग कर सकता है। BusyMark सिस्टम शॉर्टकट नहीं बदल सकता।'; + + @override + String get shortcutConflicts => 'यह शॉर्टकट इन क्रियाओं से टकराता है:'; + + @override + String get shortcutRemove => 'असाइनमेंट हटाएँ'; + + @override + String get shortcutRestoreDefault => 'डिफ़ॉल्ट पुनर्स्थापित करें'; + + @override + String get shortcutAssign => 'असाइन करें'; + + @override + String get shortcutReassign => 'फिर से असाइन करें'; + + @override + String get shortcutContextApplication => 'अनुप्रयोग'; + + @override + String get shortcutContextDocument => 'दस्तावेज़ दृश्य'; + + @override + String get shortcutContextEditor => 'संपादक और स्रोत'; + + @override + String get shortcutContextWritersideSource => 'Writerside स्रोत'; + + @override + String get shortcutContextWorkspace => 'कार्यस्थान नेविगेशन'; + + @override + String get shortcutContextTree => 'साइडबार ट्री'; + + @override + String get searchHeadingsCommand => 'शीर्षक खोजें'; + + @override + String get shortcutFixed => + 'संपादक द्वारा प्रबंधित; कॉन्फ़िगर नहीं किया जा सकता।'; + + @override + String get shortcutDefaultConflict => + 'डिफ़ॉल्ट शॉर्टकट का कस्टम असाइनमेंट से टकराव है।'; + + @override + String get shortcutCurrentBinding => 'वर्तमान शॉर्टकट'; + + @override + String get shortcutProposedBinding => 'प्रस्तावित शॉर्टकट'; + + @override + String get notesActions => 'नोटों की कार्रवाइयाँ'; + + @override + String get noteActions => 'नोट की कार्रवाइयाँ'; + + @override + String get noteProperties => 'नोट के गुण'; + + @override + String get notesExportAll => 'नोट्स निर्यात करें · सभी नोट्स…'; + + @override + String get notesExportCategory => 'नोट्स निर्यात करें · श्रेणी…'; + + @override + String get notesOfflineAll => 'ऑफ़लाइन उपलब्ध करें · सभी नोट्स'; + + @override + String get notesOfflineCategory => 'ऑफ़लाइन उपलब्ध करें · श्रेणी'; + + @override + String get notesOfflineRemoveAll => 'ऑफ़लाइन संरक्षण हटाएँ · सभी नोट्स'; + + @override + String get notesOfflineRemoveCategory => 'ऑफ़लाइन संरक्षण हटाएँ · श्रेणी'; + + @override + String get notesOfflinePause => 'डाउनलोड रोकें'; + + @override + String get notesOfflineResume => 'डाउनलोड जारी रखें'; + + @override + String get notesOfflineKeep => 'ऑफ़लाइन उपलब्ध करें'; + + @override + String get notesOfflineRetentionHelp => + 'यह आवश्यकता हटाने से कैश की मौजूदा सामग्री नहीं हटती। श्रेणी या खाते की आवश्यकता अब भी यह नोट सुरक्षित रख सकती है।'; + + @override + String get notesOfflineBroader => + 'यह नोट श्रेणी या खाते की आवश्यकता के कारण भी सुरक्षित है।'; + + @override + String get notesOfflineExternal => + 'बाहरी मीडिया को कनेक्शन चाहिए। BusyMark केवल नोट का पाठ और प्रबंधित संलग्नक सुरक्षित रखता है।'; + + @override + String get notesEmptyAll => + 'अभी कोई नोट नहीं है। शुरू करने के लिए नोट बनाएँ।'; + + @override + String get notesEmptyFavorites => 'कोई पसंदीदा नोट नहीं है।'; + + @override + String get notesEmptyCategory => 'इस श्रेणी में कोई नोट नहीं है।'; + + @override + String get notesEmptyUncategorized => 'कोई बिना श्रेणी वाला नोट नहीं है।'; + + @override + String get notesEmptyRecovery => + 'पुनर्प्राप्ति के लिए कोई सहेजा हुआ नोट नहीं है।'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · विफल. फिर प्रयास करें'; + + @override + String get notesOperationFailed => + 'कार्रवाई पूरी नहीं हुई। आपका मसौदा सुरक्षित है। सहेजने और विरोधों की जाँच करके फिर प्रयास करें।'; + + @override + String get notesInspectionFailed => 'अटैचमेंट · विफल. फिर प्रयास करें'; + + @override + String get notesStartingConnection => 'Nextcloud कनेक्शन शुरू हो रहा है…'; + + @override + String get notesVerifyingConnection => 'नोटों तक पहुँच जाँची जा रही है…'; + + @override + String get notesCancellingConnection => + 'साइन-इन रद्द करके सफ़ाई की जा रही है…'; + + @override + String get notesRemovingConnection => 'स्थानीय खाता हटाया जा रहा है…'; + + @override + String get notesRemovedWarning => + 'स्थानीय Nextcloud खाता हटा दिया गया। ऐप पासवर्ड रद्द होने की पुष्टि नहीं हुई। Nextcloud की सुरक्षा सेटिंग में BusyMark का ऐप पासवर्ड हटाएँ।'; + + @override + String get notesBuffersNotSaved => + 'सभी खुले नोट स्थानीय रूप से नहीं सहेजे जा सके। कार्यक्षेत्र में सहेजने, विरोधों या सामान्यीकरण की समीक्षा करें। स्थानीय बदलावों को अब भी Nextcloud से समन्वय की ज़रूरत हो सकती है।'; + + @override + String get notesSettingsLeave => + 'जाने से पहले Nextcloud सेटिंग के बदलाव सहेजें?'; + + @override + String get notesSettingsLeaveHelp => + 'सर्वर Notes सेटिंग्स · न सहेजे गए बदलाव'; + + @override + String get notesSettingsReload => 'सर्वर सेटिंग फिर लोड करें…'; + + @override + String get notesSettingsReloadHelp => + 'फिर लोड करने से मसौदे की जगह वर्तमान Nextcloud सेटिंग आएगी। मसौदा छोड़कर फिर लोड करें?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud ने सेटिंग स्वीकार की, पर नोट रीफ़्रेश नहीं हुए। दिखाई गई सेटिंग सर्वर द्वारा स्वीकृत हैं। नोटों से रीफ़्रेश फिर आज़माएँ।'; + + @override + String get notesUnavailable => + 'ऑफ़लाइन · पाठ स्थानीय रूप से उपलब्ध है · स्थानीय रूप से सहेजा · सिंक लंबित'; + + @override + String get notesStorageFull => + 'Nextcloud का संग्रहण भर गया है। सर्वर पर जगह खाली करके फिर प्रयास करें।'; + + @override + String get notesRejected => + 'Nextcloud ने यह बदलाव अस्वीकार कर दिया। फिर प्रयास करने से पहले नोट जाँचें और स्थानीय प्रति सहेजें।'; + + @override + String get notesThrottled => + 'Nextcloud ने प्रतीक्षा को कहा है। फिर प्रयास की समय-सीमा के बाद समन्वय जारी होगा।'; + + @override + String get notesRecoveryRequired => 'टकराव · तुलना करें'; + + @override + String get notesScanning => 'फ़ाइलें जाँची जा रही हैं…'; + + @override + String get notesCancelling => + 'रद्द हो रहा है… वर्तमान कार्रवाई समाप्त होने की प्रतीक्षा है।'; + + @override + String get notesCancelled => 'रद्द किया गया'; + + @override + String get notesPartial => 'आंशिक रूप से पूरा'; + + @override + String get notesTransferFailed => 'विफल. फिर प्रयास करें'; + + @override + String get notesImportPartialHelp => + 'पिछले आइटम पहले ही स्थानीय रूप से सहेजे हो सकते हैं। प्रतियाँ बनाए बिना जारी रखने के लिए यही आयात फिर शुरू करें।'; + + @override + String get notesNotStarted => 'शुरू नहीं हुआ'; + + @override + String get notesCreated => 'बनाया गया'; + + @override + String get notesRemoteDeleteUnavailable => + 'सर्वर का पता · असमर्थित · हटाएँ · अटैचमेंट'; + + @override + String get notesCurrentText => 'वर्तमान सुरक्षित पाठ'; + + @override + String get notesRecovering => 'नए नोट के रूप में पुनर्प्राप्त हो रहा है…'; + + @override + String get notesReadOnlyHelp => + 'केवल पढ़ने योग्य · पसंदीदा · स्थानीय प्रति सहेजें…'; + + @override + String get notesSort => 'नोट क्रमबद्ध करें'; + + @override + String get notesPathRelative => + 'नोट फ़ोल्डर का पथ आपके Nextcloud संग्रहण के सापेक्ष है।'; + + @override + String get notesSettingsStale => + 'Nextcloud पर सेटिंग बदल गई है। मसौदा बदलने से पहले सर्वर के मान फिर लोड करके जाँचें।'; + + @override + String get notesDownloadsPaused => 'डाउनलोड रुके हुए हैं'; + + @override + String get notesExportNotPublished => 'कोई निर्यात प्रकाशित नहीं हुआ।'; + + @override + String get notesKeyringUnavailable => + 'डेस्कटॉप कीरिंग अनलॉक करके फिर प्रयास करें।'; + + @override + String get notesConnectFailed => + 'Nextcloud Notes से कनेक्ट नहीं हो सका। सर्वर का पता जाँचें और फिर प्रयास करें।'; + + @override + String get notesAccountMismatch => + 'उसी Nextcloud खाते में साइन-इन करें। दूसरे खाते से कनेक्ट करने के लिए पहले यह खाता हटाएँ।'; + + @override + String get notesLocalRemovalFailed => + 'स्थानीय खाते की सफ़ाई विफल रही। हटाने का फिर प्रयास करने के लिए खाता रखा गया है।'; + + @override + String get notesTransferring => 'नोट स्थानांतरित हो रहे हैं…'; + + @override + String get notesTechnicalDetails => 'तकनीकी विवरण'; + + @override + String get notesPauseRequirement => 'ऑफ़लाइन आवश्यकता रोकें'; + + @override + String get notesResumeRequirement => 'ऑफ़लाइन आवश्यकता जारी रखें'; } diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index d04626c4..5550ffea 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); + @override + String get notesNewFromRetainedText => 'Catatan baru dari teks tersimpan'; + + @override + String get notesAccountThrottled => + 'Nextcloud meminta jeda. Tunggu sebelum mulai masuk lagi.'; + + @override + String get notesSettingsRefreshing => + 'Menyegarkan Notes setelah pembaruan pengaturan dikonfirmasi'; + + @override + String get notesExportOperationFailed => + 'Ekspor gagal sebelum diterbitkan. Periksa sumber dan tujuan, lalu ekspor lagi.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Catatan yang dipulihkan sudah tersimpan, tetapi gagal dibuka. Selesaikan perubahan editor atau properti yang tertunda, lalu buka catatan yang sudah ada ini.'; + + @override + String get notesSettingsPendingNotes => + 'Catatan tertunda, konflik, atau pembuatan yang belum pasti menghalangi perubahan ini. Sinkronkan atau tinjau di Notes, lalu simpan lagi draf pengaturan yang dipertahankan.'; + + @override + String get notesSettingsPendingAttachments => + 'Operasi lampiran tertunda menghalangi perubahan ini. Tinjau lampiran di Notes, lalu simpan lagi draf pengaturan yang dipertahankan.'; + + @override + String get notesAccountServerAddress => + 'Gunakan alamat HTTPS kanonis Nextcloud, lalu masuk lagi. Alamat atau pengalihan tidak dapat diverifikasi.'; + + @override + String get notesAccountUnsupported => + 'Notes tidak tersedia atau API tidak didukung. Aktifkan Nextcloud Notes dengan API versi utama 1, minor 4 atau lebih baru.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud menolak kredensial. Otorisasi ulang atau hubungkan kembali akun ini.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud melarang pemeriksaan kemampuan Notes. Minta administrator mengizinkannya, lalu coba lagi.'; + + @override + String get notesAccountMalformed => + 'Respons Nextcloud tidak dapat diverifikasi. Periksa pemasangan server, lalu masuk lagi.'; + + @override + String get notesAccountTimedOut => + 'Waktu masuk habis. Mulai lagi dan selesaikan otorisasi di peramban.'; + + @override + String get notesAccountBrowserUnavailable => + 'Peramban sistem tidak dapat dibuka. Periksa peramban bawaan, lalu masuk lagi.'; + + @override + String get notesAccountNetwork => + 'Nextcloud tidak dapat dijangkau. Periksa koneksi dan server, lalu masuk lagi.'; + + @override + String get notesAccountRejected => + 'Nextcloud menolak permintaan kemampuan. Periksa akses bersama administrator, lalu coba lagi.'; + + @override + String get notesAccountFailed => + 'Penyiapan akun tidak selesai. Coba masuk lagi.'; + + @override + String get notesTransferSourceChanged => + 'Sumber berubah setelah ditinjau. Tinjau berkas saat ini atau buat snapshot baru sebelum mencoba lagi.'; + + @override + String get notesTransferUnreadable => + 'Sumber tidak dapat dibaca. Periksa akses berkas atau folder, lalu tinjau lagi.'; + + @override + String get notesTransferInvalidSnapshot => + 'Snapshot tidak valid atau tidak didukung. Pilih snapshot BusyMark Notes yang valid atau sumber Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Media melebihi batas impor. Perkecil media item terkait sebelum meninjaunya lagi.'; + + @override + String get notesTransferUnsafeSource => + 'Sumber berisi jalur tidak aman. Pilih berkas di dalam folder sumber, lalu tinjau lagi.'; + + @override + String get notesTransferDestination => + 'Ekspor tidak dapat ditulis. Periksa akses dan ruang kosong di tujuan, lalu ekspor lagi.'; + + @override + String get notesTransferOperationFailed => + 'Transfer tidak selesai. Tinjau item terkait dan coba lagi. Item yang sudah diimpor tetap tersimpan.'; + + @override + String get notesExportPublishedWithOmissions => + 'Ekspor tersedia dengan beberapa bagian terlewat'; + + @override + String get notesBatchUnchanged => 'Nilai yang diminta sudah ditetapkan.'; + + @override + String get notesBatchReadOnly => + 'Catatan ini hanya-baca. Properti terlindungi tidak dapat diubah; Favorit tetap diizinkan.'; + + @override + String get notesBatchReviewChanged => + 'Catatan berubah setelah ditinjau. Tinjau keadaan saat ini sebelum menerapkan tindakan lagi.'; + + @override + String get notesBatchMissing => + 'Catatan ini tidak lagi tersedia. Segarkan daftar dan tinjau sasaran lagi.'; + + @override + String get notesBatchStorage => + 'Penyimpanan lokal gagal. Periksa penyimpanan dan coba item ini lagi; item yang berhasil tetap tersimpan.'; + + @override + String get notesRemoteReviewChanged => + 'Catatan server berubah. Draf penggabungan Anda dipertahankan. Tinjau perbandingan terbaru dan pilih penyelesaian lagi.'; + + @override + String get notesReviewRemote => 'Saya telah meninjau versi server saat ini'; + @override String get wsModuleCapsNotPublished => 'Kapitalisasi modul tidak diterbitkan.'; @@ -584,15 +709,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get keyboardShortcuts => 'Pintasan keyboard'; - @override - String get commandPalette => 'Palet perintah'; - - @override - String get commandPaletteHint => 'Ketikkan perintah'; - - @override - String get commandPaletteEmpty => 'Tidak ada perintah yang cocok'; - @override String get commandUnavailableInContext => 'Tidak tersedia dalam konteks editor saat ini'; @@ -811,7 +927,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Tampilkan referensi pintasan keyboard ini'; + 'Sesuaikan pintasan keyboard aplikasi'; @override String get shortcutSyntaxReferenceDescription => 'Buka referensi sintaks'; @@ -1062,7 +1178,7 @@ class AppLocalizationsId extends AppLocalizations { String get validateOnEdit => 'Validasi saat mengedit'; @override - String get clearRecentWorkspaces => 'Hapus ruang kerja terbaru'; + String get clearRecentWorkspaces => 'Hapus riwayat terbaru'; @override String get editingButtonsPosition => 'Posisi tombol pengeditan'; @@ -1843,15 +1959,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get sourceSearchNextMatch => 'Kecocokan berikutnya'; - @override - String get sourceSearchCaseSensitive => 'Peka huruf besar-kecil'; - - @override - String get sourceSearchWholeWord => 'Seluruh kata'; - - @override - String get sourceSearchRegex => 'Regex'; - @override String get sourceSearchReplacement => 'Ganti dengan'; @@ -1978,13 +2085,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get saveAs => 'Simpan sebagai'; - @override - String get sourceSearchInvalidRegex => 'Ekspresi reguler tidak valid'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Kecocokan regex dengan panjang nol tidak didukung (misalnya ^, \$, atau lookahead yang hanya cocok dengan posisi).'; - @override String get workspaceSearchIncomplete => 'Hanya sebagian hasil yang ditampilkan. Tampilkan lebih banyak atau persempit pencarian.'; @@ -2093,6 +2193,11 @@ class AppLocalizationsId extends AppLocalizations { @override String get documentKindResourceFile => 'File sumber daya'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Tidak dapat memperbarui item terbaru: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Gagal dibuka: $error'; @@ -4358,9 +4463,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get clipboardHistory => 'Sejarah Tempel'; - @override - String get clipboardHistorySearchHint => 'Cari riwayat papan klip'; - @override String get clipboardHistoryDescription => 'Buka riwayat tempel lokal sesi.'; @@ -4381,10 +4483,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get clipboardNoItems => 'Riwayat tempel kosong.'; - @override - String get clipboardNoMatches => - 'Tidak ada item tempel yang cocok dengan pencarian ini.'; - @override String get clipboardDisabled => 'Koleksi Riwayat Tempel dinonaktifkan di Pengaturan.'; @@ -4442,13 +4540,6 @@ class AppLocalizationsId extends AppLocalizations { String get localHistoryNoRevisions => 'Dokumen ini tidak memiliki revisi Sejarah Lokal.'; - @override - String get localHistoryNoMatches => - 'Tidak ada revisi Sejarah Lokal yang cocok dengan pencarian ini.'; - - @override - String get localHistorySearchHint => 'Cari riwayat lokal'; - @override String get localHistorySelectedRevision => 'Revisi yang dipilih'; @@ -5396,4 +5487,399 @@ class AppLocalizationsId extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Elemen yang diperiksa tidak tersedia. Periksa elemen untuk melanjutkan.'; + + @override + String get searchDocumentsCategory => 'Dokumen'; + + @override + String get searchTopicsCategory => 'Topik'; + + @override + String get searchCurrentDocument => 'Dokumen saat ini'; + + @override + String get searchCurrentWorkspace => 'Ruang kerja saat ini'; + + @override + String get browseNotes => 'Jelajahi catatan'; + + @override + String get searchActionsCommand => 'Cari tindakan'; + + @override + String get searchDocumentsCommand => 'Cari dokumen'; + + @override + String get searchTopicsCommand => 'Cari topik'; + + @override + String get searchRecentCommand => 'Cari terbaru'; + + @override + String get searchResourceUnavailable => 'Sumber daya ini tidak tersedia.'; + + @override + String get notesSync => 'Sinkronkan catatan'; + + @override + String get searchInWorkspace => 'Cari dalam Ruang Kerja'; + + @override + String get searchCategory => 'Kategori pencarian'; + + @override + String get clearHistorySearchScope => 'Cari riwayat seluruh dokumen'; + + @override + String get searchInDocument => 'Cari dalam dokumen'; + + @override + String get searchDocumentHint => 'Cari dalam dokumen ini…'; + + @override + String get searchWorkspaceHint => 'Cari dalam ruang kerja ini…'; + + @override + String get searchOutlineHint => 'Cari judul bagian…'; + + @override + String get searchHistoryHint => 'Cari dalam riwayat lokal…'; + + @override + String get searchEnterText => 'Masukkan teks untuk mencari'; + + @override + String get searchAccountUnavailable => 'Akun tidak tersedia'; + + @override + String get searchDocumentsHint => 'Cari dokumen…'; + + @override + String get searchTopicsHint => 'Cari topik…'; + + @override + String get searchRecentHint => 'Cari terbaru…'; + + @override + String get searchActionsHint => 'Cari tindakan…'; + + @override + String get searchRequiresNotesWorkspace => 'Memerlukan ruang kerja Catatan'; + + @override + String get searchSidebarLabel => 'Cari:'; + + @override + String get searchDocumentTextCategory => 'Teks dokumen'; + + @override + String get searchWorkspaceTextCategory => 'Teks ruang kerja'; + + @override + String get searchHeadingsCategory => 'Judul'; + + @override + String get shortcutUnassigned => 'Belum ditetapkan'; + + @override + String get shortcutPressKeys => 'Tekan kombinasi tombol…'; + + @override + String get shortcutRecord => 'Rekam pintasan'; + + @override + String get shortcutRestoreAll => 'Pulihkan semua bawaan'; + + @override + String get shortcutRestoreAllQuestion => + 'Pulihkan pintasan bawaan semua tindakan? Penetapan khusus akan dihapus.'; + + @override + String get shortcutSaveFailed => + 'Pintasan aktif tetapi tidak dapat disimpan. Coba lagi agar tetap tersedia setelah mulai ulang.'; + + @override + String get shortcutRetry => 'Coba simpan lagi'; + + @override + String get shortcutChanged => + 'Penetapan berubah. Tinjau pintasan saat ini dan coba lagi.'; + + @override + String get shortcutInvalid => + 'Kombinasi ini tidak dapat ditetapkan dalam konteks ini.'; + + @override + String get shortcutSystemWarning => + 'Desktop Anda mungkin memakai kombinasi ini. BusyMark tidak dapat mengganti pintasan sistem.'; + + @override + String get shortcutConflicts => + 'Pintasan ini bertentangan dengan tindakan berikut:'; + + @override + String get shortcutRemove => 'Hapus penetapan'; + + @override + String get shortcutRestoreDefault => 'Pulihkan bawaan'; + + @override + String get shortcutAssign => 'Tetapkan'; + + @override + String get shortcutReassign => 'Tetapkan ulang'; + + @override + String get shortcutContextApplication => 'Aplikasi'; + + @override + String get shortcutContextDocument => 'Tampilan dokumen'; + + @override + String get shortcutContextEditor => 'Editor dan sumber'; + + @override + String get shortcutContextWritersideSource => 'Sumber Writerside'; + + @override + String get shortcutContextWorkspace => 'Navigasi ruang kerja'; + + @override + String get shortcutContextTree => 'Pohon bilah samping'; + + @override + String get searchHeadingsCommand => 'Cari judul'; + + @override + String get shortcutFixed => + 'Dikelola oleh editor; tidak dapat dikonfigurasi.'; + + @override + String get shortcutDefaultConflict => + 'Pintasan bawaan bertentangan dengan penetapan khusus.'; + + @override + String get shortcutCurrentBinding => 'Pintasan saat ini'; + + @override + String get shortcutProposedBinding => 'Pintasan yang diusulkan'; + + @override + String get notesActions => 'Tindakan catatan'; + + @override + String get noteActions => 'Tindakan untuk catatan'; + + @override + String get noteProperties => 'Properti Catatan'; + + @override + String get notesExportAll => 'Ekspor catatan · Semua catatan…'; + + @override + String get notesExportCategory => 'Ekspor catatan · Kategori…'; + + @override + String get notesOfflineAll => 'Sediakan secara luring · Semua catatan'; + + @override + String get notesOfflineCategory => 'Sediakan secara luring · Kategori'; + + @override + String get notesOfflineRemoveAll => + 'Hapus penyimpanan luring · Semua catatan'; + + @override + String get notesOfflineRemoveCategory => + 'Hapus penyimpanan luring · Kategori'; + + @override + String get notesOfflinePause => 'Jeda unduhan'; + + @override + String get notesOfflineResume => 'Lanjutkan unduhan'; + + @override + String get notesOfflineKeep => 'Sediakan secara luring'; + + @override + String get notesOfflineRetentionHelp => + 'Menghapus persyaratan ini mempertahankan cache yang ada. Persyaratan kategori atau akun mungkin masih menyimpan catatan ini.'; + + @override + String get notesOfflineBroader => + 'Catatan ini juga disimpan oleh persyaratan kategori atau akun.'; + + @override + String get notesOfflineExternal => + 'Media eksternal memerlukan koneksi. BusyMark hanya menyimpan teks catatan dan lampiran terkelola.'; + + @override + String get notesEmptyAll => 'Belum ada catatan. Buat catatan untuk memulai.'; + + @override + String get notesEmptyFavorites => 'Tidak ada catatan favorit.'; + + @override + String get notesEmptyCategory => 'Tidak ada catatan dalam kategori ini.'; + + @override + String get notesEmptyUncategorized => 'Tidak ada catatan tanpa kategori.'; + + @override + String get notesEmptyRecovery => + 'Tidak ada catatan tersimpan untuk dipulihkan.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Gagal. Coba lagi'; + + @override + String get notesOperationFailed => + 'Operasi tidak dapat diselesaikan. Draf Anda tetap disimpan. Tinjau penyimpanan dan konflik, lalu coba lagi.'; + + @override + String get notesInspectionFailed => 'Lampiran · Gagal. Coba lagi'; + + @override + String get notesStartingConnection => 'Memulai koneksi Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Memverifikasi akses Catatan…'; + + @override + String get notesCancellingConnection => 'Membatalkan masuk dan membersihkan…'; + + @override + String get notesRemovingConnection => 'Menghapus akun lokal…'; + + @override + String get notesRemovedWarning => + 'Akun Nextcloud lokal telah dihapus. Pencabutan kata sandi aplikasi tidak dikonfirmasi. Hapus kata sandi BusyMark di pengaturan Keamanan Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Tidak semua catatan terbuka dapat disimpan secara lokal. Tinjau penyimpanan, konflik, atau normalisasi di ruang kerja. Perubahan lokal mungkin masih perlu disinkronkan dengan Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Simpan perubahan pengaturan Nextcloud sebelum keluar?'; + + @override + String get notesSettingsLeaveHelp => + 'Pengaturan Notes server · Perubahan yang belum disimpan'; + + @override + String get notesSettingsReload => 'Muat ulang pengaturan server…'; + + @override + String get notesSettingsReloadHelp => + 'Memuat ulang mengganti draf dengan pengaturan Nextcloud saat ini. Buang draf dan muat ulang?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud menerima pengaturan, tetapi penyegaran catatan gagal. Nilai yang ditampilkan telah dikonfirmasi server. Coba segarkan lagi dari Catatan.'; + + @override + String get notesUnavailable => + 'Luring · Teks tersedia secara lokal · Disimpan lokal · sinkronisasi tertunda'; + + @override + String get notesStorageFull => + 'Penyimpanan Nextcloud penuh. Kosongkan ruang di server, lalu coba lagi.'; + + @override + String get notesRejected => + 'Nextcloud menolak perubahan ini. Tinjau catatan dan simpan salinan lokal sebelum mencoba lagi.'; + + @override + String get notesThrottled => + 'Nextcloud meminta BusyMark menunggu. Sinkronisasi berlanjut setelah batas waktu percobaan ulang.'; + + @override + String get notesRecoveryRequired => 'Konflik · Bandingkan'; + + @override + String get notesScanning => 'Memindai berkas…'; + + @override + String get notesCancelling => + 'Membatalkan… Menunggu operasi saat ini selesai.'; + + @override + String get notesCancelled => 'Dibatalkan'; + + @override + String get notesPartial => 'Selesai sebagian'; + + @override + String get notesTransferFailed => 'Gagal. Coba lagi'; + + @override + String get notesImportPartialHelp => + 'Item sebelumnya mungkin telah disimpan secara lokal. Lanjutkan impor ini untuk meneruskan tanpa duplikat.'; + + @override + String get notesNotStarted => 'Belum dimulai'; + + @override + String get notesCreated => 'Dibuat'; + + @override + String get notesRemoteDeleteUnavailable => + 'URL server · Tidak didukung · Hapus · Lampiran'; + + @override + String get notesCurrentText => 'Teks yang saat ini disimpan'; + + @override + String get notesRecovering => 'Memulihkan sebagai catatan baru…'; + + @override + String get notesReadOnlyHelp => + 'Hanya baca · Favorit · Simpan salinan lokal…'; + + @override + String get notesSort => 'Urutkan catatan'; + + @override + String get notesPathRelative => + 'Jalur folder catatan relatif terhadap penyimpanan Nextcloud Anda.'; + + @override + String get notesSettingsStale => + 'Pengaturan berubah di Nextcloud. Muat ulang dan tinjau nilai server sebelum mengganti draf.'; + + @override + String get notesDownloadsPaused => 'Unduhan dijeda'; + + @override + String get notesExportNotPublished => 'Tidak ada ekspor yang dipublikasikan.'; + + @override + String get notesKeyringUnavailable => + 'Buka kunci keyring desktop dan coba lagi.'; + + @override + String get notesConnectFailed => + 'Tidak dapat terhubung ke Nextcloud Notes. Periksa alamat server dan coba lagi.'; + + @override + String get notesAccountMismatch => + 'Masuk ke akun Nextcloud yang sama. Hapus akun ini terlebih dahulu untuk menghubungkan akun lain.'; + + @override + String get notesLocalRemovalFailed => + 'Pembersihan akun lokal gagal. Akun dipertahankan agar penghapusan dapat dicoba lagi.'; + + @override + String get notesTransferring => 'Mentransfer catatan…'; + + @override + String get notesTechnicalDetails => 'Detail teknis'; + + @override + String get notesPauseRequirement => 'Jeda persyaratan luring'; + + @override + String get notesResumeRequirement => 'Lanjutkan persyaratan luring'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index eaf42622..05011cbb 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsIt extends AppLocalizations { AppLocalizationsIt([String locale = 'it']) : super(locale); + @override + String get notesNewFromRetainedText => 'Nuova nota dal testo conservato'; + + @override + String get notesAccountThrottled => + 'Nextcloud richiede una pausa. Attendi prima di accedere nuovamente.'; + + @override + String get notesSettingsRefreshing => + 'Aggiornamento di Notes dopo la conferma delle impostazioni'; + + @override + String get notesExportOperationFailed => + 'Esportazione fallita prima della pubblicazione. Controlla origine e destinazione, poi riprova.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'La nota recuperata è stata salvata, ma non si è aperta. Risolvi le modifiche in sospeso nell’editor o nelle proprietà, poi apri questa nota.'; + + @override + String get notesSettingsPendingNotes => + 'Note in sospeso, conflitti o creazioni incerte bloccano la modifica. Sincronizzali o esaminali in Notes, poi salva di nuovo la bozza conservata.'; + + @override + String get notesSettingsPendingAttachments => + 'Operazioni sugli allegati bloccano la modifica. Esamina gli allegati in Notes, poi salva di nuovo la bozza conservata.'; + + @override + String get notesAccountServerAddress => + 'Usa l’indirizzo HTTPS canonico di Nextcloud e riavvia l’accesso. L’indirizzo o il reindirizzamento non è verificabile.'; + + @override + String get notesAccountUnsupported => + 'Notes non è disponibile o l’API non è supportata. Attiva Nextcloud Notes con API principale 1, secondaria 4 o successiva.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud ha rifiutato le credenziali. Autorizza nuovamente l’account o riconnettilo.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud ha negato la verifica delle funzionalità Notes. Chiedi all’amministratore di consentirla e riprova.'; + + @override + String get notesAccountMalformed => + 'La risposta di Nextcloud non è verificabile. Controlla l’installazione del server e riavvia l’accesso.'; + + @override + String get notesAccountTimedOut => + 'Accesso scaduto. Riavvialo e completa l’autorizzazione nel browser.'; + + @override + String get notesAccountBrowserUnavailable => + 'Impossibile aprire il browser di sistema. Controlla il browser predefinito e riavvia l’accesso.'; + + @override + String get notesAccountNetwork => + 'Nextcloud non è raggiungibile. Controlla connessione e server e riprova ad accedere.'; + + @override + String get notesAccountRejected => + 'Nextcloud ha rifiutato la richiesta di funzionalità. Verifica l’accesso con l’amministratore e riprova.'; + + @override + String get notesAccountFailed => + 'Configurazione dell’account non riuscita. Riprova ad accedere.'; + + @override + String get notesTransferSourceChanged => + 'L’origine è cambiata dopo la revisione. Esamina il file attuale o crea una nuova istantanea prima di riprovare.'; + + @override + String get notesTransferUnreadable => + 'Impossibile leggere l’origine. Controlla l’accesso al file o alla cartella e riesaminala.'; + + @override + String get notesTransferInvalidSnapshot => + 'Istantanea non valida o non supportata. Scegli un’istantanea BusyMark Notes valida o un’origine Markdown.'; + + @override + String get notesTransferMediaLimit => + 'I contenuti multimediali superano il limite d’importazione. Riduci quelli dell’elemento interessato e riesaminalo.'; + + @override + String get notesTransferUnsafeSource => + 'L’origine contiene un percorso non sicuro. Scegli file nella cartella di origine e riesaminali.'; + + @override + String get notesTransferDestination => + 'Impossibile scrivere l’esportazione. Controlla accesso e spazio libero nella destinazione e riprova.'; + + @override + String get notesTransferOperationFailed => + 'Trasferimento non riuscito. Esamina gli elementi interessati e riprova. Quelli già importati rimangono salvati.'; + + @override + String get notesExportPublishedWithOmissions => + 'Esportazione disponibile con omissioni'; + + @override + String get notesBatchUnchanged => 'I valori richiesti erano già impostati.'; + + @override + String get notesBatchReadOnly => + 'La nota è di sola lettura. Le proprietà protette non possono cambiare; Preferito rimane modificabile.'; + + @override + String get notesBatchReviewChanged => + 'La nota è cambiata dopo la revisione. Esamina lo stato attuale prima di ripetere l’azione.'; + + @override + String get notesBatchMissing => + 'La nota non è più disponibile. Aggiorna l’elenco e verifica gli elementi scelti.'; + + @override + String get notesBatchStorage => + 'Salvataggio locale fallito. Controlla lo spazio e riprova questo elemento; quelli riusciti rimangono salvati.'; + + @override + String get notesRemoteReviewChanged => + 'La nota sul server è cambiata. La bozza di unione è conservata. Esamina il nuovo confronto e scegli di nuovo la risoluzione.'; + + @override + String get notesReviewRemote => 'Ho esaminato la versione attuale del server'; + @override String get wsModuleCapsNotPublished => 'La capitalizzazione del modulo non viene pubblicata.'; @@ -586,15 +711,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get keyboardShortcuts => 'Scorciatoie da tastiera'; - @override - String get commandPalette => 'Tavolozza dei comandi'; - - @override - String get commandPaletteHint => 'Digita un comando'; - - @override - String get commandPaletteEmpty => 'Nessun comando corrispondente'; - @override String get commandUnavailableInContext => 'Questo comando non è disponibile nel contesto corrente.'; @@ -813,7 +929,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Mostra il riferimento alle scorciatoie da tastiera'; + 'Personalizza le scorciatoie da tastiera dell’applicazione'; @override String get shortcutSyntaxReferenceDescription => @@ -1070,7 +1186,7 @@ class AppLocalizationsIt extends AppLocalizations { String get validateOnEdit => 'Convalida durante la modifica'; @override - String get clearRecentWorkspaces => 'Cancella le aree di lavoro recenti'; + String get clearRecentWorkspaces => 'Cancella la cronologia recente'; @override String get editingButtonsPosition => 'Posizione dei pulsanti di modifica'; @@ -1858,15 +1974,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get sourceSearchNextMatch => 'Corrispondenza successiva'; - @override - String get sourceSearchCaseSensitive => 'Maiuscole/minuscole'; - - @override - String get sourceSearchWholeWord => 'Parola intera'; - - @override - String get sourceSearchRegex => 'Espressione regolare'; - @override String get sourceSearchReplacement => 'Sostituisci con'; @@ -1995,13 +2102,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get saveAs => 'Salva con nome'; - @override - String get sourceSearchInvalidRegex => 'Espressione regolare non valida'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Le corrispondenze regex di lunghezza zero non sono supportate (ad esempio ^, \$ o lookahead che corrispondono solo a una posizione).'; - @override String get workspaceSearchIncomplete => 'È mostrata solo una parte dei risultati. Mostrane altri o restringi la ricerca.'; @@ -2111,6 +2211,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get documentKindResourceFile => 'File di risorse'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Impossibile aggiornare gli elementi recenti: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Apertura non riuscita: $error'; @@ -4384,10 +4489,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get clipboardHistory => 'Cronologia della Copia'; - @override - String get clipboardHistorySearchHint => - 'Cerca nella cronologia degli appunti'; - @override String get clipboardHistoryDescription => 'Apri la cronologia della copia locale della sessione.'; @@ -4411,10 +4512,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get clipboardNoItems => 'La cronologia della copia è vuota.'; - @override - String get clipboardNoMatches => - 'Nessun elemento della copia corrisponde a questa ricerca.'; - @override String get clipboardDisabled => 'La collezione Cronologia della Copia è disabilitata nelle Impostazioni.'; @@ -4472,13 +4569,6 @@ class AppLocalizationsIt extends AppLocalizations { String get localHistoryNoRevisions => 'Questo documento non ha revisioni nella cronologia locale.'; - @override - String get localHistoryNoMatches => - 'Nessuna revisione corrisponde a questa ricerca.'; - - @override - String get localHistorySearchHint => 'Cerca nella cronologia locale'; - @override String get localHistorySelectedRevision => 'Revisione selezionata'; @@ -5443,4 +5533,400 @@ class AppLocalizationsIt extends AppLocalizations { @override String get wsPropertiesUnavailable => 'L’elemento esaminato non è disponibile. Esamina un elemento per continuare.'; + + @override + String get searchDocumentsCategory => 'Documenti'; + + @override + String get searchTopicsCategory => 'Argomenti'; + + @override + String get searchCurrentDocument => 'Documento corrente'; + + @override + String get searchCurrentWorkspace => 'Area di lavoro corrente'; + + @override + String get browseNotes => 'Sfoglia note'; + + @override + String get searchActionsCommand => 'Cerca azioni'; + + @override + String get searchDocumentsCommand => 'Cerca documenti'; + + @override + String get searchTopicsCommand => 'Cerca argomenti'; + + @override + String get searchRecentCommand => 'Cerca recenti'; + + @override + String get searchResourceUnavailable => 'Questa risorsa non è disponibile.'; + + @override + String get notesSync => 'Sincronizza note'; + + @override + String get searchInWorkspace => 'Cerca nello spazio di lavoro'; + + @override + String get searchCategory => 'Categoria di ricerca'; + + @override + String get clearHistorySearchScope => 'Cerca nella cronologia dei documenti'; + + @override + String get searchInDocument => 'Cerca nel documento'; + + @override + String get searchDocumentHint => 'Cerca in questo documento…'; + + @override + String get searchWorkspaceHint => 'Cerca in questo spazio di lavoro…'; + + @override + String get searchOutlineHint => 'Cerca titoli…'; + + @override + String get searchHistoryHint => 'Cerca nella cronologia locale…'; + + @override + String get searchEnterText => 'Inserisci il testo da cercare'; + + @override + String get searchAccountUnavailable => 'Account non disponibile'; + + @override + String get searchDocumentsHint => 'Cerca documenti…'; + + @override + String get searchTopicsHint => 'Cerca argomenti…'; + + @override + String get searchRecentHint => 'Cerca recenti…'; + + @override + String get searchActionsHint => 'Cerca azioni…'; + + @override + String get searchRequiresNotesWorkspace => + 'Richiede uno spazio di lavoro Note'; + + @override + String get searchSidebarLabel => 'Ricerca:'; + + @override + String get searchDocumentTextCategory => 'Testo del documento'; + + @override + String get searchWorkspaceTextCategory => 'Testo dell’area di lavoro'; + + @override + String get searchHeadingsCategory => 'Intestazioni'; + + @override + String get shortcutUnassigned => 'Non assegnata'; + + @override + String get shortcutPressKeys => 'Premi una combinazione di tasti…'; + + @override + String get shortcutRecord => 'Acquisisci scorciatoia'; + + @override + String get shortcutRestoreAll => + 'Ripristina tutte le impostazioni predefinite'; + + @override + String get shortcutRestoreAllQuestion => + 'Ripristinare le scorciatoie predefinite di tutte le azioni? Le assegnazioni personalizzate verranno rimosse.'; + + @override + String get shortcutSaveFailed => + 'Le scorciatoie sono attive ma non sono state salvate. Riprova per mantenerle dopo il riavvio.'; + + @override + String get shortcutRetry => 'Riprova salvataggio'; + + @override + String get shortcutChanged => + 'Le assegnazioni sono cambiate. Controlla le scorciatoie attuali e riprova.'; + + @override + String get shortcutInvalid => + 'Questa combinazione non può essere assegnata in questo contesto.'; + + @override + String get shortcutSystemWarning => + 'Questa combinazione potrebbe essere usata dal desktop. BusyMark non può sostituire le scorciatoie di sistema.'; + + @override + String get shortcutConflicts => + 'Questa scorciatoia è in conflitto con queste azioni:'; + + @override + String get shortcutRemove => 'Rimuovi assegnazione'; + + @override + String get shortcutRestoreDefault => 'Ripristina predefinita'; + + @override + String get shortcutAssign => 'Assegna'; + + @override + String get shortcutReassign => 'Riassegna'; + + @override + String get shortcutContextApplication => 'Applicazione'; + + @override + String get shortcutContextDocument => 'Viste del documento'; + + @override + String get shortcutContextEditor => 'Editor e sorgente'; + + @override + String get shortcutContextWritersideSource => 'Sorgente Writerside'; + + @override + String get shortcutContextWorkspace => 'Navigazione dell’area di lavoro'; + + @override + String get shortcutContextTree => 'Alberi laterali'; + + @override + String get searchHeadingsCommand => 'Cerca titoli'; + + @override + String get shortcutFixed => 'Gestito dall’editor; non configurabile.'; + + @override + String get shortcutDefaultConflict => + 'La scorciatoia predefinita è in conflitto con un’assegnazione personalizzata.'; + + @override + String get shortcutCurrentBinding => 'Scorciatoia attuale'; + + @override + String get shortcutProposedBinding => 'Scorciatoia proposta'; + + @override + String get notesActions => 'Azioni delle note'; + + @override + String get noteActions => 'Azioni della nota'; + + @override + String get noteProperties => 'Proprietà della nota'; + + @override + String get notesExportAll => 'Esporta note · Tutte le note…'; + + @override + String get notesExportCategory => 'Esporta note · Categoria…'; + + @override + String get notesOfflineAll => 'Rendi disponibile offline · Tutte le note'; + + @override + String get notesOfflineCategory => 'Rendi disponibile offline · Categoria'; + + @override + String get notesOfflineRemoveAll => + 'Rimuovi conservazione offline · Tutte le note'; + + @override + String get notesOfflineRemoveCategory => + 'Rimuovi conservazione offline · Categoria'; + + @override + String get notesOfflinePause => 'Sospendi download'; + + @override + String get notesOfflineResume => 'Riprendi download'; + + @override + String get notesOfflineKeep => 'Rendi disponibile offline'; + + @override + String get notesOfflineRetentionHelp => + 'Rimuovere questo requisito conserva i contenuti nella cache. Un requisito di categoria o account può ancora conservare la nota.'; + + @override + String get notesOfflineBroader => + 'La nota è conservata anche da un requisito di categoria o account.'; + + @override + String get notesOfflineExternal => + 'I contenuti multimediali esterni richiedono una connessione. BusyMark conserva solo testo e allegati gestiti.'; + + @override + String get notesEmptyAll => 'Nessuna nota. Creane una per iniziare.'; + + @override + String get notesEmptyFavorites => 'Nessuna nota preferita.'; + + @override + String get notesEmptyCategory => 'Nessuna nota in questa categoria.'; + + @override + String get notesEmptyUncategorized => 'Nessuna nota senza categoria.'; + + @override + String get notesEmptyRecovery => 'Nessuna nota conservata da recuperare.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Non riuscito. Riprova'; + + @override + String get notesOperationFailed => + 'L’operazione non è terminata. La bozza è conservata. Controlla il salvataggio e i conflitti, poi riprova.'; + + @override + String get notesInspectionFailed => 'Allegati · Non riuscito. Riprova'; + + @override + String get notesStartingConnection => 'Avvio della connessione Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Verifica dell’accesso alle note…'; + + @override + String get notesCancellingConnection => + 'Annullamento dell’accesso e pulizia…'; + + @override + String get notesRemovingConnection => 'Rimozione dell’account locale…'; + + @override + String get notesRemovedWarning => + 'L’account Nextcloud locale è stato rimosso. La revoca della password dell’app non è confermata. Rimuovi la password di BusyMark nelle impostazioni di sicurezza di Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Non tutte le note aperte sono state salvate localmente. Controlla salvataggio, conflitti o normalizzazione nell’area di lavoro. Le modifiche locali potrebbero richiedere ancora la sincronizzazione con Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Salvare le modifiche alle impostazioni Nextcloud prima di uscire?'; + + @override + String get notesSettingsLeaveHelp => + 'Impostazioni Notes del server · Modifiche non salvate'; + + @override + String get notesSettingsReload => 'Ricarica impostazioni del server…'; + + @override + String get notesSettingsReloadHelp => + 'Il ricaricamento sostituisce la bozza con le impostazioni attuali di Nextcloud. Scartare la bozza e ricaricare?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud ha accettato le impostazioni, ma l’aggiornamento delle note è fallito. Sono mostrati i valori confermati dal server. Riprova l’aggiornamento dalle note.'; + + @override + String get notesUnavailable => + 'Non in linea · Testo disponibile localmente · Salvato localmente · sincronizzazione in attesa'; + + @override + String get notesStorageFull => + 'Lo spazio Nextcloud è esaurito. Libera spazio sul server e riprova.'; + + @override + String get notesRejected => + 'Nextcloud ha rifiutato questa modifica. Controlla la nota e salva una copia locale prima di riprovare.'; + + @override + String get notesThrottled => + 'Nextcloud ha chiesto di attendere. La sincronizzazione riprenderà al termine dell’attesa.'; + + @override + String get notesRecoveryRequired => 'Conflitti · Confronta'; + + @override + String get notesScanning => 'Analisi dei file…'; + + @override + String get notesCancelling => + 'Annullamento… In attesa che termini l’operazione in corso.'; + + @override + String get notesCancelled => 'Annullato'; + + @override + String get notesPartial => 'Completato parzialmente'; + + @override + String get notesTransferFailed => 'Non riuscito. Riprova'; + + @override + String get notesImportPartialHelp => + 'Gli elementi precedenti potrebbero essere già salvati localmente. Riprendi questa importazione per evitare duplicati.'; + + @override + String get notesNotStarted => 'Non avviato'; + + @override + String get notesCreated => 'Creato'; + + @override + String get notesRemoteDeleteUnavailable => + 'URL del server · Non supportato · Elimina · Allegati'; + + @override + String get notesCurrentText => 'Testo attualmente conservato'; + + @override + String get notesRecovering => 'Recupero come nuova nota…'; + + @override + String get notesReadOnlyHelp => + 'Sola lettura · Preferiti · Salva copia locale…'; + + @override + String get notesSort => 'Ordina note'; + + @override + String get notesPathRelative => + 'Il percorso della cartella delle note è relativo al tuo spazio Nextcloud.'; + + @override + String get notesSettingsStale => + 'Le impostazioni sono cambiate su Nextcloud. Ricarica e controlla i valori del server prima di sostituire la bozza.'; + + @override + String get notesDownloadsPaused => 'Download sospesi'; + + @override + String get notesExportNotPublished => 'Nessuna esportazione pubblicata.'; + + @override + String get notesKeyringUnavailable => + 'Sblocca il portachiavi del desktop e riprova.'; + + @override + String get notesConnectFailed => + 'Impossibile connettersi a Nextcloud Notes. Controlla l’indirizzo del server e riprova.'; + + @override + String get notesAccountMismatch => + 'Accedi allo stesso account Nextcloud. Rimuovi prima questo account per collegarne un altro.'; + + @override + String get notesLocalRemovalFailed => + 'La pulizia dell’account locale è fallita. L’account è conservato per riprovare la rimozione.'; + + @override + String get notesTransferring => 'Trasferimento delle note…'; + + @override + String get notesTechnicalDetails => 'Dettagli tecnici'; + + @override + String get notesPauseRequirement => 'Sospendi requisito offline'; + + @override + String get notesResumeRequirement => 'Riprendi requisito offline'; } diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 02686a7f..8a3032fc 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -10,6 +10,126 @@ import 'app_localizations.dart'; class AppLocalizationsJa extends AppLocalizations { AppLocalizationsJa([String locale = 'ja']) : super(locale); + @override + String get notesNewFromRetainedText => '保持されたテキストから新規ノートを作成'; + + @override + String get notesAccountThrottled => + 'Nextcloud が待機を求めています。しばらく待ってから再度ログインしてください。'; + + @override + String get notesSettingsRefreshing => '確認済みの設定に基づいて Notes を更新中'; + + @override + String get notesExportOperationFailed => + '公開前にエクスポートが失敗しました。元と保存先を確認して再実行してください。'; + + @override + String get notesRecoveryCreatedNotOpened => + '復旧したノートは保存済みですが、開けませんでした。未処理の編集やプロパティ変更を解決し、この既存ノートを開いてください。'; + + @override + String get notesSettingsPendingNotes => + '未処理のノート、競合、作成結果の不確定なノートが変更を妨げています。Notes で同期または確認し、保持された設定の下書きを再保存してください。'; + + @override + String get notesSettingsPendingAttachments => + '未処理の添付操作が変更を妨げています。Notes で添付を確認し、保持された設定の下書きを再保存してください。'; + + @override + String get notesAccountServerAddress => + 'Nextcloud の正規の HTTPS アドレスでログインをやり直してください。アドレスまたはリダイレクトを確認できませんでした。'; + + @override + String get notesAccountUnsupported => + 'Notes が利用できないか API が未対応です。API メジャーバージョン 1、マイナーバージョン 4 以降の Nextcloud Notes を有効にしてください。'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud が認証情報を拒否しました。再認証または再接続してください。'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud が Notes の機能確認を拒否しました。管理者に許可を依頼して再試行してください。'; + + @override + String get notesAccountMalformed => + 'Nextcloud の応答を検証できませんでした。サーバーの設定を確認し、再度ログインしてください。'; + + @override + String get notesAccountTimedOut => 'ログインがタイムアウトしました。やり直してブラウザーで認証を完了してください。'; + + @override + String get notesAccountBrowserUnavailable => + 'システムのブラウザーを開けませんでした。既定のブラウザーを確認して再度ログインしてください。'; + + @override + String get notesAccountNetwork => + 'Nextcloud に接続できませんでした。接続とサーバーを確認して再度ログインしてください。'; + + @override + String get notesAccountRejected => + 'Nextcloud が機能リクエストを拒否しました。管理者とアクセス権を確認して再試行してください。'; + + @override + String get notesAccountFailed => 'アカウント設定を完了できませんでした。再度ログインしてください。'; + + @override + String get notesTransferSourceChanged => + '確認後に元の内容が変わりました。現在のファイルを再確認するか、新しいスナップショットを作成してください。'; + + @override + String get notesTransferUnreadable => + '元の内容を読めませんでした。ファイルやフォルダーのアクセス権を確認して再確認してください。'; + + @override + String get notesTransferInvalidSnapshot => + '無効または未対応のスナップショットです。有効な BusyMark Notes スナップショットか Markdown を選んでください。'; + + @override + String get notesTransferMediaLimit => + 'メディアがインポートのサイズ制限を超えています。該当項目のメディアを小さくして再確認してください。'; + + @override + String get notesTransferUnsafeSource => + '元の内容に安全でないパスがあります。元のフォルダー内のファイルを選び、再確認してください。'; + + @override + String get notesTransferDestination => + 'エクスポートを書き込めませんでした。保存先のアクセス権と空き容量を確認して再実行してください。'; + + @override + String get notesTransferOperationFailed => + '転送を完了できませんでした。該当項目を確認して再試行してください。すでにインポートした項目は保存されています。'; + + @override + String get notesExportPublishedWithOmissions => '一部を除いてエクスポート済み'; + + @override + String get notesBatchUnchanged => '要求された値はすでに設定されています。'; + + @override + String get notesBatchReadOnly => + 'このノートは読み取り専用です。保護されたプロパティは変更できませんが、お気に入りは変更できます。'; + + @override + String get notesBatchReviewChanged => + '確認後にノートが変わりました。現在の状態を確認してから操作をやり直してください。'; + + @override + String get notesBatchMissing => 'このノートは利用できなくなりました。一覧を更新して対象を再確認してください。'; + + @override + String get notesBatchStorage => + 'ローカル保存に失敗しました。保存領域を確認してこの項目を再試行してください。成功した項目は保存済みです。'; + + @override + String get notesRemoteReviewChanged => + 'サーバーのノートが変わりました。マージの下書きは保持されています。更新された比較を確認し、解決方法を再度選んでください。'; + + @override + String get notesReviewRemote => '現在のサーバーバージョンを確認しました'; + @override String get wsModuleCapsNotPublished => 'モジュールの大文字・小文字設定は公開されません。'; @@ -562,15 +682,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get keyboardShortcuts => 'キーボードショートカット'; - @override - String get commandPalette => 'コマンドパレット'; - - @override - String get commandPaletteHint => 'コマンドを入力'; - - @override - String get commandPaletteEmpty => '一致するコマンドはありません'; - @override String get commandUnavailableInContext => '現在のエディターのコンテキストでは利用できません'; @@ -786,7 +897,7 @@ class AppLocalizationsJa extends AppLocalizations { String get shortcutSearchDescription => '現在のワークスペースを検索'; @override - String get shortcutKeyboardShortcutsDescription => 'このキーボードショートカット一覧を表示'; + String get shortcutKeyboardShortcutsDescription => 'アプリケーションのキーボードショートカットを設定'; @override String get shortcutSyntaxReferenceDescription => '構文リファレンスを開く'; @@ -1014,7 +1125,7 @@ class AppLocalizationsJa extends AppLocalizations { String get validateOnEdit => '編集時に検証'; @override - String get clearRecentWorkspaces => '最近使ったワークスペースを消去'; + String get clearRecentWorkspaces => '最近の履歴を消去'; @override String get editingButtonsPosition => '編集ボタンの位置'; @@ -1782,15 +1893,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get sourceSearchNextMatch => '次の一致'; - @override - String get sourceSearchCaseSensitive => '大文字と小文字を区別'; - - @override - String get sourceSearchWholeWord => '単語単位'; - - @override - String get sourceSearchRegex => '正規表現'; - @override String get sourceSearchReplacement => '置換後'; @@ -1911,13 +2013,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get saveAs => '名前を付けて保存'; - @override - String get sourceSearchInvalidRegex => '無効な正規表現'; - - @override - String get sourceSearchZeroLengthUnsupported => - '長さがゼロの正規表現の一致には対応していません(^、\$、位置のみに一致する先読みなど)。'; - @override String get workspaceSearchIncomplete => '一部の結果のみ表示しています。さらに表示するか、検索を絞り込んでください。'; @@ -2024,6 +2119,11 @@ class AppLocalizationsJa extends AppLocalizations { @override String get documentKindResourceFile => 'リソースファイル'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return '最近使った項目を更新できませんでした: $error。'; + } + @override String workspaceErrorOpenFailed(String error) { return '開くことができませんでした:$error'; @@ -4233,9 +4333,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get clipboardHistory => 'コピーボード履歴'; - @override - String get clipboardHistorySearchHint => 'クリップボード履歴を検索'; - @override String get clipboardHistoryDescription => 'セッション固有のコピーボード履歴を開く。'; @@ -4256,9 +4353,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get clipboardNoItems => 'コピーボード履歴は空です。'; - @override - String get clipboardNoMatches => 'この検索に一致するコピーボードの項目はありません。'; - @override String get clipboardDisabled => 'コピーボード履歴のコレクションは設定で無効になっています。'; @@ -4309,12 +4403,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get localHistoryNoRevisions => 'このドキュメントにはローカル履歴の修正はありません。'; - @override - String get localHistoryNoMatches => 'この検索に一致するローカル履歴の修正はありません。'; - - @override - String get localHistorySearchHint => 'ローカル履歴を検索'; - @override String get localHistorySelectedRevision => '選択されたリビジョン'; @@ -5246,4 +5334,380 @@ class AppLocalizationsJa extends AppLocalizations { @override String get wsPropertiesUnavailable => '確認中の要素は利用できません。続行するには要素を確認してください。'; + + @override + String get searchDocumentsCategory => 'ドキュメント'; + + @override + String get searchTopicsCategory => 'トピック'; + + @override + String get searchCurrentDocument => '現在のドキュメント'; + + @override + String get searchCurrentWorkspace => '現在のワークスペース'; + + @override + String get browseNotes => 'ノートを参照'; + + @override + String get searchActionsCommand => 'アクションを検索'; + + @override + String get searchDocumentsCommand => 'ドキュメントを検索'; + + @override + String get searchTopicsCommand => 'トピックを検索'; + + @override + String get searchRecentCommand => '最近の項目を検索'; + + @override + String get searchResourceUnavailable => 'このリソースは利用できません。'; + + @override + String get notesSync => 'ノートを同期'; + + @override + String get searchInWorkspace => 'ワークスペース内検索'; + + @override + String get searchCategory => '検索カテゴリ'; + + @override + String get clearHistorySearchScope => 'すべてのドキュメントの履歴を検索'; + + @override + String get searchInDocument => 'ドキュメント内を検索'; + + @override + String get searchDocumentHint => 'このドキュメントを検索…'; + + @override + String get searchWorkspaceHint => 'このワークスペースを検索…'; + + @override + String get searchOutlineHint => '見出しを検索…'; + + @override + String get searchHistoryHint => 'ローカル履歴を検索…'; + + @override + String get searchEnterText => '検索するテキストを入力'; + + @override + String get searchAccountUnavailable => 'アカウントを利用できません'; + + @override + String get searchDocumentsHint => 'ドキュメントを検索…'; + + @override + String get searchTopicsHint => 'トピックを検索…'; + + @override + String get searchRecentHint => '最近の項目を検索…'; + + @override + String get searchActionsHint => 'アクションを検索…'; + + @override + String get searchRequiresNotesWorkspace => 'ノートのワークスペースが必要です'; + + @override + String get searchSidebarLabel => '検索:'; + + @override + String get searchDocumentTextCategory => '文書のテキスト'; + + @override + String get searchWorkspaceTextCategory => 'ワークスペースのテキスト'; + + @override + String get searchHeadingsCategory => '見出し'; + + @override + String get shortcutUnassigned => '未割り当て'; + + @override + String get shortcutPressKeys => 'キーの組み合わせを押してください…'; + + @override + String get shortcutRecord => 'ショートカットを記録'; + + @override + String get shortcutRestoreAll => 'すべて既定に戻す'; + + @override + String get shortcutRestoreAllQuestion => + 'すべてのアクションのショートカットを既定に戻しますか?独自の割り当ては削除されます。'; + + @override + String get shortcutSaveFailed => + 'ショートカットは有効ですが保存できませんでした。再起動後も保持するには保存を再試行してください。'; + + @override + String get shortcutRetry => '保存を再試行'; + + @override + String get shortcutChanged => '割り当てが変更されました。現在のショートカットを確認して再試行してください。'; + + @override + String get shortcutInvalid => 'この組み合わせはこのコンテキストでは割り当てられません。'; + + @override + String get shortcutSystemWarning => + 'この組み合わせはデスクトップで使われている可能性があります。BusyMarkはシステムのショートカットを上書きできません。'; + + @override + String get shortcutConflicts => 'このショートカットは次のアクションと競合します:'; + + @override + String get shortcutRemove => '割り当てを削除'; + + @override + String get shortcutRestoreDefault => '既定に戻す'; + + @override + String get shortcutAssign => '割り当て'; + + @override + String get shortcutReassign => '再割り当て'; + + @override + String get shortcutContextApplication => 'アプリケーション'; + + @override + String get shortcutContextDocument => '文書の表示'; + + @override + String get shortcutContextEditor => 'エディターとソース'; + + @override + String get shortcutContextWritersideSource => 'Writersideのソース'; + + @override + String get shortcutContextWorkspace => 'ワークスペースの移動'; + + @override + String get shortcutContextTree => 'サイドバーのツリー'; + + @override + String get searchHeadingsCommand => '見出しを検索'; + + @override + String get shortcutFixed => 'エディターが管理するため、変更できません。'; + + @override + String get shortcutDefaultConflict => '既定のショートカットがカスタム割り当てと競合しています。'; + + @override + String get shortcutCurrentBinding => '現在のショートカット'; + + @override + String get shortcutProposedBinding => '新しいショートカット'; + + @override + String get notesActions => 'ノートの操作'; + + @override + String get noteActions => 'このノートの操作'; + + @override + String get noteProperties => 'ノートのプロパティ'; + + @override + String get notesExportAll => 'ノートをエクスポート · すべてのノート…'; + + @override + String get notesExportCategory => 'ノートをエクスポート · カテゴリ…'; + + @override + String get notesOfflineAll => 'オフラインで利用可能にする · すべてのノート'; + + @override + String get notesOfflineCategory => 'オフラインで利用可能にする · カテゴリ'; + + @override + String get notesOfflineRemoveAll => 'オフライン保持を解除 · すべてのノート'; + + @override + String get notesOfflineRemoveCategory => 'オフライン保持を解除 · カテゴリ'; + + @override + String get notesOfflinePause => 'ダウンロードを一時停止'; + + @override + String get notesOfflineResume => 'ダウンロードを再開'; + + @override + String get notesOfflineKeep => 'オフラインで利用可能にする'; + + @override + String get notesOfflineRetentionHelp => + 'この要件を解除しても既存のキャッシュは削除されません。カテゴリやアカウントの要件により保持される場合があります。'; + + @override + String get notesOfflineBroader => 'このノートはカテゴリまたはアカウントの要件によっても保持されています。'; + + @override + String get notesOfflineExternal => + '外部メディアには接続が必要です。BusyMark が保持するのはノート本文と管理対象の添付ファイルのみです。'; + + @override + String get notesEmptyAll => 'ノートはまだありません。新しく作成してください。'; + + @override + String get notesEmptyFavorites => 'お気に入りのノートはありません。'; + + @override + String get notesEmptyCategory => 'このカテゴリにノートはありません。'; + + @override + String get notesEmptyUncategorized => '未分類のノートはありません。'; + + @override + String get notesEmptyRecovery => '復元できる保存済みノートはありません。'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · 失敗. 再試行'; + + @override + String get notesOperationFailed => + '処理を完了できませんでした。下書きは保持されています。保存と競合を確認して再試行してください。'; + + @override + String get notesInspectionFailed => '添付ファイル · 失敗. 再試行'; + + @override + String get notesStartingConnection => 'Nextcloud への接続を開始中…'; + + @override + String get notesVerifyingConnection => 'ノートへのアクセスを確認中…'; + + @override + String get notesCancellingConnection => 'サインインをキャンセルして後処理中…'; + + @override + String get notesRemovingConnection => 'ローカルアカウントを削除中…'; + + @override + String get notesRemovedWarning => + 'ローカルの Nextcloud アカウントを削除しました。アプリパスワードの失効は確認できませんでした。Nextcloud のセキュリティ設定で BusyMark のパスワードを削除してください。'; + + @override + String get notesBuffersNotSaved => + '開いているすべてのノートをローカルに保存できませんでした。ワークスペースで保存、競合、正規化を確認してください。ローカルの変更は Nextcloud との同期がまだ必要な場合があります。'; + + @override + String get notesSettingsLeave => '移動する前に Nextcloud 設定の変更を保存しますか?'; + + @override + String get notesSettingsLeaveHelp => 'サーバーのNotes設定 · 未保存の変更'; + + @override + String get notesSettingsReload => 'サーバー設定を再読み込み…'; + + @override + String get notesSettingsReloadHelp => + '再読み込みすると下書きが現在の Nextcloud 設定に置き換わります。下書きを破棄して再読み込みしますか?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud は設定を受理しましたが、ノートの更新に失敗しました。表示値はサーバーが確認した値です。ノートから更新を再試行してください。'; + + @override + String get notesUnavailable => 'オフライン · テキストはローカルで利用可能 · ローカルに保存済み・同期待ち'; + + @override + String get notesStorageFull => 'Nextcloud の空き容量がありません。サーバーの容量を空けて再試行してください。'; + + @override + String get notesRejected => + 'Nextcloud が変更を拒否しました。ノートを確認してローカルコピーを保存してから再試行してください。'; + + @override + String get notesThrottled => 'Nextcloud が待機を要求しました。指定された再試行時刻を過ぎると同期が再開します。'; + + @override + String get notesRecoveryRequired => '競合 · 比較'; + + @override + String get notesScanning => 'ファイルをスキャン中…'; + + @override + String get notesCancelling => 'キャンセル中… 現在の処理の終了を待っています。'; + + @override + String get notesCancelled => 'キャンセル済み'; + + @override + String get notesPartial => '一部完了'; + + @override + String get notesTransferFailed => '失敗. 再試行'; + + @override + String get notesImportPartialHelp => + '以前の項目はすでにローカルに保存されている場合があります。重複を防ぐため、このインポートを再開してください。'; + + @override + String get notesNotStarted => '未開始'; + + @override + String get notesCreated => '作成済み'; + + @override + String get notesRemoteDeleteUnavailable => 'サーバーURL · 未対応 · 削除 · 添付ファイル'; + + @override + String get notesCurrentText => '現在保持されているテキスト'; + + @override + String get notesRecovering => '新しいノートとして復元中…'; + + @override + String get notesReadOnlyHelp => '読み取り専用 · お気に入り · ローカルコピーを保存…'; + + @override + String get notesSort => 'ノートの並べ替え'; + + @override + String get notesPathRelative => 'ノートフォルダーのパスは Nextcloud ストレージからの相対パスです。'; + + @override + String get notesSettingsStale => + 'Nextcloud の設定が変更されました。サーバーの値を再読み込みして確認してから下書きを置き換えてください。'; + + @override + String get notesDownloadsPaused => 'ダウンロードは一時停止中です'; + + @override + String get notesExportNotPublished => 'エクスポートは公開されませんでした。'; + + @override + String get notesKeyringUnavailable => 'デスクトップのキーリングを解除して再試行してください。'; + + @override + String get notesConnectFailed => + 'Nextcloud Notes に接続できませんでした。サーバーのアドレスを確認して再試行してください。'; + + @override + String get notesAccountMismatch => + '同じ Nextcloud アカウントでサインインしてください。別のアカウントを接続するには現在のアカウントを先に削除してください。'; + + @override + String get notesLocalRemovalFailed => + 'ローカルアカウントの後処理に失敗しました。削除を再試行できるようアカウントを保持しています。'; + + @override + String get notesTransferring => 'ノートを転送中…'; + + @override + String get notesTechnicalDetails => '技術情報'; + + @override + String get notesPauseRequirement => 'オフライン保持要件を一時停止'; + + @override + String get notesResumeRequirement => 'オフライン保持要件を再開'; } diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 8d060c70..34ea1cab 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -10,6 +10,128 @@ import 'app_localizations.dart'; class AppLocalizationsKo extends AppLocalizations { AppLocalizationsKo([String locale = 'ko']) : super(locale); + @override + String get notesNewFromRetainedText => '보관된 텍스트로 새 노트 만들기'; + + @override + String get notesAccountThrottled => + 'Nextcloud가 일시 중지를 요청했습니다. 기다린 뒤 다시 로그인하세요.'; + + @override + String get notesSettingsRefreshing => '확인된 설정 변경 후 Notes 새로 고치는 중'; + + @override + String get notesExportOperationFailed => + '게시 전에 내보내기가 실패했습니다. 원본과 대상을 확인하고 다시 내보내세요.'; + + @override + String get notesRecoveryCreatedNotOpened => + '복구한 노트는 저장되었지만 열지 못했습니다. 보류 중인 편집 또는 속성 변경을 해결한 뒤 이 기존 노트를 여세요.'; + + @override + String get notesSettingsPendingNotes => + '보류 중인 노트, 충돌 또는 생성 결과가 불확실한 노트가 변경을 막고 있습니다. Notes에서 동기화하거나 검토한 뒤 보존된 설정 초안을 다시 저장하세요.'; + + @override + String get notesSettingsPendingAttachments => + '보류 중인 첨부 작업이 변경을 막고 있습니다. Notes에서 첨부를 검토한 뒤 보존된 설정 초안을 다시 저장하세요.'; + + @override + String get notesAccountServerAddress => + 'Nextcloud의 정식 HTTPS 주소로 다시 로그인하세요. 주소 또는 리디렉션을 확인할 수 없었습니다.'; + + @override + String get notesAccountUnsupported => + 'Notes를 사용할 수 없거나 API가 지원되지 않습니다. 주 버전 1, 부 버전 4 이상의 API를 제공하는 Nextcloud Notes를 활성화하세요.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud가 인증 정보를 거부했습니다. 계정을 다시 인증하거나 연결하세요.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud가 Notes 기능 확인을 거부했습니다. 관리자에게 허용을 요청한 뒤 다시 시도하세요.'; + + @override + String get notesAccountMalformed => + 'Nextcloud 응답을 검증할 수 없었습니다. 서버 설치를 확인하고 다시 로그인하세요.'; + + @override + String get notesAccountTimedOut => + '로그인 시간이 초과되었습니다. 다시 시작하고 브라우저에서 인증을 완료하세요.'; + + @override + String get notesAccountBrowserUnavailable => + '시스템 브라우저를 열지 못했습니다. 기본 브라우저를 확인하고 다시 로그인하세요.'; + + @override + String get notesAccountNetwork => + 'Nextcloud에 연결하지 못했습니다. 연결과 서버를 확인하고 다시 로그인하세요.'; + + @override + String get notesAccountRejected => + 'Nextcloud가 기능 요청을 거부했습니다. 관리자와 접근 권한을 확인하고 다시 시도하세요.'; + + @override + String get notesAccountFailed => '계정 설정을 완료하지 못했습니다. 다시 로그인하세요.'; + + @override + String get notesTransferSourceChanged => + '검토 후 원본이 변경되었습니다. 현재 파일을 다시 검토하거나 새 스냅샷을 만든 뒤 재시도하세요.'; + + @override + String get notesTransferUnreadable => + '원본을 읽을 수 없습니다. 파일 또는 폴더 접근 권한을 확인하고 다시 검토하세요.'; + + @override + String get notesTransferInvalidSnapshot => + '스냅샷이 올바르지 않거나 지원되지 않습니다. 유효한 BusyMark Notes 스냅샷 또는 Markdown 원본을 선택하세요.'; + + @override + String get notesTransferMediaLimit => + '미디어가 가져오기 크기 제한을 초과합니다. 해당 항목의 미디어를 줄이고 다시 검토하세요.'; + + @override + String get notesTransferUnsafeSource => + '원본에 안전하지 않은 경로가 있습니다. 원본 폴더 안의 파일을 선택하고 다시 검토하세요.'; + + @override + String get notesTransferDestination => + '내보내기를 기록하지 못했습니다. 대상의 접근 권한과 여유 공간을 확인하고 다시 내보내세요.'; + + @override + String get notesTransferOperationFailed => + '전송을 완료하지 못했습니다. 해당 항목을 검토하고 다시 시도하세요. 이전에 가져온 항목은 저장되어 있습니다.'; + + @override + String get notesExportPublishedWithOmissions => '누락된 항목이 있는 내보내기 사용 가능'; + + @override + String get notesBatchUnchanged => '요청한 값이 이미 설정되어 있습니다.'; + + @override + String get notesBatchReadOnly => + '이 노트는 읽기 전용입니다. 보호된 속성은 바꿀 수 없지만 즐겨찾기는 변경할 수 있습니다.'; + + @override + String get notesBatchReviewChanged => + '검토 후 노트가 변경되었습니다. 현재 상태를 검토한 뒤 작업을 다시 적용하세요.'; + + @override + String get notesBatchMissing => + '이 노트를 더 이상 사용할 수 없습니다. 목록을 새로 고치고 대상을 다시 검토하세요.'; + + @override + String get notesBatchStorage => + '로컬 저장에 실패했습니다. 저장소를 확인하고 이 항목을 다시 시도하세요. 성공한 항목은 저장되어 있습니다.'; + + @override + String get notesRemoteReviewChanged => + '서버 노트가 변경되었습니다. 병합 초안은 보존됩니다. 새 비교를 검토하고 해결 방법을 다시 선택하세요.'; + + @override + String get notesReviewRemote => '현재 서버 버전을 검토했습니다'; + @override String get wsModuleCapsNotPublished => '모듈의 대소문자 설정은 게시되지 않습니다.'; @@ -561,15 +683,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get keyboardShortcuts => '키보드 단축키'; - @override - String get commandPalette => '명령 팔레트'; - - @override - String get commandPaletteHint => '명령을 입력하세요'; - - @override - String get commandPaletteEmpty => '일치하는 명령이 없습니다.'; - @override String get commandUnavailableInContext => '현재 편집기 컨텍스트에서는 사용할 수 없습니다.'; @@ -784,7 +897,7 @@ class AppLocalizationsKo extends AppLocalizations { String get shortcutSearchDescription => '현재 작업공간 검색'; @override - String get shortcutKeyboardShortcutsDescription => '이 키보드 단축키 참조 표시'; + String get shortcutKeyboardShortcutsDescription => '애플리케이션 키보드 단축키 사용자 지정'; @override String get shortcutSyntaxReferenceDescription => '구문 참조 열기'; @@ -1010,7 +1123,7 @@ class AppLocalizationsKo extends AppLocalizations { String get validateOnEdit => '편집할 때 검증'; @override - String get clearRecentWorkspaces => '최근 작업공간 지우기'; + String get clearRecentWorkspaces => '최근 기록 지우기'; @override String get editingButtonsPosition => '편집 버튼 위치'; @@ -1779,15 +1892,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get sourceSearchNextMatch => '다음 일치 항목'; - @override - String get sourceSearchCaseSensitive => '대소문자 구분'; - - @override - String get sourceSearchWholeWord => '전체 단어'; - - @override - String get sourceSearchRegex => '정규식'; - @override String get sourceSearchReplacement => '다음으로 바꾸기'; @@ -1909,13 +2013,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get saveAs => '다른 이름으로 저장'; - @override - String get sourceSearchInvalidRegex => '잘못된 정규식'; - - @override - String get sourceSearchZeroLengthUnsupported => - '길이가 0인 정규식 일치는 지원하지 않습니다(예: ^, \$, 위치에만 일치하는 전방 탐색).'; - @override String get workspaceSearchIncomplete => '일부 결과만 표시됩니다. 더 표시하거나 검색 범위를 좁히세요.'; @@ -2021,6 +2118,11 @@ class AppLocalizationsKo extends AppLocalizations { @override String get documentKindResourceFile => '리소스 파일'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return '최근 항목을 업데이트하지 못했습니다: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return '열기 실패: $error'; @@ -4228,9 +4330,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get clipboardHistory => '클립보드 기록'; - @override - String get clipboardHistorySearchHint => '클립보드 기록 검색'; - @override String get clipboardHistoryDescription => '세션별 클립보드 기록을 엽니다.'; @@ -4251,9 +4350,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get clipboardNoItems => '클립보드 기록이 비어 있습니다.'; - @override - String get clipboardNoMatches => '이 검색과 일치하는 클립보드 항목이 없습니다.'; - @override String get clipboardDisabled => '클립보드 기록 컬렉션이 설정에서 비활성화되었습니다.'; @@ -4304,12 +4400,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get localHistoryNoRevisions => '이 문서는 로컬 히스토리 수정 사항이 없습니다.'; - @override - String get localHistoryNoMatches => '이 검색과 일치하는 로컬 히스토리 수정 사항이 없습니다.'; - - @override - String get localHistorySearchHint => '로컬 기록 검색'; - @override String get localHistorySelectedRevision => '선택된 수정'; @@ -5244,4 +5334,381 @@ class AppLocalizationsKo extends AppLocalizations { @override String get wsPropertiesUnavailable => '검사 중인 요소를 사용할 수 없습니다. 계속하려면 요소를 검사하세요.'; + + @override + String get searchDocumentsCategory => '문서'; + + @override + String get searchTopicsCategory => '주제'; + + @override + String get searchCurrentDocument => '현재 문서'; + + @override + String get searchCurrentWorkspace => '현재 작업 공간'; + + @override + String get browseNotes => '노트 찾아보기'; + + @override + String get searchActionsCommand => '작업 검색'; + + @override + String get searchDocumentsCommand => '문서 검색'; + + @override + String get searchTopicsCommand => '주제 검색'; + + @override + String get searchRecentCommand => '최근 항목 검색'; + + @override + String get searchResourceUnavailable => '이 리소스를 사용할 수 없습니다.'; + + @override + String get notesSync => '노트 동기화'; + + @override + String get searchInWorkspace => '작업 공간에서 검색'; + + @override + String get searchCategory => '검색 범주'; + + @override + String get clearHistorySearchScope => '문서 기록 전체 검색'; + + @override + String get searchInDocument => '문서에서 검색'; + + @override + String get searchDocumentHint => '이 문서에서 검색…'; + + @override + String get searchWorkspaceHint => '이 작업 공간에서 검색…'; + + @override + String get searchOutlineHint => '제목 검색…'; + + @override + String get searchHistoryHint => '로컬 기록 검색…'; + + @override + String get searchEnterText => '검색할 텍스트 입력'; + + @override + String get searchAccountUnavailable => '계정을 사용할 수 없음'; + + @override + String get searchDocumentsHint => '문서 검색…'; + + @override + String get searchTopicsHint => '주제 검색…'; + + @override + String get searchRecentHint => '최근 항목 검색…'; + + @override + String get searchActionsHint => '작업 검색…'; + + @override + String get searchRequiresNotesWorkspace => '노트 작업 공간 필요'; + + @override + String get searchSidebarLabel => '검색:'; + + @override + String get searchDocumentTextCategory => '문서 텍스트'; + + @override + String get searchWorkspaceTextCategory => '작업 공간 텍스트'; + + @override + String get searchHeadingsCategory => '제목'; + + @override + String get shortcutUnassigned => '할당되지 않음'; + + @override + String get shortcutPressKeys => '키 조합을 누르세요…'; + + @override + String get shortcutRecord => '단축키 기록'; + + @override + String get shortcutRestoreAll => '모든 기본값 복원'; + + @override + String get shortcutRestoreAllQuestion => + '모든 동작의 기본 단축키를 복원할까요? 사용자 지정 할당이 제거됩니다.'; + + @override + String get shortcutSaveFailed => + '단축키가 활성화되었지만 저장하지 못했습니다. 다시 시작 후에도 유지하려면 저장을 다시 시도하세요.'; + + @override + String get shortcutRetry => '저장 다시 시도'; + + @override + String get shortcutChanged => '할당이 변경되었습니다. 현재 단축키를 확인하고 다시 시도하세요.'; + + @override + String get shortcutInvalid => '이 조합은 이 문맥에서 할당할 수 없습니다.'; + + @override + String get shortcutSystemWarning => + '데스크톱에서 이 조합을 사용할 수 있습니다. BusyMark는 시스템 단축키를 재정의할 수 없습니다.'; + + @override + String get shortcutConflicts => '이 단축키는 다음 동작과 충돌합니다:'; + + @override + String get shortcutRemove => '할당 제거'; + + @override + String get shortcutRestoreDefault => '기본값 복원'; + + @override + String get shortcutAssign => '할당'; + + @override + String get shortcutReassign => '다시 할당'; + + @override + String get shortcutContextApplication => '애플리케이션'; + + @override + String get shortcutContextDocument => '문서 보기'; + + @override + String get shortcutContextEditor => '편집기 및 소스'; + + @override + String get shortcutContextWritersideSource => 'Writerside 소스'; + + @override + String get shortcutContextWorkspace => '작업 공간 탐색'; + + @override + String get shortcutContextTree => '사이드바 트리'; + + @override + String get searchHeadingsCommand => '제목 검색'; + + @override + String get shortcutFixed => '편집기에서 관리하므로 설정할 수 없습니다.'; + + @override + String get shortcutDefaultConflict => '기본 단축키가 사용자 지정 할당과 충돌합니다.'; + + @override + String get shortcutCurrentBinding => '현재 단축키'; + + @override + String get shortcutProposedBinding => '제안된 단축키'; + + @override + String get notesActions => '노트 작업'; + + @override + String get noteActions => '이 노트의 작업'; + + @override + String get noteProperties => '노트 속성'; + + @override + String get notesExportAll => '노트 내보내기 · 모든 노트…'; + + @override + String get notesExportCategory => '노트 내보내기 · 범주…'; + + @override + String get notesOfflineAll => '오프라인 사용 설정 · 모든 노트'; + + @override + String get notesOfflineCategory => '오프라인 사용 설정 · 범주'; + + @override + String get notesOfflineRemoveAll => '오프라인 보존 해제 · 모든 노트'; + + @override + String get notesOfflineRemoveCategory => '오프라인 보존 해제 · 범주'; + + @override + String get notesOfflinePause => '다운로드 일시 중지'; + + @override + String get notesOfflineResume => '다운로드 재개'; + + @override + String get notesOfflineKeep => '오프라인 사용 설정'; + + @override + String get notesOfflineRetentionHelp => + '이 요구 사항을 제거해도 기존 캐시 내용은 유지됩니다. 범주나 계정 요구 사항에 따라 노트가 계속 보관될 수 있습니다.'; + + @override + String get notesOfflineBroader => '이 노트는 범주 또는 계정 요구 사항에 따라서도 보관됩니다.'; + + @override + String get notesOfflineExternal => + '외부 미디어에는 연결이 필요합니다. BusyMark는 노트 텍스트와 관리되는 첨부 파일만 보관합니다.'; + + @override + String get notesEmptyAll => '아직 노트가 없습니다. 새 노트를 만드세요.'; + + @override + String get notesEmptyFavorites => '즐겨찾는 노트가 없습니다.'; + + @override + String get notesEmptyCategory => '이 범주에 노트가 없습니다.'; + + @override + String get notesEmptyUncategorized => '분류되지 않은 노트가 없습니다.'; + + @override + String get notesEmptyRecovery => '복구할 보관 노트가 없습니다.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · 실패. 다시 시도'; + + @override + String get notesOperationFailed => + '작업을 완료하지 못했습니다. 초안은 보관됩니다. 저장과 충돌을 검토한 뒤 다시 시도하세요.'; + + @override + String get notesInspectionFailed => '첨부 파일 · 실패. 다시 시도'; + + @override + String get notesStartingConnection => 'Nextcloud 연결 시작 중…'; + + @override + String get notesVerifyingConnection => '노트 접근 확인 중…'; + + @override + String get notesCancellingConnection => '로그인을 취소하고 정리 중…'; + + @override + String get notesRemovingConnection => '로컬 계정 제거 중…'; + + @override + String get notesRemovedWarning => + '로컬 Nextcloud 계정을 제거했습니다. 앱 암호 해지를 확인하지 못했습니다. Nextcloud 보안 설정에서 BusyMark의 앱 암호를 제거하세요.'; + + @override + String get notesBuffersNotSaved => + '열린 노트를 모두 로컬에 저장하지 못했습니다. 작업 공간에서 저장, 충돌 또는 정규화를 검토하세요. 로컬 변경 사항은 아직 Nextcloud와 동기화해야 할 수 있습니다.'; + + @override + String get notesSettingsLeave => '나가기 전에 Nextcloud 설정 변경 사항을 저장할까요?'; + + @override + String get notesSettingsLeaveHelp => '서버 Notes 설정 · 저장되지 않은 변경 사항'; + + @override + String get notesSettingsReload => '서버 설정 다시 불러오기…'; + + @override + String get notesSettingsReloadHelp => + '다시 불러오면 초안이 현재 Nextcloud 설정으로 대체됩니다. 초안을 버리고 다시 불러올까요?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud가 설정을 수락했지만 노트 새로 고침에 실패했습니다. 표시된 값은 서버에서 확인한 값입니다. 노트에서 새로 고침을 다시 시도하세요.'; + + @override + String get notesUnavailable => '오프라인 · 텍스트를 로컬에서 사용 가능 · 로컬에 저장됨 · 동기화 대기'; + + @override + String get notesStorageFull => + 'Nextcloud 저장 공간이 가득 찼습니다. 서버 공간을 확보한 뒤 다시 시도하세요.'; + + @override + String get notesRejected => + 'Nextcloud가 이 변경을 거부했습니다. 노트를 검토하고 로컬 사본을 저장한 뒤 다시 시도하세요.'; + + @override + String get notesThrottled => 'Nextcloud가 대기를 요청했습니다. 재시도 기한이 지나면 동기화가 재개됩니다.'; + + @override + String get notesRecoveryRequired => '충돌 · 비교'; + + @override + String get notesScanning => '파일 검색 중…'; + + @override + String get notesCancelling => '취소 중… 현재 작업이 끝나기를 기다리는 중입니다.'; + + @override + String get notesCancelled => '취소됨'; + + @override + String get notesPartial => '부분 완료'; + + @override + String get notesTransferFailed => '실패. 다시 시도'; + + @override + String get notesImportPartialHelp => + '이전 항목은 이미 로컬에 저장되었을 수 있습니다. 중복을 방지하려면 이 가져오기를 재개하세요.'; + + @override + String get notesNotStarted => '시작하지 않음'; + + @override + String get notesCreated => '생성됨'; + + @override + String get notesRemoteDeleteUnavailable => '서버 주소 · 지원되지 않음 · 삭제 · 첨부 파일'; + + @override + String get notesCurrentText => '현재 보관된 텍스트'; + + @override + String get notesRecovering => '새 노트로 복구 중…'; + + @override + String get notesReadOnlyHelp => '읽기 전용 · 즐겨찾기 · 로컬 사본 저장…'; + + @override + String get notesSort => '노트 정렬'; + + @override + String get notesPathRelative => '노트 폴더 경로는 Nextcloud 저장소 기준의 상대 경로입니다.'; + + @override + String get notesSettingsStale => + 'Nextcloud에서 설정이 변경되었습니다. 초안을 대체하기 전에 서버 값을 다시 불러와 검토하세요.'; + + @override + String get notesDownloadsPaused => '다운로드 일시 중지됨'; + + @override + String get notesExportNotPublished => '내보내기가 게시되지 않았습니다.'; + + @override + String get notesKeyringUnavailable => '데스크톱 키링을 잠금 해제한 뒤 다시 시도하세요.'; + + @override + String get notesConnectFailed => + 'Nextcloud Notes에 연결하지 못했습니다. 서버 주소를 확인하고 다시 시도하세요.'; + + @override + String get notesAccountMismatch => + '같은 Nextcloud 계정으로 로그인하세요. 다른 계정을 연결하려면 이 계정을 먼저 제거하세요.'; + + @override + String get notesLocalRemovalFailed => + '로컬 계정 정리에 실패했습니다. 제거를 다시 시도할 수 있도록 계정을 유지합니다.'; + + @override + String get notesTransferring => '노트 전송 중…'; + + @override + String get notesTechnicalDetails => '기술 정보'; + + @override + String get notesPauseRequirement => '오프라인 보관 요구 사항 일시 중지'; + + @override + String get notesResumeRequirement => '오프라인 보관 요구 사항 재개'; } diff --git a/lib/l10n/generated/app_localizations_nb.dart b/lib/l10n/generated/app_localizations_nb.dart index 020f3e77..a816e774 100644 --- a/lib/l10n/generated/app_localizations_nb.dart +++ b/lib/l10n/generated/app_localizations_nb.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsNb extends AppLocalizations { AppLocalizationsNb([String locale = 'nb']) : super(locale); + @override + String get notesNewFromRetainedText => 'Nytt notat fra bevart tekst'; + + @override + String get notesAccountThrottled => + 'Nextcloud ba om en pause. Vent før du starter innloggingen på nytt.'; + + @override + String get notesSettingsRefreshing => + 'Oppdaterer Notes etter bekreftet innstillingsendring'; + + @override + String get notesExportOperationFailed => + 'Eksporten mislyktes før publisering. Kontroller kilde og mål, og eksporter igjen.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Den gjenopprettede notaten er lagret, men kunne ikke åpnes. Avklar ventende redigerings- eller egenskapsendringer, og åpne denne eksisterende notaten.'; + + @override + String get notesSettingsPendingNotes => + 'Ventende notater, konflikter eller usikre opprettelser blokkerer endringen. Synkroniser eller gjennomgå dem i Notes, og prøv det bevarte innstillingsutkastet igjen.'; + + @override + String get notesSettingsPendingAttachments => + 'Ventende vedleggsoperasjoner blokkerer endringen. Gjennomgå vedlegg i Notes, og prøv det bevarte innstillingsutkastet igjen.'; + + @override + String get notesAccountServerAddress => + 'Bruk Nextclouds kanoniske HTTPS-adresse og start innloggingen på nytt. Adressen eller videresendingen kunne ikke bekreftes.'; + + @override + String get notesAccountUnsupported => + 'Notes er utilgjengelig eller API-et støttes ikke. Aktiver Nextcloud Notes med API-hovedversjon 1, underversjon 4 eller nyere.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud avviste påloggingsinformasjonen. Godkjenn kontoen på nytt eller koble til igjen.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud nektet kontroll av Notes-funksjoner. Be administratoren tillate den, og prøv igjen.'; + + @override + String get notesAccountMalformed => + 'Nextcloud-svaret kunne ikke bekreftes. Kontroller serverinstallasjonen og logg inn igjen.'; + + @override + String get notesAccountTimedOut => + 'Innloggingen ble tidsavbrutt. Start igjen og fullfør godkjenningen i nettleseren.'; + + @override + String get notesAccountBrowserUnavailable => + 'Systemnettleseren kunne ikke åpnes. Kontroller standardnettleseren og logg inn igjen.'; + + @override + String get notesAccountNetwork => + 'Nextcloud kunne ikke nås. Kontroller tilkobling og server, og logg inn igjen.'; + + @override + String get notesAccountRejected => + 'Nextcloud avviste funksjonsforespørselen. Kontroller tilgang med administratoren og prøv igjen.'; + + @override + String get notesAccountFailed => + 'Kontooppsettet kunne ikke fullføres. Prøv å logge inn igjen.'; + + @override + String get notesTransferSourceChanged => + 'Kilden ble endret etter gjennomgang. Gjennomgå gjeldende fil eller lag et nytt øyeblikksbilde før nytt forsøk.'; + + @override + String get notesTransferUnreadable => + 'Kilden kunne ikke leses. Kontroller tilgang til fil eller mappe, og gjennomgå igjen.'; + + @override + String get notesTransferInvalidSnapshot => + 'Øyeblikksbildet er ugyldig eller ustøttet. Velg et gyldig BusyMark Notes-bilde eller en Markdown-kilde.'; + + @override + String get notesTransferMediaLimit => + 'Medier overskrider importgrensen. Reduser mediene for berørt element og gjennomgå igjen.'; + + @override + String get notesTransferUnsafeSource => + 'Kilden inneholder en utrygg sti. Velg filer i kildemappen og gjennomgå igjen.'; + + @override + String get notesTransferDestination => + 'Eksporten kunne ikke skrives. Kontroller tilgang og ledig plass på målet, og eksporter igjen.'; + + @override + String get notesTransferOperationFailed => + 'Overføringen kunne ikke fullføres. Gjennomgå berørte elementer og prøv igjen. Tidligere importerte elementer er lagret.'; + + @override + String get notesExportPublishedWithOmissions => + 'Eksport tilgjengelig med utelatelser'; + + @override + String get notesBatchUnchanged => 'De ønskede verdiene var allerede angitt.'; + + @override + String get notesBatchReadOnly => + 'Notaten er skrivebeskyttet. Beskyttede egenskaper kan ikke endres; Favoritt er fortsatt tillatt.'; + + @override + String get notesBatchReviewChanged => + 'Notaten ble endret etter gjennomgang. Gjennomgå gjeldende tilstand før handlingen gjentas.'; + + @override + String get notesBatchMissing => + 'Notaten er ikke lenger tilgjengelig. Oppdater listen og gjennomgå målene igjen.'; + + @override + String get notesBatchStorage => + 'Lokal lagring mislyktes. Kontroller lagringsplassen og prøv dette elementet igjen; vellykkede elementer er lagret.'; + + @override + String get notesRemoteReviewChanged => + 'Servernotaten ble endret. Fletteutkastet er bevart. Gjennomgå den oppdaterte sammenligningen og velg løsning på nytt.'; + + @override + String get notesReviewRemote => 'Jeg har gjennomgått gjeldende serverversjon'; + @override String get wsModuleCapsNotPublished => 'Modulens bokstavstil publiseres ikke.'; @@ -588,15 +713,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get keyboardShortcuts => 'Tastatursnarveier'; - @override - String get commandPalette => 'Kommandopalett'; - - @override - String get commandPaletteHint => 'Skriv inn en kommando'; - - @override - String get commandPaletteEmpty => 'Ingen samsvarende kommandoer'; - @override String get commandUnavailableInContext => 'Denne kommandoen er ikke tilgjengelig i gjeldende kontekst.'; @@ -815,7 +931,7 @@ class AppLocalizationsNb extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Vis denne oversikten over tastatursnarveier'; + 'Tilpass programmets hurtigtaster'; @override String get shortcutSyntaxReferenceDescription => 'Åpne syntaksreferansen'; @@ -1063,7 +1179,7 @@ class AppLocalizationsNb extends AppLocalizations { String get validateOnEdit => 'Valider under redigering'; @override - String get clearRecentWorkspaces => 'Tøm nylige arbeidsområder'; + String get clearRecentWorkspaces => 'Tøm nylig historikk'; @override String get editingButtonsPosition => 'Plassering av redigeringsknapper'; @@ -1847,15 +1963,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get sourceSearchNextMatch => 'Neste treff'; - @override - String get sourceSearchCaseSensitive => 'Skill mellom store og små bokstaver'; - - @override - String get sourceSearchWholeWord => 'Hele ord'; - - @override - String get sourceSearchRegex => 'Regulært uttrykk'; - @override String get sourceSearchReplacement => 'Erstatt med'; @@ -1983,13 +2090,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get saveAs => 'Lagre som'; - @override - String get sourceSearchInvalidRegex => 'Ugyldig regulært uttrykk'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Regex-treff med null lengde støttes ikke (for eksempel ^, \$ eller fremoversøk som bare samsvarer med en posisjon).'; - @override String get workspaceSearchIncomplete => 'Bare noen av resultatene vises. Vis flere eller avgrens søket.'; @@ -2098,6 +2198,11 @@ class AppLocalizationsNb extends AppLocalizations { @override String get documentKindResourceFile => 'Ressursfil'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Kunne ikke oppdatere nylige elementer: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Åpning mislyktes: $error'; @@ -4354,9 +4459,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get clipboardHistory => 'Utklippshistorie'; - @override - String get clipboardHistorySearchHint => 'Søk i utklippstavlehistorikken'; - @override String get clipboardHistoryDescription => 'Åpne historien for utklippet som er lokal for sesjonen.'; @@ -4379,10 +4481,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get clipboardNoItems => 'Utklippshistorien er tom.'; - @override - String get clipboardNoMatches => - 'Ingen utklippsposter samsvarer med denne søkingen.'; - @override String get clipboardDisabled => 'Samlingen \"Utklippshistorie\" er deaktivert i Innstillinger.'; @@ -4440,12 +4538,6 @@ class AppLocalizationsNb extends AppLocalizations { String get localHistoryNoRevisions => 'Dette dokumentet har ingen revisjoner i Lokal Historikk.'; - @override - String get localHistoryNoMatches => 'Ingen revisjoner matcher denne søken.'; - - @override - String get localHistorySearchHint => 'Søk i lokal historikk'; - @override String get localHistorySelectedRevision => 'Valgt revisjon'; @@ -5402,4 +5494,398 @@ class AppLocalizationsNb extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Det inspiserte elementet er utilgjengelig. Inspiser et element for å fortsette.'; + + @override + String get searchDocumentsCategory => 'Dokumenter'; + + @override + String get searchTopicsCategory => 'Emner'; + + @override + String get searchCurrentDocument => 'Gjeldende dokument'; + + @override + String get searchCurrentWorkspace => 'Gjeldende arbeidsområde'; + + @override + String get browseNotes => 'Bla gjennom notater'; + + @override + String get searchActionsCommand => 'Søk etter handlinger'; + + @override + String get searchDocumentsCommand => 'Søk etter dokumenter'; + + @override + String get searchTopicsCommand => 'Søk etter emner'; + + @override + String get searchRecentCommand => 'Søk i nylige'; + + @override + String get searchResourceUnavailable => 'Denne ressursen er utilgjengelig.'; + + @override + String get notesSync => 'Synkroniser notater'; + + @override + String get searchInWorkspace => 'Søk i arbeidsområde'; + + @override + String get searchCategory => 'Søkekategori'; + + @override + String get clearHistorySearchScope => 'Søk i dokumenthistorikken'; + + @override + String get searchInDocument => 'Søk i dokument'; + + @override + String get searchDocumentHint => 'Søk i dette dokumentet…'; + + @override + String get searchWorkspaceHint => 'Søk i dette arbeidsområdet…'; + + @override + String get searchOutlineHint => 'Søk i overskrifter…'; + + @override + String get searchHistoryHint => 'Søk i lokal historikk…'; + + @override + String get searchEnterText => 'Skriv inn tekst for å søke'; + + @override + String get searchAccountUnavailable => 'Konto utilgjengelig'; + + @override + String get searchDocumentsHint => 'Søk etter dokumenter…'; + + @override + String get searchTopicsHint => 'Søk etter emner…'; + + @override + String get searchRecentHint => 'Søk i nylige…'; + + @override + String get searchActionsHint => 'Søk etter handlinger…'; + + @override + String get searchRequiresNotesWorkspace => 'Krever et notatarbeidsområde'; + + @override + String get searchSidebarLabel => 'Søk:'; + + @override + String get searchDocumentTextCategory => 'Dokumenttekst'; + + @override + String get searchWorkspaceTextCategory => 'Arbeidsområdetekst'; + + @override + String get searchHeadingsCategory => 'Overskrifter'; + + @override + String get shortcutUnassigned => 'Ikke tilordnet'; + + @override + String get shortcutPressKeys => 'Trykk en tastekombinasjon…'; + + @override + String get shortcutRecord => 'Registrer hurtigtast'; + + @override + String get shortcutRestoreAll => 'Gjenopprett alle standarder'; + + @override + String get shortcutRestoreAllQuestion => + 'Gjenopprette standardhurtigtaster for alle handlinger? Egne tilordninger fjernes.'; + + @override + String get shortcutSaveFailed => + 'Hurtigtastene er aktive, men kunne ikke lagres. Prøv igjen for å beholde dem etter omstart.'; + + @override + String get shortcutRetry => 'Prøv lagring igjen'; + + @override + String get shortcutChanged => + 'Tilordningene er endret. Se gjennom gjeldende hurtigtaster og prøv igjen.'; + + @override + String get shortcutInvalid => + 'Denne kombinasjonen kan ikke tilordnes i denne sammenhengen.'; + + @override + String get shortcutSystemWarning => + 'Skrivebordet kan bruke denne kombinasjonen. BusyMark kan ikke overstyre systemhurtigtaster.'; + + @override + String get shortcutConflicts => + 'Denne hurtigtasten overlapper disse handlingene:'; + + @override + String get shortcutRemove => 'Fjern tilordning'; + + @override + String get shortcutRestoreDefault => 'Gjenopprett standard'; + + @override + String get shortcutAssign => 'Tilordne'; + + @override + String get shortcutReassign => 'Tilordne på nytt'; + + @override + String get shortcutContextApplication => 'Program'; + + @override + String get shortcutContextDocument => 'Dokumentvisninger'; + + @override + String get shortcutContextEditor => 'Redigering og kilde'; + + @override + String get shortcutContextWritersideSource => 'Writerside-kilde'; + + @override + String get shortcutContextWorkspace => 'Arbeidsområdenavigasjon'; + + @override + String get shortcutContextTree => 'Sidefelttrær'; + + @override + String get searchHeadingsCommand => 'Søk i overskrifter'; + + @override + String get shortcutFixed => 'Håndteres av redigereren; kan ikke tilpasses.'; + + @override + String get shortcutDefaultConflict => + 'Standardhurtigtasten er i konflikt med en egendefinert tilordning.'; + + @override + String get shortcutCurrentBinding => 'Gjeldende hurtigtast'; + + @override + String get shortcutProposedBinding => 'Foreslått hurtigtast'; + + @override + String get notesActions => 'Notathandlinger'; + + @override + String get noteActions => 'Handlinger for notatet'; + + @override + String get noteProperties => 'Notategenskaper'; + + @override + String get notesExportAll => 'Eksporter notater · Alle notater…'; + + @override + String get notesExportCategory => 'Eksporter notater · Kategori…'; + + @override + String get notesOfflineAll => 'Gjør tilgjengelig frakoblet · Alle notater'; + + @override + String get notesOfflineCategory => 'Gjør tilgjengelig frakoblet · Kategori'; + + @override + String get notesOfflineRemoveAll => + 'Fjern frakoblet oppbevaring · Alle notater'; + + @override + String get notesOfflineRemoveCategory => + 'Fjern frakoblet oppbevaring · Kategori'; + + @override + String get notesOfflinePause => 'Sett nedlastinger på pause'; + + @override + String get notesOfflineResume => 'Fortsett nedlastinger'; + + @override + String get notesOfflineKeep => 'Gjør tilgjengelig frakoblet'; + + @override + String get notesOfflineRetentionHelp => + 'Å fjerne dette kravet beholder eksisterende hurtigbuffer. Et kategori- eller kontokrav kan fortsatt beholde notatet.'; + + @override + String get notesOfflineBroader => + 'Notatet beholdes også av et kategori- eller kontokrav.'; + + @override + String get notesOfflineExternal => + 'Eksterne medier krever en tilkobling. BusyMark beholder bare notattekst og administrerte vedlegg.'; + + @override + String get notesEmptyAll => + 'Ingen notater ennå. Opprett et notat for å begynne.'; + + @override + String get notesEmptyFavorites => 'Ingen favorittnotater.'; + + @override + String get notesEmptyCategory => 'Ingen notater i denne kategorien.'; + + @override + String get notesEmptyUncategorized => 'Ingen ukategoriserte notater.'; + + @override + String get notesEmptyRecovery => 'Ingen beholdte notater å gjenopprette.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Mislyktes. Prøv igjen'; + + @override + String get notesOperationFailed => + 'Handlingen kunne ikke fullføres. Utkastet er beholdt. Kontroller lagring og konflikter, og prøv igjen.'; + + @override + String get notesInspectionFailed => 'Vedlegg · Mislyktes. Prøv igjen'; + + @override + String get notesStartingConnection => 'Starter Nextcloud-tilkoblingen…'; + + @override + String get notesVerifyingConnection => 'Kontrollerer tilgang til notater…'; + + @override + String get notesCancellingConnection => 'Avbryter innlogging og rydder opp…'; + + @override + String get notesRemovingConnection => 'Fjerner den lokale kontoen…'; + + @override + String get notesRemovedWarning => + 'Den lokale Nextcloud-kontoen er fjernet. Tilbakekalling av app-passordet er ikke bekreftet. Fjern BusyMarks passord i sikkerhetsinnstillingene i Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Ikke alle åpne notater kunne lagres lokalt. Kontroller lagring, konflikter eller normalisering i arbeidsområdet. Lokale endringer må kanskje fortsatt synkroniseres med Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Lagre endringer i Nextcloud-innstillinger før du går videre?'; + + @override + String get notesSettingsLeaveHelp => + 'Notes-innstillinger på serveren · Ulagrede endringer'; + + @override + String get notesSettingsReload => 'Last inn serverinnstillinger på nytt…'; + + @override + String get notesSettingsReloadHelp => + 'Ny innlasting erstatter utkastet med gjeldende Nextcloud-innstillinger. Forkaste utkastet og laste inn på nytt?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud godtok innstillingene, men oppdatering av notatene mislyktes. Verdiene som vises er bekreftet av serveren. Prøv å oppdatere fra Notater igjen.'; + + @override + String get notesUnavailable => + 'Frakoblet · Tekst tilgjengelig lokalt · Lagret lokalt · venter på synkronisering'; + + @override + String get notesStorageFull => + 'Nextcloud-lagringen er full. Frigjør plass på serveren og prøv igjen.'; + + @override + String get notesRejected => + 'Nextcloud avviste endringen. Kontroller notatet og lagre en lokal kopi før du prøver igjen.'; + + @override + String get notesThrottled => + 'Nextcloud ba om en pause. Synkroniseringen fortsetter etter ventefristen.'; + + @override + String get notesRecoveryRequired => 'Konflikter · Sammenlign'; + + @override + String get notesScanning => 'Skanner filer…'; + + @override + String get notesCancelling => + 'Avbryter… Venter på at gjeldende handling avsluttes.'; + + @override + String get notesCancelled => 'Avbrutt'; + + @override + String get notesPartial => 'Delvis fullført'; + + @override + String get notesTransferFailed => 'Mislyktes. Prøv igjen'; + + @override + String get notesImportPartialHelp => + 'Tidligere elementer kan allerede være lagret lokalt. Fortsett denne importen for å unngå duplikater.'; + + @override + String get notesNotStarted => 'Ikke startet'; + + @override + String get notesCreated => 'Opprettet'; + + @override + String get notesRemoteDeleteUnavailable => + 'Serveradresse · Ikke støttet · Slett · Vedlegg'; + + @override + String get notesCurrentText => 'Gjeldende beholdte tekst'; + + @override + String get notesRecovering => 'Gjenoppretter som et nytt notat…'; + + @override + String get notesReadOnlyHelp => + 'Skrivebeskyttet · Favoritter · Lagre lokal kopi…'; + + @override + String get notesSort => 'Sorter notater'; + + @override + String get notesPathRelative => + 'Notatmappens sti er relativ til Nextcloud-lagringen din.'; + + @override + String get notesSettingsStale => + 'Innstillingene er endret i Nextcloud. Last inn og kontroller serververdiene før du erstatter utkastet.'; + + @override + String get notesDownloadsPaused => 'Nedlastinger satt på pause'; + + @override + String get notesExportNotPublished => 'Ingen eksport ble publisert.'; + + @override + String get notesKeyringUnavailable => + 'Lås opp skrivebordets nøkkelring og prøv igjen.'; + + @override + String get notesConnectFailed => + 'Kunne ikke koble til Nextcloud Notes. Kontroller serveradressen og prøv igjen.'; + + @override + String get notesAccountMismatch => + 'Logg inn på samme Nextcloud-konto. Fjern denne kontoen først for å koble til en annen.'; + + @override + String get notesLocalRemovalFailed => + 'Lokal kontoopprydding mislyktes. Kontoen beholdes slik at du kan prøve å fjerne den igjen.'; + + @override + String get notesTransferring => 'Overfører notater…'; + + @override + String get notesTechnicalDetails => 'Tekniske detaljer'; + + @override + String get notesPauseRequirement => 'Sett frakoblet krav på pause'; + + @override + String get notesResumeRequirement => 'Fortsett frakoblet krav'; } diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index 5f8ee0e5..363d7828 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsNl extends AppLocalizations { AppLocalizationsNl([String locale = 'nl']) : super(locale); + @override + String get notesNewFromRetainedText => 'Nieuwe notitie van bewaarde tekst'; + + @override + String get notesAccountThrottled => + 'Nextcloud vraagt om een pauze. Wacht voordat u opnieuw aanmeldt.'; + + @override + String get notesSettingsRefreshing => + 'Notes vernieuwen na de bevestigde instellingenwijziging'; + + @override + String get notesExportOperationFailed => + 'De export is vóór publicatie mislukt. Controleer bron en bestemming en exporteer opnieuw.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'De herstelde notitie is opgeslagen maar kon niet worden geopend. Los openstaande editor- of eigenschapswijzigingen op en open deze bestaande notitie.'; + + @override + String get notesSettingsPendingNotes => + 'Openstaande notities, conflicten of onzekere aanmaak blokkeren deze wijziging. Synchroniseer of beoordeel ze in Notes en probeer dit bewaarde concept opnieuw.'; + + @override + String get notesSettingsPendingAttachments => + 'Openstaande bijlagebewerkingen blokkeren deze wijziging. Beoordeel ze in Notes en probeer dit bewaarde instellingenconcept opnieuw.'; + + @override + String get notesAccountServerAddress => + 'Gebruik het canonieke HTTPS-adres van Nextcloud en meld opnieuw aan. Het adres of de omleiding kon niet worden geverifieerd.'; + + @override + String get notesAccountUnsupported => + 'Notes is niet beschikbaar of de API wordt niet ondersteund. Schakel Nextcloud Notes in met API-hoofdversie 1, subversie 4 of hoger.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud heeft de aanmeldgegevens geweigerd. Autoriseer dit account opnieuw of verbind opnieuw.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud heeft de controle van Notes-mogelijkheden geweigerd. Vraag toestemming aan de beheerder en probeer opnieuw.'; + + @override + String get notesAccountMalformed => + 'Het Nextcloud-antwoord kon niet worden geverifieerd. Controleer de serverinstallatie en meld opnieuw aan.'; + + @override + String get notesAccountTimedOut => + 'De aanmelding is verlopen. Begin opnieuw en voltooi de toestemming in de browser.'; + + @override + String get notesAccountBrowserUnavailable => + 'De systeembrowser kon niet worden geopend. Controleer de standaardbrowser en meld opnieuw aan.'; + + @override + String get notesAccountNetwork => + 'Nextcloud is onbereikbaar. Controleer de verbinding en server en meld opnieuw aan.'; + + @override + String get notesAccountRejected => + 'Nextcloud heeft het mogelijkhedenverzoek geweigerd. Controleer toegang met de beheerder en probeer opnieuw.'; + + @override + String get notesAccountFailed => + 'Accountinstelling is mislukt. Probeer opnieuw aan te melden.'; + + @override + String get notesTransferSourceChanged => + 'De bron is na beoordeling gewijzigd. Beoordeel het huidige bestand of maak een nieuwe momentopname.'; + + @override + String get notesTransferUnreadable => + 'De bron kon niet worden gelezen. Controleer toegang tot het bestand of de map en beoordeel opnieuw.'; + + @override + String get notesTransferInvalidSnapshot => + 'Ongeldige of niet-ondersteunde momentopname. Kies een geldige BusyMark Notes-momentopname of Markdown-bron.'; + + @override + String get notesTransferMediaLimit => + 'Media overschrijden de importlimiet. Verklein de media van het betreffende item en beoordeel opnieuw.'; + + @override + String get notesTransferUnsafeSource => + 'De bron bevat een onveilig pad. Kies bestanden binnen de bronmap en beoordeel opnieuw.'; + + @override + String get notesTransferDestination => + 'De export kon niet worden geschreven. Controleer toegang en vrije ruimte bij de bestemming en exporteer opnieuw.'; + + @override + String get notesTransferOperationFailed => + 'De overdracht is mislukt. Beoordeel de betreffende items en probeer opnieuw. Eerder geïmporteerde items blijven opgeslagen.'; + + @override + String get notesExportPublishedWithOmissions => + 'Export met weglatingen beschikbaar'; + + @override + String get notesBatchUnchanged => 'De gevraagde waarden waren al ingesteld.'; + + @override + String get notesBatchReadOnly => + 'Deze notitie is alleen-lezen. Beveiligde eigenschappen kunnen niet wijzigen; Favoriet blijft toegestaan.'; + + @override + String get notesBatchReviewChanged => + 'Deze notitie is na beoordeling gewijzigd. Beoordeel de huidige staat voordat u de actie herhaalt.'; + + @override + String get notesBatchMissing => + 'Deze notitie is niet meer beschikbaar. Vernieuw de lijst en beoordeel de doelen opnieuw.'; + + @override + String get notesBatchStorage => + 'Lokaal opslaan is mislukt. Controleer de opslag en probeer dit item opnieuw; geslaagde items blijven opgeslagen.'; + + @override + String get notesRemoteReviewChanged => + 'De servernotitie is gewijzigd. Uw samenvoegconcept blijft bewaard. Beoordeel de nieuwe vergelijking en kies opnieuw een oplossing.'; + + @override + String get notesReviewRemote => 'Ik heb de huidige serverversie beoordeeld'; + @override String get wsModuleCapsNotPublished => 'Hoofdlettergebruik van de module wordt niet gepubliceerd.'; @@ -589,15 +714,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get keyboardShortcuts => 'Sneltoetsen op het toetsenbord'; - @override - String get commandPalette => 'Commandopalet'; - - @override - String get commandPaletteHint => 'Typ een opdracht'; - - @override - String get commandPaletteEmpty => 'Geen overeenkomende opdrachten'; - @override String get commandUnavailableInContext => 'Niet beschikbaar in de huidige editorcontext'; @@ -816,7 +932,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Toon deze sneltoetsreferentie'; + 'Sneltoetsen van de toepassing aanpassen'; @override String get shortcutSyntaxReferenceDescription => 'Syntaxisreferentie openen'; @@ -1064,7 +1180,7 @@ class AppLocalizationsNl extends AppLocalizations { String get validateOnEdit => 'Valideren bij bewerken'; @override - String get clearRecentWorkspaces => 'Wis recente werkruimten'; + String get clearRecentWorkspaces => 'Recente geschiedenis wissen'; @override String get editingButtonsPosition => 'Positie van knoppen bewerken'; @@ -1850,15 +1966,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get sourceSearchNextMatch => 'Volgende overeenkomst'; - @override - String get sourceSearchCaseSensitive => 'Hoofdlettergevoelig'; - - @override - String get sourceSearchWholeWord => 'Heel woord'; - - @override - String get sourceSearchRegex => 'Regex'; - @override String get sourceSearchReplacement => 'Vervangen door'; @@ -1992,13 +2099,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get saveAs => 'Opslaan als'; - @override - String get sourceSearchInvalidRegex => 'Ongeldige reguliere expressie'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Regex-overeenkomsten met lengte nul worden niet ondersteund (zoals ^, \$ of lookaheads die alleen een positie vinden).'; - @override String get workspaceSearchIncomplete => 'Slechts een deel van de resultaten wordt getoond. Toon meer of verfijn de zoekopdracht.'; @@ -2108,6 +2208,11 @@ class AppLocalizationsNl extends AppLocalizations { @override String get documentKindResourceFile => 'Bronbestand'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Recente items konden niet worden bijgewerkt: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Openen mislukt: $error'; @@ -4393,9 +4498,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get clipboardHistory => 'Historie van de klembord'; - @override - String get clipboardHistorySearchHint => 'Klembordgeschiedenis doorzoeken'; - @override String get clipboardHistoryDescription => 'Open de klembordgeschiedenis per sessie.'; @@ -4419,10 +4521,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get clipboardNoItems => 'De klembordgeschiedenis is leeg.'; - @override - String get clipboardNoMatches => - 'Geen klemborditems komen overeen met deze zoekopdracht.'; - @override String get clipboardDisabled => 'De collectie \"Historie van het klembord\" is uitgeschakeld in de instellingen.'; @@ -4480,13 +4578,6 @@ class AppLocalizationsNl extends AppLocalizations { String get localHistoryNoRevisions => 'Dit document heeft geen revisies van de Lokale Geschiedenis.'; - @override - String get localHistoryNoMatches => - 'Geen revisies van de Lokale Geschiedenis komen overeen met deze zoekopdracht.'; - - @override - String get localHistorySearchHint => 'Lokale geschiedenis doorzoeken'; - @override String get localHistorySelectedRevision => 'Geselecteerde revisie'; @@ -5448,4 +5539,397 @@ class AppLocalizationsNl extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Het geïnspecteerde element is niet beschikbaar. Inspecteer een element om door te gaan.'; + + @override + String get searchDocumentsCategory => 'Documenten'; + + @override + String get searchTopicsCategory => 'Onderwerpen'; + + @override + String get searchCurrentDocument => 'Huidig document'; + + @override + String get searchCurrentWorkspace => 'Huidige werkruimte'; + + @override + String get browseNotes => 'Notities bekijken'; + + @override + String get searchActionsCommand => 'Acties zoeken'; + + @override + String get searchDocumentsCommand => 'Documenten zoeken'; + + @override + String get searchTopicsCommand => 'Onderwerpen zoeken'; + + @override + String get searchRecentCommand => 'Recente items zoeken'; + + @override + String get searchResourceUnavailable => 'Deze bron is niet beschikbaar.'; + + @override + String get notesSync => 'Notities synchroniseren'; + + @override + String get searchInWorkspace => 'Zoeken in werkruimte'; + + @override + String get searchCategory => 'Zoekcategorie'; + + @override + String get clearHistorySearchScope => 'Zoeken in documentgeschiedenis'; + + @override + String get searchInDocument => 'Zoeken in document'; + + @override + String get searchDocumentHint => 'Dit document doorzoeken…'; + + @override + String get searchWorkspaceHint => 'Deze werkruimte doorzoeken…'; + + @override + String get searchOutlineHint => 'Koppen zoeken…'; + + @override + String get searchHistoryHint => 'Lokale geschiedenis doorzoeken…'; + + @override + String get searchEnterText => 'Voer tekst in om te zoeken'; + + @override + String get searchAccountUnavailable => 'Account niet beschikbaar'; + + @override + String get searchDocumentsHint => 'Documenten zoeken…'; + + @override + String get searchTopicsHint => 'Onderwerpen zoeken…'; + + @override + String get searchRecentHint => 'Recente items zoeken…'; + + @override + String get searchActionsHint => 'Acties zoeken…'; + + @override + String get searchRequiresNotesWorkspace => 'Vereist een notitiewerkruimte'; + + @override + String get searchSidebarLabel => 'Zoeken:'; + + @override + String get searchDocumentTextCategory => 'Documenttekst'; + + @override + String get searchWorkspaceTextCategory => 'Werkruimtetekst'; + + @override + String get searchHeadingsCategory => 'Koppen'; + + @override + String get shortcutUnassigned => 'Niet toegewezen'; + + @override + String get shortcutPressKeys => 'Druk op een toetscombinatie…'; + + @override + String get shortcutRecord => 'Sneltoets opnemen'; + + @override + String get shortcutRestoreAll => 'Alle standaardwaarden herstellen'; + + @override + String get shortcutRestoreAllQuestion => + 'Standaardsneltoetsen van alle acties herstellen? Aangepaste toewijzingen worden verwijderd.'; + + @override + String get shortcutSaveFailed => + 'Sneltoetsen zijn actief, maar konden niet worden opgeslagen. Probeer opnieuw om ze na herstart te behouden.'; + + @override + String get shortcutRetry => 'Opslaan opnieuw proberen'; + + @override + String get shortcutChanged => + 'Toewijzingen zijn gewijzigd. Controleer de huidige sneltoetsen en probeer opnieuw.'; + + @override + String get shortcutInvalid => + 'Deze combinatie kan in deze context niet worden toegewezen.'; + + @override + String get shortcutSystemWarning => + 'Deze combinatie wordt mogelijk door uw bureaublad gebruikt. BusyMark kan systeemsneltoetsen niet overschrijven.'; + + @override + String get shortcutConflicts => 'Deze sneltoets overlapt met deze acties:'; + + @override + String get shortcutRemove => 'Toewijzing verwijderen'; + + @override + String get shortcutRestoreDefault => 'Standaard herstellen'; + + @override + String get shortcutAssign => 'Toewijzen'; + + @override + String get shortcutReassign => 'Opnieuw toewijzen'; + + @override + String get shortcutContextApplication => 'Toepassing'; + + @override + String get shortcutContextDocument => 'Documentweergaven'; + + @override + String get shortcutContextEditor => 'Editor en bron'; + + @override + String get shortcutContextWritersideSource => 'Writerside-bron'; + + @override + String get shortcutContextWorkspace => 'Werkruimtenavigatie'; + + @override + String get shortcutContextTree => 'Zijbalkbomen'; + + @override + String get searchHeadingsCommand => 'Koppen zoeken'; + + @override + String get shortcutFixed => 'Beheerd door de editor; niet instelbaar.'; + + @override + String get shortcutDefaultConflict => + 'De standaardsneltoets conflicteert met een aangepaste toewijzing.'; + + @override + String get shortcutCurrentBinding => 'Huidige sneltoets'; + + @override + String get shortcutProposedBinding => 'Voorgestelde sneltoets'; + + @override + String get notesActions => 'Notitieacties'; + + @override + String get noteActions => 'Acties voor deze notitie'; + + @override + String get noteProperties => 'Notitie-eigenschappen'; + + @override + String get notesExportAll => 'Notities exporteren · Alle notities…'; + + @override + String get notesExportCategory => 'Notities exporteren · Categorie…'; + + @override + String get notesOfflineAll => 'Offline beschikbaar maken · Alle notities'; + + @override + String get notesOfflineCategory => 'Offline beschikbaar maken · Categorie'; + + @override + String get notesOfflineRemoveAll => + 'Offlinebewaring verwijderen · Alle notities'; + + @override + String get notesOfflineRemoveCategory => + 'Offlinebewaring verwijderen · Categorie'; + + @override + String get notesOfflinePause => 'Downloads pauzeren'; + + @override + String get notesOfflineResume => 'Downloads hervatten'; + + @override + String get notesOfflineKeep => 'Offline beschikbaar maken'; + + @override + String get notesOfflineRetentionHelp => + 'Het verwijderen van deze vereiste behoudt de cache. Een categorie- of accountvereiste kan de notitie nog steeds bewaren.'; + + @override + String get notesOfflineBroader => + 'Deze notitie wordt ook bewaard door een categorie- of accountvereiste.'; + + @override + String get notesOfflineExternal => + 'Externe media vereisen een verbinding. BusyMark bewaart alleen notitietekst en beheerde bijlagen.'; + + @override + String get notesEmptyAll => + 'Nog geen notities. Maak een notitie om te beginnen.'; + + @override + String get notesEmptyFavorites => 'Geen favoriete notities.'; + + @override + String get notesEmptyCategory => 'Geen notities in deze categorie.'; + + @override + String get notesEmptyUncategorized => 'Geen notities zonder categorie.'; + + @override + String get notesEmptyRecovery => 'Geen bewaarde notities om te herstellen.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Mislukt. Opnieuw proberen'; + + @override + String get notesOperationFailed => + 'De bewerking kon niet worden voltooid. Uw concept blijft bewaard. Controleer opslaan en conflicten en probeer opnieuw.'; + + @override + String get notesInspectionFailed => 'Bijlagen · Mislukt. Opnieuw proberen'; + + @override + String get notesStartingConnection => 'Nextcloud-verbinding starten…'; + + @override + String get notesVerifyingConnection => 'Toegang tot Notities controleren…'; + + @override + String get notesCancellingConnection => 'Aanmelden annuleren en opruimen…'; + + @override + String get notesRemovingConnection => 'Lokaal account verwijderen…'; + + @override + String get notesRemovedWarning => + 'Het lokale Nextcloud-account is verwijderd. Het intrekken van het app-wachtwoord is niet bevestigd. Verwijder het wachtwoord van BusyMark in de beveiligingsinstellingen van Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Niet alle open notities konden lokaal worden opgeslagen. Controleer opslaan, conflicten of normalisatie in de werkruimte. Lokale wijzigingen moeten mogelijk nog met Nextcloud synchroniseren.'; + + @override + String get notesSettingsLeave => + 'Wijzigingen in Nextcloud-instellingen opslaan voor vertrek?'; + + @override + String get notesSettingsLeaveHelp => + 'Notes-instellingen op de server · Niet-opgeslagen wijzigingen'; + + @override + String get notesSettingsReload => 'Serverinstellingen opnieuw laden…'; + + @override + String get notesSettingsReloadHelp => + 'Opnieuw laden vervangt uw concept door de huidige Nextcloud-instellingen. Concept verwerpen en opnieuw laden?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud heeft de instellingen geaccepteerd, maar het vernieuwen van notities is mislukt. De getoonde waarden zijn door de server bevestigd. Vernieuw opnieuw vanuit Notities.'; + + @override + String get notesUnavailable => + 'Geen verbinding · Tekst lokaal beschikbaar · Lokaal opgeslagen · synchronisatie in behandeling'; + + @override + String get notesStorageFull => + 'De Nextcloud-opslag is vol. Maak ruimte vrij op de server en probeer opnieuw.'; + + @override + String get notesRejected => + 'Nextcloud heeft deze wijziging geweigerd. Controleer de notitie en sla een lokale kopie op voordat u opnieuw probeert.'; + + @override + String get notesThrottled => + 'Nextcloud heeft gevraagd te wachten. Synchronisatie gaat verder na de wachttijd.'; + + @override + String get notesRecoveryRequired => 'Conflicten · Vergelijken'; + + @override + String get notesScanning => 'Bestanden scannen…'; + + @override + String get notesCancelling => + 'Annuleren… Wachten tot de huidige bewerking is afgerond.'; + + @override + String get notesCancelled => 'Geannuleerd'; + + @override + String get notesPartial => 'Gedeeltelijk voltooid'; + + @override + String get notesTransferFailed => 'Mislukt. Opnieuw proberen'; + + @override + String get notesImportPartialHelp => + 'Eerdere items kunnen al lokaal opgeslagen zijn. Hervat deze import om zonder duplicaten verder te gaan.'; + + @override + String get notesNotStarted => 'Niet gestart'; + + @override + String get notesCreated => 'Gemaakt'; + + @override + String get notesRemoteDeleteUnavailable => + 'Server-URL · Niet ondersteund · Verwijderen · Bijlagen'; + + @override + String get notesCurrentText => 'Huidige bewaarde tekst'; + + @override + String get notesRecovering => 'Herstellen als nieuwe notitie…'; + + @override + String get notesReadOnlyHelp => + 'Alleen lezen · Favorieten · Lokale kopie opslaan…'; + + @override + String get notesSort => 'Notities sorteren'; + + @override + String get notesPathRelative => + 'Het pad van de notitiemap is relatief aan uw Nextcloud-opslag.'; + + @override + String get notesSettingsStale => + 'De instellingen zijn gewijzigd op Nextcloud. Laad en controleer de serverwaarden voordat u het concept vervangt.'; + + @override + String get notesDownloadsPaused => 'Downloads gepauzeerd'; + + @override + String get notesExportNotPublished => 'Er is geen export gepubliceerd.'; + + @override + String get notesKeyringUnavailable => + 'Ontgrendel de bureaubladsleutelbos en probeer opnieuw.'; + + @override + String get notesConnectFailed => + 'Kan niet verbinden met Nextcloud Notes. Controleer het serveradres en probeer opnieuw.'; + + @override + String get notesAccountMismatch => + 'Meld u aan bij hetzelfde Nextcloud-account. Verwijder dit account eerst om een ander te verbinden.'; + + @override + String get notesLocalRemovalFailed => + 'Het lokaal opruimen is mislukt. Het account blijft bewaard zodat u de verwijdering opnieuw kunt proberen.'; + + @override + String get notesTransferring => 'Notities overdragen…'; + + @override + String get notesTechnicalDetails => 'Technische details'; + + @override + String get notesPauseRequirement => 'Offlinevereiste pauzeren'; + + @override + String get notesResumeRequirement => 'Offlinevereiste hervatten'; } diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 08351df2..3e22aaf9 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsPl extends AppLocalizations { AppLocalizationsPl([String locale = 'pl']) : super(locale); + @override + String get notesNewFromRetainedText => 'Nowa notatka z zachowanego tekstu'; + + @override + String get notesAccountThrottled => + 'Nextcloud prosi o przerwę. Zaczekaj przed ponownym logowaniem.'; + + @override + String get notesSettingsRefreshing => + 'Odświeżanie Notes po potwierdzeniu ustawień'; + + @override + String get notesExportOperationFailed => + 'Eksport nie został opublikowany. Sprawdź źródło i miejsce docelowe, a następnie ponów.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Odzyskana notatka została zapisana, ale nie otwarta. Rozwiąż oczekujące zmiany edytora lub właściwości i otwórz tę istniejącą notatkę.'; + + @override + String get notesSettingsPendingNotes => + 'Oczekujące notatki, konflikty lub niepewne utworzenia blokują zmianę. Zsynchronizuj lub sprawdź je w Notes, potem ponów zapis zachowanej wersji roboczej ustawień.'; + + @override + String get notesSettingsPendingAttachments => + 'Operacje załączników blokują zmianę. Sprawdź załączniki w Notes, potem ponów zapis zachowanej wersji roboczej ustawień.'; + + @override + String get notesAccountServerAddress => + 'Użyj kanonicznego adresu HTTPS Nextcloud i zaloguj się ponownie. Nie udało się zweryfikować adresu lub przekierowania.'; + + @override + String get notesAccountUnsupported => + 'Notes jest niedostępne lub API nieobsługiwane. Włącz Nextcloud Notes z główną wersją API 1 i podrzędną co najmniej 4.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud odrzucił dane logowania. Ponownie autoryzuj konto lub połącz je.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud zabronił sprawdzenia możliwości Notes. Poproś administratora o zgodę i ponów.'; + + @override + String get notesAccountMalformed => + 'Nie udało się zweryfikować odpowiedzi Nextcloud. Sprawdź instalację serwera i zaloguj się ponownie.'; + + @override + String get notesAccountTimedOut => + 'Czas logowania upłynął. Rozpocznij ponownie i dokończ autoryzację w przeglądarce.'; + + @override + String get notesAccountBrowserUnavailable => + 'Nie udało się otworzyć przeglądarki systemowej. Sprawdź domyślną przeglądarkę i ponów logowanie.'; + + @override + String get notesAccountNetwork => + 'Nextcloud jest nieosiągalny. Sprawdź połączenie i serwer, a następnie ponów logowanie.'; + + @override + String get notesAccountRejected => + 'Nextcloud odrzucił żądanie możliwości. Sprawdź dostęp z administratorem i ponów.'; + + @override + String get notesAccountFailed => + 'Nie udało się skonfigurować konta. Zaloguj się ponownie.'; + + @override + String get notesTransferSourceChanged => + 'Źródło zmieniło się po przeglądzie. Sprawdź bieżący plik lub utwórz nową migawkę przed ponowieniem.'; + + @override + String get notesTransferUnreadable => + 'Nie można odczytać źródła. Sprawdź dostęp do pliku lub katalogu i przejrzyj ponownie.'; + + @override + String get notesTransferInvalidSnapshot => + 'Migawka jest nieprawidłowa lub nieobsługiwana. Wybierz poprawną migawkę BusyMark Notes lub źródło Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Media przekraczają limit importu. Zmniejsz media danego elementu i przejrzyj ponownie.'; + + @override + String get notesTransferUnsafeSource => + 'Źródło zawiera niebezpieczną ścieżkę. Wybierz pliki wewnątrz katalogu źródłowego i przejrzyj ponownie.'; + + @override + String get notesTransferDestination => + 'Nie można zapisać eksportu. Sprawdź dostęp i wolne miejsce w miejscu docelowym i ponów.'; + + @override + String get notesTransferOperationFailed => + 'Transfer nie został ukończony. Sprawdź elementy i ponów. Wcześniej zaimportowane elementy pozostają zapisane.'; + + @override + String get notesExportPublishedWithOmissions => + 'Eksport dostępny z pominięciami'; + + @override + String get notesBatchUnchanged => 'Żądane wartości były już ustawione.'; + + @override + String get notesBatchReadOnly => + 'Notatka jest tylko do odczytu. Nie można zmieniać chronionych właściwości; Ulubione pozostaje dozwolone.'; + + @override + String get notesBatchReviewChanged => + 'Notatka zmieniła się po przeglądzie. Sprawdź jej aktualny stan przed ponowieniem działania.'; + + @override + String get notesBatchMissing => + 'Notatka nie jest już dostępna. Odśwież listę i sprawdź wybrane elementy.'; + + @override + String get notesBatchStorage => + 'Zapis lokalny nie powiódł się. Sprawdź pamięć i ponów ten element; udane elementy są zapisane.'; + + @override + String get notesRemoteReviewChanged => + 'Notatka na serwerze się zmieniła. Wersja robocza scalania jest zachowana. Sprawdź nowe porównanie i ponownie wybierz rozwiązanie.'; + + @override + String get notesReviewRemote => 'Sprawdzono aktualną wersję na serwerze'; + @override String get wsModuleCapsNotPublished => 'Reguła wielkości liter modułu nie jest publikowana.'; @@ -583,15 +708,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get keyboardShortcuts => 'Skróty klawiaturowe'; - @override - String get commandPalette => 'Paleta poleceń'; - - @override - String get commandPaletteHint => 'Wpisz polecenie'; - - @override - String get commandPaletteEmpty => 'Brak pasujących poleceń'; - @override String get commandUnavailableInContext => 'To polecenie nie jest dostępne w bieżącym kontekście.'; @@ -810,7 +926,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Pokaż ten wykaz skrótów klawiaturowych'; + 'Dostosuj skróty klawiaturowe aplikacji'; @override String get shortcutSyntaxReferenceDescription => @@ -1067,8 +1183,7 @@ class AppLocalizationsPl extends AppLocalizations { String get validateOnEdit => 'Sprawdzaj poprawność podczas edycji'; @override - String get clearRecentWorkspaces => - 'Wyczyść listę ostatnich obszarów roboczych'; + String get clearRecentWorkspaces => 'Wyczyść ostatnią historię'; @override String get editingButtonsPosition => 'Położenie przycisków edycji'; @@ -1868,15 +1983,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get sourceSearchNextMatch => 'Następne dopasowanie'; - @override - String get sourceSearchCaseSensitive => 'Uwzględniaj wielkość liter'; - - @override - String get sourceSearchWholeWord => 'Całe słowo'; - - @override - String get sourceSearchRegex => 'Wyrażenie regularne'; - @override String get sourceSearchReplacement => 'Zamień na'; @@ -2003,13 +2109,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get saveAs => 'Zapisz jako'; - @override - String get sourceSearchInvalidRegex => 'Nieprawidłowe wyrażenie regularne'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Dopasowania wyrażeń regularnych o zerowej długości nie są obsługiwane (np. ^, \$ lub asercje dopasowujące tylko pozycję).'; - @override String get workspaceSearchIncomplete => 'Wyświetlono tylko część wyników. Pokaż więcej lub zawęź wyszukiwanie.'; @@ -2119,6 +2218,11 @@ class AppLocalizationsPl extends AppLocalizations { @override String get documentKindResourceFile => 'Plik zasobów'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Nie udało się zaktualizować ostatnich elementów: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Otwieranie nie powiodło się: $error'; @@ -4403,9 +4507,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get clipboardHistory => 'Historia w Koszyku'; - @override - String get clipboardHistorySearchHint => 'Przeszukaj historię schowka'; - @override String get clipboardHistoryDescription => 'Otwórz historię w koszyku lokalną dla sesji.'; @@ -4428,10 +4529,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get clipboardNoItems => 'Historia w koszyku jest pusta.'; - @override - String get clipboardNoMatches => - 'Żadne elementy w koszyku nie pasują do tego wyszukiwania.'; - @override String get clipboardDisabled => 'Zbiór \"Historia w koszyku\" wyłączony w Ustawieniach.'; @@ -4489,13 +4586,6 @@ class AppLocalizationsPl extends AppLocalizations { String get localHistoryNoRevisions => 'Ten dokument nie ma wersji w Lokalnej Historii.'; - @override - String get localHistoryNoMatches => - 'Żadne wersje nie pasują do tego wyszukiwania.'; - - @override - String get localHistorySearchHint => 'Przeszukaj historię lokalną'; - @override String get localHistorySelectedRevision => 'Wybrana wersja'; @@ -5453,4 +5543,399 @@ class AppLocalizationsPl extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Sprawdzany element jest niedostępny. Sprawdź element, aby kontynuować.'; + + @override + String get searchDocumentsCategory => 'Dokumenty'; + + @override + String get searchTopicsCategory => 'Tematy'; + + @override + String get searchCurrentDocument => 'Bieżący dokument'; + + @override + String get searchCurrentWorkspace => 'Bieżący obszar roboczy'; + + @override + String get browseNotes => 'Przeglądaj notatki'; + + @override + String get searchActionsCommand => 'Szukaj działań'; + + @override + String get searchDocumentsCommand => 'Szukaj dokumentów'; + + @override + String get searchTopicsCommand => 'Szukaj tematów'; + + @override + String get searchRecentCommand => 'Szukaj ostatnich'; + + @override + String get searchResourceUnavailable => 'Ten zasób jest niedostępny.'; + + @override + String get notesSync => 'Synchronizuj notatki'; + + @override + String get searchInWorkspace => 'Szukaj w obszarze roboczym'; + + @override + String get searchCategory => 'Kategoria wyszukiwania'; + + @override + String get clearHistorySearchScope => 'Szukaj w historii dokumentów'; + + @override + String get searchInDocument => 'Szukaj w dokumencie'; + + @override + String get searchDocumentHint => 'Przeszukaj ten dokument…'; + + @override + String get searchWorkspaceHint => 'Przeszukaj ten obszar roboczy…'; + + @override + String get searchOutlineHint => 'Szukaj nagłówków…'; + + @override + String get searchHistoryHint => 'Przeszukaj historię lokalną…'; + + @override + String get searchEnterText => 'Wpisz tekst do wyszukania'; + + @override + String get searchAccountUnavailable => 'Konto niedostępne'; + + @override + String get searchDocumentsHint => 'Szukaj dokumentów…'; + + @override + String get searchTopicsHint => 'Szukaj tematów…'; + + @override + String get searchRecentHint => 'Szukaj ostatnich…'; + + @override + String get searchActionsHint => 'Szukaj działań…'; + + @override + String get searchRequiresNotesWorkspace => 'Wymaga obszaru roboczego Notatek'; + + @override + String get searchSidebarLabel => 'Szukaj:'; + + @override + String get searchDocumentTextCategory => 'Tekst dokumentu'; + + @override + String get searchWorkspaceTextCategory => 'Tekst obszaru roboczego'; + + @override + String get searchHeadingsCategory => 'Nagłówki'; + + @override + String get shortcutUnassigned => 'Nieprzypisany'; + + @override + String get shortcutPressKeys => 'Naciśnij kombinację klawiszy…'; + + @override + String get shortcutRecord => 'Zarejestruj skrót'; + + @override + String get shortcutRestoreAll => 'Przywróć wszystkie domyślne'; + + @override + String get shortcutRestoreAllQuestion => + 'Przywrócić domyślne skróty wszystkich działań? Własne przypisania zostaną usunięte.'; + + @override + String get shortcutSaveFailed => + 'Skróty są aktywne, lecz nie udało się ich zapisać. Spróbuj ponownie, aby zachować je po restarcie.'; + + @override + String get shortcutRetry => 'Ponów zapis'; + + @override + String get shortcutChanged => + 'Przypisania uległy zmianie. Sprawdź bieżące skróty i spróbuj ponownie.'; + + @override + String get shortcutInvalid => + 'Nie można przypisać tej kombinacji w tym kontekście.'; + + @override + String get shortcutSystemWarning => + 'Pulpit może używać tej kombinacji. BusyMark nie może zastępować skrótów systemowych.'; + + @override + String get shortcutConflicts => 'Ten skrót koliduje z tymi działaniami:'; + + @override + String get shortcutRemove => 'Usuń przypisanie'; + + @override + String get shortcutRestoreDefault => 'Przywróć domyślny'; + + @override + String get shortcutAssign => 'Przypisz'; + + @override + String get shortcutReassign => 'Przypisz ponownie'; + + @override + String get shortcutContextApplication => 'Aplikacja'; + + @override + String get shortcutContextDocument => 'Widoki dokumentu'; + + @override + String get shortcutContextEditor => 'Edytor i źródło'; + + @override + String get shortcutContextWritersideSource => 'Źródło Writerside'; + + @override + String get shortcutContextWorkspace => 'Nawigacja obszaru roboczego'; + + @override + String get shortcutContextTree => 'Drzewa paska bocznego'; + + @override + String get searchHeadingsCommand => 'Szukaj nagłówków'; + + @override + String get shortcutFixed => + 'Zarządzane przez edytor; nie można skonfigurować.'; + + @override + String get shortcutDefaultConflict => + 'Domyślny skrót koliduje z niestandardowym przypisaniem.'; + + @override + String get shortcutCurrentBinding => 'Bieżący skrót'; + + @override + String get shortcutProposedBinding => 'Proponowany skrót'; + + @override + String get notesActions => 'Działania na notatkach'; + + @override + String get noteActions => 'Działania na notatce'; + + @override + String get noteProperties => 'Właściwości notatki'; + + @override + String get notesExportAll => 'Eksportuj notatki · Wszystkie notatki…'; + + @override + String get notesExportCategory => 'Eksportuj notatki · Kategoria…'; + + @override + String get notesOfflineAll => 'Udostępnij offline · Wszystkie notatki'; + + @override + String get notesOfflineCategory => 'Udostępnij offline · Kategoria'; + + @override + String get notesOfflineRemoveAll => + 'Usuń przechowywanie offline · Wszystkie notatki'; + + @override + String get notesOfflineRemoveCategory => + 'Usuń przechowywanie offline · Kategoria'; + + @override + String get notesOfflinePause => 'Wstrzymaj pobieranie'; + + @override + String get notesOfflineResume => 'Wznów pobieranie'; + + @override + String get notesOfflineKeep => 'Udostępnij offline'; + + @override + String get notesOfflineRetentionHelp => + 'Usunięcie tego wymagania zachowuje zawartość pamięci podręcznej. Wymaganie kategorii lub konta może nadal zachowywać notatkę.'; + + @override + String get notesOfflineBroader => + 'Ta notatka jest też zachowywana przez wymaganie kategorii lub konta.'; + + @override + String get notesOfflineExternal => + 'Zewnętrzne multimedia wymagają połączenia. BusyMark zachowuje tylko tekst i zarządzane załączniki.'; + + @override + String get notesEmptyAll => 'Nie ma jeszcze notatek. Utwórz pierwszą.'; + + @override + String get notesEmptyFavorites => 'Brak ulubionych notatek.'; + + @override + String get notesEmptyCategory => 'Brak notatek w tej kategorii.'; + + @override + String get notesEmptyUncategorized => 'Brak notatek bez kategorii.'; + + @override + String get notesEmptyRecovery => 'Brak zachowanych notatek do odzyskania.'; + + @override + String get notesLoadFailed => + 'Nextcloud Notes · Niepowodzenie. Spróbuj ponownie'; + + @override + String get notesOperationFailed => + 'Operacja nie została ukończona. Szkic jest zachowany. Sprawdź zapisywanie i konflikty, a potem spróbuj ponownie.'; + + @override + String get notesInspectionFailed => + 'Załączniki · Niepowodzenie. Spróbuj ponownie'; + + @override + String get notesStartingConnection => 'Rozpoczynanie połączenia z Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Weryfikowanie dostępu do notatek…'; + + @override + String get notesCancellingConnection => 'Anulowanie logowania i sprzątanie…'; + + @override + String get notesRemovingConnection => 'Usuwanie konta lokalnego…'; + + @override + String get notesRemovedWarning => + 'Lokalne konto Nextcloud usunięto. Nie potwierdzono unieważnienia hasła aplikacji. Usuń hasło BusyMark w ustawieniach bezpieczeństwa Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Nie wszystkie otwarte notatki zapisano lokalnie. Sprawdź zapisywanie, konflikty lub normalizację w obszarze roboczym. Lokalne zmiany mogą nadal wymagać synchronizacji z Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Zapisać zmiany ustawień Nextcloud przed wyjściem?'; + + @override + String get notesSettingsLeaveHelp => + 'Ustawienia Notes na serwerze · Niezapisane zmiany'; + + @override + String get notesSettingsReload => 'Wczytaj ponownie ustawienia serwera…'; + + @override + String get notesSettingsReloadHelp => + 'Ponowne wczytanie zastąpi szkic aktualnymi ustawieniami Nextcloud. Odrzucić szkic i wczytać ponownie?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud zaakceptował ustawienia, lecz odświeżenie notatek nie powiodło się. Pokazano wartości potwierdzone przez serwer. Ponów odświeżenie z Notatek.'; + + @override + String get notesUnavailable => + 'Bez połączenia · Tekst dostępny lokalnie · Zapisano lokalnie · oczekuje na synchronizację'; + + @override + String get notesStorageFull => + 'Pamięć Nextcloud jest pełna. Zwolnij miejsce na serwerze i spróbuj ponownie.'; + + @override + String get notesRejected => + 'Nextcloud odrzucił tę zmianę. Sprawdź notatkę i zapisz lokalną kopię przed ponowieniem.'; + + @override + String get notesThrottled => + 'Nextcloud zażądał oczekiwania. Synchronizacja wznowi się po upływie wyznaczonego czasu.'; + + @override + String get notesRecoveryRequired => 'Konflikty · Porównaj'; + + @override + String get notesScanning => 'Skanowanie plików…'; + + @override + String get notesCancelling => + 'Anulowanie… Oczekiwanie na zakończenie bieżącej operacji.'; + + @override + String get notesCancelled => 'Anulowano'; + + @override + String get notesPartial => 'Częściowo ukończono'; + + @override + String get notesTransferFailed => 'Niepowodzenie. Spróbuj ponownie'; + + @override + String get notesImportPartialHelp => + 'Poprzednie elementy mogą już być zapisane lokalnie. Wznów ten import, aby kontynuować bez duplikatów.'; + + @override + String get notesNotStarted => 'Nie rozpoczęto'; + + @override + String get notesCreated => 'Utworzono'; + + @override + String get notesRemoteDeleteUnavailable => + 'Adres serwera · Nieobsługiwane · Usuń · Załączniki'; + + @override + String get notesCurrentText => 'Aktualnie zachowany tekst'; + + @override + String get notesRecovering => 'Odzyskiwanie jako nowa notatka…'; + + @override + String get notesReadOnlyHelp => + 'Tylko do odczytu · Ulubione · Zapisz kopię lokalną…'; + + @override + String get notesSort => 'Sortuj notatki'; + + @override + String get notesPathRelative => + 'Ścieżka folderu notatek jest względna wobec Twojego magazynu Nextcloud.'; + + @override + String get notesSettingsStale => + 'Ustawienia zmieniły się w Nextcloud. Wczytaj i sprawdź wartości serwera przed zastąpieniem szkicu.'; + + @override + String get notesDownloadsPaused => 'Pobieranie wstrzymane'; + + @override + String get notesExportNotPublished => 'Nie opublikowano eksportu.'; + + @override + String get notesKeyringUnavailable => + 'Odblokuj systemowy pęk kluczy i spróbuj ponownie.'; + + @override + String get notesConnectFailed => + 'Nie można połączyć się z Nextcloud Notes. Sprawdź adres serwera i spróbuj ponownie.'; + + @override + String get notesAccountMismatch => + 'Zaloguj się na to samo konto Nextcloud. Najpierw usuń to konto, aby połączyć inne.'; + + @override + String get notesLocalRemovalFailed => + 'Lokalne czyszczenie konta nie powiodło się. Konto zachowano, aby ponowić usuwanie.'; + + @override + String get notesTransferring => 'Przenoszenie notatek…'; + + @override + String get notesTechnicalDetails => 'Szczegóły techniczne'; + + @override + String get notesPauseRequirement => 'Wstrzymaj wymaganie offline'; + + @override + String get notesResumeRequirement => 'Wznów wymaganie offline'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index fee2c505..3abbb940 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -10,6 +10,132 @@ import 'app_localizations.dart'; class AppLocalizationsPt extends AppLocalizations { AppLocalizationsPt([String locale = 'pt']) : super(locale); + @override + String get notesNewFromRetainedText => + 'Nova nota a partir do texto preservado'; + + @override + String get notesAccountThrottled => + 'Nextcloud pediu uma pausa. Aguarde antes de voltar a autenticar-se.'; + + @override + String get notesSettingsRefreshing => + 'A atualizar Notes após a confirmação das definições'; + + @override + String get notesExportOperationFailed => + 'A exportação falhou antes da publicação. Verifique a origem e o destino e volte a exportar.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'A nota recuperada foi guardada, mas não abriu. Resolva as alterações pendentes no editor ou nas propriedades e abra esta nota existente.'; + + @override + String get notesSettingsPendingNotes => + 'Notas pendentes, conflitos ou criações incertas impedem esta alteração. Sincronize ou reveja em Notes e volte a guardar este rascunho preservado.'; + + @override + String get notesSettingsPendingAttachments => + 'Operações de anexos impedem esta alteração. Reveja os anexos em Notes e volte a guardar este rascunho preservado.'; + + @override + String get notesAccountServerAddress => + 'Use o endereço HTTPS canónico do Nextcloud e reinicie a autenticação. Não foi possível verificar o endereço ou o redirecionamento.'; + + @override + String get notesAccountUnsupported => + 'Notes está indisponível ou a API não é suportada. Ative Nextcloud Notes com API principal 1 e secundária 4 ou posterior.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud rejeitou as credenciais. Autorize novamente a conta ou volte a ligá-la.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud recusou a verificação de funcionalidades de Notes. Peça autorização ao administrador e tente novamente.'; + + @override + String get notesAccountMalformed => + 'Não foi possível verificar a resposta do Nextcloud. Verifique a instalação do servidor e reinicie a autenticação.'; + + @override + String get notesAccountTimedOut => + 'A autenticação expirou. Reinicie-a e conclua a autorização no navegador.'; + + @override + String get notesAccountBrowserUnavailable => + 'Não foi possível abrir o navegador do sistema. Verifique o navegador predefinido e reinicie a autenticação.'; + + @override + String get notesAccountNetwork => + 'Nextcloud está inacessível. Verifique a ligação e o servidor e tente autenticar-se novamente.'; + + @override + String get notesAccountRejected => + 'Nextcloud rejeitou o pedido de funcionalidades. Verifique o acesso com o administrador e tente novamente.'; + + @override + String get notesAccountFailed => + 'Não foi possível configurar a conta. Tente autenticar-se novamente.'; + + @override + String get notesTransferSourceChanged => + 'A origem mudou desde a revisão. Reveja o ficheiro atual ou capture uma nova imagem antes de tentar novamente.'; + + @override + String get notesTransferUnreadable => + 'Não foi possível ler a origem. Verifique o acesso ao ficheiro ou pasta e reveja.'; + + @override + String get notesTransferInvalidSnapshot => + 'Imagem inválida ou não suportada. Escolha uma imagem BusyMark Notes válida ou uma origem Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Os conteúdos multimédia excedem o limite de importação. Reduza os do item afetado e reveja-o.'; + + @override + String get notesTransferUnsafeSource => + 'A origem contém um caminho inseguro. Escolha ficheiros na pasta de origem e reveja.'; + + @override + String get notesTransferDestination => + 'Não foi possível escrever a exportação. Verifique o acesso e espaço livre no destino e tente novamente.'; + + @override + String get notesTransferOperationFailed => + 'A transferência falhou. Reveja os itens afetados e tente novamente. Os já importados continuam guardados.'; + + @override + String get notesExportPublishedWithOmissions => + 'Exportação disponível com omissões'; + + @override + String get notesBatchUnchanged => 'Os valores pedidos já estavam definidos.'; + + @override + String get notesBatchReadOnly => + 'Esta nota é só de leitura. As propriedades protegidas não podem mudar; Favorito continua permitido.'; + + @override + String get notesBatchReviewChanged => + 'Esta nota mudou desde a revisão. Reveja o estado atual antes de repetir a ação.'; + + @override + String get notesBatchMissing => + 'Esta nota já não está disponível. Atualize a lista e reveja os destinos.'; + + @override + String get notesBatchStorage => + 'Falha ao guardar localmente. Verifique o armazenamento e repita este item; os bem-sucedidos continuam guardados.'; + + @override + String get notesRemoteReviewChanged => + 'A nota no servidor mudou. O rascunho de união foi preservado. Reveja a comparação atualizada e escolha novamente a resolução.'; + + @override + String get notesReviewRemote => 'Revi a versão atual do servidor'; + @override String get wsModuleCapsNotPublished => 'A capitalização do módulo não é publicada.'; @@ -586,15 +712,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get keyboardShortcuts => 'Atalhos de teclado'; - @override - String get commandPalette => 'Paleta de comandos'; - - @override - String get commandPaletteHint => 'Introduza um comando'; - - @override - String get commandPaletteEmpty => 'Nenhum comando correspondente'; - @override String get commandUnavailableInContext => 'Este comando não está disponível no contexto atual.'; @@ -814,7 +931,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Mostrar esta referência de atalhos de teclado'; + 'Personalizar os atalhos de teclado da aplicação'; @override String get shortcutSyntaxReferenceDescription => @@ -1068,7 +1185,7 @@ class AppLocalizationsPt extends AppLocalizations { String get validateOnEdit => 'Validar ao editar'; @override - String get clearRecentWorkspaces => 'Limpar espaços de trabalho recentes'; + String get clearRecentWorkspaces => 'Limpar histórico recente'; @override String get editingButtonsPosition => 'Posição dos botões de edição'; @@ -1854,16 +1971,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get sourceSearchNextMatch => 'Próxima correspondência'; - @override - String get sourceSearchCaseSensitive => - 'Diferenciar maiúsculas de minúsculas'; - - @override - String get sourceSearchWholeWord => 'Palavra inteira'; - - @override - String get sourceSearchRegex => 'Expressão regular'; - @override String get sourceSearchReplacement => 'Substituir por'; @@ -1991,13 +2098,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get saveAs => 'Guardar como'; - @override - String get sourceSearchInvalidRegex => 'Expressão regular inválida'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Não são suportadas correspondências regex de comprimento zero (por exemplo, ^, \$ ou antecipações que correspondem apenas a uma posição).'; - @override String get workspaceSearchIncomplete => 'Só são apresentados alguns resultados. Mostre mais ou restrinja a pesquisa.'; @@ -2107,6 +2207,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get documentKindResourceFile => 'Ficheiro de recursos'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Não foi possível atualizar os itens recentes: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Falha ao abrir: $error'; @@ -4403,10 +4508,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get clipboardHistory => 'Histórico da Área de Transferência'; - @override - String get clipboardHistorySearchHint => - 'Pesquisar no histórico da área de transferência'; - @override String get clipboardHistoryDescription => 'Abrir o histórico da área de transferência local da sessão.'; @@ -4431,10 +4532,6 @@ class AppLocalizationsPt extends AppLocalizations { String get clipboardNoItems => 'O histórico da área de transferência está vazio.'; - @override - String get clipboardNoMatches => - 'Nenhum item da área de transferência corresponde a esta pesquisa.'; - @override String get clipboardDisabled => 'A coleção de Histórico da Área de Transferência está desativada nas Configurações.'; @@ -4491,12 +4588,6 @@ class AppLocalizationsPt extends AppLocalizations { String get localHistoryNoRevisions => 'Este documento não tem revisões no Histórico Local.'; - @override - String get localHistoryNoMatches => 'Nenhuma revisão corresponde à pesquisa.'; - - @override - String get localHistorySearchHint => 'Pesquisar no histórico local'; - @override String get localHistorySelectedRevision => 'Revisão selecionada'; @@ -5461,410 +5552,931 @@ class AppLocalizationsPt extends AppLocalizations { @override String get wsPropertiesUnavailable => 'O elemento inspecionado está indisponível. Inspecione um elemento para continuar.'; -} - -/// The translations for Portuguese, as used in Brazil (`pt_BR`). -class AppLocalizationsPtBr extends AppLocalizationsPt { - AppLocalizationsPtBr() : super('pt_BR'); @override - String get wsModuleCapsNotPublished => - 'A capitalização do módulo não é publicada.'; - - @override - String get wsTitleOverridesAction => 'Títulos da instância e do índice…'; + String get searchDocumentsCategory => 'Documentos'; @override - String get wsHeadingNotPublished => 'Não é publicado por este gerador.'; + String get searchTopicsCategory => 'Tópicos'; @override - String get wsMarkdownInstanceTitleLimitation => - 'O builder fixado não publica títulos por instância em tópicos Markdown. A marcação existente é preservada; consulte o código-fonte.'; + String get searchCurrentDocument => 'Documento atual'; @override - String get wsTopicIdentity => 'ID do tópico'; + String get searchCurrentWorkspace => 'Área de trabalho atual'; @override - String get wsElementAnchor => 'Âncora do elemento'; + String get browseNotes => 'Explorar notas'; @override - String get wsHeadingText => 'Texto do cabeçalho'; + String get searchActionsCommand => 'Pesquisar ações'; @override - String get wsDefaultTitle => 'Título padrão'; + String get searchDocumentsCommand => 'Pesquisar documentos'; @override - String get wsTopicNavigation => 'Navegação do tópico'; + String get searchTopicsCommand => 'Pesquisar tópicos'; @override - String get wsHeadingOptions => 'Atributos do cabeçalho'; + String get searchRecentCommand => 'Pesquisar recentes'; @override - String get wsDocumentOptions => 'Configurações do documento'; + String get searchResourceUnavailable => 'Este recurso não está disponível.'; @override - String get wsSettingsPresent => 'Existem configurações explícitas'; + String get notesSync => 'Sincronizar notas'; @override - String get wsTitleOverrideExplanation => - 'Substitui o título publicado; o texto do cabeçalho permanece.'; + String get searchInWorkspace => 'Pesquisar no espaço de trabalho'; @override - String get wsIgnoreVariables => 'Tratar variáveis como texto literal'; + String get searchCategory => 'Categoria de pesquisa'; @override - String get wsIgnoreVariablesExplanation => - 'Quando ativo, o texto semelhante a variáveis não é substituído.'; + String get clearHistorySearchScope => 'Pesquisar no histórico de documentos'; @override - String get wsInstanceSelection => 'Instâncias de ajuda'; + String get searchInDocument => 'Pesquisar no documento'; @override - String get wsInstanceExplanation => - 'Selecione saídas por nomes, grupos, listas, exclusões ou expressões.'; + String get searchDocumentHint => 'Pesquisar neste documento…'; @override - String get wsFilterExplanation => - 'Filtra conteúdo reutilizável por critérios; não seleciona uma instância de ajuda.'; + String get searchWorkspaceHint => 'Pesquisar neste espaço de trabalho…'; @override - String get wsContextHelp => 'ID de ajuda contextual'; + String get searchOutlineHint => 'Pesquisar títulos…'; @override - String get wsCapitalization => 'Capitalização'; + String get searchHistoryHint => 'Pesquisar no histórico local…'; @override - String get wsLibraryTopic => 'Tópico de biblioteca'; + String get searchEnterText => 'Introduza texto para pesquisar'; @override - String get wsLastModified => 'Última alteração'; + String get searchAccountUnavailable => 'Conta indisponível'; @override - String get wsMissingTopicHeading => - 'Não existe H1 do tópico. Adicione-o no código-fonte.'; + String get searchDocumentsHint => 'Pesquisar documentos…'; @override - String get wsMultipleTopicHeadings => - 'O primeiro H1 determina o título do tópico. Os restantes são cabeçalhos separados.'; + String get searchTopicsHint => 'Pesquisar tópicos…'; @override - String get wsTopicIdentityMismatch => - 'O id da raiz difere do ID baseado no arquivo. Use Renomear arquivo do tópico ou consulte o código-fonte.'; + String get searchRecentHint => 'Pesquisar recentes…'; @override - String get wsUnsupportedProperties => - 'Os atributos não suportados são preservados. Consulte-os no código-fonte.'; + String get searchActionsHint => 'Pesquisar ações…'; @override - String get wsGeneratedAnchor => - 'Âncora gerada; introduzir um valor cria uma substituição.'; + String get searchRequiresNotesWorkspace => + 'Requer um espaço de trabalho de Notas'; @override - String get wsHeadingBuilderLimitation => - 'O builder fixado não publica atributos H1. Pertencem ao cabeçalho, não à raiz XML.'; + String get searchSidebarLabel => 'Pesquisar:'; @override - String get closeOtherTabs => 'Fechar as outras abas'; + String get searchDocumentTextCategory => 'Texto do documento'; @override - String get tocTopicFromTemplate => 'Tópico a partir de modelo...'; + String get searchWorkspaceTextCategory => 'Texto do espaço de trabalho'; @override - String get tocSaveAsTemplate => 'Salvar como modelo'; + String get searchHeadingsCategory => 'Cabeçalhos'; @override - String get tocTemplateDialog => 'Criar tópico a partir de modelo'; + String get shortcutUnassigned => 'Não atribuído'; @override - String get tocTemplatesEditor => 'Modelos de arquivos e código'; + String get shortcutPressKeys => 'Prima uma combinação de teclas…'; @override - String get tocEditTemplates => 'Editar modelos...'; + String get shortcutRecord => 'Capturar atalho'; @override - String get tocCreateCustomTemplate => 'Criar modelo personalizado...'; + String get shortcutRestoreAll => 'Repor todas as predefinições'; @override - String get tocTemplateCreated => 'Modelo criado'; + String get shortcutRestoreAllQuestion => + 'Repor os atalhos predefinidos de todas as ações? As atribuições personalizadas serão removidas.'; @override - String get tocTemplateDefault => 'Padrão'; + String get shortcutSaveFailed => + 'Os atalhos estão ativos, mas não foram guardados. Tente novamente para os manter após reiniciar.'; @override - String get tocTemplateCustom => 'Personalizados'; + String get shortcutRetry => 'Tentar guardar novamente'; @override - String get tocTemplateFilename => 'Nome do arquivo:'; + String get shortcutChanged => + 'As atribuições mudaram. Reveja os atalhos atuais e tente novamente.'; @override - String get tocTemplateMarkdown => 'Markdown (.md)'; + String get shortcutInvalid => + 'Esta combinação não pode ser atribuída neste contexto.'; @override - String get tocTemplateXml => 'XML (.topic)'; + String get shortcutSystemWarning => + 'O ambiente de trabalho pode usar esta combinação. O BusyMark não pode substituir atalhos do sistema.'; @override - String get tocTemplatesInternal => 'Internos'; + String get shortcutConflicts => + 'Este atalho entra em conflito com estas ações:'; @override - String get tocTemplateUnnamed => 'Sem nome'; + String get shortcutRemove => 'Remover atribuição'; @override - String get tocTemplateNew => 'Novo modelo...'; + String get shortcutRestoreDefault => 'Repor predefinição'; @override - String get tocTemplateName => 'Nome:'; + String get shortcutAssign => 'Atribuir'; @override - String get tocTemplateExtension => 'Extensão:'; + String get shortcutReassign => 'Reatribuir'; @override - String get tocTemplateLoadError => - 'Não foi possível carregar os modelos. Os modelos existentes não foram alterados.'; + String get shortcutContextApplication => 'Aplicação'; @override - String get tocTemplateSaveError => - 'Não foi possível salvar os modelos. Suas alterações continuam disponíveis aqui.'; + String get shortcutContextDocument => 'Vistas do documento'; @override - String get tocTemplateConflict => - 'Os modelos foram alterados em outra janela. Cancele e reabra o editor.'; + String get shortcutContextEditor => 'Editor e código-fonte'; @override - String get tocTemplateDuplicateName => - 'Já existe um modelo com este nome e extensão.'; + String get shortcutContextWritersideSource => 'Código-fonte do Writerside'; @override - String get tocTemplateFormat => 'Formato:'; + String get shortcutContextWorkspace => 'Navegação do espaço de trabalho'; @override - String get tocTemplateReset => 'Redefinir'; + String get shortcutContextTree => 'Árvores laterais'; @override - String get tocTemplateRetry => 'Tentar novamente'; + String get searchHeadingsCommand => 'Pesquisar títulos'; @override - String tocRemoveElementsDialog(int count) { - return 'Remover $count elementos do sumário'; - } + String get shortcutFixed => 'Gerido pelo editor; não é configurável.'; @override - String get tocRemoveElementsRetainFiles => - 'Os arquivos de origem associados a estes elementos do sumário não serão excluídos.'; + String get shortcutDefaultConflict => + 'O atalho predefinido está em conflito com uma atribuição personalizada.'; @override - String get tocOk => 'OK'; + String get shortcutCurrentBinding => 'Atalho atual'; @override - String get tocSelectExistingTopic => - 'Selecionar tópico para adicionar à instância atual'; + String get shortcutProposedBinding => 'Atalho proposto'; @override - String get tocGroupName => 'Nome do grupo'; + String get notesActions => 'Ações das notas'; @override - String get tocMoveLineUp => 'Mover linha para cima'; + String get noteActions => 'Ações da nota'; @override - String get tocMoveLineDown => 'Mover linha para baixo'; + String get noteProperties => 'Propriedades da nota'; @override - String get tocEmptyMdTopic => 'Tópico MD vazio'; + String get notesExportAll => 'Exportar notas · Todas as notas…'; @override - String get tocEmptyXmlTopic => 'Tópico XML vazio'; + String get notesExportCategory => 'Exportar notas · Categoria…'; @override - String get tocCopySpecial => 'Cópia especial'; + String get notesOfflineAll => 'Disponibilizar offline · Todas as notas'; @override - String tocCopyFileName(String name) { - return 'Nome do arquivo do tópico \'$name\''; - } + String get notesOfflineCategory => 'Disponibilizar offline · Categoria'; @override - String get tocCopyFilePath => 'Caminho do arquivo do tópico'; + String get notesOfflineRemoveAll => + 'Remover retenção offline · Todas as notas'; @override - String tocCopyTopicTitle(String title) { - return 'Título do tópico \'$title\''; - } + String get notesOfflineRemoveCategory => + 'Remover retenção offline · Categoria'; @override - String tocCopyElementId(String id) { - return 'ID do elemento do índice \'$id\''; - } + String get notesOfflinePause => 'Pausar transferências'; @override - String get tocCopyFileNameUnavailable => 'Nome do arquivo do tópico'; + String get notesOfflineResume => 'Retomar transferências'; @override - String get tocCopyTopicTitleUnavailable => 'Título do tópico'; + String get notesOfflineKeep => 'Disponibilizar offline'; @override - String get tocCopyElementIdUnavailable => 'ID do elemento do índice'; + String get notesOfflineRetentionHelp => + 'Remover este requisito mantém o conteúdo em cache. Um requisito da categoria ou da conta pode continuar a manter a nota.'; @override - String get tocEditTitleAction => 'Editar título...'; + String get notesOfflineBroader => + 'Esta nota também é mantida por um requisito da categoria ou da conta.'; @override - String get tocEditTitleDialog => 'Editar título'; + String get notesOfflineExternal => + 'O conteúdo multimédia externo requer ligação. O BusyMark mantém apenas o texto e os anexos geridos.'; @override - String get tocRemoveElementAction => 'Remover elemento do índice...'; + String get notesEmptyAll => 'Ainda não há notas. Crie uma para começar.'; @override - String get tocSetHomePage => 'Definir como página inicial'; + String get notesEmptyFavorites => 'Não há notas favoritas.'; @override - String tocGoToElement(String fileName) { - return 'Ir para o elemento do índice em \'$fileName\''; - } + String get notesEmptyCategory => 'Não há notas nesta categoria.'; @override - String get tocSynchronize => 'Sincronizar índice e editor'; + String get notesEmptyUncategorized => 'Não há notas sem categoria.'; @override - String get tocEmptyGroupAction => 'Grupo vazio'; + String get notesEmptyRecovery => 'Não há notas guardadas para recuperar.'; @override - String get tocNewEmptyGroup => 'Novo grupo vazio'; + String get notesLoadFailed => 'Nextcloud Notes · Falhou. Tentar novamente'; @override - String get tocGroupAction => 'Agrupar'; + String get notesOperationFailed => + 'Não foi possível concluir a operação. O rascunho foi mantido. Reveja a gravação e os conflitos e tente novamente.'; @override - String get tocNewGroup => 'Novo grupo'; + String get notesInspectionFailed => 'Anexos · Falhou. Tentar novamente'; @override - String get tocSortChildren => 'Ordenar subtópicos alfabeticamente'; + String get notesStartingConnection => 'A iniciar ligação ao Nextcloud…'; @override - String get tocLinkTopicFiles => 'Vincular arquivos de tópicos ao índice...'; + String get notesVerifyingConnection => 'A verificar o acesso às notas…'; @override - String get addLocalMarkdownFiles => 'Adicionar arquivos Markdown locais'; + String get notesCancellingConnection => + 'A cancelar o início de sessão e a limpar…'; @override - String get tocTitleField => 'Título no índice:'; + String get notesRemovingConnection => 'A remover a conta local…'; @override - String get tocTopicTitleField => 'Título do tópico:'; + String get notesRemovedWarning => + 'A conta local Nextcloud foi removida. Não foi confirmada a revogação da palavra-passe da aplicação. Remova a palavra-passe do BusyMark nas definições de segurança do Nextcloud.'; @override - String get tocAdvancedSettings => 'Configurações avançadas'; + String get notesBuffersNotSaved => + 'Não foi possível guardar todas as notas abertas localmente. Reveja a gravação, os conflitos ou a normalização na área de trabalho. As alterações locais podem ainda precisar de sincronização com o Nextcloud.'; @override - String tocInstanceTitleField(String instanceId) { - return 'Título para \'$instanceId\':'; - } + String get notesSettingsLeave => + 'Guardar as alterações às definições do Nextcloud antes de sair?'; @override - String get tocOnlyTitleField => 'Título exclusivo do índice:'; + String get notesSettingsLeaveHelp => + 'Definições de Notes do servidor · Alterações não guardadas'; @override - String get tocInstanceTitleExplanation => - 'Usado apenas na instância atual. Por padrão, é herdado do título do tópico.'; + String get notesSettingsReload => 'Recarregar definições do servidor…'; @override - String get tocOnlyTitleExplanation => - 'Usado apenas no índice. Por padrão, é herdado do título do tópico ou do título específico da instância, se definido. Os títulos são explicados'; + String get notesSettingsReloadHelp => + 'Recarregar substitui o rascunho pelas definições atuais do Nextcloud. Descartar o rascunho e recarregar?'; @override - String get tocTitleDocumentationLink => 'aqui'; + String get notesSettingsRefreshFailed => + 'O Nextcloud aceitou as definições, mas a atualização das notas falhou. São apresentados os valores confirmados pelo servidor. Tente atualizar novamente a partir das Notas.'; @override - String get tocFindUsages => 'Localizar'; + String get notesUnavailable => + 'Sem ligação · Texto disponível localmente · Guardado localmente · sincronização pendente'; @override - String get tocRefactorMenu => 'Refatorar'; + String get notesStorageFull => + 'O armazenamento Nextcloud está cheio. Liberte espaço no servidor e tente novamente.'; @override - String get tocSafeDelete => 'Exclusão segura'; + String get notesRejected => + 'O Nextcloud rejeitou esta alteração. Reveja a nota e guarde uma cópia local antes de tentar novamente.'; @override - String get tocDuplicate => 'Duplicar'; + String get notesThrottled => + 'O Nextcloud pediu para aguardar. A sincronização será retomada após o prazo de espera.'; @override - String get tocDuplicateFilename => 'Nome do arquivo do tópico:'; + String get notesRecoveryRequired => 'Conflitos · Comparar'; @override - String get tocDuplicateDialog => 'Duplicar tópico'; + String get notesScanning => 'A analisar ficheiros…'; @override - String get pdfTitlePage => 'Página de título'; + String get notesCancelling => + 'A cancelar… A aguardar que a operação atual termine.'; @override - String get pdfIncludeTitlePage => 'Incluir página de título'; + String get notesCancelled => 'Cancelado'; @override - String get pdfTitlePageNote => - 'Esta página não tem cabeçalho, rodapé nem número de página visível.'; + String get notesPartial => 'Parcialmente concluído'; @override - String get pdfTitlePageTitle => 'Título'; + String get notesTransferFailed => 'Falhou. Tentar novamente'; @override - String get pdfTitlePageSubtitle => 'Subtítulo'; + String get notesImportPartialHelp => + 'Os itens anteriores podem já estar guardados localmente. Retome esta importação para continuar sem os duplicar.'; @override - String get pdfTitlePageAuthor => 'Autor'; + String get notesNotStarted => 'Não iniciado'; @override - String get pdfTitlePageOrganization => 'Organização'; + String get notesCreated => 'Criado'; @override - String get pdfTitlePageVersion => 'Versão'; + String get notesRemoteDeleteUnavailable => + 'URL do servidor · Não suportada · Eliminar · Anexos'; @override - String get pdfTitlePageDate => 'Data'; + String get notesCurrentText => 'Texto atualmente guardado'; @override - String get pdfTitlePageTitleRequired => - 'Insira um título para a página de título.'; + String get notesRecovering => 'A recuperar como nova nota…'; @override - String get pdfTocSectionNote => - 'Começa em uma página separada e pode ocupar várias páginas.'; + String get notesReadOnlyHelp => + 'Só de leitura · Favoritos · Guardar cópia local…'; @override - String get appTitle => 'BusyMark'; + String get notesSort => 'Ordenar notas'; @override - String get appSubtitle => - 'Editor para arquivos Markdown e projetos de documentação compatíveis com o Writerside.'; + String get notesPathRelative => + 'O caminho da pasta de notas é relativo ao seu armazenamento Nextcloud.'; @override - String get aboutBusyMark => 'Sobre BusyMark'; + String get notesSettingsStale => + 'As definições mudaram no Nextcloud. Recarregue e reveja os valores do servidor antes de substituir o rascunho.'; @override - String get aboutTagline => 'Editor de Markdown e Writerside'; + String get notesDownloadsPaused => 'Transferências pausadas'; @override - String get aboutLicenseLabel => 'Licença'; + String get notesExportNotPublished => 'Não foi publicada nenhuma exportação.'; @override - String get aboutLicenseName => 'Apache License 2.0'; + String get notesKeyringUnavailable => + 'Desbloqueie o porta-chaves do ambiente de trabalho e tente novamente.'; @override - String get aboutWebsite => 'Site'; + String get notesConnectFailed => + 'Não foi possível ligar ao Nextcloud Notes. Verifique o endereço do servidor e tente novamente.'; @override - String get aboutSourceCode => 'Código-fonte'; + String get notesAccountMismatch => + 'Inicie sessão na mesma conta Nextcloud. Remova esta conta primeiro para ligar outra.'; @override - String get reportIssue => 'Comunicar um problema'; + String get notesLocalRemovalFailed => + 'A limpeza local da conta falhou. A conta é mantida para poder tentar removê-la novamente.'; @override - String get feedbackCategory => 'Categoria'; + String get notesTransferring => 'A transferir notas…'; @override - String get feedbackChooseCategory => 'Escolha uma categoria'; + String get notesTechnicalDetails => 'Detalhes técnicos'; @override - String get feedbackCategoryProblem => 'Problema ou erro'; + String get notesPauseRequirement => 'Pausar requisito offline'; @override - String get feedbackCategoryFeature => 'Pedido de funcionalidade'; + String get notesResumeRequirement => 'Retomar requisito offline'; +} + +/// The translations for Portuguese, as used in Brazil (`pt_BR`). +class AppLocalizationsPtBr extends AppLocalizationsPt { + AppLocalizationsPtBr() : super('pt_BR'); + + @override + String get notesNewFromRetainedText => + 'Nova nota a partir do texto preservado'; + + @override + String get notesAccountThrottled => + 'Nextcloud pediu uma pausa. Aguarde antes de entrar novamente.'; + + @override + String get notesSettingsRefreshing => + 'A atualizar Notes após a confirmação das configurações'; + + @override + String get notesExportOperationFailed => + 'A exportação falhou antes da publicação. Verifique a origem e o destino e volte a exportar.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'A nota recuperada foi salva, mas não abriu. Resolva as alterações pendentes no editor ou nas propriedades e abra esta nota existente.'; + + @override + String get notesSettingsPendingNotes => + 'Notas pendentes, conflitos ou criações incertas impedem esta alteração. Sincronize ou reveja em Notes e volte a salvar este rascunho preservado.'; + + @override + String get notesSettingsPendingAttachments => + 'Operações de anexos impedem esta alteração. Reveja os anexos em Notes e volte a salvar este rascunho preservado.'; + + @override + String get notesAccountServerAddress => + 'Use o endereço HTTPS canônico do Nextcloud e reinicie a autenticação. Não foi possível verificar o endereço ou o redirecionamento.'; + + @override + String get notesAccountUnsupported => + 'Notes está indisponível ou a API não é suportada. Ative Nextcloud Notes com API principal 1 e secundária 4 ou posterior.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud rejeitou as credenciais. Autorize novamente a conta ou volte a ligá-la.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud recusou a verificação de funcionalidades de Notes. Peça autorização ao administrador e tente novamente.'; + + @override + String get notesAccountMalformed => + 'Não foi possível verificar a resposta do Nextcloud. Verifique a instalação do servidor e reinicie a autenticação.'; + + @override + String get notesAccountTimedOut => + 'A autenticação expirou. Reinicie-a e conclua a autorização no navegador.'; + + @override + String get notesAccountBrowserUnavailable => + 'Não foi possível abrir o navegador do sistema. Verifique o navegador padrão e reinicie a autenticação.'; + + @override + String get notesAccountNetwork => + 'Nextcloud está inacessível. Verifique a conexão e o servidor e tente autenticar-se novamente.'; + + @override + String get notesAccountRejected => + 'Nextcloud rejeitou o pedido de funcionalidades. Verifique o acesso com o administrador e tente novamente.'; + + @override + String get notesAccountFailed => + 'Não foi possível configurar a conta. Tente autenticar-se novamente.'; + + @override + String get notesTransferSourceChanged => + 'A origem mudou desde a revisão. Reveja o arquivo atual ou capture uma nova imagem antes de tentar novamente.'; + + @override + String get notesTransferUnreadable => + 'Não foi possível ler a origem. Verifique o acesso ao arquivo ou pasta e reveja.'; + + @override + String get notesTransferInvalidSnapshot => + 'Imagem inválida ou não suportada. Escolha uma imagem BusyMark Notes válida ou uma origem Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Os conteúdos multimédia excedem o limite de importação. Reduza os do item afetado e reveja-o.'; + + @override + String get notesTransferUnsafeSource => + 'A origem contém um caminho inseguro. Escolha arquivos na pasta de origem e reveja.'; + + @override + String get notesTransferDestination => + 'Não foi possível escrever a exportação. Verifique o acesso e espaço livre no destino e tente novamente.'; + + @override + String get notesTransferOperationFailed => + 'A transferência falhou. Reveja os itens afetados e tente novamente. Os já importados continuam salvos.'; + + @override + String get notesExportPublishedWithOmissions => + 'Exportação disponível com omissões'; + + @override + String get notesBatchUnchanged => 'Os valores pedidos já estavam definidos.'; + + @override + String get notesBatchReadOnly => + 'Esta nota é só de leitura. As propriedades protegidas não podem mudar; Favorito continua permitido.'; + + @override + String get notesBatchReviewChanged => + 'Esta nota mudou desde a revisão. Reveja o estado atual antes de repetir a ação.'; + + @override + String get notesBatchMissing => + 'Esta nota já não está disponível. Atualize a lista e reveja os destinos.'; + + @override + String get notesBatchStorage => + 'Falha ao salvar localmente. Verifique o armazenamento e repita este item; os bem-sucedidos continuam salvos.'; + + @override + String get notesRemoteReviewChanged => + 'A nota no servidor mudou. O rascunho de união foi preservado. Reveja a comparação atualizada e escolha novamente a resolução.'; + + @override + String get notesReviewRemote => 'Revi a versão atual do servidor'; + + @override + String get wsModuleCapsNotPublished => + 'A capitalização do módulo não é publicada.'; + + @override + String get wsTitleOverridesAction => 'Títulos da instância e do índice…'; + + @override + String get wsHeadingNotPublished => 'Não é publicado por este gerador.'; + + @override + String get wsMarkdownInstanceTitleLimitation => + 'O builder fixado não publica títulos por instância em tópicos Markdown. A marcação existente é preservada; consulte o código-fonte.'; + + @override + String get wsTopicIdentity => 'ID do tópico'; + + @override + String get wsElementAnchor => 'Âncora do elemento'; + + @override + String get wsHeadingText => 'Texto do cabeçalho'; + + @override + String get wsDefaultTitle => 'Título padrão'; + + @override + String get wsTopicNavigation => 'Navegação do tópico'; + + @override + String get wsHeadingOptions => 'Atributos do cabeçalho'; + + @override + String get wsDocumentOptions => 'Configurações do documento'; + + @override + String get wsSettingsPresent => 'Existem configurações explícitas'; + + @override + String get wsTitleOverrideExplanation => + 'Substitui o título publicado; o texto do cabeçalho permanece.'; + + @override + String get wsIgnoreVariables => 'Tratar variáveis como texto literal'; + + @override + String get wsIgnoreVariablesExplanation => + 'Quando ativo, o texto semelhante a variáveis não é substituído.'; + + @override + String get wsInstanceSelection => 'Instâncias de ajuda'; + + @override + String get wsInstanceExplanation => + 'Selecione saídas por nomes, grupos, listas, exclusões ou expressões.'; + + @override + String get wsFilterExplanation => + 'Filtra conteúdo reutilizável por critérios; não seleciona uma instância de ajuda.'; + + @override + String get wsContextHelp => 'ID de ajuda contextual'; + + @override + String get wsCapitalization => 'Capitalização'; + + @override + String get wsLibraryTopic => 'Tópico de biblioteca'; + + @override + String get wsLastModified => 'Última alteração'; + + @override + String get wsMissingTopicHeading => + 'Não existe H1 do tópico. Adicione-o no código-fonte.'; + + @override + String get wsMultipleTopicHeadings => + 'O primeiro H1 determina o título do tópico. Os restantes são cabeçalhos separados.'; + + @override + String get wsTopicIdentityMismatch => + 'O id da raiz difere do ID baseado no arquivo. Use Renomear arquivo do tópico ou consulte o código-fonte.'; + + @override + String get wsUnsupportedProperties => + 'Os atributos não suportados são preservados. Consulte-os no código-fonte.'; + + @override + String get wsGeneratedAnchor => + 'Âncora gerada; introduzir um valor cria uma substituição.'; + + @override + String get wsHeadingBuilderLimitation => + 'O builder fixado não publica atributos H1. Pertencem ao cabeçalho, não à raiz XML.'; + + @override + String get closeOtherTabs => 'Fechar as outras abas'; + + @override + String get tocTopicFromTemplate => 'Tópico a partir de modelo...'; + + @override + String get tocSaveAsTemplate => 'Salvar como modelo'; + + @override + String get tocTemplateDialog => 'Criar tópico a partir de modelo'; + + @override + String get tocTemplatesEditor => 'Modelos de arquivos e código'; + + @override + String get tocEditTemplates => 'Editar modelos...'; + + @override + String get tocCreateCustomTemplate => 'Criar modelo personalizado...'; + + @override + String get tocTemplateCreated => 'Modelo criado'; + + @override + String get tocTemplateDefault => 'Padrão'; + + @override + String get tocTemplateCustom => 'Personalizados'; + + @override + String get tocTemplateFilename => 'Nome do arquivo:'; + + @override + String get tocTemplateMarkdown => 'Markdown (.md)'; + + @override + String get tocTemplateXml => 'XML (.topic)'; + + @override + String get tocTemplatesInternal => 'Internos'; + + @override + String get tocTemplateUnnamed => 'Sem nome'; + + @override + String get tocTemplateNew => 'Novo modelo...'; + + @override + String get tocTemplateName => 'Nome:'; + + @override + String get tocTemplateExtension => 'Extensão:'; + + @override + String get tocTemplateLoadError => + 'Não foi possível carregar os modelos. Os modelos existentes não foram alterados.'; + + @override + String get tocTemplateSaveError => + 'Não foi possível salvar os modelos. Suas alterações continuam disponíveis aqui.'; + + @override + String get tocTemplateConflict => + 'Os modelos foram alterados em outra janela. Cancele e reabra o editor.'; + + @override + String get tocTemplateDuplicateName => + 'Já existe um modelo com este nome e extensão.'; + + @override + String get tocTemplateFormat => 'Formato:'; + + @override + String get tocTemplateReset => 'Redefinir'; + + @override + String get tocTemplateRetry => 'Tentar novamente'; + + @override + String tocRemoveElementsDialog(int count) { + return 'Remover $count elementos do sumário'; + } + + @override + String get tocRemoveElementsRetainFiles => + 'Os arquivos de origem associados a estes elementos do sumário não serão excluídos.'; + + @override + String get tocOk => 'OK'; + + @override + String get tocSelectExistingTopic => + 'Selecionar tópico para adicionar à instância atual'; + + @override + String get tocGroupName => 'Nome do grupo'; + + @override + String get tocMoveLineUp => 'Mover linha para cima'; + + @override + String get tocMoveLineDown => 'Mover linha para baixo'; + + @override + String get tocEmptyMdTopic => 'Tópico MD vazio'; + + @override + String get tocEmptyXmlTopic => 'Tópico XML vazio'; + + @override + String get tocCopySpecial => 'Cópia especial'; + + @override + String tocCopyFileName(String name) { + return 'Nome do arquivo do tópico \'$name\''; + } + + @override + String get tocCopyFilePath => 'Caminho do arquivo do tópico'; + + @override + String tocCopyTopicTitle(String title) { + return 'Título do tópico \'$title\''; + } + + @override + String tocCopyElementId(String id) { + return 'ID do elemento do índice \'$id\''; + } + + @override + String get tocCopyFileNameUnavailable => 'Nome do arquivo do tópico'; + + @override + String get tocCopyTopicTitleUnavailable => 'Título do tópico'; + + @override + String get tocCopyElementIdUnavailable => 'ID do elemento do índice'; + + @override + String get tocEditTitleAction => 'Editar título...'; + + @override + String get tocEditTitleDialog => 'Editar título'; + + @override + String get tocRemoveElementAction => 'Remover elemento do índice...'; + + @override + String get tocSetHomePage => 'Definir como página inicial'; + + @override + String tocGoToElement(String fileName) { + return 'Ir para o elemento do índice em \'$fileName\''; + } + + @override + String get tocSynchronize => 'Sincronizar índice e editor'; + + @override + String get tocEmptyGroupAction => 'Grupo vazio'; + + @override + String get tocNewEmptyGroup => 'Novo grupo vazio'; + + @override + String get tocGroupAction => 'Agrupar'; + + @override + String get tocNewGroup => 'Novo grupo'; + + @override + String get tocSortChildren => 'Ordenar subtópicos alfabeticamente'; + + @override + String get tocLinkTopicFiles => 'Vincular arquivos de tópicos ao índice...'; + + @override + String get addLocalMarkdownFiles => 'Adicionar arquivos Markdown locais'; + + @override + String get tocTitleField => 'Título no índice:'; + + @override + String get tocTopicTitleField => 'Título do tópico:'; + + @override + String get tocAdvancedSettings => 'Configurações avançadas'; + + @override + String tocInstanceTitleField(String instanceId) { + return 'Título para \'$instanceId\':'; + } + + @override + String get tocOnlyTitleField => 'Título exclusivo do índice:'; + + @override + String get tocInstanceTitleExplanation => + 'Usado apenas na instância atual. Por padrão, é herdado do título do tópico.'; + + @override + String get tocOnlyTitleExplanation => + 'Usado apenas no índice. Por padrão, é herdado do título do tópico ou do título específico da instância, se definido. Os títulos são explicados'; + + @override + String get tocTitleDocumentationLink => 'aqui'; + + @override + String get tocFindUsages => 'Localizar'; + + @override + String get tocRefactorMenu => 'Refatorar'; + + @override + String get tocSafeDelete => 'Exclusão segura'; + + @override + String get tocDuplicate => 'Duplicar'; + + @override + String get tocDuplicateFilename => 'Nome do arquivo do tópico:'; + + @override + String get tocDuplicateDialog => 'Duplicar tópico'; + + @override + String get pdfTitlePage => 'Página de título'; + + @override + String get pdfIncludeTitlePage => 'Incluir página de título'; + + @override + String get pdfTitlePageNote => + 'Esta página não tem cabeçalho, rodapé nem número de página visível.'; + + @override + String get pdfTitlePageTitle => 'Título'; + + @override + String get pdfTitlePageSubtitle => 'Subtítulo'; + + @override + String get pdfTitlePageAuthor => 'Autor'; + + @override + String get pdfTitlePageOrganization => 'Organização'; + + @override + String get pdfTitlePageVersion => 'Versão'; + + @override + String get pdfTitlePageDate => 'Data'; + + @override + String get pdfTitlePageTitleRequired => + 'Insira um título para a página de título.'; + + @override + String get pdfTocSectionNote => + 'Começa em uma página separada e pode ocupar várias páginas.'; + + @override + String get appTitle => 'BusyMark'; + + @override + String get appSubtitle => + 'Editor para arquivos Markdown e projetos de documentação compatíveis com o Writerside.'; + + @override + String get aboutBusyMark => 'Sobre BusyMark'; + + @override + String get aboutTagline => 'Editor de Markdown e Writerside'; + + @override + String get aboutLicenseLabel => 'Licença'; + + @override + String get aboutLicenseName => 'Apache License 2.0'; + + @override + String get aboutWebsite => 'Site'; + + @override + String get aboutSourceCode => 'Código-fonte'; + + @override + String get reportIssue => 'Comunicar um problema'; + + @override + String get feedbackCategory => 'Categoria'; + + @override + String get feedbackChooseCategory => 'Escolha uma categoria'; + + @override + String get feedbackCategoryProblem => 'Problema ou erro'; + + @override + String get feedbackCategoryFeature => 'Pedido de funcionalidade'; @override String get feedbackCategoryPrivacySecurity => @@ -6043,15 +6655,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get keyboardShortcuts => 'Atalhos de teclado'; - @override - String get commandPalette => 'Paleta de comandos'; - - @override - String get commandPaletteHint => 'Digite um comando'; - - @override - String get commandPaletteEmpty => 'Nenhum comando correspondente'; - @override String get commandUnavailableInContext => 'Este comando não está disponível no contexto atual.'; @@ -6271,7 +6874,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get shortcutKeyboardShortcutsDescription => - 'Mostrar esta referência de atalhos de teclado'; + 'Personalizar os atalhos de teclado do aplicativo'; @override String get shortcutSyntaxReferenceDescription => @@ -6526,7 +7129,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get validateOnEdit => 'Validar ao editar'; @override - String get clearRecentWorkspaces => 'Limpar espaços de trabalho recentes'; + String get clearRecentWorkspaces => 'Limpar histórico recente'; @override String get editingButtonsPosition => 'Posição dos botões de edição'; @@ -7310,17 +7913,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get sourceSearchPreviousMatch => 'Correspondência anterior'; @override - String get sourceSearchNextMatch => 'Próxima correspondência'; - - @override - String get sourceSearchCaseSensitive => - 'Diferenciar maiúsculas de minúsculas'; - - @override - String get sourceSearchWholeWord => 'Palavra inteira'; - - @override - String get sourceSearchRegex => 'Expressão regular'; + String get sourceSearchNextMatch => 'Próxima correspondência'; @override String get sourceSearchReplacement => 'Substituir por'; @@ -7449,13 +8042,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get saveAs => 'Salvar como'; - @override - String get sourceSearchInvalidRegex => 'Expressão regular inválida'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Não há suporte para correspondências regex de comprimento zero (por exemplo, ^, \$ ou antecipações que correspondem apenas a uma posição).'; - @override String get workspaceSearchIncomplete => 'Apenas alguns resultados são exibidos. Mostre mais ou restrinja a pesquisa.'; @@ -7565,6 +8151,11 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get documentKindResourceFile => 'Arquivo de recursos'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Não foi possível atualizar os itens recentes: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Falha ao abrir: $error'; @@ -9858,10 +10449,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get clipboardHistory => 'Histórico da Área de Transferência'; - @override - String get clipboardHistorySearchHint => - 'Pesquisar no histórico da área de transferência'; - @override String get clipboardHistoryDescription => 'Abrir o histórico da área de transferência local da sessão.'; @@ -9886,10 +10473,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get clipboardNoItems => 'O histórico da área de transferência está vazio.'; - @override - String get clipboardNoMatches => - 'Nenhum item da área de transferência corresponde a esta pesquisa.'; - @override String get clipboardDisabled => 'A coleção de Histórico da Área de Transferência está desativada nas Configurações.'; @@ -9947,13 +10530,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get localHistoryNoRevisions => 'Este documento não possui revisões no Histórico Local.'; - @override - String get localHistoryNoMatches => - 'Nenhuma revisão no Histórico Local corresponde a esta pesquisa.'; - - @override - String get localHistorySearchHint => 'Pesquisar no histórico local'; - @override String get localHistorySelectedRevision => 'Revisão selecionada'; @@ -10916,4 +11492,398 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get wsPropertiesUnavailable => 'O elemento inspecionado está indisponível. Inspecione um elemento para continuar.'; + + @override + String get searchDocumentsCategory => 'Documentos'; + + @override + String get searchTopicsCategory => 'Tópicos'; + + @override + String get searchCurrentDocument => 'Documento atual'; + + @override + String get searchCurrentWorkspace => 'Área de trabalho atual'; + + @override + String get browseNotes => 'Explorar notas'; + + @override + String get searchActionsCommand => 'Pesquisar ações'; + + @override + String get searchDocumentsCommand => 'Pesquisar documentos'; + + @override + String get searchTopicsCommand => 'Pesquisar tópicos'; + + @override + String get searchRecentCommand => 'Pesquisar recentes'; + + @override + String get searchResourceUnavailable => 'Este recurso não está disponível.'; + + @override + String get notesSync => 'Sincronizar notas'; + + @override + String get searchInWorkspace => 'Pesquisar no espaço de trabalho'; + + @override + String get searchCategory => 'Categoria de pesquisa'; + + @override + String get clearHistorySearchScope => 'Pesquisar no histórico de documentos'; + + @override + String get searchInDocument => 'Pesquisar no documento'; + + @override + String get searchDocumentHint => 'Pesquisar neste documento…'; + + @override + String get searchWorkspaceHint => 'Pesquisar neste espaço de trabalho…'; + + @override + String get searchOutlineHint => 'Pesquisar títulos…'; + + @override + String get searchHistoryHint => 'Pesquisar no histórico local…'; + + @override + String get searchEnterText => 'Digite texto para pesquisar'; + + @override + String get searchAccountUnavailable => 'Conta indisponível'; + + @override + String get searchDocumentsHint => 'Pesquisar documentos…'; + + @override + String get searchTopicsHint => 'Pesquisar tópicos…'; + + @override + String get searchRecentHint => 'Pesquisar recentes…'; + + @override + String get searchActionsHint => 'Pesquisar ações…'; + + @override + String get searchRequiresNotesWorkspace => + 'Requer um espaço de trabalho de Notas'; + + @override + String get searchSidebarLabel => 'Pesquisar:'; + + @override + String get searchDocumentTextCategory => 'Texto do documento'; + + @override + String get searchWorkspaceTextCategory => 'Texto do espaço de trabalho'; + + @override + String get searchHeadingsCategory => 'Títulos'; + + @override + String get shortcutUnassigned => 'Não atribuído'; + + @override + String get shortcutPressKeys => 'Pressione uma combinação de teclas…'; + + @override + String get shortcutRecord => 'Capturar atalho'; + + @override + String get shortcutRestoreAll => 'Restaurar todos os padrões'; + + @override + String get shortcutRestoreAllQuestion => + 'Restaurar os atalhos padrão de todas as ações? As atribuições personalizadas serão removidas.'; + + @override + String get shortcutSaveFailed => + 'Os atalhos estão ativos, mas não foram salvos. Tente novamente para mantê-los após reiniciar.'; + + @override + String get shortcutRetry => 'Tentar salvar novamente'; + + @override + String get shortcutChanged => + 'As atribuições mudaram. Confira os atalhos atuais e tente novamente.'; + + @override + String get shortcutInvalid => + 'Esta combinação não pode ser atribuída neste contexto.'; + + @override + String get shortcutSystemWarning => + 'O ambiente de trabalho pode usar esta combinação. O BusyMark não pode substituir atalhos do sistema.'; + + @override + String get shortcutConflicts => + 'Este atalho entra em conflito com estas ações:'; + + @override + String get shortcutRemove => 'Remover atribuição'; + + @override + String get shortcutRestoreDefault => 'Restaurar padrão'; + + @override + String get shortcutAssign => 'Atribuir'; + + @override + String get shortcutReassign => 'Reatribuir'; + + @override + String get shortcutContextApplication => 'Aplicativo'; + + @override + String get shortcutContextDocument => 'Visualizações do documento'; + + @override + String get shortcutContextEditor => 'Editor e código-fonte'; + + @override + String get shortcutContextWritersideSource => 'Código-fonte do Writerside'; + + @override + String get shortcutContextWorkspace => 'Navegação do espaço de trabalho'; + + @override + String get shortcutContextTree => 'Árvores laterais'; + + @override + String get searchHeadingsCommand => 'Pesquisar títulos'; + + @override + String get shortcutFixed => 'Gerenciado pelo editor; não é configurável.'; + + @override + String get shortcutDefaultConflict => + 'O atalho padrão está em conflito com uma atribuição personalizada.'; + + @override + String get shortcutCurrentBinding => 'Atalho atual'; + + @override + String get shortcutProposedBinding => 'Atalho proposto'; + + @override + String get notesActions => 'Ações das notas'; + + @override + String get noteActions => 'Ações da nota'; + + @override + String get noteProperties => 'Propriedades da nota'; + + @override + String get notesExportAll => 'Exportar notas · Todas as notas…'; + + @override + String get notesExportCategory => 'Exportar notas · Categoria…'; + + @override + String get notesOfflineAll => 'Disponibilizar offline · Todas as notas'; + + @override + String get notesOfflineCategory => 'Disponibilizar offline · Categoria'; + + @override + String get notesOfflineRemoveAll => + 'Remover retenção offline · Todas as notas'; + + @override + String get notesOfflineRemoveCategory => + 'Remover retenção offline · Categoria'; + + @override + String get notesOfflinePause => 'Pausar downloads'; + + @override + String get notesOfflineResume => 'Retomar downloads'; + + @override + String get notesOfflineKeep => 'Disponibilizar offline'; + + @override + String get notesOfflineRetentionHelp => + 'Remover este requisito mantém o conteúdo em cache. Um requisito da categoria ou da conta pode continuar retendo a nota.'; + + @override + String get notesOfflineBroader => + 'Esta nota também é retida por um requisito da categoria ou da conta.'; + + @override + String get notesOfflineExternal => + 'Mídia externa exige conexão. O BusyMark mantém apenas texto e anexos gerenciados.'; + + @override + String get notesEmptyAll => 'Ainda não há notas. Crie uma para começar.'; + + @override + String get notesEmptyFavorites => 'Não há notas favoritas.'; + + @override + String get notesEmptyCategory => 'Não há notas nesta categoria.'; + + @override + String get notesEmptyUncategorized => 'Não há notas sem categoria.'; + + @override + String get notesEmptyRecovery => 'Não há notas retidas para recuperar.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Falhou. Tentar novamente'; + + @override + String get notesOperationFailed => + 'Não foi possível concluir a operação. Seu rascunho foi mantido. Revise o salvamento e os conflitos e tente novamente.'; + + @override + String get notesInspectionFailed => 'Anexos · Falhou. Tentar novamente'; + + @override + String get notesStartingConnection => 'Iniciando conexão com o Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Verificando o acesso às notas…'; + + @override + String get notesCancellingConnection => 'Cancelando a entrada e limpando…'; + + @override + String get notesRemovingConnection => 'Removendo a conta local…'; + + @override + String get notesRemovedWarning => + 'A conta local do Nextcloud foi removida. A revogação da senha de aplicativo não foi confirmada. Remova a senha do BusyMark nas configurações de segurança do Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Não foi possível salvar todas as notas abertas localmente. Revise o salvamento, os conflitos ou a normalização na área de trabalho. As alterações locais ainda podem precisar de sincronização com o Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Salvar alterações nas configurações do Nextcloud antes de sair?'; + + @override + String get notesSettingsLeaveHelp => + 'Configurações de Notes do servidor · Alterações não salvas'; + + @override + String get notesSettingsReload => 'Recarregar configurações do servidor…'; + + @override + String get notesSettingsReloadHelp => + 'Recarregar substitui o rascunho pelas configurações atuais do Nextcloud. Descartar o rascunho e recarregar?'; + + @override + String get notesSettingsRefreshFailed => + 'O Nextcloud aceitou as configurações, mas a atualização das notas falhou. Os valores exibidos foram confirmados pelo servidor. Tente atualizar novamente pelas Notas.'; + + @override + String get notesUnavailable => + 'Sem conexão · Texto disponível localmente · Salvo localmente · sincronização pendente'; + + @override + String get notesStorageFull => + 'O armazenamento do Nextcloud está cheio. Libere espaço no servidor e tente novamente.'; + + @override + String get notesRejected => + 'O Nextcloud rejeitou esta alteração. Revise a nota e salve uma cópia local antes de tentar novamente.'; + + @override + String get notesThrottled => + 'O Nextcloud pediu para aguardar. A sincronização será retomada após o prazo de espera.'; + + @override + String get notesRecoveryRequired => 'Conflitos · Comparar'; + + @override + String get notesScanning => 'Verificando arquivos…'; + + @override + String get notesCancelling => + 'Cancelando… Aguardando a conclusão da operação atual.'; + + @override + String get notesCancelled => 'Cancelado'; + + @override + String get notesPartial => 'Parcialmente concluído'; + + @override + String get notesTransferFailed => 'Falhou. Tentar novamente'; + + @override + String get notesImportPartialHelp => + 'Itens anteriores podem já estar salvos localmente. Retome esta importação para continuar sem duplicá-los.'; + + @override + String get notesNotStarted => 'Não iniciado'; + + @override + String get notesCreated => 'Criado'; + + @override + String get notesRemoteDeleteUnavailable => + 'URL do servidor · Não compatível · Excluir · Anexos'; + + @override + String get notesCurrentText => 'Texto retido atualmente'; + + @override + String get notesRecovering => 'Recuperando como nova nota…'; + + @override + String get notesReadOnlyHelp => + 'Somente leitura · Favoritos · Salvar cópia local…'; + + @override + String get notesSort => 'Ordenar notas'; + + @override + String get notesPathRelative => + 'O caminho da pasta de notas é relativo ao seu armazenamento Nextcloud.'; + + @override + String get notesSettingsStale => + 'As configurações mudaram no Nextcloud. Recarregue e revise os valores do servidor antes de substituir o rascunho.'; + + @override + String get notesDownloadsPaused => 'Downloads pausados'; + + @override + String get notesExportNotPublished => 'Nenhuma exportação foi publicada.'; + + @override + String get notesKeyringUnavailable => + 'Desbloqueie o chaveiro da área de trabalho e tente novamente.'; + + @override + String get notesConnectFailed => + 'Não foi possível conectar ao Nextcloud Notes. Verifique o endereço do servidor e tente novamente.'; + + @override + String get notesAccountMismatch => + 'Entre na mesma conta Nextcloud. Remova esta conta primeiro para conectar outra.'; + + @override + String get notesLocalRemovalFailed => + 'A limpeza local da conta falhou. A conta foi mantida para que você possa tentar removê-la novamente.'; + + @override + String get notesTransferring => 'Transferindo notas…'; + + @override + String get notesTechnicalDetails => 'Detalhes técnicos'; + + @override + String get notesPauseRequirement => 'Pausar requisito offline'; + + @override + String get notesResumeRequirement => 'Retomar requisito offline'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 1f63a5fd..f2c9e3a9 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsRu extends AppLocalizations { AppLocalizationsRu([String locale = 'ru']) : super(locale); + @override + String get notesNewFromRetainedText => 'Новая заметка из сохранённого текста'; + + @override + String get notesAccountThrottled => + 'Nextcloud запросил паузу. Подождите перед повторным входом.'; + + @override + String get notesSettingsRefreshing => + 'Обновление Notes после подтверждения настроек'; + + @override + String get notesExportOperationFailed => + 'Экспорт не опубликован. Проверьте источник и место назначения, затем повторите.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Восстановленная заметка сохранена, но не открылась. Завершите правки редактора или свойств и откройте эту существующую заметку.'; + + @override + String get notesSettingsPendingNotes => + 'Изменению мешают ожидающие заметки, конфликты или неопределённые создания. Синхронизируйте или проверьте их в Notes, затем повторите сохранение черновика настроек.'; + + @override + String get notesSettingsPendingAttachments => + 'Изменению мешают операции с вложениями. Проверьте их в Notes, затем повторите сохранение сохранённого черновика настроек.'; + + @override + String get notesAccountServerAddress => + 'Используйте канонический HTTPS-адрес Nextcloud и войдите снова. Адрес или перенаправление не удалось проверить.'; + + @override + String get notesAccountUnsupported => + 'Notes недоступен или API не поддерживается. Включите Nextcloud Notes с API основной версии 1, дополнительной 4 или новее.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud отклонил учётные данные. Повторно авторизуйте или подключите аккаунт.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud запретил проверку возможностей Notes. Попросите администратора разрешить её и повторите.'; + + @override + String get notesAccountMalformed => + 'Не удалось проверить ответ Nextcloud. Проверьте установку сервера и войдите снова.'; + + @override + String get notesAccountTimedOut => + 'Время входа истекло. Начните заново и завершите авторизацию в браузере.'; + + @override + String get notesAccountBrowserUnavailable => + 'Системный браузер не открылся. Проверьте браузер по умолчанию и войдите снова.'; + + @override + String get notesAccountNetwork => + 'Nextcloud недоступен. Проверьте соединение и сервер и войдите снова.'; + + @override + String get notesAccountRejected => + 'Nextcloud отклонил запрос возможностей. Проверьте доступ с администратором и повторите.'; + + @override + String get notesAccountFailed => + 'Настройка аккаунта не завершена. Повторите вход.'; + + @override + String get notesTransferSourceChanged => + 'Источник изменился после проверки. Проверьте текущий файл или создайте новый снимок перед повтором.'; + + @override + String get notesTransferUnreadable => + 'Источник не удалось прочитать. Проверьте доступ к файлу или папке и выполните проверку снова.'; + + @override + String get notesTransferInvalidSnapshot => + 'Снимок недействителен или не поддерживается. Выберите корректный снимок BusyMark Notes или источник Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Медиа превышает предел импорта. Уменьшите медиа затронутого элемента и проверьте снова.'; + + @override + String get notesTransferUnsafeSource => + 'Источник содержит небезопасный путь. Выберите файлы внутри исходной папки и проверьте снова.'; + + @override + String get notesTransferDestination => + 'Не удалось записать экспорт. Проверьте доступ и свободное место в назначении, затем повторите.'; + + @override + String get notesTransferOperationFailed => + 'Передача не завершена. Проверьте затронутые элементы и повторите. Ранее импортированные элементы сохранены.'; + + @override + String get notesExportPublishedWithOmissions => + 'Экспорт доступен с пропусками'; + + @override + String get notesBatchUnchanged => 'Запрошенные значения уже установлены.'; + + @override + String get notesBatchReadOnly => + 'Заметка доступна только для чтения. Защищённые свойства менять нельзя; избранное менять можно.'; + + @override + String get notesBatchReviewChanged => + 'Заметка изменилась после проверки. Проверьте её текущее состояние перед повтором действия.'; + + @override + String get notesBatchMissing => + 'Заметка больше недоступна. Обновите список и проверьте выбранные элементы.'; + + @override + String get notesBatchStorage => + 'Локальное сохранение не удалось. Проверьте хранилище и повторите этот элемент; успешные элементы сохранены.'; + + @override + String get notesRemoteReviewChanged => + 'Заметка на сервере изменилась. Черновик слияния сохранён. Проверьте новое сравнение и заново выберите решение.'; + + @override + String get notesReviewRemote => 'Я проверил текущую версию на сервере'; + @override String get wsModuleCapsNotPublished => 'Правило регистра модуля не публикуется.'; @@ -586,15 +711,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get keyboardShortcuts => 'Сочетания клавиш'; - @override - String get commandPalette => 'Палитра команд'; - - @override - String get commandPaletteHint => 'Введите команду'; - - @override - String get commandPaletteEmpty => 'Нет подходящих команд'; - @override String get commandUnavailableInContext => 'Эта команда недоступна в текущем контексте.'; @@ -814,7 +930,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Показать справку по сочетаниям клавиш'; + 'Настроить сочетания клавиш приложения'; @override String get shortcutSyntaxReferenceDescription => @@ -1063,8 +1179,7 @@ class AppLocalizationsRu extends AppLocalizations { String get validateOnEdit => 'Проверять при редактировании'; @override - String get clearRecentWorkspaces => - 'Очистить список недавних рабочих областей'; + String get clearRecentWorkspaces => 'Очистить недавнюю историю'; @override String get editingButtonsPosition => 'Положение кнопок редактирования'; @@ -1865,15 +1980,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get sourceSearchNextMatch => 'Следующее совпадение'; - @override - String get sourceSearchCaseSensitive => 'С учётом регистра'; - - @override - String get sourceSearchWholeWord => 'Слово целиком'; - - @override - String get sourceSearchRegex => 'Регулярное выражение'; - @override String get sourceSearchReplacement => 'Заменить на'; @@ -2000,13 +2106,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get saveAs => 'Сохранить как'; - @override - String get sourceSearchInvalidRegex => 'Некорректное регулярное выражение'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Совпадения регулярных выражений нулевой длины не поддерживаются (например, ^, \$ или проверки вперёд, совпадающие только с позицией).'; - @override String get workspaceSearchIncomplete => 'Показана только часть результатов. Покажите больше или уточните поиск.'; @@ -2115,6 +2214,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String get documentKindResourceFile => 'Файл ресурсов'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Не удалось обновить недавние элементы: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Не удалось открыть: $error'; @@ -4406,9 +4510,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get clipboardHistory => 'История буфера обмена'; - @override - String get clipboardHistorySearchHint => 'Поиск в истории буфера обмена'; - @override String get clipboardHistoryDescription => 'Открыть историю буфера обмена, специфичную для сеанса.'; @@ -4432,10 +4533,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get clipboardNoItems => 'История буфера обмена пуста.'; - @override - String get clipboardNoMatches => - 'Нет элементов буфера обмена, соответствующих этому поиску.'; - @override String get clipboardDisabled => 'Коллекция \"История буфера обмена\" отключена в настройках.'; @@ -4493,13 +4590,6 @@ class AppLocalizationsRu extends AppLocalizations { String get localHistoryNoRevisions => 'Этот документ не имеет версий в локальной истории.'; - @override - String get localHistoryNoMatches => - 'Ни одна версия не соответствует этому поиску.'; - - @override - String get localHistorySearchHint => 'Поиск в локальной истории'; - @override String get localHistorySelectedRevision => 'Выбранная версия'; @@ -5456,4 +5546,401 @@ class AppLocalizationsRu extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Проверяемый элемент недоступен. Выберите элемент для продолжения.'; + + @override + String get searchDocumentsCategory => 'Документы'; + + @override + String get searchTopicsCategory => 'Темы'; + + @override + String get searchCurrentDocument => 'Текущий документ'; + + @override + String get searchCurrentWorkspace => 'Текущая рабочая область'; + + @override + String get browseNotes => 'Обзор заметок'; + + @override + String get searchActionsCommand => 'Поиск действий'; + + @override + String get searchDocumentsCommand => 'Поиск документов'; + + @override + String get searchTopicsCommand => 'Поиск тем'; + + @override + String get searchRecentCommand => 'Поиск недавних'; + + @override + String get searchResourceUnavailable => 'Этот ресурс недоступен.'; + + @override + String get notesSync => 'Синхронизировать заметки'; + + @override + String get searchInWorkspace => 'Поиск в рабочей области'; + + @override + String get searchCategory => 'Категория поиска'; + + @override + String get clearHistorySearchScope => 'Поиск в истории документов'; + + @override + String get searchInDocument => 'Поиск в документе'; + + @override + String get searchDocumentHint => 'Поиск в этом документе…'; + + @override + String get searchWorkspaceHint => 'Поиск в этой рабочей области…'; + + @override + String get searchOutlineHint => 'Поиск заголовков…'; + + @override + String get searchHistoryHint => 'Поиск в локальной истории…'; + + @override + String get searchEnterText => 'Введите текст для поиска'; + + @override + String get searchAccountUnavailable => 'Аккаунт недоступен'; + + @override + String get searchDocumentsHint => 'Поиск документов…'; + + @override + String get searchTopicsHint => 'Поиск тем…'; + + @override + String get searchRecentHint => 'Поиск недавних…'; + + @override + String get searchActionsHint => 'Поиск действий…'; + + @override + String get searchRequiresNotesWorkspace => + 'Требуется рабочая область заметок'; + + @override + String get searchSidebarLabel => 'Поиск:'; + + @override + String get searchDocumentTextCategory => 'Текст документа'; + + @override + String get searchWorkspaceTextCategory => 'Текст рабочей области'; + + @override + String get searchHeadingsCategory => 'Заголовки'; + + @override + String get shortcutUnassigned => 'Не назначено'; + + @override + String get shortcutPressKeys => 'Нажмите сочетание клавиш…'; + + @override + String get shortcutRecord => 'Записать сочетание клавиш'; + + @override + String get shortcutRestoreAll => 'Восстановить все значения по умолчанию'; + + @override + String get shortcutRestoreAllQuestion => + 'Восстановить стандартные сочетания всех действий? Пользовательские назначения будут удалены.'; + + @override + String get shortcutSaveFailed => + 'Сочетания действуют, но не сохранены. Повторите сохранение, чтобы они сохранились после перезапуска.'; + + @override + String get shortcutRetry => 'Повторить сохранение'; + + @override + String get shortcutChanged => + 'Назначения изменились. Проверьте текущие сочетания и повторите попытку.'; + + @override + String get shortcutInvalid => + 'Это сочетание нельзя назначить в данном контексте.'; + + @override + String get shortcutSystemWarning => + 'Это сочетание может использоваться рабочим столом. BusyMark не может переопределять системные сочетания.'; + + @override + String get shortcutConflicts => + 'Это сочетание конфликтует со следующими действиями:'; + + @override + String get shortcutRemove => 'Удалить назначение'; + + @override + String get shortcutRestoreDefault => 'Восстановить стандартное'; + + @override + String get shortcutAssign => 'Назначить'; + + @override + String get shortcutReassign => 'Переназначить'; + + @override + String get shortcutContextApplication => 'Приложение'; + + @override + String get shortcutContextDocument => 'Представления документа'; + + @override + String get shortcutContextEditor => 'Редактор и исходный код'; + + @override + String get shortcutContextWritersideSource => 'Исходный код Writerside'; + + @override + String get shortcutContextWorkspace => 'Навигация рабочей области'; + + @override + String get shortcutContextTree => 'Деревья боковой панели'; + + @override + String get searchHeadingsCommand => 'Поиск заголовков'; + + @override + String get shortcutFixed => 'Управляется редактором; настройка недоступна.'; + + @override + String get shortcutDefaultConflict => + 'Сочетание по умолчанию конфликтует с пользовательским назначением.'; + + @override + String get shortcutCurrentBinding => 'Текущее сочетание'; + + @override + String get shortcutProposedBinding => 'Предлагаемое сочетание'; + + @override + String get notesActions => 'Действия с заметками'; + + @override + String get noteActions => 'Действия с заметкой'; + + @override + String get noteProperties => 'Свойства заметки'; + + @override + String get notesExportAll => 'Экспорт заметок · Все заметки…'; + + @override + String get notesExportCategory => 'Экспорт заметок · Категория…'; + + @override + String get notesOfflineAll => 'Сохранить для работы без сети · Все заметки'; + + @override + String get notesOfflineCategory => + 'Сохранить для работы без сети · Категория'; + + @override + String get notesOfflineRemoveAll => + 'Убрать сохранение без сети · Все заметки'; + + @override + String get notesOfflineRemoveCategory => + 'Убрать сохранение без сети · Категория'; + + @override + String get notesOfflinePause => 'Приостановить загрузки'; + + @override + String get notesOfflineResume => 'Возобновить загрузки'; + + @override + String get notesOfflineKeep => 'Сохранить для работы без сети'; + + @override + String get notesOfflineRetentionHelp => + 'Удаление требования не удаляет содержимое кэша. Требование категории или учётной записи может по-прежнему сохранять заметку.'; + + @override + String get notesOfflineBroader => + 'Заметка также сохраняется по требованию категории или учётной записи.'; + + @override + String get notesOfflineExternal => + 'Внешние медиа требуют подключения. BusyMark сохраняет только текст и управляемые вложения.'; + + @override + String get notesEmptyAll => 'Заметок пока нет. Создайте первую.'; + + @override + String get notesEmptyFavorites => 'Нет избранных заметок.'; + + @override + String get notesEmptyCategory => 'В этой категории нет заметок.'; + + @override + String get notesEmptyUncategorized => 'Нет заметок без категории.'; + + @override + String get notesEmptyRecovery => + 'Нет сохранённых заметок для восстановления.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Ошибка. Повторить'; + + @override + String get notesOperationFailed => + 'Не удалось завершить операцию. Черновик сохранён. Проверьте сохранение и конфликты, затем повторите попытку.'; + + @override + String get notesInspectionFailed => 'Вложения · Ошибка. Повторить'; + + @override + String get notesStartingConnection => 'Начало подключения к Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Проверка доступа к заметкам…'; + + @override + String get notesCancellingConnection => 'Отмена входа и очистка…'; + + @override + String get notesRemovingConnection => 'Удаление локальной учётной записи…'; + + @override + String get notesRemovedWarning => + 'Локальная учётная запись Nextcloud удалена. Отзыв пароля приложения не подтверждён. Удалите пароль BusyMark в настройках безопасности Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Не удалось сохранить все открытые заметки локально. Проверьте сохранение, конфликты или нормализацию в рабочей области. Локальные изменения могут ещё требовать синхронизации с Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Сохранить изменения настроек Nextcloud перед выходом?'; + + @override + String get notesSettingsLeaveHelp => + 'Настройки Notes на сервере · Несохранённые изменения'; + + @override + String get notesSettingsReload => 'Перезагрузить настройки сервера…'; + + @override + String get notesSettingsReloadHelp => + 'Перезагрузка заменит черновик текущими настройками Nextcloud. Отбросить черновик и перезагрузить?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud принял настройки, но обновить заметки не удалось. Показаны значения, подтверждённые сервером. Повторите обновление из Заметок.'; + + @override + String get notesUnavailable => + 'Нет сети · Текст доступен локально · Сохранено локально · ожидает синхронизации'; + + @override + String get notesStorageFull => + 'Хранилище Nextcloud заполнено. Освободите место на сервере и повторите попытку.'; + + @override + String get notesRejected => + 'Nextcloud отклонил это изменение. Проверьте заметку и сохраните локальную копию перед повторной попыткой.'; + + @override + String get notesThrottled => + 'Nextcloud попросил подождать. Синхронизация продолжится после указанного срока.'; + + @override + String get notesRecoveryRequired => 'Конфликты · Сравнить'; + + @override + String get notesScanning => 'Сканирование файлов…'; + + @override + String get notesCancelling => 'Отмена… Ожидание завершения текущей операции.'; + + @override + String get notesCancelled => 'Отменено'; + + @override + String get notesPartial => 'Частично завершено'; + + @override + String get notesTransferFailed => 'Ошибка. Повторить'; + + @override + String get notesImportPartialHelp => + 'Предыдущие элементы могут быть уже сохранены локально. Возобновите этот импорт, чтобы избежать дубликатов.'; + + @override + String get notesNotStarted => 'Не начато'; + + @override + String get notesCreated => 'Создано'; + + @override + String get notesRemoteDeleteUnavailable => + 'Адрес сервера · Не поддерживается · Удалить · Вложения'; + + @override + String get notesCurrentText => 'Текущий сохранённый текст'; + + @override + String get notesRecovering => 'Восстановление в новую заметку…'; + + @override + String get notesReadOnlyHelp => + 'Только чтение · Избранное · Сохранить локальную копию…'; + + @override + String get notesSort => 'Сортировка заметок'; + + @override + String get notesPathRelative => + 'Путь папки заметок задаётся относительно вашего хранилища Nextcloud.'; + + @override + String get notesSettingsStale => + 'Настройки на Nextcloud изменились. Перезагрузите и проверьте значения сервера перед заменой черновика.'; + + @override + String get notesDownloadsPaused => 'Загрузки приостановлены'; + + @override + String get notesExportNotPublished => 'Экспорт не опубликован.'; + + @override + String get notesKeyringUnavailable => + 'Разблокируйте системную связку ключей и повторите попытку.'; + + @override + String get notesConnectFailed => + 'Не удалось подключиться к Nextcloud Notes. Проверьте адрес сервера и повторите попытку.'; + + @override + String get notesAccountMismatch => + 'Войдите в ту же учётную запись Nextcloud. Для подключения другой сначала удалите эту.'; + + @override + String get notesLocalRemovalFailed => + 'Не удалось очистить локальные данные. Учётная запись сохранена для повторной попытки удаления.'; + + @override + String get notesTransferring => 'Передача заметок…'; + + @override + String get notesTechnicalDetails => 'Технические сведения'; + + @override + String get notesPauseRequirement => + 'Приостановить требование автономного хранения'; + + @override + String get notesResumeRequirement => + 'Возобновить требование автономного хранения'; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index e79598c2..ece405b3 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsTr extends AppLocalizations { AppLocalizationsTr([String locale = 'tr']) : super(locale); + @override + String get notesNewFromRetainedText => 'Saklanan metinden yeni not'; + + @override + String get notesAccountThrottled => + 'Nextcloud beklemenizi istedi. Yeniden oturum açmadan önce bekleyin.'; + + @override + String get notesSettingsRefreshing => + 'Onaylanan ayar güncellemesinden sonra Notes yenileniyor'; + + @override + String get notesExportOperationFailed => + 'Dışa aktarma yayımlanmadan başarısız oldu. Kaynağı ve hedefi denetleyip yeniden deneyin.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Kurtarılan not kaydedildi ancak açılamadı. Bekleyen düzenleyici veya özellik değişikliklerini çözün ve bu mevcut notu açın.'; + + @override + String get notesSettingsPendingNotes => + 'Bekleyen notlar, çakışmalar veya belirsiz oluşturmalar bu değişikliği engelliyor. Notes içinde eşitleyin ya da inceleyin, ardından korunan ayar taslağını yeniden kaydedin.'; + + @override + String get notesSettingsPendingAttachments => + 'Bekleyen ek işlemleri değişikliği engelliyor. Notes içinde ekleri inceleyip korunan ayar taslağını yeniden kaydedin.'; + + @override + String get notesAccountServerAddress => + 'Nextcloud’un kurallı HTTPS adresini kullanıp yeniden oturum açın. Adres veya yönlendirme doğrulanamadı.'; + + @override + String get notesAccountUnsupported => + 'Notes kullanılamıyor veya API desteklenmiyor. Ana API sürümü 1, alt sürümü en az 4 olan Nextcloud Notes’u etkinleştirin.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud kimlik bilgilerini reddetti. Hesabı yeniden yetkilendirin veya bağlayın.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud, Notes yetenek denetimini engelledi. Yöneticiden izin isteyip yeniden deneyin.'; + + @override + String get notesAccountMalformed => + 'Nextcloud yanıtı doğrulanamadı. Sunucu kurulumunu denetleyip yeniden oturum açın.'; + + @override + String get notesAccountTimedOut => + 'Oturum açma zaman aşımına uğradı. Yeniden başlatıp tarayıcıda yetkilendirmeyi tamamlayın.'; + + @override + String get notesAccountBrowserUnavailable => + 'Sistem tarayıcısı açılamadı. Varsayılan tarayıcıyı denetleyip yeniden oturum açın.'; + + @override + String get notesAccountNetwork => + 'Nextcloud’a ulaşılamadı. Bağlantıyı ve sunucuyu denetleyip yeniden oturum açın.'; + + @override + String get notesAccountRejected => + 'Nextcloud yetenek isteğini reddetti. Erişimi yöneticiyle denetleyip yeniden deneyin.'; + + @override + String get notesAccountFailed => + 'Hesap kurulumu tamamlanamadı. Yeniden oturum açmayı deneyin.'; + + @override + String get notesTransferSourceChanged => + 'Kaynak incelemeden sonra değişti. Yeniden denemeden önce güncel dosyayı inceleyin veya yeni bir anlık görüntü alın.'; + + @override + String get notesTransferUnreadable => + 'Kaynak okunamadı. Dosya veya klasör erişimini denetleyip yeniden inceleyin.'; + + @override + String get notesTransferInvalidSnapshot => + 'Anlık görüntü geçersiz veya desteklenmiyor. Geçerli bir BusyMark Notes görüntüsü veya Markdown kaynağı seçin.'; + + @override + String get notesTransferMediaLimit => + 'Medya içe aktarma boyut sınırını aşıyor. İlgili öğenin medyasını küçültüp yeniden inceleyin.'; + + @override + String get notesTransferUnsafeSource => + 'Kaynak güvensiz bir yol içeriyor. Kaynak klasörü içindeki dosyaları seçip yeniden inceleyin.'; + + @override + String get notesTransferDestination => + 'Dışa aktarma yazılamadı. Hedef erişimini ve boş alanı denetleyip yeniden deneyin.'; + + @override + String get notesTransferOperationFailed => + 'Aktarım tamamlanamadı. İlgili öğeleri inceleyip yeniden deneyin. Önceden içe aktarılan öğeler kayıtlı kalır.'; + + @override + String get notesExportPublishedWithOmissions => + 'Eksikleri olan dışa aktarma kullanılabilir'; + + @override + String get notesBatchUnchanged => 'İstenen değerler zaten ayarlanmıştı.'; + + @override + String get notesBatchReadOnly => + 'Bu not salt okunurdur. Korunan özellikler değiştirilemez; Favori yine değiştirilebilir.'; + + @override + String get notesBatchReviewChanged => + 'Bu not incelemeden sonra değişti. İşlemi yeniden uygulamadan önce güncel durumunu inceleyin.'; + + @override + String get notesBatchMissing => + 'Bu not artık kullanılamıyor. Listeyi yenileyip hedefleri yeniden inceleyin.'; + + @override + String get notesBatchStorage => + 'Yerel kayıt başarısız oldu. Depolamayı denetleyip bu öğeyi yeniden deneyin; başarılı öğeler kayıtlı kalır.'; + + @override + String get notesRemoteReviewChanged => + 'Sunucu notu değişti. Birleştirme taslağınız korunuyor. Güncel karşılaştırmayı inceleyip çözümü yeniden seçin.'; + + @override + String get notesReviewRemote => 'Güncel sunucu sürümünü inceledim'; + @override String get wsModuleCapsNotPublished => 'Modülün harf biçimi yayımlanmaz.'; @@ -582,15 +707,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get keyboardShortcuts => 'Klavye Kısayolları'; - @override - String get commandPalette => 'Komut Paleti'; - - @override - String get commandPaletteHint => 'Bir komut yazın'; - - @override - String get commandPaletteEmpty => 'Eşleşen komut yok'; - @override String get commandUnavailableInContext => 'Mevcut düzenleyici bağlamında kullanılamıyor'; @@ -809,7 +925,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Bu klavye kısayolu referansını göster'; + 'Uygulama klavye kısayollarını özelleştir'; @override String get shortcutSyntaxReferenceDescription => 'Sözdizimi başvurusunu aç'; @@ -1060,7 +1176,7 @@ class AppLocalizationsTr extends AppLocalizations { String get validateOnEdit => 'Düzenlemede doğrula'; @override - String get clearRecentWorkspaces => 'Son çalışma alanlarını temizle'; + String get clearRecentWorkspaces => 'Son geçmişi temizle'; @override String get editingButtonsPosition => 'Düzenleme düğmelerinin konumu'; @@ -1840,15 +1956,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get sourceSearchNextMatch => 'Sonraki eşleşme'; - @override - String get sourceSearchCaseSensitive => 'Büyük-küçük harf duyarlı'; - - @override - String get sourceSearchWholeWord => 'Tam sözcük'; - - @override - String get sourceSearchRegex => 'Düzenli ifade'; - @override String get sourceSearchReplacement => 'Şununla değiştir:'; @@ -1974,13 +2081,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get saveAs => 'Farklı Kaydet'; - @override - String get sourceSearchInvalidRegex => 'Geçersiz düzenli ifade'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Sıfır uzunluklu düzenli ifade eşleşmeleri desteklenmez (örneğin ^, \$ veya yalnızca bir konumla eşleşen ileri bakışlar).'; - @override String get workspaceSearchIncomplete => 'Sonuçların yalnızca bir kısmı gösteriliyor. Daha fazlasını gösterin veya aramayı daraltın.'; @@ -2090,6 +2190,11 @@ class AppLocalizationsTr extends AppLocalizations { @override String get documentKindResourceFile => 'Kaynak dosyası'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Son öğeler güncellenemedi: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Açma başarısız oldu: $error'; @@ -4350,9 +4455,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get clipboardHistory => 'Kopyalama Geçmişi'; - @override - String get clipboardHistorySearchHint => 'Pano geçmişinde ara'; - @override String get clipboardHistoryDescription => 'Oturuma özgü kopyalama geçmişini aç.'; @@ -4376,10 +4478,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get clipboardNoItems => 'Kopyalama geçmişi boş.'; - @override - String get clipboardNoMatches => - 'Bu arama ile eşleşen kopyalama öğesi bulunamadı.'; - @override String get clipboardDisabled => 'Kopyalama Geçmişi koleksiyonu Ayarlar\'da devre dışıdır.'; @@ -4437,13 +4535,6 @@ class AppLocalizationsTr extends AppLocalizations { String get localHistoryNoRevisions => 'Bu belge Yerel Geçmiş revizyonlarına sahip değil.'; - @override - String get localHistoryNoMatches => - 'Bu arama ile eşleşen Yerel Geçmiş revizyonu yok.'; - - @override - String get localHistorySearchHint => 'Yerel geçmişte ara'; - @override String get localHistorySelectedRevision => 'Seçilen revizyon'; @@ -5396,4 +5487,397 @@ class AppLocalizationsTr extends AppLocalizations { @override String get wsPropertiesUnavailable => 'İncelenen öğe kullanılamıyor. Devam etmek için bir öğeyi inceleyin.'; + + @override + String get searchDocumentsCategory => 'Belgeler'; + + @override + String get searchTopicsCategory => 'Konular'; + + @override + String get searchCurrentDocument => 'Geçerli belge'; + + @override + String get searchCurrentWorkspace => 'Geçerli çalışma alanı'; + + @override + String get browseNotes => 'Notlara göz at'; + + @override + String get searchActionsCommand => 'Eylemleri ara'; + + @override + String get searchDocumentsCommand => 'Belgeleri ara'; + + @override + String get searchTopicsCommand => 'Konuları ara'; + + @override + String get searchRecentCommand => 'Son kullanılanları ara'; + + @override + String get searchResourceUnavailable => 'Bu kaynak kullanılamıyor.'; + + @override + String get notesSync => 'Notları eşitle'; + + @override + String get searchInWorkspace => 'Çalışma Alanında Ara'; + + @override + String get searchCategory => 'Arama kategorisi'; + + @override + String get clearHistorySearchScope => 'Belgelerin geçmişinde ara'; + + @override + String get searchInDocument => 'Belgede ara'; + + @override + String get searchDocumentHint => 'Bu belgede ara…'; + + @override + String get searchWorkspaceHint => 'Bu çalışma alanında ara…'; + + @override + String get searchOutlineHint => 'Başlıklarda ara…'; + + @override + String get searchHistoryHint => 'Yerel geçmişte ara…'; + + @override + String get searchEnterText => 'Aranacak metni girin'; + + @override + String get searchAccountUnavailable => 'Hesap kullanılamıyor'; + + @override + String get searchDocumentsHint => 'Belgeleri ara…'; + + @override + String get searchTopicsHint => 'Konuları ara…'; + + @override + String get searchRecentHint => 'Son kullanılanları ara…'; + + @override + String get searchActionsHint => 'Eylemleri ara…'; + + @override + String get searchRequiresNotesWorkspace => 'Notlar çalışma alanı gerektirir'; + + @override + String get searchSidebarLabel => 'Ara:'; + + @override + String get searchDocumentTextCategory => 'Belge metni'; + + @override + String get searchWorkspaceTextCategory => 'Çalışma alanı metni'; + + @override + String get searchHeadingsCategory => 'Başlıklar'; + + @override + String get shortcutUnassigned => 'Atanmamış'; + + @override + String get shortcutPressKeys => 'Bir tuş birleşimine basın…'; + + @override + String get shortcutRecord => 'Kısayol kaydet'; + + @override + String get shortcutRestoreAll => 'Tüm varsayılanları geri yükle'; + + @override + String get shortcutRestoreAllQuestion => + 'Tüm eylemlerin varsayılan kısayolları geri yüklensin mi? Özel atamalar kaldırılacak.'; + + @override + String get shortcutSaveFailed => + 'Kısayollar etkin ancak kaydedilemedi. Yeniden başlatmadan sonra korumak için tekrar kaydedin.'; + + @override + String get shortcutRetry => 'Kaydetmeyi yeniden dene'; + + @override + String get shortcutChanged => + 'Atamalar değişti. Geçerli kısayolları gözden geçirip yeniden deneyin.'; + + @override + String get shortcutInvalid => 'Bu birleşim bu bağlamda atanamaz.'; + + @override + String get shortcutSystemWarning => + 'Masaüstünüz bu birleşimi kullanıyor olabilir. BusyMark sistem kısayollarını geçersiz kılamaz.'; + + @override + String get shortcutConflicts => 'Bu kısayol şu eylemlerle çakışıyor:'; + + @override + String get shortcutRemove => 'Atamayı kaldır'; + + @override + String get shortcutRestoreDefault => 'Varsayılanı geri yükle'; + + @override + String get shortcutAssign => 'Ata'; + + @override + String get shortcutReassign => 'Yeniden ata'; + + @override + String get shortcutContextApplication => 'Uygulama'; + + @override + String get shortcutContextDocument => 'Belge görünümleri'; + + @override + String get shortcutContextEditor => 'Düzenleyici ve kaynak'; + + @override + String get shortcutContextWritersideSource => 'Writerside kaynağı'; + + @override + String get shortcutContextWorkspace => 'Çalışma alanı gezintisi'; + + @override + String get shortcutContextTree => 'Kenar çubuğu ağaçları'; + + @override + String get searchHeadingsCommand => 'Başlıklarda Ara'; + + @override + String get shortcutFixed => + 'Düzenleyici tarafından yönetilir; yapılandırılamaz.'; + + @override + String get shortcutDefaultConflict => + 'Varsayılan kısayol özel bir atamayla çakışıyor.'; + + @override + String get shortcutCurrentBinding => 'Geçerli kısayol'; + + @override + String get shortcutProposedBinding => 'Önerilen kısayol'; + + @override + String get notesActions => 'Not işlemleri'; + + @override + String get noteActions => 'Bu notun işlemleri'; + + @override + String get noteProperties => 'Not Özellikleri'; + + @override + String get notesExportAll => 'Notları dışa aktar · Tüm notlar…'; + + @override + String get notesExportCategory => 'Notları dışa aktar · Kategori…'; + + @override + String get notesOfflineAll => 'Çevrimdışı kullanılabilir yap · Tüm notlar'; + + @override + String get notesOfflineCategory => 'Çevrimdışı kullanılabilir yap · Kategori'; + + @override + String get notesOfflineRemoveAll => + 'Çevrimdışı saklamayı kaldır · Tüm notlar'; + + @override + String get notesOfflineRemoveCategory => + 'Çevrimdışı saklamayı kaldır · Kategori'; + + @override + String get notesOfflinePause => 'İndirmeleri Duraklat'; + + @override + String get notesOfflineResume => 'İndirmeleri Sürdür'; + + @override + String get notesOfflineKeep => 'Çevrimdışı kullanılabilir yap'; + + @override + String get notesOfflineRetentionHelp => + 'Bu gereksinimin kaldırılması mevcut önbelleği silmez. Kategori veya hesap gereksinimi notu tutmaya devam edebilir.'; + + @override + String get notesOfflineBroader => + 'Bu not bir kategori veya hesap gereksinimi nedeniyle de tutuluyor.'; + + @override + String get notesOfflineExternal => + 'Harici medya bağlantı gerektirir. BusyMark yalnızca not metnini ve yönetilen ekleri tutar.'; + + @override + String get notesEmptyAll => 'Henüz not yok. Başlamak için bir not oluşturun.'; + + @override + String get notesEmptyFavorites => 'Favori not yok.'; + + @override + String get notesEmptyCategory => 'Bu kategoride not yok.'; + + @override + String get notesEmptyUncategorized => 'Kategorisiz not yok.'; + + @override + String get notesEmptyRecovery => 'Kurtarılacak saklanmış not yok.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Başarısız. Yeniden dene'; + + @override + String get notesOperationFailed => + 'İşlem tamamlanamadı. Taslağınız korunuyor. Kaydetmeyi ve çakışmaları inceleyip yeniden deneyin.'; + + @override + String get notesInspectionFailed => 'Ekler · Başarısız. Yeniden dene'; + + @override + String get notesStartingConnection => 'Nextcloud bağlantısı başlatılıyor…'; + + @override + String get notesVerifyingConnection => 'Notlar erişimi doğrulanıyor…'; + + @override + String get notesCancellingConnection => + 'Oturum açma iptal ediliyor ve temizleniyor…'; + + @override + String get notesRemovingConnection => 'Yerel hesap kaldırılıyor…'; + + @override + String get notesRemovedWarning => + 'Yerel Nextcloud hesabı kaldırıldı. Uygulama parolasının iptali doğrulanamadı. Nextcloud Güvenlik ayarlarından BusyMark uygulama parolasını kaldırın.'; + + @override + String get notesBuffersNotSaved => + 'Tüm açık notlar yerel olarak kaydedilemedi. Çalışma alanında kaydetmeyi, çakışmaları veya normalleştirmeyi inceleyin. Yerel değişikliklerin hâlâ Nextcloud ile eşitlenmesi gerekebilir.'; + + @override + String get notesSettingsLeave => + 'Ayrılmadan önce Nextcloud ayarlarındaki değişiklikler kaydedilsin mi?'; + + @override + String get notesSettingsLeaveHelp => + 'Sunucu Notes ayarları · Kaydedilmemiş değişiklikler'; + + @override + String get notesSettingsReload => 'Sunucu Ayarlarını Yeniden Yükle…'; + + @override + String get notesSettingsReloadHelp => + 'Yeniden yükleme, taslağınızı geçerli Nextcloud ayarlarıyla değiştirir. Taslak atılıp yeniden yüklensin mi?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud ayarları kabul etti ancak notları yenileme başarısız oldu. Gösterilen değerler sunucunun onayladığı değerlerdir. Notlar üzerinden yenilemeyi yeniden deneyin.'; + + @override + String get notesUnavailable => + 'Çevrimdışı · Metin yerel olarak kullanılabilir · Yerel kaydedildi · eşitleme bekliyor'; + + @override + String get notesStorageFull => + 'Nextcloud depolaması dolu. Sunucuda yer açıp yeniden deneyin.'; + + @override + String get notesRejected => + 'Nextcloud bu değişikliği reddetti. Yeniden denemeden önce notu inceleyin ve yerel bir kopya kaydedin.'; + + @override + String get notesThrottled => + 'Nextcloud beklemenizi istedi. Eşitleme, yeniden deneme süresi dolunca devam eder.'; + + @override + String get notesRecoveryRequired => 'Çatışmalar · Karşılaştır'; + + @override + String get notesScanning => 'Dosyalar taranıyor…'; + + @override + String get notesCancelling => + 'İptal ediliyor… Geçerli işlemin bitmesi bekleniyor.'; + + @override + String get notesCancelled => 'İptal edildi'; + + @override + String get notesPartial => 'Kısmen tamamlandı'; + + @override + String get notesTransferFailed => 'Başarısız. Yeniden dene'; + + @override + String get notesImportPartialHelp => + 'Önceki öğeler zaten yerel olarak kaydedilmiş olabilir. Kopya oluşturmadan devam etmek için bu içe aktarmayı sürdürün.'; + + @override + String get notesNotStarted => 'Başlatılmadı'; + + @override + String get notesCreated => 'Oluşturuldu'; + + @override + String get notesRemoteDeleteUnavailable => + 'Sunucu adresi · Desteklenmiyor · Sil · Ekler'; + + @override + String get notesCurrentText => 'Şu anda saklanan metin'; + + @override + String get notesRecovering => 'Yeni bir not olarak kurtarılıyor…'; + + @override + String get notesReadOnlyHelp => + 'Salt okunur · Sık kullanılanlar · Yerel kopyayı kaydet…'; + + @override + String get notesSort => 'Notları sırala'; + + @override + String get notesPathRelative => + 'Not klasörü yolu Nextcloud depolamanıza görelidir.'; + + @override + String get notesSettingsStale => + 'Nextcloud ayarları değişti. Taslağı değiştirmeden önce sunucu değerlerini yeniden yükleyip inceleyin.'; + + @override + String get notesDownloadsPaused => 'İndirmeler duraklatıldı'; + + @override + String get notesExportNotPublished => 'Hiçbir dışa aktarım yayımlanmadı.'; + + @override + String get notesKeyringUnavailable => + 'Masaüstü anahtarlığının kilidini açıp yeniden deneyin.'; + + @override + String get notesConnectFailed => + 'Nextcloud Notes bağlantısı kurulamadı. Sunucu adresini denetleyip yeniden deneyin.'; + + @override + String get notesAccountMismatch => + 'Aynı Nextcloud hesabında oturum açın. Başka bir hesap bağlamak için önce bunu kaldırın.'; + + @override + String get notesLocalRemovalFailed => + 'Yerel hesap temizliği başarısız oldu. Kaldırmayı yeniden deneyebilmeniz için hesap korundu.'; + + @override + String get notesTransferring => 'Notlar aktarılıyor…'; + + @override + String get notesTechnicalDetails => 'Teknik ayrıntılar'; + + @override + String get notesPauseRequirement => 'Çevrimdışı Gereksinimini Duraklat'; + + @override + String get notesResumeRequirement => 'Çevrimdışı Gereksinimini Sürdür'; } diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index 81c4de71..3b5f907d 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsUk extends AppLocalizations { AppLocalizationsUk([String locale = 'uk']) : super(locale); + @override + String get notesNewFromRetainedText => 'Нова нотатка зі збереженого тексту'; + + @override + String get notesAccountThrottled => + 'Nextcloud попросив зачекати. Зачекайте перед повторним входом.'; + + @override + String get notesSettingsRefreshing => + 'Оновлення Notes після підтвердження налаштувань'; + + @override + String get notesExportOperationFailed => + 'Експорт не опубліковано. Перевірте джерело та призначення й повторіть.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Відновлену нотатку збережено, але не відкрито. Завершіть зміни редактора або властивостей і відкрийте цю наявну нотатку.'; + + @override + String get notesSettingsPendingNotes => + 'Зміні заважають очікувані нотатки, конфлікти або непевні створення. Синхронізуйте чи перевірте їх у Notes і повторіть збереження чернетки налаштувань.'; + + @override + String get notesSettingsPendingAttachments => + 'Зміні заважають операції з вкладеннями. Перевірте їх у Notes і повторіть збереження збереженої чернетки.'; + + @override + String get notesAccountServerAddress => + 'Використайте канонічну HTTPS-адресу Nextcloud та ввійдіть знову. Адресу або перенаправлення не вдалося перевірити.'; + + @override + String get notesAccountUnsupported => + 'Notes недоступний або API не підтримується. Увімкніть Nextcloud Notes з основною версією API 1, додатковою 4 або новішою.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud відхилив облікові дані. Повторно авторизуйте або підключіть обліковий запис.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud заборонив перевірку можливостей Notes. Попросіть адміністратора дозволити її й повторіть.'; + + @override + String get notesAccountMalformed => + 'Не вдалося перевірити відповідь Nextcloud. Перевірте встановлення сервера та ввійдіть знову.'; + + @override + String get notesAccountTimedOut => + 'Час входу минув. Почніть знову та завершіть авторизацію в браузері.'; + + @override + String get notesAccountBrowserUnavailable => + 'Системний браузер не відкрився. Перевірте типовий браузер і ввійдіть знову.'; + + @override + String get notesAccountNetwork => + 'Nextcloud недоступний. Перевірте з’єднання та сервер і ввійдіть знову.'; + + @override + String get notesAccountRejected => + 'Nextcloud відхилив запит можливостей. Перевірте доступ з адміністратором і повторіть.'; + + @override + String get notesAccountFailed => + 'Налаштування облікового запису не завершено. Повторіть вхід.'; + + @override + String get notesTransferSourceChanged => + 'Джерело змінилося після перевірки. Перевірте поточний файл або створіть новий знімок.'; + + @override + String get notesTransferUnreadable => + 'Не вдалося прочитати джерело. Перевірте доступ до файлу чи теки й перевірте знову.'; + + @override + String get notesTransferInvalidSnapshot => + 'Знімок недійсний або не підтримується. Виберіть коректний знімок BusyMark Notes або джерело Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Медіа перевищують ліміт імпорту. Зменште медіа відповідного елемента й перевірте знову.'; + + @override + String get notesTransferUnsafeSource => + 'Джерело містить небезпечний шлях. Виберіть файли в межах вихідної теки та перевірте знову.'; + + @override + String get notesTransferDestination => + 'Не вдалося записати експорт. Перевірте доступ і вільне місце в призначенні й повторіть.'; + + @override + String get notesTransferOperationFailed => + 'Передавання не завершено. Перевірте відповідні елементи й повторіть. Раніше імпортовані елементи збережено.'; + + @override + String get notesExportPublishedWithOmissions => + 'Експорт доступний із пропусками'; + + @override + String get notesBatchUnchanged => 'Запитані значення вже встановлено.'; + + @override + String get notesBatchReadOnly => + 'Нотатка лише для читання. Захищені властивості змінювати не можна; обране змінювати можна.'; + + @override + String get notesBatchReviewChanged => + 'Нотатка змінилася після перевірки. Перевірте поточний стан перед повторенням дії.'; + + @override + String get notesBatchMissing => + 'Нотатка більше недоступна. Оновіть список і перевірте вибрані елементи.'; + + @override + String get notesBatchStorage => + 'Локальне збереження не вдалося. Перевірте сховище й повторіть цей елемент; успішні елементи збережено.'; + + @override + String get notesRemoteReviewChanged => + 'Нотатка сервера змінилася. Чернетку злиття збережено. Перевірте нове порівняння й виберіть рішення знову.'; + + @override + String get notesReviewRemote => 'Я перевірив поточну версію сервера'; + @override String get wsModuleCapsNotPublished => 'Правило регістру модуля не публікується.'; @@ -585,15 +710,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get keyboardShortcuts => 'Комбінації клавіш'; - @override - String get commandPalette => 'Палітра команд'; - - @override - String get commandPaletteHint => 'Введіть команду'; - - @override - String get commandPaletteEmpty => 'Немає відповідних команд'; - @override String get commandUnavailableInContext => 'Ця команда недоступна в поточному контексті.'; @@ -813,7 +929,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Показати довідку з комбінацій клавіш'; + 'Налаштувати сполучення клавіш застосунку'; @override String get shortcutSyntaxReferenceDescription => @@ -1068,8 +1184,7 @@ class AppLocalizationsUk extends AppLocalizations { String get validateOnEdit => 'Перевіряти під час редагування'; @override - String get clearRecentWorkspaces => - 'Очистити список останніх робочих областей'; + String get clearRecentWorkspaces => 'Очистити недавню історію'; @override String get editingButtonsPosition => 'Розташування кнопок редагування'; @@ -1873,15 +1988,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get sourceSearchNextMatch => 'Наступний збіг'; - @override - String get sourceSearchCaseSensitive => 'З урахуванням регістру'; - - @override - String get sourceSearchWholeWord => 'Ціле слово'; - - @override - String get sourceSearchRegex => 'Регулярний вираз'; - @override String get sourceSearchReplacement => 'Замінити на'; @@ -2008,13 +2114,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get saveAs => 'Зберегти як'; - @override - String get sourceSearchInvalidRegex => 'Некоректний регулярний вираз'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Збіги регулярних виразів нульової довжини не підтримуються (наприклад, ^, \$ або перевірки вперед, що збігаються лише з позицією).'; - @override String get workspaceSearchIncomplete => 'Показано лише частину результатів. Покажіть більше або звузьте пошук.'; @@ -2123,6 +2222,11 @@ class AppLocalizationsUk extends AppLocalizations { @override String get documentKindResourceFile => 'Файл ресурсів'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Не вдалося оновити нещодавні елементи: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Не вдалося відкрити: $error'; @@ -4414,9 +4518,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get clipboardHistory => 'Історія Вирізання'; - @override - String get clipboardHistorySearchHint => 'Пошук в історії буфера обміну'; - @override String get clipboardHistoryDescription => 'Відкрити історію вирізання, прив\'язану до сесії.'; @@ -4439,10 +4540,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get clipboardNoItems => 'Історія вирізання порожня.'; - @override - String get clipboardNoMatches => - 'Жоден елемент вирізання не відповідає цьому пошуку.'; - @override String get clipboardDisabled => 'Збірка \"Історія Вирізання\" вимкнена в Налаштуваннях.'; @@ -4500,13 +4597,6 @@ class AppLocalizationsUk extends AppLocalizations { String get localHistoryNoRevisions => 'Цей документ не має версій локальної історії.'; - @override - String get localHistoryNoMatches => - 'Жодна версія локальної історії не відповідає цьому пошуку.'; - - @override - String get localHistorySearchHint => 'Пошук у локальній історії'; - @override String get localHistorySelectedRevision => 'Обрана версія'; @@ -5466,4 +5556,400 @@ class AppLocalizationsUk extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Елемент, що перевіряється, недоступний. Виберіть елемент для продовження.'; + + @override + String get searchDocumentsCategory => 'Документи'; + + @override + String get searchTopicsCategory => 'Теми'; + + @override + String get searchCurrentDocument => 'Поточний документ'; + + @override + String get searchCurrentWorkspace => 'Поточна робоча область'; + + @override + String get browseNotes => 'Переглянути нотатки'; + + @override + String get searchActionsCommand => 'Пошук дій'; + + @override + String get searchDocumentsCommand => 'Пошук документів'; + + @override + String get searchTopicsCommand => 'Пошук тем'; + + @override + String get searchRecentCommand => 'Пошук нещодавніх'; + + @override + String get searchResourceUnavailable => 'Цей ресурс недоступний.'; + + @override + String get notesSync => 'Синхронізувати нотатки'; + + @override + String get searchInWorkspace => 'Пошук у робочій області'; + + @override + String get searchCategory => 'Категорія пошуку'; + + @override + String get clearHistorySearchScope => 'Пошук в історії документів'; + + @override + String get searchInDocument => 'Пошук у документі'; + + @override + String get searchDocumentHint => 'Пошук у цьому документі…'; + + @override + String get searchWorkspaceHint => 'Пошук у цьому робочому просторі…'; + + @override + String get searchOutlineHint => 'Пошук заголовків…'; + + @override + String get searchHistoryHint => 'Пошук у локальній історії…'; + + @override + String get searchEnterText => 'Введіть текст для пошуку'; + + @override + String get searchAccountUnavailable => 'Обліковий запис недоступний'; + + @override + String get searchDocumentsHint => 'Пошук документів…'; + + @override + String get searchTopicsHint => 'Пошук тем…'; + + @override + String get searchRecentHint => 'Пошук нещодавніх…'; + + @override + String get searchActionsHint => 'Пошук дій…'; + + @override + String get searchRequiresNotesWorkspace => 'Потрібен робочий простір нотаток'; + + @override + String get searchSidebarLabel => 'Пошук:'; + + @override + String get searchDocumentTextCategory => 'Текст документа'; + + @override + String get searchWorkspaceTextCategory => 'Текст робочої області'; + + @override + String get searchHeadingsCategory => 'Заголовки'; + + @override + String get shortcutUnassigned => 'Не призначено'; + + @override + String get shortcutPressKeys => 'Натисніть сполучення клавіш…'; + + @override + String get shortcutRecord => 'Записати сполучення клавіш'; + + @override + String get shortcutRestoreAll => 'Відновити всі типові значення'; + + @override + String get shortcutRestoreAllQuestion => + 'Відновити типові сполучення всіх дій? Власні призначення буде видалено.'; + + @override + String get shortcutSaveFailed => + 'Сполучення діють, але їх не збережено. Повторіть збереження, щоб залишити їх після перезапуску.'; + + @override + String get shortcutRetry => 'Повторити збереження'; + + @override + String get shortcutChanged => + 'Призначення змінилися. Перевірте поточні сполучення та повторіть спробу.'; + + @override + String get shortcutInvalid => + 'Це сполучення не можна призначити в цьому контексті.'; + + @override + String get shortcutSystemWarning => + 'Це сполучення може використовуватися стільницею. BusyMark не може перевизначати системні сполучення.'; + + @override + String get shortcutConflicts => 'Це сполучення конфліктує з такими діями:'; + + @override + String get shortcutRemove => 'Вилучити призначення'; + + @override + String get shortcutRestoreDefault => 'Відновити типове'; + + @override + String get shortcutAssign => 'Призначити'; + + @override + String get shortcutReassign => 'Перепризначити'; + + @override + String get shortcutContextApplication => 'Застосунок'; + + @override + String get shortcutContextDocument => 'Подання документа'; + + @override + String get shortcutContextEditor => 'Редактор і джерело'; + + @override + String get shortcutContextWritersideSource => 'Джерело Writerside'; + + @override + String get shortcutContextWorkspace => 'Навігація робочою областю'; + + @override + String get shortcutContextTree => 'Дерева бічної панелі'; + + @override + String get searchHeadingsCommand => 'Пошук заголовків'; + + @override + String get shortcutFixed => 'Керується редактором; налаштування недоступне.'; + + @override + String get shortcutDefaultConflict => + 'Типове сполучення конфліктує з користувацьким призначенням.'; + + @override + String get shortcutCurrentBinding => 'Поточне сполучення'; + + @override + String get shortcutProposedBinding => 'Запропоноване сполучення'; + + @override + String get notesActions => 'Дії з нотатками'; + + @override + String get noteActions => 'Дії з нотаткою'; + + @override + String get noteProperties => 'Властивості нотатки'; + + @override + String get notesExportAll => 'Експорт нотаток · Усі нотатки…'; + + @override + String get notesExportCategory => 'Експорт нотаток · Категорія…'; + + @override + String get notesOfflineAll => 'Зберегти для роботи без мережі · Усі нотатки'; + + @override + String get notesOfflineCategory => + 'Зберегти для роботи без мережі · Категорія'; + + @override + String get notesOfflineRemoveAll => + 'Вилучити збереження без мережі · Усі нотатки'; + + @override + String get notesOfflineRemoveCategory => + 'Вилучити збереження без мережі · Категорія'; + + @override + String get notesOfflinePause => 'Призупинити завантаження'; + + @override + String get notesOfflineResume => 'Відновити завантаження'; + + @override + String get notesOfflineKeep => 'Зберегти для роботи без мережі'; + + @override + String get notesOfflineRetentionHelp => + 'Видалення вимоги не видаляє вміст кешу. Вимога категорії чи облікового запису може й надалі зберігати нотатку.'; + + @override + String get notesOfflineBroader => + 'Ця нотатка також зберігається за вимогою категорії чи облікового запису.'; + + @override + String get notesOfflineExternal => + 'Зовнішні медіа потребують з’єднання. BusyMark зберігає лише текст і керовані вкладення.'; + + @override + String get notesEmptyAll => 'Нотаток ще немає. Створіть першу.'; + + @override + String get notesEmptyFavorites => 'Немає улюблених нотаток.'; + + @override + String get notesEmptyCategory => 'У цій категорії немає нотаток.'; + + @override + String get notesEmptyUncategorized => 'Немає нотаток без категорії.'; + + @override + String get notesEmptyRecovery => 'Немає збережених нотаток для відновлення.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Помилка. Повторити'; + + @override + String get notesOperationFailed => + 'Не вдалося завершити операцію. Чернетку збережено. Перевірте збереження та конфлікти й повторіть спробу.'; + + @override + String get notesInspectionFailed => 'Вкладення · Помилка. Повторити'; + + @override + String get notesStartingConnection => 'Початок підключення до Nextcloud…'; + + @override + String get notesVerifyingConnection => 'Перевірка доступу до нотаток…'; + + @override + String get notesCancellingConnection => 'Скасування входу та очищення…'; + + @override + String get notesRemovingConnection => + 'Видалення локального облікового запису…'; + + @override + String get notesRemovedWarning => + 'Локальний обліковий запис Nextcloud видалено. Відкликання пароля застосунку не підтверджено. Видаліть пароль BusyMark у налаштуваннях безпеки Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Не вдалося зберегти всі відкриті нотатки локально. Перевірте збереження, конфлікти чи нормалізацію в робочому просторі. Локальні зміни можуть ще потребувати синхронізації з Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Зберегти зміни налаштувань Nextcloud перед виходом?'; + + @override + String get notesSettingsLeaveHelp => + 'Налаштування Notes на сервері · Незбережені зміни'; + + @override + String get notesSettingsReload => 'Перезавантажити налаштування сервера…'; + + @override + String get notesSettingsReloadHelp => + 'Перезавантаження замінить чернетку поточними налаштуваннями Nextcloud. Відкинути чернетку й перезавантажити?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud прийняв налаштування, але оновлення нотаток не вдалося. Показано значення, підтверджені сервером. Повторіть оновлення з Нотаток.'; + + @override + String get notesUnavailable => + 'Немає мережі · Текст доступний локально · Збережено локально · очікує синхронізації'; + + @override + String get notesStorageFull => + 'Сховище Nextcloud заповнене. Звільніть місце на сервері та повторіть спробу.'; + + @override + String get notesRejected => + 'Nextcloud відхилив цю зміну. Перевірте нотатку та збережіть локальну копію перед повторенням.'; + + @override + String get notesThrottled => + 'Nextcloud попросив зачекати. Синхронізація продовжиться після зазначеного терміну.'; + + @override + String get notesRecoveryRequired => 'Конфлікти · Порівняти'; + + @override + String get notesScanning => 'Сканування файлів…'; + + @override + String get notesCancelling => + 'Скасування… Очікування завершення поточної операції.'; + + @override + String get notesCancelled => 'Скасовано'; + + @override + String get notesPartial => 'Частково завершено'; + + @override + String get notesTransferFailed => 'Помилка. Повторити'; + + @override + String get notesImportPartialHelp => + 'Попередні елементи можуть уже бути збережені локально. Відновіть цей імпорт, щоб уникнути дублікатів.'; + + @override + String get notesNotStarted => 'Не розпочато'; + + @override + String get notesCreated => 'Створено'; + + @override + String get notesRemoteDeleteUnavailable => + 'Адреса сервера · Не підтримується · Видалити · Вкладення'; + + @override + String get notesCurrentText => 'Поточний збережений текст'; + + @override + String get notesRecovering => 'Відновлення як нової нотатки…'; + + @override + String get notesReadOnlyHelp => + 'Лише читання · Обране · Зберегти локальну копію…'; + + @override + String get notesSort => 'Сортування нотаток'; + + @override + String get notesPathRelative => + 'Шлях папки нотаток задається відносно вашого сховища Nextcloud.'; + + @override + String get notesSettingsStale => + 'Налаштування на Nextcloud змінилися. Перезавантажте й перевірте значення сервера перед заміною чернетки.'; + + @override + String get notesDownloadsPaused => 'Завантаження призупинено'; + + @override + String get notesExportNotPublished => 'Експорт не опубліковано.'; + + @override + String get notesKeyringUnavailable => + 'Розблокуйте системну в’язку ключів і повторіть спробу.'; + + @override + String get notesConnectFailed => + 'Не вдалося підключитися до Nextcloud Notes. Перевірте адресу сервера та повторіть спробу.'; + + @override + String get notesAccountMismatch => + 'Увійдіть до того самого облікового запису Nextcloud. Щоб підключити інший, спершу видаліть цей.'; + + @override + String get notesLocalRemovalFailed => + 'Не вдалося очистити локальні дані. Обліковий запис збережено для повторного видалення.'; + + @override + String get notesTransferring => 'Передавання нотаток…'; + + @override + String get notesTechnicalDetails => 'Технічні відомості'; + + @override + String get notesPauseRequirement => + 'Призупинити вимогу автономного зберігання'; + + @override + String get notesResumeRequirement => + 'Відновити вимогу автономного зберігання'; } diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index c1d9082e..dbc8643c 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -10,6 +10,131 @@ import 'app_localizations.dart'; class AppLocalizationsVi extends AppLocalizations { AppLocalizationsVi([String locale = 'vi']) : super(locale); + @override + String get notesNewFromRetainedText => 'Ghi chú mới từ văn bản được giữ lại'; + + @override + String get notesAccountThrottled => + 'Nextcloud yêu cầu tạm dừng. Hãy đợi trước khi đăng nhập lại.'; + + @override + String get notesSettingsRefreshing => + 'Đang làm mới Notes sau khi xác nhận cài đặt'; + + @override + String get notesExportOperationFailed => + 'Xuất thất bại trước khi công bố. Kiểm tra nguồn và đích rồi xuất lại.'; + + @override + String get notesRecoveryCreatedNotOpened => + 'Ghi chú khôi phục đã được lưu nhưng không mở được. Giải quyết thay đổi đang chờ trong trình soạn thảo hoặc thuộc tính rồi mở ghi chú hiện có này.'; + + @override + String get notesSettingsPendingNotes => + 'Ghi chú đang chờ, xung đột hoặc lần tạo chưa rõ kết quả đang chặn thay đổi. Đồng bộ hoặc xem xét trong Notes rồi lưu lại bản nháp cài đặt được giữ.'; + + @override + String get notesSettingsPendingAttachments => + 'Thao tác tệp đính kèm đang chờ đang chặn thay đổi. Xem xét tệp đính kèm trong Notes rồi lưu lại bản nháp cài đặt được giữ.'; + + @override + String get notesAccountServerAddress => + 'Dùng địa chỉ HTTPS chuẩn của Nextcloud rồi đăng nhập lại. Không thể xác minh địa chỉ hoặc chuyển hướng.'; + + @override + String get notesAccountUnsupported => + 'Notes không khả dụng hoặc API không được hỗ trợ. Bật Nextcloud Notes với API phiên bản chính 1, phụ từ 4 trở lên.'; + + @override + String get notesAccountAuthorizeAgain => + 'Nextcloud từ chối thông tin xác thực. Cấp quyền lại hoặc kết nối lại tài khoản.'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud cấm kiểm tra khả năng Notes. Nhờ quản trị viên cho phép rồi thử lại.'; + + @override + String get notesAccountMalformed => + 'Không thể xác minh phản hồi Nextcloud. Kiểm tra cài đặt máy chủ rồi đăng nhập lại.'; + + @override + String get notesAccountTimedOut => + 'Đăng nhập hết thời gian. Bắt đầu lại và hoàn tất cấp quyền trong trình duyệt.'; + + @override + String get notesAccountBrowserUnavailable => + 'Không mở được trình duyệt hệ thống. Kiểm tra trình duyệt mặc định rồi đăng nhập lại.'; + + @override + String get notesAccountNetwork => + 'Không thể kết nối Nextcloud. Kiểm tra kết nối và máy chủ rồi đăng nhập lại.'; + + @override + String get notesAccountRejected => + 'Nextcloud từ chối yêu cầu khả năng. Kiểm tra quyền truy cập với quản trị viên rồi thử lại.'; + + @override + String get notesAccountFailed => + 'Không hoàn tất thiết lập tài khoản. Hãy đăng nhập lại.'; + + @override + String get notesTransferSourceChanged => + 'Nguồn đã đổi sau khi xem xét. Xem xét tệp hiện tại hoặc tạo ảnh chụp mới trước khi thử lại.'; + + @override + String get notesTransferUnreadable => + 'Không thể đọc nguồn. Kiểm tra quyền truy cập tệp hoặc thư mục rồi xem xét lại.'; + + @override + String get notesTransferInvalidSnapshot => + 'Ảnh chụp không hợp lệ hoặc không được hỗ trợ. Chọn ảnh chụp BusyMark Notes hợp lệ hoặc nguồn Markdown.'; + + @override + String get notesTransferMediaLimit => + 'Nội dung đa phương tiện vượt giới hạn nhập. Giảm kích thước của mục liên quan rồi xem xét lại.'; + + @override + String get notesTransferUnsafeSource => + 'Nguồn chứa đường dẫn không an toàn. Chọn tệp trong thư mục nguồn rồi xem xét lại.'; + + @override + String get notesTransferDestination => + 'Không thể ghi bản xuất. Kiểm tra quyền truy cập và chỗ trống ở đích rồi xuất lại.'; + + @override + String get notesTransferOperationFailed => + 'Không hoàn tất truyền. Xem xét các mục liên quan rồi thử lại. Các mục đã nhập vẫn được lưu.'; + + @override + String get notesExportPublishedWithOmissions => + 'Bản xuất có sẵn nhưng có mục bị bỏ qua'; + + @override + String get notesBatchUnchanged => 'Các giá trị yêu cầu đã được đặt.'; + + @override + String get notesBatchReadOnly => + 'Ghi chú này chỉ đọc. Không thể sửa thuộc tính được bảo vệ; vẫn có thể đổi Yêu thích.'; + + @override + String get notesBatchReviewChanged => + 'Ghi chú đã đổi sau khi xem xét. Xem xét trạng thái hiện tại trước khi thực hiện lại.'; + + @override + String get notesBatchMissing => + 'Ghi chú không còn khả dụng. Làm mới danh sách rồi xem xét lại các mục đích.'; + + @override + String get notesBatchStorage => + 'Lưu cục bộ thất bại. Kiểm tra bộ nhớ rồi thử lại mục này; các mục thành công vẫn được lưu.'; + + @override + String get notesRemoteReviewChanged => + 'Ghi chú trên máy chủ đã đổi. Bản nháp hợp nhất được giữ. Xem xét so sánh mới rồi chọn lại cách giải quyết.'; + + @override + String get notesReviewRemote => 'Tôi đã xem xét phiên bản máy chủ hiện tại'; + @override String get wsModuleCapsNotPublished => 'Quy tắc viết hoa của mô-đun không được xuất bản.'; @@ -582,15 +707,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get keyboardShortcuts => 'Phím tắt'; - @override - String get commandPalette => 'Bảng lệnh'; - - @override - String get commandPaletteHint => 'Nhập lệnh'; - - @override - String get commandPaletteEmpty => 'Không có lệnh phù hợp'; - @override String get commandUnavailableInContext => 'Lệnh không khả dụng trong ngữ cảnh trình biên tập hiện tại'; @@ -810,7 +926,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String get shortcutKeyboardShortcutsDescription => - 'Hiển thị tham chiếu phím tắt này'; + 'Tùy chỉnh phím tắt của ứng dụng'; @override String get shortcutSyntaxReferenceDescription => 'Mở tham chiếu cú pháp'; @@ -1055,7 +1171,7 @@ class AppLocalizationsVi extends AppLocalizations { String get validateOnEdit => 'Kiểm tra khi chỉnh sửa'; @override - String get clearRecentWorkspaces => 'Xóa không gian làm việc gần đây'; + String get clearRecentWorkspaces => 'Xóa lịch sử gần đây'; @override String get editingButtonsPosition => 'Vị trí các nút chỉnh sửa'; @@ -1836,15 +1952,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get sourceSearchNextMatch => 'Kết quả khớp tiếp theo'; - @override - String get sourceSearchCaseSensitive => 'Phân biệt chữ hoa, chữ thường'; - - @override - String get sourceSearchWholeWord => 'Toàn bộ từ'; - - @override - String get sourceSearchRegex => 'Biểu thức chính quy'; - @override String get sourceSearchReplacement => 'Thay thế bằng'; @@ -1971,13 +2078,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get saveAs => 'Lưu thành'; - @override - String get sourceSearchInvalidRegex => 'Biểu thức chính quy không hợp lệ'; - - @override - String get sourceSearchZeroLengthUnsupported => - 'Không hỗ trợ kết quả khớp regex có độ dài bằng 0 (ví dụ ^, \$ hoặc lookahead chỉ khớp với một vị trí).'; - @override String get workspaceSearchIncomplete => 'Chỉ hiển thị một phần kết quả. Hiển thị thêm hoặc thu hẹp tìm kiếm.'; @@ -2087,6 +2187,11 @@ class AppLocalizationsVi extends AppLocalizations { @override String get documentKindResourceFile => 'Tệp tài nguyên'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return 'Không thể cập nhật các mục gần đây: $error.'; + } + @override String workspaceErrorOpenFailed(String error) { return 'Mở không thành công: $error'; @@ -4347,9 +4452,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get clipboardHistory => 'Lịch sử sao chép'; - @override - String get clipboardHistorySearchHint => 'Tìm kiếm lịch sử bảng nhớ tạm'; - @override String get clipboardHistoryDescription => 'Mở lịch sử sao chép cục bộ phiên.'; @@ -4371,10 +4473,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get clipboardNoItems => 'Lịch sử sao chép trống.'; - @override - String get clipboardNoMatches => - 'Không có mục sao chép nào khớp với tìm kiếm này.'; - @override String get clipboardDisabled => 'Bộ sưu tập Lịch sử sao chép bị vô hiệu hóa trong Cài đặt.'; @@ -4431,13 +4529,6 @@ class AppLocalizationsVi extends AppLocalizations { String get localHistoryNoRevisions => 'Tài liệu này không có các phiên bản Lịch sử cục bộ.'; - @override - String get localHistoryNoMatches => - 'Không có phiên bản nào trong Lịch sử cục bộ khớp với tìm kiếm này.'; - - @override - String get localHistorySearchHint => 'Tìm kiếm lịch sử cục bộ'; - @override String get localHistorySelectedRevision => 'Phiên bản đã chọn'; @@ -5387,4 +5478,396 @@ class AppLocalizationsVi extends AppLocalizations { @override String get wsPropertiesUnavailable => 'Phần tử đang kiểm tra không khả dụng. Kiểm tra một phần tử để tiếp tục.'; + + @override + String get searchDocumentsCategory => 'Tài liệu'; + + @override + String get searchTopicsCategory => 'Chủ đề'; + + @override + String get searchCurrentDocument => 'Tài liệu hiện tại'; + + @override + String get searchCurrentWorkspace => 'Không gian làm việc hiện tại'; + + @override + String get browseNotes => 'Duyệt ghi chú'; + + @override + String get searchActionsCommand => 'Tìm thao tác'; + + @override + String get searchDocumentsCommand => 'Tìm tài liệu'; + + @override + String get searchTopicsCommand => 'Tìm chủ đề'; + + @override + String get searchRecentCommand => 'Tìm mục gần đây'; + + @override + String get searchResourceUnavailable => 'Tài nguyên này không khả dụng.'; + + @override + String get notesSync => 'Đồng bộ ghi chú'; + + @override + String get searchInWorkspace => 'Tìm trong không gian làm việc'; + + @override + String get searchCategory => 'Danh mục tìm kiếm'; + + @override + String get clearHistorySearchScope => 'Tìm trong lịch sử tài liệu'; + + @override + String get searchInDocument => 'Tìm trong tài liệu'; + + @override + String get searchDocumentHint => 'Tìm trong tài liệu này…'; + + @override + String get searchWorkspaceHint => 'Tìm trong không gian làm việc này…'; + + @override + String get searchOutlineHint => 'Tìm tiêu đề…'; + + @override + String get searchHistoryHint => 'Tìm trong lịch sử cục bộ…'; + + @override + String get searchEnterText => 'Nhập văn bản để tìm kiếm'; + + @override + String get searchAccountUnavailable => 'Tài khoản không khả dụng'; + + @override + String get searchDocumentsHint => 'Tìm tài liệu…'; + + @override + String get searchTopicsHint => 'Tìm chủ đề…'; + + @override + String get searchRecentHint => 'Tìm mục gần đây…'; + + @override + String get searchActionsHint => 'Tìm thao tác…'; + + @override + String get searchRequiresNotesWorkspace => + 'Yêu cầu không gian làm việc Ghi chú'; + + @override + String get searchSidebarLabel => 'Tìm kiếm:'; + + @override + String get searchDocumentTextCategory => 'Văn bản tài liệu'; + + @override + String get searchWorkspaceTextCategory => 'Văn bản không gian làm việc'; + + @override + String get searchHeadingsCategory => 'Tiêu đề'; + + @override + String get shortcutUnassigned => 'Chưa gán'; + + @override + String get shortcutPressKeys => 'Nhấn tổ hợp phím…'; + + @override + String get shortcutRecord => 'Ghi phím tắt'; + + @override + String get shortcutRestoreAll => 'Khôi phục tất cả mặc định'; + + @override + String get shortcutRestoreAllQuestion => + 'Khôi phục phím tắt mặc định của mọi thao tác? Các gán tùy chỉnh sẽ bị xóa.'; + + @override + String get shortcutSaveFailed => + 'Phím tắt đang có hiệu lực nhưng chưa lưu được. Thử lưu lại để giữ chúng sau khi khởi động lại.'; + + @override + String get shortcutRetry => 'Thử lưu lại'; + + @override + String get shortcutChanged => + 'Các gán đã thay đổi. Xem lại phím tắt hiện tại và thử lại.'; + + @override + String get shortcutInvalid => 'Không thể gán tổ hợp này trong ngữ cảnh này.'; + + @override + String get shortcutSystemWarning => + 'Môi trường màn hình có thể dùng tổ hợp này. BusyMark không thể ghi đè phím tắt hệ thống.'; + + @override + String get shortcutConflicts => 'Phím tắt này xung đột với các thao tác sau:'; + + @override + String get shortcutRemove => 'Xóa gán'; + + @override + String get shortcutRestoreDefault => 'Khôi phục mặc định'; + + @override + String get shortcutAssign => 'Gán'; + + @override + String get shortcutReassign => 'Gán lại'; + + @override + String get shortcutContextApplication => 'Ứng dụng'; + + @override + String get shortcutContextDocument => 'Chế độ xem tài liệu'; + + @override + String get shortcutContextEditor => 'Trình sửa và mã nguồn'; + + @override + String get shortcutContextWritersideSource => 'Mã nguồn Writerside'; + + @override + String get shortcutContextWorkspace => 'Điều hướng không gian làm việc'; + + @override + String get shortcutContextTree => 'Cây thanh bên'; + + @override + String get searchHeadingsCommand => 'Tìm tiêu đề'; + + @override + String get shortcutFixed => 'Do trình soạn thảo quản lý; không thể cấu hình.'; + + @override + String get shortcutDefaultConflict => + 'Phím tắt mặc định xung đột với gán tùy chỉnh.'; + + @override + String get shortcutCurrentBinding => 'Phím tắt hiện tại'; + + @override + String get shortcutProposedBinding => 'Phím tắt đề xuất'; + + @override + String get notesActions => 'Thao tác ghi chú'; + + @override + String get noteActions => 'Thao tác với ghi chú'; + + @override + String get noteProperties => 'Thuộc tính ghi chú'; + + @override + String get notesExportAll => 'Xuất ghi chú · Tất cả ghi chú…'; + + @override + String get notesExportCategory => 'Xuất ghi chú · Danh mục…'; + + @override + String get notesOfflineAll => 'Cho phép dùng ngoại tuyến · Tất cả ghi chú'; + + @override + String get notesOfflineCategory => 'Cho phép dùng ngoại tuyến · Danh mục'; + + @override + String get notesOfflineRemoveAll => + 'Bỏ yêu cầu lưu ngoại tuyến · Tất cả ghi chú'; + + @override + String get notesOfflineRemoveCategory => + 'Bỏ yêu cầu lưu ngoại tuyến · Danh mục'; + + @override + String get notesOfflinePause => 'Tạm dừng tải xuống'; + + @override + String get notesOfflineResume => 'Tiếp tục tải xuống'; + + @override + String get notesOfflineKeep => 'Cho phép dùng ngoại tuyến'; + + @override + String get notesOfflineRetentionHelp => + 'Bỏ yêu cầu này vẫn giữ nội dung đã lưu đệm. Yêu cầu của danh mục hoặc tài khoản có thể vẫn giữ ghi chú này.'; + + @override + String get notesOfflineBroader => + 'Ghi chú này cũng được giữ theo yêu cầu của danh mục hoặc tài khoản.'; + + @override + String get notesOfflineExternal => + 'Nội dung đa phương tiện bên ngoài cần kết nối. BusyMark chỉ giữ văn bản ghi chú và tệp đính kèm được quản lý.'; + + @override + String get notesEmptyAll => 'Chưa có ghi chú. Hãy tạo ghi chú đầu tiên.'; + + @override + String get notesEmptyFavorites => 'Không có ghi chú yêu thích.'; + + @override + String get notesEmptyCategory => 'Không có ghi chú trong danh mục này.'; + + @override + String get notesEmptyUncategorized => 'Không có ghi chú chưa phân loại.'; + + @override + String get notesEmptyRecovery => + 'Không có ghi chú được giữ lại để khôi phục.'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · Thất bại. Thử lại'; + + @override + String get notesOperationFailed => + 'Không thể hoàn tất thao tác. Bản nháp vẫn được giữ. Kiểm tra việc lưu và xung đột rồi thử lại.'; + + @override + String get notesInspectionFailed => 'Tệp đính kèm · Thất bại. Thử lại'; + + @override + String get notesStartingConnection => 'Đang bắt đầu kết nối Nextcloud…'; + + @override + String get notesVerifyingConnection => + 'Đang xác minh quyền truy cập ghi chú…'; + + @override + String get notesCancellingConnection => 'Đang hủy đăng nhập và dọn dẹp…'; + + @override + String get notesRemovingConnection => 'Đang xóa tài khoản cục bộ…'; + + @override + String get notesRemovedWarning => + 'Đã xóa tài khoản Nextcloud cục bộ. Chưa xác nhận thu hồi mật khẩu ứng dụng. Hãy xóa mật khẩu BusyMark trong cài đặt Bảo mật Nextcloud.'; + + @override + String get notesBuffersNotSaved => + 'Không thể lưu cục bộ tất cả ghi chú đang mở. Kiểm tra việc lưu, xung đột hoặc chuẩn hóa trong không gian làm việc. Thay đổi cục bộ có thể vẫn cần đồng bộ với Nextcloud.'; + + @override + String get notesSettingsLeave => + 'Lưu thay đổi cài đặt Nextcloud trước khi rời đi?'; + + @override + String get notesSettingsLeaveHelp => + 'Cài đặt Notes trên máy chủ · Thay đổi chưa lưu'; + + @override + String get notesSettingsReload => 'Tải lại cài đặt máy chủ…'; + + @override + String get notesSettingsReloadHelp => + 'Tải lại sẽ thay bản nháp bằng cài đặt Nextcloud hiện tại. Bỏ bản nháp và tải lại?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud đã nhận cài đặt nhưng không làm mới được ghi chú. Giá trị hiển thị đã được máy chủ xác nhận. Hãy thử làm mới từ Ghi chú.'; + + @override + String get notesUnavailable => + 'Ngoại tuyến · Văn bản có sẵn cục bộ · Đã lưu cục bộ · chờ đồng bộ'; + + @override + String get notesStorageFull => + 'Bộ nhớ Nextcloud đã đầy. Giải phóng dung lượng máy chủ rồi thử lại.'; + + @override + String get notesRejected => + 'Nextcloud từ chối thay đổi này. Kiểm tra ghi chú và lưu bản sao cục bộ trước khi thử lại.'; + + @override + String get notesThrottled => + 'Nextcloud yêu cầu chờ. Đồng bộ sẽ tiếp tục sau thời hạn thử lại.'; + + @override + String get notesRecoveryRequired => 'Xung đột · So sánh'; + + @override + String get notesScanning => 'Đang quét tệp…'; + + @override + String get notesCancelling => 'Đang hủy… Đợi thao tác hiện tại kết thúc.'; + + @override + String get notesCancelled => 'Đã hủy'; + + @override + String get notesPartial => 'Hoàn tất một phần'; + + @override + String get notesTransferFailed => 'Thất bại. Thử lại'; + + @override + String get notesImportPartialHelp => + 'Các mục trước có thể đã được lưu cục bộ. Tiếp tục lần nhập này để tránh tạo bản trùng.'; + + @override + String get notesNotStarted => 'Chưa bắt đầu'; + + @override + String get notesCreated => 'Đã tạo'; + + @override + String get notesRemoteDeleteUnavailable => + 'Địa chỉ máy chủ · Không được hỗ trợ · Xóa · Tệp đính kèm'; + + @override + String get notesCurrentText => 'Văn bản hiện đang được giữ lại'; + + @override + String get notesRecovering => 'Đang khôi phục thành ghi chú mới…'; + + @override + String get notesReadOnlyHelp => 'Chỉ đọc · Yêu thích · Lưu bản sao cục bộ…'; + + @override + String get notesSort => 'Sắp xếp ghi chú'; + + @override + String get notesPathRelative => + 'Đường dẫn thư mục ghi chú tương đối với bộ nhớ Nextcloud của bạn.'; + + @override + String get notesSettingsStale => + 'Cài đặt trên Nextcloud đã thay đổi. Tải lại và kiểm tra giá trị máy chủ trước khi thay bản nháp.'; + + @override + String get notesDownloadsPaused => 'Đã tạm dừng tải xuống'; + + @override + String get notesExportNotPublished => 'Không có bản xuất nào được công bố.'; + + @override + String get notesKeyringUnavailable => + 'Mở khóa kho khóa của máy tính rồi thử lại.'; + + @override + String get notesConnectFailed => + 'Không thể kết nối Nextcloud Notes. Kiểm tra địa chỉ máy chủ rồi thử lại.'; + + @override + String get notesAccountMismatch => + 'Đăng nhập cùng tài khoản Nextcloud. Xóa tài khoản này trước để kết nối tài khoản khác.'; + + @override + String get notesLocalRemovalFailed => + 'Dọn dẹp tài khoản cục bộ thất bại. Tài khoản được giữ để bạn thử xóa lại.'; + + @override + String get notesTransferring => 'Đang chuyển ghi chú…'; + + @override + String get notesTechnicalDetails => 'Chi tiết kỹ thuật'; + + @override + String get notesPauseRequirement => 'Tạm dừng yêu cầu giữ ngoại tuyến'; + + @override + String get notesResumeRequirement => 'Tiếp tục yêu cầu giữ ngoại tuyến'; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 501b563c..544f1183 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -10,6 +10,109 @@ import 'app_localizations.dart'; class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); + @override + String get notesNewFromRetainedText => '从保留的文本新建笔记'; + + @override + String get notesAccountThrottled => 'Nextcloud 要求暂停。请等待后再重新登录。'; + + @override + String get notesSettingsRefreshing => '正在按已确认的设置更新笔记'; + + @override + String get notesExportOperationFailed => '导出在发布前失败。请检查来源和目标后重新导出。'; + + @override + String get notesRecoveryCreatedNotOpened => + '恢复的笔记已保存,但无法打开。请处理待保存的编辑或属性,再打开这篇已有笔记。'; + + @override + String get notesSettingsPendingNotes => + '待处理的笔记、冲突或结果不明的创建阻止了此更改。请在笔记中同步或检查,再重试保留的设置草稿。'; + + @override + String get notesSettingsPendingAttachments => + '待处理的附件操作阻止了此更改。请在笔记中检查附件,再重试保留的设置草稿。'; + + @override + String get notesAccountServerAddress => + '请使用 Nextcloud 的规范 HTTPS 地址并重新登录。无法验证地址或重定向。'; + + @override + String get notesAccountUnsupported => + '笔记不可用或 API 不受支持。请启用 API 主版本为 1、次版本不低于 4 的 Nextcloud Notes。'; + + @override + String get notesAccountAuthorizeAgain => 'Nextcloud 拒绝了凭据。请重新授权或重新连接账户。'; + + @override + String get notesAccountCheckForbidden => + 'Nextcloud 拒绝了笔记功能检查。请联系管理员允许此检查后重试。'; + + @override + String get notesAccountMalformed => '无法验证 Nextcloud 的响应。请检查服务器安装并重新登录。'; + + @override + String get notesAccountTimedOut => '登录超时。请重新登录并在浏览器中完成授权。'; + + @override + String get notesAccountBrowserUnavailable => '无法打开系统浏览器。请检查默认浏览器并重新登录。'; + + @override + String get notesAccountNetwork => '无法连接 Nextcloud。请检查网络和服务器后重新登录。'; + + @override + String get notesAccountRejected => 'Nextcloud 拒绝了功能请求。请与管理员确认访问权限后重试。'; + + @override + String get notesAccountFailed => '无法完成账户设置。请重新登录。'; + + @override + String get notesTransferSourceChanged => '来源在审核后已更改。请重新审核当前文件或创建新快照后重试。'; + + @override + String get notesTransferUnreadable => '无法读取来源。请检查文件或文件夹的访问权限,再重新审核。'; + + @override + String get notesTransferInvalidSnapshot => + '快照无效或不受支持。请选择有效的 BusyMark Notes 快照或 Markdown 来源。'; + + @override + String get notesTransferMediaLimit => '媒体超出导入大小限制。请缩小受影响项目的媒体,再重新审核。'; + + @override + String get notesTransferUnsafeSource => '来源包含不安全的路径。请选择来源文件夹内的文件并重新审核。'; + + @override + String get notesTransferDestination => '无法写入导出内容。请检查目标的访问权限和可用空间后重新导出。'; + + @override + String get notesTransferOperationFailed => '传输未能完成。请检查受影响的项目并重试。之前导入的项目仍已保存。'; + + @override + String get notesExportPublishedWithOmissions => '导出已发布,但有遗漏'; + + @override + String get notesBatchUnchanged => '请求的值已经设置。'; + + @override + String get notesBatchReadOnly => '此笔记为只读。受保护的属性无法更改,但仍可修改收藏状态。'; + + @override + String get notesBatchReviewChanged => '此笔记在审核后已更改。请审核当前状态后重新执行操作。'; + + @override + String get notesBatchMissing => '此笔记已不可用。请刷新列表并重新检查操作目标。'; + + @override + String get notesBatchStorage => '本地保存失败。请检查存储并重试此项目;成功的项目仍已保存。'; + + @override + String get notesRemoteReviewChanged => '服务器笔记已更改。合并草稿已保留。请审核更新的比较并重新选择解决方式。'; + + @override + String get notesReviewRemote => '我已审核当前服务器版本'; + @override String get wsModuleCapsNotPublished => '模块的大小写规则不会发布。'; @@ -555,15 +658,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get keyboardShortcuts => '键盘快捷键'; - @override - String get commandPalette => '命令面板'; - - @override - String get commandPaletteHint => '输入命令'; - - @override - String get commandPaletteEmpty => '没有匹配的命令'; - @override String get commandUnavailableInContext => '当前编辑器上下文中无法使用此命令'; @@ -776,7 +870,7 @@ class AppLocalizationsZh extends AppLocalizations { String get shortcutSearchDescription => '搜索当前工作区'; @override - String get shortcutKeyboardShortcutsDescription => '显示此键盘快捷键参考'; + String get shortcutKeyboardShortcutsDescription => '自定义应用程序键盘快捷键'; @override String get shortcutSyntaxReferenceDescription => '打开语法参考'; @@ -1001,7 +1095,7 @@ class AppLocalizationsZh extends AppLocalizations { String get validateOnEdit => '编辑时验证'; @override - String get clearRecentWorkspaces => '清除最近使用的工作区'; + String get clearRecentWorkspaces => '清除最近记录'; @override String get editingButtonsPosition => '编辑按钮位置'; @@ -1758,15 +1852,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get sourceSearchNextMatch => '下一个匹配项'; - @override - String get sourceSearchCaseSensitive => '区分大小写'; - - @override - String get sourceSearchWholeWord => '全字匹配'; - - @override - String get sourceSearchRegex => '正则表达式'; - @override String get sourceSearchReplacement => '替换为'; @@ -1885,13 +1970,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get saveAs => '另存为'; - @override - String get sourceSearchInvalidRegex => '无效的正则表达式'; - - @override - String get sourceSearchZeroLengthUnsupported => - '不支持零长度的正则表达式匹配(例如 ^、\$ 或仅匹配位置的前瞻断言)。'; - @override String get workspaceSearchIncomplete => '仅显示了部分结果。请显示更多结果或缩小搜索范围。'; @@ -1996,6 +2074,11 @@ class AppLocalizationsZh extends AppLocalizations { @override String get documentKindResourceFile => '资源文件'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return '无法更新最近使用的项目:$error。'; + } + @override String workspaceErrorOpenFailed(String error) { return '打开失败:$error'; @@ -4160,9 +4243,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get clipboardHistory => '剪贴板历史'; - @override - String get clipboardHistorySearchHint => '搜索剪贴板历史记录'; - @override String get clipboardHistoryDescription => '打开会话本地剪贴板历史。'; @@ -4183,9 +4263,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get clipboardNoItems => '剪贴板历史为空。'; - @override - String get clipboardNoMatches => '没有匹配此搜索的剪贴板项目。'; - @override String get clipboardDisabled => '剪贴板历史集合在设置中已禁用。'; @@ -4236,12 +4313,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get localHistoryNoRevisions => '此文档没有本地历史修订。'; - @override - String get localHistoryNoMatches => '没有本地历史修订与此搜索匹配。'; - - @override - String get localHistorySearchHint => '搜索本地历史记录'; - @override String get localHistorySelectedRevision => '已选择的修订'; @@ -5165,444 +5236,910 @@ class AppLocalizationsZh extends AppLocalizations { @override String get wsPropertiesUnavailable => '所检查的元素不可用。请检查一个元素以继续。'; -} -/// The translations for Chinese, as used in China (`zh_CN`). -class AppLocalizationsZhCn extends AppLocalizationsZh { - AppLocalizationsZhCn() : super('zh_CN'); + @override + String get searchDocumentsCategory => '文档'; @override - String get wsModuleCapsNotPublished => '模块的大小写规则不会发布。'; + String get searchTopicsCategory => '主题'; @override - String get wsTitleOverridesAction => '实例与目录标题…'; + String get searchCurrentDocument => '当前文档'; @override - String get wsHeadingNotPublished => '此构建器不会发布此属性。'; + String get searchCurrentWorkspace => '当前工作区'; @override - String get wsMarkdownInstanceTitleLimitation => - '固定版本构建器不发布 Markdown 主题的实例标题覆盖。保留现有标记,请在源码中查看。'; + String get browseNotes => '浏览笔记'; @override - String get wsTopicIdentity => '主题 ID'; + String get searchActionsCommand => '搜索操作'; @override - String get wsElementAnchor => '元素锚点'; + String get searchDocumentsCommand => '搜索文档'; @override - String get wsHeadingText => '标题文本'; + String get searchTopicsCommand => '搜索主题'; @override - String get wsDefaultTitle => '默认标题'; + String get searchRecentCommand => '搜索最近项目'; @override - String get wsTopicNavigation => '主题导航'; + String get searchResourceUnavailable => '此资源不可用。'; @override - String get wsHeadingOptions => '标题属性'; + String get notesSync => '同步笔记'; @override - String get wsDocumentOptions => '文档设置'; + String get searchInWorkspace => '在工作区中搜索'; @override - String get wsSettingsPresent => '存在已编写设置'; + String get searchCategory => '搜索类别'; @override - String get wsTitleOverrideExplanation => '覆盖发布的标题;标题文本保持不变。'; + String get clearHistorySearchScope => '搜索文档历史记录'; @override - String get wsIgnoreVariables => '将变量视为字面文本'; + String get searchInDocument => '在文档中搜索'; @override - String get wsIgnoreVariablesExplanation => '启用时,不替换类似变量的文本。'; + String get searchDocumentHint => '搜索此文档…'; @override - String get wsInstanceSelection => '帮助实例'; + String get searchWorkspaceHint => '搜索此工作区…'; @override - String get wsInstanceExplanation => '通过名称、组、列表、排除项或表达式选择帮助输出。'; + String get searchOutlineHint => '搜索标题…'; @override - String get wsFilterExplanation => '按条件筛选可复用内容;这不会选择帮助实例。'; + String get searchHistoryHint => '搜索本地历史记录…'; @override - String get wsContextHelp => '上下文帮助 ID'; + String get searchEnterText => '输入要搜索的文本'; @override - String get wsCapitalization => '大小写'; + String get searchAccountUnavailable => '账户不可用'; @override - String get wsLibraryTopic => '库主题'; + String get searchDocumentsHint => '搜索文档…'; @override - String get wsLastModified => '最后修改'; + String get searchTopicsHint => '搜索主题…'; @override - String get wsMissingTopicHeading => '尚未编写主题 H1。请在源码中添加。'; + String get searchRecentHint => '搜索最近项目…'; @override - String get wsMultipleTopicHeadings => '第一个 H1 决定主题标题,后续 H1 是独立标题。'; + String get searchActionsHint => '搜索操作…'; @override - String get wsTopicIdentityMismatch => '根 id 与文件名派生的主题 ID 不同。请重命名主题文件或查看源码。'; + String get searchRequiresNotesWorkspace => '需要笔记工作区'; @override - String get wsUnsupportedProperties => '保留不受支持的属性。请在源码中查看。'; + String get searchSidebarLabel => '搜索:'; @override - String get wsGeneratedAnchor => '自动生成的锚点;输入值可覆盖。'; + String get searchDocumentTextCategory => '文档文本'; @override - String get wsHeadingBuilderLimitation => - '固定版本构建器不发布 H1 属性。属性属于标题,而非 XML 根元素。'; + String get searchWorkspaceTextCategory => '工作区文本'; @override - String get closeOtherTabs => '关闭其他标签页'; + String get searchHeadingsCategory => '标题'; @override - String get tocTopicFromTemplate => '从模板创建主题...'; + String get shortcutUnassigned => '未分配'; @override - String get tocSaveAsTemplate => '保存为模板'; + String get shortcutPressKeys => '请按下组合键…'; @override - String get tocTemplateDialog => '从模板创建主题'; + String get shortcutRecord => '录制快捷键'; @override - String get tocTemplatesEditor => '文件和代码模板'; + String get shortcutRestoreAll => '恢复所有默认设置'; @override - String get tocEditTemplates => '编辑模板...'; + String get shortcutRestoreAllQuestion => '恢复所有操作的默认快捷键?自定义分配将被移除。'; @override - String get tocCreateCustomTemplate => '创建自定义模板...'; + String get shortcutSaveFailed => '快捷键已生效,但无法保存。请重试保存以便重启后保留。'; @override - String get tocTemplateCreated => '已创建模板'; + String get shortcutRetry => '重试保存'; @override - String get tocTemplateDefault => '默认'; + String get shortcutChanged => '分配已更改。请检查当前快捷键并重试。'; @override - String get tocTemplateCustom => '自定义'; + String get shortcutInvalid => '无法在此上下文中分配此组合键。'; @override - String get tocTemplateFilename => '文件名:'; + String get shortcutSystemWarning => '桌面可能正在使用此组合键。BusyMark 无法覆盖系统快捷键。'; @override - String get tocTemplateMarkdown => 'Markdown (.md)'; + String get shortcutConflicts => '此快捷键与以下操作冲突:'; @override - String get tocTemplateXml => 'XML (.topic)'; + String get shortcutRemove => '移除分配'; @override - String get tocTemplatesInternal => '内部'; + String get shortcutRestoreDefault => '恢复默认设置'; @override - String get tocTemplateUnnamed => '未命名'; + String get shortcutAssign => '分配'; @override - String get tocTemplateNew => '新建模板...'; + String get shortcutReassign => '重新分配'; @override - String get tocTemplateName => '名称:'; + String get shortcutContextApplication => '应用程序'; @override - String get tocTemplateExtension => '扩展名:'; + String get shortcutContextDocument => '文档视图'; @override - String get tocTemplateLoadError => '无法加载模板。现有模板未更改。'; + String get shortcutContextEditor => '编辑器和源码'; @override - String get tocTemplateSaveError => '无法保存模板。您的编辑内容仍保留在此处。'; + String get shortcutContextWritersideSource => 'Writerside 源码'; @override - String get tocTemplateConflict => '模板已在其他窗口中更改。请取消并重新打开编辑器。'; + String get shortcutContextWorkspace => '工作区导航'; @override - String get tocTemplateDuplicateName => '已存在相同名称和扩展名的模板。'; + String get shortcutContextTree => '侧栏树'; @override - String get tocTemplateFormat => '格式:'; + String get searchHeadingsCommand => '搜索标题'; @override - String get tocTemplateReset => '重置'; + String get shortcutFixed => '由编辑器管理,无法配置。'; @override - String get tocTemplateRetry => '重试'; + String get shortcutDefaultConflict => '默认快捷键与自定义分配冲突。'; @override - String tocRemoveElementsDialog(int count) { - return '移除 $count 个目录元素'; - } + String get shortcutCurrentBinding => '当前快捷键'; @override - String get tocRemoveElementsRetainFiles => '不会删除与这些目录元素关联的源文件。'; + String get shortcutProposedBinding => '拟用快捷键'; @override - String get tocOk => '确定'; + String get notesActions => '笔记操作'; @override - String get tocSelectExistingTopic => '选择要添加到当前实例的主题'; + String get noteActions => '此笔记的操作'; @override - String get tocGroupName => '组名'; + String get noteProperties => '笔记属性'; @override - String get tocMoveLineUp => '上移行'; + String get notesExportAll => '导出笔记 · 所有笔记…'; @override - String get tocMoveLineDown => '下移行'; + String get notesExportCategory => '导出笔记 · 类别…'; @override - String get tocEmptyMdTopic => '空 MD 主题'; + String get notesOfflineAll => '设为离线可用 · 所有笔记'; @override - String get tocEmptyXmlTopic => '空 XML 主题'; + String get notesOfflineCategory => '设为离线可用 · 类别'; @override - String get tocCopySpecial => '特殊复制'; + String get notesOfflineRemoveAll => '移除离线保留要求 · 所有笔记'; @override - String tocCopyFileName(String name) { - return '主题文件名 \'$name\''; - } + String get notesOfflineRemoveCategory => '移除离线保留要求 · 类别'; @override - String get tocCopyFilePath => '主题文件路径'; + String get notesOfflinePause => '暂停下载'; @override - String tocCopyTopicTitle(String title) { - return '主题标题 \'$title\''; - } + String get notesOfflineResume => '继续下载'; @override - String tocCopyElementId(String id) { - return '目录元素 ID \'$id\''; - } + String get notesOfflineKeep => '设为离线可用'; @override - String get tocCopyFileNameUnavailable => '主题文件名'; + String get notesOfflineRetentionHelp => '移除此保留要求不会删除现有缓存。分类或账户的要求仍可能保留此笔记。'; @override - String get tocCopyTopicTitleUnavailable => '主题标题'; + String get notesOfflineBroader => '此笔记也因分类或账户的要求而被保留。'; @override - String get tocCopyElementIdUnavailable => '目录元素 ID'; + String get notesOfflineExternal => '外部媒体需要网络连接。BusyMark 只保留笔记文本和托管附件。'; @override - String get tocEditTitleAction => '编辑标题...'; + String get notesEmptyAll => '还没有笔记。请新建一条笔记。'; @override - String get tocEditTitleDialog => '编辑标题'; + String get notesEmptyFavorites => '没有收藏的笔记。'; @override - String get tocRemoveElementAction => '移除目录元素...'; + String get notesEmptyCategory => '此分类中没有笔记。'; @override - String get tocSetHomePage => '设为主页'; + String get notesEmptyUncategorized => '没有未分类笔记。'; @override - String tocGoToElement(String fileName) { - return '转到“$fileName”中的目录元素'; - } + String get notesEmptyRecovery => '没有可恢复的本地保留笔记。'; @override - String get tocSynchronize => '同步目录与编辑器'; + String get notesLoadFailed => 'Nextcloud Notes · 失败. 重试'; @override - String get tocEmptyGroupAction => '空组'; + String get notesOperationFailed => '无法完成操作。草稿已保留。请检查保存状态和冲突后重试。'; @override - String get tocNewEmptyGroup => '新建空组'; + String get notesInspectionFailed => '附件 · 失败. 重试'; @override - String get tocGroupAction => '分组'; + String get notesStartingConnection => '正在连接 Nextcloud…'; @override - String get tocNewGroup => '新建组'; + String get notesVerifyingConnection => '正在验证笔记访问权限…'; @override - String get tocSortChildren => '按字母顺序排列子主题'; + String get notesCancellingConnection => '正在取消登录并清理…'; @override - String get tocLinkTopicFiles => '将主题文件链接到目录...'; + String get notesRemovingConnection => '正在移除本地账户…'; @override - String get addLocalMarkdownFiles => '添加本地 Markdown 文件'; + String get notesRemovedWarning => + '本地 Nextcloud 账户已移除,但无法确认应用密码已撤销。请在 Nextcloud 安全设置中移除 BusyMark 的应用密码。'; @override - String get tocTitleField => '目录标题:'; + String get notesBuffersNotSaved => + '无法将所有打开的笔记保存到本地。请在工作区检查保存、冲突或规范化处理。待处理的本地更改可能仍需与 Nextcloud 同步。'; @override - String get tocTopicTitleField => '主题标题:'; + String get notesSettingsLeave => '离开前保存 Nextcloud 设置更改吗?'; @override - String get tocAdvancedSettings => '高级设置'; + String get notesSettingsLeaveHelp => '服务器 Notes 设置 · 未保存的更改'; @override - String tocInstanceTitleField(String instanceId) { - return '“$instanceId”的标题:'; - } + String get notesSettingsReload => '重新加载服务器设置…'; @override - String get tocOnlyTitleField => '仅用于目录的标题:'; + String get notesSettingsReloadHelp => '重新加载会用 Nextcloud 当前设置替换草稿。放弃草稿并重新加载吗?'; @override - String get tocInstanceTitleExplanation => '仅用于当前实例。默认继承主题标题。'; + String get notesSettingsRefreshFailed => + 'Nextcloud 已接受设置,但刷新笔记失败。显示的是服务器确认的设置值。请从笔记界面重试刷新。'; @override - String get tocOnlyTitleExplanation => - '仅用于目录。默认继承主题标题;如果设置了实例专用标题,则继承该标题。标题的说明见'; + String get notesUnavailable => '离线 · 文本已在本地可用 · 已保存到本地 · 等待同步'; @override - String get tocTitleDocumentationLink => '此处'; + String get notesStorageFull => 'Nextcloud 存储空间已满。请释放服务器空间后重试。'; @override - String get tocFindUsages => '查找'; + String get notesRejected => 'Nextcloud 拒绝了此更改。请检查笔记并保存本地副本后重试。'; @override - String get tocRefactorMenu => '重构'; + String get notesThrottled => 'Nextcloud 要求稍候。同步将在重试期限后继续。'; @override - String get tocSafeDelete => '安全删除'; + String get notesRecoveryRequired => '冲突 · 比较'; @override - String get tocDuplicate => '创建副本'; + String get notesScanning => '正在扫描文件…'; @override - String get tocDuplicateFilename => '主题文件名:'; + String get notesCancelling => '正在取消…等待当前操作结束。'; @override - String get tocDuplicateDialog => '创建主题副本'; + String get notesCancelled => '已取消'; @override - String get pdfTitlePage => '标题页'; + String get notesPartial => '部分完成'; @override - String get pdfIncludeTitlePage => '包含标题页'; + String get notesTransferFailed => '失败. 重试'; @override - String get pdfTitlePageNote => '此页不显示页眉、页脚或页码。'; + String get notesImportPartialHelp => '之前的项目可能已保存在本地。请继续本次导入,以免创建重复笔记。'; @override - String get pdfTitlePageTitle => '标题'; + String get notesNotStarted => '尚未开始'; @override - String get pdfTitlePageSubtitle => '副标题'; + String get notesCreated => '已创建'; @override - String get pdfTitlePageAuthor => '作者'; + String get notesRemoteDeleteUnavailable => '服务器地址 · 不支持 · 删除 · 附件'; @override - String get pdfTitlePageOrganization => '组织'; + String get notesCurrentText => '当前保留的文本'; @override - String get pdfTitlePageVersion => '版本'; + String get notesRecovering => '正在恢复为新笔记…'; @override - String get pdfTitlePageDate => '日期'; + String get notesReadOnlyHelp => '只读 · 收藏 · 保存本地副本…'; @override - String get pdfTitlePageTitleRequired => '请输入标题页的标题。'; + String get notesSort => '笔记排序'; @override - String get pdfTocSectionNote => '从单独一页开始,可以跨多页。'; + String get notesPathRelative => '笔记文件夹路径相对于您的 Nextcloud 存储空间。'; @override - String get appTitle => 'BusyMark'; + String get notesSettingsStale => 'Nextcloud 上的设置已更改。请重新加载并检查服务器值,再替换草稿。'; @override - String get appSubtitle => 'Markdown 文件和兼容 Writerside 的文档项目编辑器。'; + String get notesDownloadsPaused => '下载已暂停'; @override - String get aboutBusyMark => '关于 BusyMark'; + String get notesExportNotPublished => '未发布任何导出。'; @override - String get aboutTagline => 'Markdown 和 Writerside 编辑器'; + String get notesKeyringUnavailable => '请解锁桌面密钥环后重试。'; @override - String get aboutLicenseLabel => '许可证'; + String get notesConnectFailed => '无法连接 Nextcloud Notes。请检查服务器地址后重试。'; @override - String get aboutLicenseName => 'Apache License 2.0'; + String get notesAccountMismatch => '请登录同一 Nextcloud 账户。要连接其他账户,请先移除此账户。'; @override - String get aboutWebsite => '网站'; + String get notesLocalRemovalFailed => '本地账户清理失败。账户已保留,以便重试移除。'; @override - String get aboutSourceCode => '源代码'; + String get notesTransferring => '正在传输笔记…'; @override - String get reportIssue => '报告问题'; + String get notesTechnicalDetails => '技术详情'; @override - String get feedbackCategory => '类别'; + String get notesPauseRequirement => '暂停离线保留要求'; @override - String get feedbackChooseCategory => '选择类别'; + String get notesResumeRequirement => '继续离线保留要求'; +} + +/// The translations for Chinese, as used in China (`zh_CN`). +class AppLocalizationsZhCn extends AppLocalizationsZh { + AppLocalizationsZhCn() : super('zh_CN'); @override - String get feedbackCategoryProblem => '问题或错误'; + String get notesNewFromRetainedText => '从保留的文本新建笔记'; @override - String get feedbackCategoryFeature => '功能请求'; + String get notesAccountThrottled => 'Nextcloud 要求暂停。请等待后再重新登录。'; @override - String get feedbackCategoryPrivacySecurity => '隐私或安全问题'; + String get notesSettingsRefreshing => '正在按已确认的设置更新笔记'; @override - String get feedbackCategoryUsability => '可用性问题'; + String get notesExportOperationFailed => '导出在发布前失败。请检查来源和目标后重新导出。'; @override - String get feedbackCategoryOther => '其他'; + String get notesRecoveryCreatedNotOpened => + '恢复的笔记已保存,但无法打开。请处理待保存的编辑或属性,再打开这篇已有笔记。'; @override - String get feedbackSubject => '主题'; + String get notesSettingsPendingNotes => + '待处理的笔记、冲突或结果不明的创建阻止了此更改。请在笔记中同步或检查,再重试保留的设置草稿。'; @override - String get feedbackMessage => '详细留言'; + String get notesSettingsPendingAttachments => + '待处理的附件操作阻止了此更改。请在笔记中检查附件,再重试保留的设置草稿。'; @override - String get feedbackReplyEmail => '回复邮箱(可选)'; + String get notesAccountServerAddress => + '请使用 Nextcloud 的规范 HTTPS 地址并重新登录。无法验证地址或重定向。'; @override - String get feedbackIncludeTechnicalDetails => '包含技术详细信息'; + String get notesAccountUnsupported => + '笔记不可用或 API 不受支持。请启用 API 主版本为 1、次版本不低于 4 的 Nextcloud Notes。'; @override - String get feedbackTechnicalDetailsDisclosure => - '启用后,此选项仅会添加您的 Linux 操作系统版本和 BusyMark 应用语言。不会附加日志、文件、账户数据或其他诊断信息。'; + String get notesAccountAuthorizeAgain => 'Nextcloud 拒绝了凭据。请重新授权或重新连接账户。'; @override - String get feedbackSubmit => '提交'; + String get notesAccountCheckForbidden => + 'Nextcloud 拒绝了笔记功能检查。请联系管理员允许此检查后重试。'; @override - String get feedbackSubmitting => '正在提交…'; + String get notesAccountMalformed => '无法验证 Nextcloud 的响应。请检查服务器安装并重新登录。'; @override - String get feedbackCategoryRequired => '请选择类别。'; + String get notesAccountTimedOut => '登录超时。请重新登录并在浏览器中完成授权。'; @override - String get feedbackSubjectLength => '主题必须包含 3 到 120 个字符。'; + String get notesAccountBrowserUnavailable => '无法打开系统浏览器。请检查默认浏览器并重新登录。'; @override - String get feedbackMessageLength => '信息必须包含 10 到 5,000 个字符。'; + String get notesAccountNetwork => '无法连接 Nextcloud。请检查网络和服务器后重新登录。'; @override - String get feedbackReplyEmailInvalid => '请输入有效的邮箱地址,或将此字段留空。'; + String get notesAccountRejected => 'Nextcloud 拒绝了功能请求。请与管理员确认访问权限后重试。'; @override - String get feedbackConnectionFailure => 'BusyMark 无法连接。请检查网络连接后重试。'; + String get notesAccountFailed => '无法完成账户设置。请重新登录。'; @override - String get feedbackTimeoutFailure => '请求超时。请重试。'; + String get notesTransferSourceChanged => '来源在审核后已更改。请重新审核当前文件或创建新快照后重试。'; @override - String get feedbackRateLimitedFailure => '此连接发送的报告过多。请稍候再试。'; + String get notesTransferUnreadable => '无法读取来源。请检查文件或文件夹的访问权限,再重新审核。'; @override - String get feedbackRejectedFailure => '服务器拒绝了此报告。请检查表单字段后重试。'; + String get notesTransferInvalidSnapshot => + '快照无效或不受支持。请选择有效的 BusyMark Notes 快照或 Markdown 来源。'; + + @override + String get notesTransferMediaLimit => '媒体超出导入大小限制。请缩小受影响项目的媒体,再重新审核。'; + + @override + String get notesTransferUnsafeSource => '来源包含不安全的路径。请选择来源文件夹内的文件并重新审核。'; + + @override + String get notesTransferDestination => '无法写入导出内容。请检查目标的访问权限和可用空间后重新导出。'; + + @override + String get notesTransferOperationFailed => '传输未能完成。请检查受影响的项目并重试。之前导入的项目仍已保存。'; + + @override + String get notesExportPublishedWithOmissions => '导出已发布,但有遗漏'; + + @override + String get notesBatchUnchanged => '请求的值已经设置。'; + + @override + String get notesBatchReadOnly => '此笔记为只读。受保护的属性无法更改,但仍可修改收藏状态。'; + + @override + String get notesBatchReviewChanged => '此笔记在审核后已更改。请审核当前状态后重新执行操作。'; + + @override + String get notesBatchMissing => '此笔记已不可用。请刷新列表并重新检查操作目标。'; + + @override + String get notesBatchStorage => '本地保存失败。请检查存储并重试此项目;成功的项目仍已保存。'; + + @override + String get notesRemoteReviewChanged => '服务器笔记已更改。合并草稿已保留。请审核更新的比较并重新选择解决方式。'; + + @override + String get notesReviewRemote => '我已审核当前服务器版本'; + + @override + String get wsModuleCapsNotPublished => '模块的大小写规则不会发布。'; + + @override + String get wsTitleOverridesAction => '实例与目录标题…'; + + @override + String get wsHeadingNotPublished => '此构建器不会发布此属性。'; + + @override + String get wsMarkdownInstanceTitleLimitation => + '固定版本构建器不发布 Markdown 主题的实例标题覆盖。保留现有标记,请在源码中查看。'; + + @override + String get wsTopicIdentity => '主题 ID'; + + @override + String get wsElementAnchor => '元素锚点'; + + @override + String get wsHeadingText => '标题文本'; + + @override + String get wsDefaultTitle => '默认标题'; + + @override + String get wsTopicNavigation => '主题导航'; + + @override + String get wsHeadingOptions => '标题属性'; + + @override + String get wsDocumentOptions => '文档设置'; + + @override + String get wsSettingsPresent => '存在已编写设置'; + + @override + String get wsTitleOverrideExplanation => '覆盖发布的标题;标题文本保持不变。'; + + @override + String get wsIgnoreVariables => '将变量视为字面文本'; + + @override + String get wsIgnoreVariablesExplanation => '启用时,不替换类似变量的文本。'; + + @override + String get wsInstanceSelection => '帮助实例'; + + @override + String get wsInstanceExplanation => '通过名称、组、列表、排除项或表达式选择帮助输出。'; + + @override + String get wsFilterExplanation => '按条件筛选可复用内容;这不会选择帮助实例。'; + + @override + String get wsContextHelp => '上下文帮助 ID'; + + @override + String get wsCapitalization => '大小写'; + + @override + String get wsLibraryTopic => '库主题'; + + @override + String get wsLastModified => '最后修改'; + + @override + String get wsMissingTopicHeading => '尚未编写主题 H1。请在源码中添加。'; + + @override + String get wsMultipleTopicHeadings => '第一个 H1 决定主题标题,后续 H1 是独立标题。'; + + @override + String get wsTopicIdentityMismatch => '根 id 与文件名派生的主题 ID 不同。请重命名主题文件或查看源码。'; + + @override + String get wsUnsupportedProperties => '保留不受支持的属性。请在源码中查看。'; + + @override + String get wsGeneratedAnchor => '自动生成的锚点;输入值可覆盖。'; + + @override + String get wsHeadingBuilderLimitation => + '固定版本构建器不发布 H1 属性。属性属于标题,而非 XML 根元素。'; + + @override + String get closeOtherTabs => '关闭其他标签页'; + + @override + String get tocTopicFromTemplate => '从模板创建主题...'; + + @override + String get tocSaveAsTemplate => '保存为模板'; + + @override + String get tocTemplateDialog => '从模板创建主题'; + + @override + String get tocTemplatesEditor => '文件和代码模板'; + + @override + String get tocEditTemplates => '编辑模板...'; + + @override + String get tocCreateCustomTemplate => '创建自定义模板...'; + + @override + String get tocTemplateCreated => '已创建模板'; + + @override + String get tocTemplateDefault => '默认'; + + @override + String get tocTemplateCustom => '自定义'; + + @override + String get tocTemplateFilename => '文件名:'; + + @override + String get tocTemplateMarkdown => 'Markdown (.md)'; + + @override + String get tocTemplateXml => 'XML (.topic)'; + + @override + String get tocTemplatesInternal => '内部'; + + @override + String get tocTemplateUnnamed => '未命名'; + + @override + String get tocTemplateNew => '新建模板...'; + + @override + String get tocTemplateName => '名称:'; + + @override + String get tocTemplateExtension => '扩展名:'; + + @override + String get tocTemplateLoadError => '无法加载模板。现有模板未更改。'; + + @override + String get tocTemplateSaveError => '无法保存模板。您的编辑内容仍保留在此处。'; + + @override + String get tocTemplateConflict => '模板已在其他窗口中更改。请取消并重新打开编辑器。'; + + @override + String get tocTemplateDuplicateName => '已存在相同名称和扩展名的模板。'; + + @override + String get tocTemplateFormat => '格式:'; + + @override + String get tocTemplateReset => '重置'; + + @override + String get tocTemplateRetry => '重试'; + + @override + String tocRemoveElementsDialog(int count) { + return '移除 $count 个目录元素'; + } + + @override + String get tocRemoveElementsRetainFiles => '不会删除与这些目录元素关联的源文件。'; + + @override + String get tocOk => '确定'; + + @override + String get tocSelectExistingTopic => '选择要添加到当前实例的主题'; + + @override + String get tocGroupName => '组名'; + + @override + String get tocMoveLineUp => '上移行'; + + @override + String get tocMoveLineDown => '下移行'; + + @override + String get tocEmptyMdTopic => '空 MD 主题'; + + @override + String get tocEmptyXmlTopic => '空 XML 主题'; + + @override + String get tocCopySpecial => '特殊复制'; + + @override + String tocCopyFileName(String name) { + return '主题文件名 \'$name\''; + } + + @override + String get tocCopyFilePath => '主题文件路径'; + + @override + String tocCopyTopicTitle(String title) { + return '主题标题 \'$title\''; + } + + @override + String tocCopyElementId(String id) { + return '目录元素 ID \'$id\''; + } + + @override + String get tocCopyFileNameUnavailable => '主题文件名'; + + @override + String get tocCopyTopicTitleUnavailable => '主题标题'; + + @override + String get tocCopyElementIdUnavailable => '目录元素 ID'; + + @override + String get tocEditTitleAction => '编辑标题...'; + + @override + String get tocEditTitleDialog => '编辑标题'; + + @override + String get tocRemoveElementAction => '移除目录元素...'; + + @override + String get tocSetHomePage => '设为主页'; + + @override + String tocGoToElement(String fileName) { + return '转到“$fileName”中的目录元素'; + } + + @override + String get tocSynchronize => '同步目录与编辑器'; + + @override + String get tocEmptyGroupAction => '空组'; + + @override + String get tocNewEmptyGroup => '新建空组'; + + @override + String get tocGroupAction => '分组'; + + @override + String get tocNewGroup => '新建组'; + + @override + String get tocSortChildren => '按字母顺序排列子主题'; + + @override + String get tocLinkTopicFiles => '将主题文件链接到目录...'; + + @override + String get addLocalMarkdownFiles => '添加本地 Markdown 文件'; + + @override + String get tocTitleField => '目录标题:'; + + @override + String get tocTopicTitleField => '主题标题:'; + + @override + String get tocAdvancedSettings => '高级设置'; + + @override + String tocInstanceTitleField(String instanceId) { + return '“$instanceId”的标题:'; + } + + @override + String get tocOnlyTitleField => '仅用于目录的标题:'; + + @override + String get tocInstanceTitleExplanation => '仅用于当前实例。默认继承主题标题。'; + + @override + String get tocOnlyTitleExplanation => + '仅用于目录。默认继承主题标题;如果设置了实例专用标题,则继承该标题。标题的说明见'; + + @override + String get tocTitleDocumentationLink => '此处'; + + @override + String get tocFindUsages => '查找'; + + @override + String get tocRefactorMenu => '重构'; + + @override + String get tocSafeDelete => '安全删除'; + + @override + String get tocDuplicate => '创建副本'; + + @override + String get tocDuplicateFilename => '主题文件名:'; + + @override + String get tocDuplicateDialog => '创建主题副本'; + + @override + String get pdfTitlePage => '标题页'; + + @override + String get pdfIncludeTitlePage => '包含标题页'; + + @override + String get pdfTitlePageNote => '此页不显示页眉、页脚或页码。'; + + @override + String get pdfTitlePageTitle => '标题'; + + @override + String get pdfTitlePageSubtitle => '副标题'; + + @override + String get pdfTitlePageAuthor => '作者'; + + @override + String get pdfTitlePageOrganization => '组织'; + + @override + String get pdfTitlePageVersion => '版本'; + + @override + String get pdfTitlePageDate => '日期'; + + @override + String get pdfTitlePageTitleRequired => '请输入标题页的标题。'; + + @override + String get pdfTocSectionNote => '从单独一页开始,可以跨多页。'; + + @override + String get appTitle => 'BusyMark'; + + @override + String get appSubtitle => 'Markdown 文件和兼容 Writerside 的文档项目编辑器。'; + + @override + String get aboutBusyMark => '关于 BusyMark'; + + @override + String get aboutTagline => 'Markdown 和 Writerside 编辑器'; + + @override + String get aboutLicenseLabel => '许可证'; + + @override + String get aboutLicenseName => 'Apache License 2.0'; + + @override + String get aboutWebsite => '网站'; + + @override + String get aboutSourceCode => '源代码'; + + @override + String get reportIssue => '报告问题'; + + @override + String get feedbackCategory => '类别'; + + @override + String get feedbackChooseCategory => '选择类别'; + + @override + String get feedbackCategoryProblem => '问题或错误'; + + @override + String get feedbackCategoryFeature => '功能请求'; + + @override + String get feedbackCategoryPrivacySecurity => '隐私或安全问题'; + + @override + String get feedbackCategoryUsability => '可用性问题'; + + @override + String get feedbackCategoryOther => '其他'; + + @override + String get feedbackSubject => '主题'; + + @override + String get feedbackMessage => '详细留言'; + + @override + String get feedbackReplyEmail => '回复邮箱(可选)'; + + @override + String get feedbackIncludeTechnicalDetails => '包含技术详细信息'; + + @override + String get feedbackTechnicalDetailsDisclosure => + '启用后,此选项仅会添加您的 Linux 操作系统版本和 BusyMark 应用语言。不会附加日志、文件、账户数据或其他诊断信息。'; + + @override + String get feedbackSubmit => '提交'; + + @override + String get feedbackSubmitting => '正在提交…'; + + @override + String get feedbackCategoryRequired => '请选择类别。'; + + @override + String get feedbackSubjectLength => '主题必须包含 3 到 120 个字符。'; + + @override + String get feedbackMessageLength => '信息必须包含 10 到 5,000 个字符。'; + + @override + String get feedbackReplyEmailInvalid => '请输入有效的邮箱地址,或将此字段留空。'; + + @override + String get feedbackConnectionFailure => 'BusyMark 无法连接。请检查网络连接后重试。'; + + @override + String get feedbackTimeoutFailure => '请求超时。请重试。'; + + @override + String get feedbackRateLimitedFailure => '此连接发送的报告过多。请稍候再试。'; + + @override + String get feedbackRejectedFailure => '服务器拒绝了此报告。请检查表单字段后重试。'; @override String get feedbackServerFailure => '服务器无法接收此报告。请稍后重试。'; @@ -5716,15 +6253,6 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get keyboardShortcuts => '键盘快捷键'; - @override - String get commandPalette => '命令面板'; - - @override - String get commandPaletteHint => '输入命令'; - - @override - String get commandPaletteEmpty => '没有匹配的命令'; - @override String get commandUnavailableInContext => '当前编辑器上下文中无法使用此命令'; @@ -5937,7 +6465,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { String get shortcutSearchDescription => '搜索当前工作区'; @override - String get shortcutKeyboardShortcutsDescription => '显示此键盘快捷键参考'; + String get shortcutKeyboardShortcutsDescription => '自定义应用程序键盘快捷键'; @override String get shortcutSyntaxReferenceDescription => '打开语法参考'; @@ -6162,7 +6690,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { String get validateOnEdit => '编辑时验证'; @override - String get clearRecentWorkspaces => '清除最近使用的工作区'; + String get clearRecentWorkspaces => '清除最近记录'; @override String get editingButtonsPosition => '编辑按钮位置'; @@ -6917,16 +7445,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { String get sourceSearchPreviousMatch => '上一个匹配项'; @override - String get sourceSearchNextMatch => '下一个匹配项'; - - @override - String get sourceSearchCaseSensitive => '区分大小写'; - - @override - String get sourceSearchWholeWord => '全字匹配'; - - @override - String get sourceSearchRegex => '正则表达式'; + String get sourceSearchNextMatch => '下一个匹配项'; @override String get sourceSearchReplacement => '替换为'; @@ -7046,13 +7565,6 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get saveAs => '另存为'; - @override - String get sourceSearchInvalidRegex => '无效的正则表达式'; - - @override - String get sourceSearchZeroLengthUnsupported => - '不支持零长度的正则表达式匹配(例如 ^、\$ 或仅匹配位置的前瞻断言)。'; - @override String get workspaceSearchIncomplete => '仅显示了部分结果。请显示更多结果或缩小搜索范围。'; @@ -7157,6 +7669,11 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get documentKindResourceFile => '资源文件'; + @override + String workspaceErrorRecentWorkspaceUpdateFailed(String error) { + return '无法更新最近使用的项目:$error。'; + } + @override String workspaceErrorOpenFailed(String error) { return '打开失败:$error'; @@ -9321,9 +9838,6 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get clipboardHistory => '剪贴板历史'; - @override - String get clipboardHistorySearchHint => '搜索剪贴板历史记录'; - @override String get clipboardHistoryDescription => '打开会话本地剪贴板历史。'; @@ -9344,9 +9858,6 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get clipboardNoItems => '剪贴板历史为空。'; - @override - String get clipboardNoMatches => '没有匹配此搜索的剪贴板项目。'; - @override String get clipboardDisabled => '剪贴板历史集合在设置中已禁用。'; @@ -9397,12 +9908,6 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get localHistoryNoRevisions => '此文档没有本地历史修订。'; - @override - String get localHistoryNoMatches => '没有本地历史修订与此搜索匹配。'; - - @override - String get localHistorySearchHint => '搜索本地历史记录'; - @override String get localHistorySelectedRevision => '已选择的修订'; @@ -10326,4 +10831,367 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get wsPropertiesUnavailable => '所检查的元素不可用。请检查一个元素以继续。'; + + @override + String get searchDocumentsCategory => '文档'; + + @override + String get searchTopicsCategory => '主题'; + + @override + String get searchCurrentDocument => '当前文档'; + + @override + String get searchCurrentWorkspace => '当前工作区'; + + @override + String get browseNotes => '浏览笔记'; + + @override + String get searchActionsCommand => '搜索操作'; + + @override + String get searchDocumentsCommand => '搜索文档'; + + @override + String get searchTopicsCommand => '搜索主题'; + + @override + String get searchRecentCommand => '搜索最近项目'; + + @override + String get searchResourceUnavailable => '此资源不可用。'; + + @override + String get notesSync => '同步笔记'; + + @override + String get searchInWorkspace => '在工作区中搜索'; + + @override + String get searchCategory => '搜索类别'; + + @override + String get clearHistorySearchScope => '搜索文档历史记录'; + + @override + String get searchInDocument => '在文档中搜索'; + + @override + String get searchDocumentHint => '搜索此文档…'; + + @override + String get searchWorkspaceHint => '搜索此工作区…'; + + @override + String get searchOutlineHint => '搜索标题…'; + + @override + String get searchHistoryHint => '搜索本地历史记录…'; + + @override + String get searchEnterText => '输入要搜索的文本'; + + @override + String get searchAccountUnavailable => '账户不可用'; + + @override + String get searchDocumentsHint => '搜索文档…'; + + @override + String get searchTopicsHint => '搜索主题…'; + + @override + String get searchRecentHint => '搜索最近项目…'; + + @override + String get searchActionsHint => '搜索操作…'; + + @override + String get searchRequiresNotesWorkspace => '需要笔记工作区'; + + @override + String get searchSidebarLabel => '搜索:'; + + @override + String get searchDocumentTextCategory => '文档文本'; + + @override + String get searchWorkspaceTextCategory => '工作区文本'; + + @override + String get searchHeadingsCategory => '标题'; + + @override + String get shortcutUnassigned => '未分配'; + + @override + String get shortcutPressKeys => '请按下组合键…'; + + @override + String get shortcutRecord => '录制快捷键'; + + @override + String get shortcutRestoreAll => '恢复所有默认设置'; + + @override + String get shortcutRestoreAllQuestion => '恢复所有操作的默认快捷键?自定义分配将被移除。'; + + @override + String get shortcutSaveFailed => '快捷键已生效,但无法保存。请重试保存以便重启后保留。'; + + @override + String get shortcutRetry => '重试保存'; + + @override + String get shortcutChanged => '分配已更改。请检查当前快捷键并重试。'; + + @override + String get shortcutInvalid => '无法在此上下文中分配此组合键。'; + + @override + String get shortcutSystemWarning => '桌面可能正在使用此组合键。BusyMark 无法覆盖系统快捷键。'; + + @override + String get shortcutConflicts => '此快捷键与以下操作冲突:'; + + @override + String get shortcutRemove => '移除分配'; + + @override + String get shortcutRestoreDefault => '恢复默认设置'; + + @override + String get shortcutAssign => '分配'; + + @override + String get shortcutReassign => '重新分配'; + + @override + String get shortcutContextApplication => '应用程序'; + + @override + String get shortcutContextDocument => '文档视图'; + + @override + String get shortcutContextEditor => '编辑器和源码'; + + @override + String get shortcutContextWritersideSource => 'Writerside 源码'; + + @override + String get shortcutContextWorkspace => '工作区导航'; + + @override + String get shortcutContextTree => '侧栏树'; + + @override + String get searchHeadingsCommand => '搜索标题'; + + @override + String get shortcutFixed => '由编辑器管理,无法配置。'; + + @override + String get shortcutDefaultConflict => '默认快捷键与自定义分配冲突。'; + + @override + String get shortcutCurrentBinding => '当前快捷键'; + + @override + String get shortcutProposedBinding => '拟用快捷键'; + + @override + String get notesActions => '笔记操作'; + + @override + String get noteActions => '此笔记的操作'; + + @override + String get noteProperties => '笔记属性'; + + @override + String get notesExportAll => '导出笔记 · 所有笔记…'; + + @override + String get notesExportCategory => '导出笔记 · 类别…'; + + @override + String get notesOfflineAll => '设为离线可用 · 所有笔记'; + + @override + String get notesOfflineCategory => '设为离线可用 · 类别'; + + @override + String get notesOfflineRemoveAll => '移除离线保留要求 · 所有笔记'; + + @override + String get notesOfflineRemoveCategory => '移除离线保留要求 · 类别'; + + @override + String get notesOfflinePause => '暂停下载'; + + @override + String get notesOfflineResume => '继续下载'; + + @override + String get notesOfflineKeep => '设为离线可用'; + + @override + String get notesOfflineRetentionHelp => '移除此保留要求不会删除现有缓存。分类或账户的要求仍可能保留此笔记。'; + + @override + String get notesOfflineBroader => '此笔记也因分类或账户的要求而被保留。'; + + @override + String get notesOfflineExternal => '外部媒体需要网络连接。BusyMark 只保留笔记文本和托管附件。'; + + @override + String get notesEmptyAll => '还没有笔记。请新建一条笔记。'; + + @override + String get notesEmptyFavorites => '没有收藏的笔记。'; + + @override + String get notesEmptyCategory => '此分类中没有笔记。'; + + @override + String get notesEmptyUncategorized => '没有未分类笔记。'; + + @override + String get notesEmptyRecovery => '没有可恢复的本地保留笔记。'; + + @override + String get notesLoadFailed => 'Nextcloud Notes · 失败. 重试'; + + @override + String get notesOperationFailed => '无法完成操作。草稿已保留。请检查保存状态和冲突后重试。'; + + @override + String get notesInspectionFailed => '附件 · 失败. 重试'; + + @override + String get notesStartingConnection => '正在连接 Nextcloud…'; + + @override + String get notesVerifyingConnection => '正在验证笔记访问权限…'; + + @override + String get notesCancellingConnection => '正在取消登录并清理…'; + + @override + String get notesRemovingConnection => '正在移除本地账户…'; + + @override + String get notesRemovedWarning => + '本地 Nextcloud 账户已移除,但无法确认应用密码已撤销。请在 Nextcloud 安全设置中移除 BusyMark 的应用密码。'; + + @override + String get notesBuffersNotSaved => + '无法将所有打开的笔记保存到本地。请在工作区检查保存、冲突或规范化处理。待处理的本地更改可能仍需与 Nextcloud 同步。'; + + @override + String get notesSettingsLeave => '离开前保存 Nextcloud 设置更改吗?'; + + @override + String get notesSettingsLeaveHelp => '服务器 Notes 设置 · 未保存的更改'; + + @override + String get notesSettingsReload => '重新加载服务器设置…'; + + @override + String get notesSettingsReloadHelp => '重新加载会用 Nextcloud 当前设置替换草稿。放弃草稿并重新加载吗?'; + + @override + String get notesSettingsRefreshFailed => + 'Nextcloud 已接受设置,但刷新笔记失败。显示的是服务器确认的设置值。请从笔记界面重试刷新。'; + + @override + String get notesUnavailable => '离线 · 文本已在本地可用 · 已保存到本地 · 等待同步'; + + @override + String get notesStorageFull => 'Nextcloud 存储空间已满。请释放服务器空间后重试。'; + + @override + String get notesRejected => 'Nextcloud 拒绝了此更改。请检查笔记并保存本地副本后重试。'; + + @override + String get notesThrottled => 'Nextcloud 要求稍候。同步将在重试期限后继续。'; + + @override + String get notesRecoveryRequired => '冲突 · 比较'; + + @override + String get notesScanning => '正在扫描文件…'; + + @override + String get notesCancelling => '正在取消…等待当前操作结束。'; + + @override + String get notesCancelled => '已取消'; + + @override + String get notesPartial => '部分完成'; + + @override + String get notesTransferFailed => '失败. 重试'; + + @override + String get notesImportPartialHelp => '之前的项目可能已保存在本地。请继续本次导入,以免创建重复笔记。'; + + @override + String get notesNotStarted => '尚未开始'; + + @override + String get notesCreated => '已创建'; + + @override + String get notesRemoteDeleteUnavailable => '服务器地址 · 不支持 · 删除 · 附件'; + + @override + String get notesCurrentText => '当前保留的文本'; + + @override + String get notesRecovering => '正在恢复为新笔记…'; + + @override + String get notesReadOnlyHelp => '只读 · 收藏 · 保存本地副本…'; + + @override + String get notesSort => '笔记排序'; + + @override + String get notesPathRelative => '笔记文件夹路径相对于您的 Nextcloud 存储空间。'; + + @override + String get notesSettingsStale => 'Nextcloud 上的设置已更改。请重新加载并检查服务器值,再替换草稿。'; + + @override + String get notesDownloadsPaused => '下载已暂停'; + + @override + String get notesExportNotPublished => '未发布任何导出。'; + + @override + String get notesKeyringUnavailable => '请解锁桌面密钥环后重试。'; + + @override + String get notesConnectFailed => '无法连接 Nextcloud Notes。请检查服务器地址后重试。'; + + @override + String get notesAccountMismatch => '请登录同一 Nextcloud 账户。要连接其他账户,请先移除此账户。'; + + @override + String get notesLocalRemovalFailed => '本地账户清理失败。账户已保留,以便重试移除。'; + + @override + String get notesTransferring => '正在传输笔记…'; + + @override + String get notesTechnicalDetails => '技术详情'; + + @override + String get notesPauseRequirement => '暂停离线保留要求'; + + @override + String get notesResumeRequirement => '继续离线保留要求'; } diff --git a/lib/main.dart b/lib/main.dart index 5d2a8fcd..4af70c27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,9 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart'; import 'package:yaru/yaru.dart'; +import 'src/app/app_settings.dart'; import 'src/app/busymark_app.dart'; import 'src/app/startup_path.dart'; import 'src/app/system_accent.dart'; + import 'src/git/application/git_controller.dart'; import 'src/git/data/git_cli_gateway.dart'; import 'src/platform/gtk_animation_settings_service.dart'; @@ -21,7 +23,9 @@ import 'src/visualization/visualization_release_smoke.dart'; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); - if (Platform.isLinux) await YaruWindow.ensureInitialized(); + if (Platform.isLinux) { + await YaruWindow.ensureInitialized(); + } if (visualizationReleaseSmokeReportPath(args) case final reportPath?) { exit(await runVisualizationReleaseSmoke(reportPath)); } @@ -40,9 +44,16 @@ Future main(List args) async { ]); final headerIcons = GtkHeaderIconService(); await headerIcons.initialize(); + const settingsStore = JsonFileLocalSettingsStore(); + final startupSettings = await StartupAppSettings.load(settingsStore); + runApp( ProviderScope( overrides: [ + localSettingsStoreProvider.overrideWithValue(settingsStore), + appSettingsControllerProvider.overrideWith( + () => AppSettingsController(startupSettings: startupSettings), + ), initialGtkAnimationsEnabledProvider.overrideWithValue( chrome[0] as bool?, ), diff --git a/lib/src/app/app_metadata.dart b/lib/src/app/app_metadata.dart index eb14e97a..c0593c74 100644 --- a/lib/src/app/app_metadata.dart +++ b/lib/src/app/app_metadata.dart @@ -1 +1 @@ -const busyMarkAppVersion = '0.6.2'; +const busyMarkAppVersion = '0.6.3'; diff --git a/lib/src/app/app_router.dart b/lib/src/app/app_router.dart index 67f7b336..3bf271d8 100644 --- a/lib/src/app/app_router.dart +++ b/lib/src/app/app_router.dart @@ -5,6 +5,8 @@ import 'package:go_router/go_router.dart'; import '../workspace/presentation/settings_screen.dart'; import '../workspace/presentation/welcome_screen.dart'; import '../workspace/presentation/workspace_screen.dart'; +import '../workspace/workspace_controller.dart'; +import '../nextcloud_notes/presentation/nextcloud_settings.dart'; const settingsRoutePath = '/settings'; const _settingsReturnTargetParameter = 'returnTo'; @@ -45,6 +47,33 @@ String settingsLocationForUri(Uri currentUri) { return settingsLocation(settingsReturnTargetForUri(currentUri)); } +void openKeyboardShortcutsSettings(BuildContext context) { + final router = GoRouter.of(context); + final uri = router.routeInformationProvider.value.uri; + if (uri.path == '/workspace') { + final container = ProviderScope.containerOf(context, listen: false); + if (!container + .read(workspaceControllerProvider.notifier) + .propertyEdits + .flush()) { + return; + } + } + final location = Uri.parse(settingsLocationForUri(uri)) + .replace( + queryParameters: { + ...Uri.parse(settingsLocationForUri(uri)).queryParameters, + 'page': 'keyboardShortcuts', + }, + ) + .toString(); + if (uri.path == settingsRoutePath) { + router.replace(location); + } else { + router.go(location); + } +} + final rootNavigatorKey = GlobalKey( debugLabel: 'BusyMark root navigator', ); @@ -66,6 +95,7 @@ final appRouterProvider = Provider((ref) { ), GoRoute( path: settingsRoutePath, + onExit: (context, state) => confirmNotesSettingsDeparture(context), pageBuilder: (context, state) => NoTransitionPage( child: SettingsScreen( returnTarget: SettingsReturnTarget.fromSettingsUri(state.uri), diff --git a/lib/src/app/app_settings.dart b/lib/src/app/app_settings.dart index 14f3c282..70f0b970 100644 --- a/lib/src/app/app_settings.dart +++ b/lib/src/app/app_settings.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show mapEquals; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -10,6 +11,7 @@ import 'package:path_provider/path_provider.dart'; import '../export/export_options.dart'; import '../spellcheck/spelling_language.dart'; import 'app_locale.dart'; +import 'shortcut_binding.dart'; enum BusyMarkThemeModePreference { system, light, dark } @@ -46,16 +48,28 @@ extension BusyMarkThemeModePreferenceX on BusyMarkThemeModePreference { } } -class RecentWorkspace { - const RecentWorkspace({ +class RecentResource { + const RecentResource({ required this.path, required this.kind, required this.lastOpenedAt, + this.accountId, + this.accountLabel, + this.localId, + this.ownerRoot, + this.title, + this.document = false, }); - factory RecentWorkspace.fromJson(Map json) { - return RecentWorkspace( + factory RecentResource.fromJson(Map json) { + return RecentResource( path: json['path']?.toString() ?? '', + accountId: json['accountId'] as String?, + accountLabel: json['accountLabel'] as String?, + localId: json['localId'] as String?, + ownerRoot: json['ownerRoot'] as String?, + title: json['title'] as String?, + document: json['document'] as bool? ?? json['kind'] == 'singleMarkdown', kind: json['kind']?.toString() ?? 'unknown', lastOpenedAt: DateTime.tryParse(json['lastOpenedAt']?.toString() ?? '') ?? @@ -66,14 +80,35 @@ class RecentWorkspace { final String path; final String kind; final DateTime lastOpenedAt; + final String? accountId, accountLabel, localId, ownerRoot, title; + final bool document; + bool get isDocument => document || kind == 'singleMarkdown'; + String get identity => accountId == null + ? p.normalize(p.absolute(path)) + : '$accountId:${localId ?? 'workspace'}'; Map toJson() => { 'path': path, 'kind': kind, + if (accountId != null) 'accountId': accountId, + if (accountLabel != null) 'accountLabel': accountLabel, + if (localId != null) 'localId': localId, + if (ownerRoot != null) 'ownerRoot': ownerRoot, + if (title != null) 'title': title, + 'document': isDocument, 'lastOpenedAt': lastOpenedAt.toIso8601String(), }; } +// Keep the existing JSON representation and key; resource type never affects +// retention. Stable identities are deduplicated after timestamp ordering. +List _normalizeRecent(Iterable resources) { + final ordered = resources.toList() + ..sort((a, b) => b.lastOpenedAt.compareTo(a.lastOpenedAt)); + final seen = {}; + return ordered.where((r) => seen.add(r.identity)).take(12).toList(); +} + class AppSettings { const AppSettings({ required this.themeModePreference, @@ -115,6 +150,7 @@ class AppSettings { required this.confirmCloseWithUnsavedChanges, required this.recentWorkspaces, this.lastOpenedPath, + this.shortcutOverrides = const {}, }); factory AppSettings.defaults() { @@ -279,14 +315,18 @@ class AppSettings { confirmCloseWithUnsavedChanges: json['confirmCloseWithUnsavedChanges'] as bool? ?? defaults.confirmCloseWithUnsavedChanges, + shortcutOverrides: readShortcutOverrides(json['shortcutOverrides']), lastOpenedPath: json['lastOpenedPath']?.toString(), recentWorkspaces: recent is List - ? recent - .whereType() - .map((item) => item.cast()) - .map(RecentWorkspace.fromJson) - .where((item) => item.path.isNotEmpty) - .toList() + ? _normalizeRecent( + recent + .whereType() + .map((item) => item.cast()) + .map(RecentResource.fromJson) + .where( + (item) => item.path.isNotEmpty || item.accountId != null, + ), + ) : defaults.recentWorkspaces, ); } @@ -329,13 +369,17 @@ class AppSettings { final bool reopenPreviousWorkspaceOnStartup; final bool confirmCloseWithUnsavedChanges; final String? lastOpenedPath; - final List recentWorkspaces; + final Map shortcutOverrides; + final List recentWorkspaces; ThemeMode get themeMode => themeModePreference.themeMode; Locale? get locale => busyMarkLocaleFromTag(localeTag); Map toJson() => { + 'shortcutOverrides': { + for (final e in shortcutOverrides.entries) e.key: e.value?.toJson(), + }, 'themeModePreference': themeModePreference.name, 'localeTag': localeTag, 'sidebarVisible': sidebarVisible, @@ -420,6 +464,7 @@ class AppSettings { } AppSettings copyWith({ + Map? shortcutOverrides, BusyMarkThemeModePreference? themeModePreference, Object? localeTag = _unset, bool? sidebarVisible, @@ -458,9 +503,10 @@ class AppSettings { bool? reopenPreviousWorkspaceOnStartup, bool? confirmCloseWithUnsavedChanges, String? lastOpenedPath, - List? recentWorkspaces, + List? recentWorkspaces, }) { return AppSettings( + shortcutOverrides: shortcutOverrides ?? this.shortcutOverrides, themeModePreference: themeModePreference ?? this.themeModePreference, localeTag: identical(localeTag, _unset) ? this.localeTag @@ -523,7 +569,9 @@ class AppSettings { confirmCloseWithUnsavedChanges: confirmCloseWithUnsavedChanges ?? this.confirmCloseWithUnsavedChanges, lastOpenedPath: lastOpenedPath ?? this.lastOpenedPath, - recentWorkspaces: recentWorkspaces ?? this.recentWorkspaces, + recentWorkspaces: recentWorkspaces == null + ? this.recentWorkspaces + : _normalizeRecent(recentWorkspaces), ); } } @@ -533,6 +581,41 @@ abstract class LocalSettingsStore { Future save(Map json); } +/// One-use handoff of local preferences read before the first application frame. +/// Bound to the store that acknowledged them; later controller initialization +/// reads the store again instead of reusing a startup snapshot. +class StartupAppSettings { + StartupAppSettings._(this._store, this._snapshot); + + final LocalSettingsStore _store; + ({AppSettings settings, String? acknowledgedJson})? _snapshot; + + static Future load(LocalSettingsStore store) async { + AppSettings settings; + String? acknowledgedJson; + try { + settings = AppSettings.fromJson(await store.load()); + acknowledgedJson = _canonicalSettingsJson(settings.toJson()); + } on Object { + // Match the controller's existing failed-load policy. Defaults are usable + // but are not an acknowledged disk snapshot for write reconciliation. + settings = AppSettings.defaults(); + } + return StartupAppSettings._(store, ( + settings: settings, + acknowledgedJson: acknowledgedJson, + )); + } + + ({AppSettings settings, String? acknowledgedJson})? _take( + LocalSettingsStore store, + ) { + final snapshot = _snapshot; + _snapshot = null; + return identical(store, _store) ? snapshot : null; + } +} + class JsonFileLocalSettingsStore implements LocalSettingsStore { const JsonFileLocalSettingsStore({ String? settingsFilePathOverride, @@ -546,10 +629,26 @@ class JsonFileLocalSettingsStore implements LocalSettingsStore { @override Future> load() async { final file = await _settingsFile(); - if (!await file.exists()) { - return {}; + final type = await FileSystemEntity.type(file.path); + if (type != FileSystemEntityType.file && + type != FileSystemEntityType.notFound) { + throw FileSystemException( + 'Settings path is not a regular file', + file.path, + ); + } + // Other read failures must leave the controller's acknowledged snapshot + // unset. A notFound type can also mean that the path is inaccessible. + final String content; + try { + content = await file.readAsString(); + } on FileSystemException catch (error) { + final code = error.osError?.errorCode; + if (code == 2 || Platform.isWindows && code == 3) { + return {}; + } + rethrow; } - final content = await file.readAsString(); if (content.trim().isEmpty) { return {}; } @@ -600,6 +699,10 @@ class JsonFileLocalSettingsStore implements LocalSettingsStore { } class AppSettingsController extends Notifier { + AppSettingsController({StartupAppSettings? startupSettings}) + : _startupSettings = startupSettings; + + final StartupAppSettings? _startupSettings; bool Function()? processPendingProperties; Future setElementPropertiesVisible(bool value) { if (!value && processPendingProperties?.call() == false) { @@ -612,13 +715,24 @@ class AppSettingsController extends Notifier { _mutate((s) => s.copyWith(elementPropertiesWidth: value.clamp(260, 600))); late LocalSettingsStore _store; Future? _lastSave; + String? _persistedSettingsJson; final _pendingInitialMutations = []; + final _pendingInitialPersistence = >[]; var _loaded = false; late Future _loadFuture; @override AppSettings build() { _store = ref.read(localSettingsStoreProvider); + final startup = _startupSettings?._take(_store); + if (startup != null) { + _loaded = true; + _persistedSettingsJson = startup.acknowledgedJson; + _loadFuture = Future.value(); + return startup.settings.copyWith(elementPropertiesVisible: false); + } + _loaded = false; + _persistedSettingsJson = null; _loadFuture = _load(); unawaited(_loadFuture); return AppSettings.defaults(); @@ -626,6 +740,47 @@ class AppSettingsController extends Notifier { Future waitUntilLoaded() => _loadFuture; + Future flushPersistence() async { + await waitUntilLoaded(); + while (true) { + final pending = _lastSave; + await pending; + if (identical(pending, _lastSave)) return; + } + } + + /// Invoked after validation against the current in-memory assignments. All + /// preferences still use the same ordered writer and settings file. + bool _trackShortcutSaves = false; + Future saveShortcutOverrides( + Map overrides, + ) { + _trackShortcutSaves = true; + final snapshot = Map.unmodifiable(overrides); + if (!_loaded) return _saveShortcutsAfterLoad(snapshot); + return _save( + state.copyWith(shortcutOverrides: snapshot), + reusePersisted: true, + ); + } + + Future _saveShortcutsAfterLoad( + Map overrides, + ) async { + await waitUntilLoaded(); + if (!ref.mounted) return false; + return _save( + state.copyWith(shortcutOverrides: overrides), + reusePersisted: true, + ); + } + + Future retryShortcutSave() async { + await waitUntilLoaded(); + if (!ref.mounted) return false; + return _save(state, reusePersisted: true); + } + Future setPdfExportOptions(PdfExportOptions options) { options.validateOrThrow(); return _mutate((settings) => settings.copyWith(pdfExportOptions: options)); @@ -690,6 +845,7 @@ class AppSettingsController extends Notifier { documentViewMode: mode, previewVisible: mode != DocumentViewModePreference.source, ), + reusePersisted: true, ); } @@ -941,39 +1097,143 @@ class AppSettingsController extends Notifier { Future recordOpenedWorkspace({ required String path, required String kind, + String? accountId, + String? title, }) { - final openedAt = DateTime.now(); - return _mutate((settings) { - final recent = [ - RecentWorkspace(path: path, kind: kind, lastOpenedAt: openedAt), - ...settings.recentWorkspaces.where((item) => item.path != path), - ].take(12).toList(); - return settings.copyWith(lastOpenedPath: path, recentWorkspaces: recent); - }); - } - - Future clearRecentWorkspaces() { - return _mutate((settings) => settings.copyWith(recentWorkspaces: [])); + return recordRecent( + RecentResource( + path: accountId == null ? p.normalize(p.absolute(path)) : '', + kind: kind, + accountId: accountId, + title: title, + document: kind == 'singleMarkdown', + lastOpenedAt: DateTime.now(), + ), + updateLastOpenedPath: accountId == null, + ); } - Future removeRecentWorkspace(String path) { + Future recordRecent( + RecentResource resource, { + bool updateLastOpenedPath = false, + }) => _mutate((settings) { + final ordered = [ + resource, + ...settings.recentWorkspaces.where( + (r) => r.identity != resource.identity, + ), + ]; + final retained = _normalizeRecent(ordered); + return settings.copyWith( + recentWorkspaces: retained, + lastOpenedPath: updateLastOpenedPath + ? resource.path + : settings.lastOpenedPath, + ); + }, propagateFailure: true); + + Future clearRecentWorkspaces() => + _mutate((settings) => settings.copyWith(recentWorkspaces: [])); + + Future removeRecentWorkspace(String path) => _mutate( + (settings) => settings.copyWith( + recentWorkspaces: settings.recentWorkspaces + .where( + (r) => + r.identity != path && + !(r.accountId == null && p.equals(r.path, path)), + ) + .toList(), + ), + ); + + Future updateRecentNoteTitles(Map<(String, String), String> titles) { + if (!state.recentWorkspaces.any( + (r) => + r.accountId != null && + r.localId != null && + titles[(r.accountId!, r.localId!)] != null && + titles[(r.accountId!, r.localId!)] != r.title, + )) { + return Future.value(); + } return _mutate( (settings) => settings.copyWith( recentWorkspaces: [ - for (final item in settings.recentWorkspaces) - if (!p.equals(item.path, path)) item, + for (final r in settings.recentWorkspaces) + if (r.accountId != null && + r.localId != null && + titles[(r.accountId!, r.localId!)] != null) + RecentResource( + path: r.path, + kind: r.kind, + lastOpenedAt: r.lastOpenedAt, + document: r.isDocument, + accountId: r.accountId, + accountLabel: r.accountLabel, + localId: r.localId, + title: titles[(r.accountId!, r.localId!)], + ownerRoot: r.ownerRoot, + ) + else + r, ], ), ); } + Future removeRecentAccount(String accountId) => _mutate( + (settings) => settings.copyWith( + recentWorkspaces: settings.recentWorkspaces + .where((r) => r.accountId != accountId) + .toList(), + ), + ); + + Future moveRecentPath(String from, String to) => _mutate( + (settings) => settings.copyWith( + recentWorkspaces: [ + for (final r in settings.recentWorkspaces) + if (r.accountId == null && + (p.equals(r.path, from) || p.isWithin(from, r.path))) + RecentResource( + path: p.equals(r.path, from) + ? to + : p.join(to, p.relative(r.path, from: from)), + kind: r.kind, + lastOpenedAt: r.lastOpenedAt, + document: r.isDocument, + ownerRoot: r.ownerRoot == null + ? null + : p.equals(r.ownerRoot!, from) + ? to + : p.isWithin(from, r.ownerRoot!) + ? p.join(to, p.relative(r.ownerRoot!, from: from)) + : r.ownerRoot, + title: r.title, + ) + else + r, + ], + ), + ); + Future _load() async { AppSettings loaded; try { loaded = AppSettings.fromJson(await _store.load()); + _persistedSettingsJson = _canonicalSettingsJson(loaded.toJson()); } on Object { loaded = AppSettings.defaults(); } + if (!ref.mounted) { + for (final completion in _pendingInitialPersistence) { + completion.completeError(StateError('Settings controller disposed')); + } + _pendingInitialPersistence.clear(); + _pendingInitialMutations.clear(); + return; + } // Visibility belongs to this run; retain the preferred inspector width. // Explicit requests made during hydration are applied below. loaded = loaded.copyWith(elementPropertiesVisible: false); @@ -982,39 +1242,103 @@ class AppSettingsController extends Notifier { loaded = mutation(loaded); } _pendingInitialMutations.clear(); + final completions = List>.of(_pendingInitialPersistence); + _pendingInitialPersistence.clear(); _loaded = true; state = loaded; if (hadPendingInitialMutations) { - await _save(loaded); + try { + // Initial actions share one publication, but opening callers retain + // its authoritative completion/error instead of a dispatch future. + await _save(loaded, propagateFailure: completions.isNotEmpty); + for (final completion in completions) { + completion.complete(); + } + } on Object catch (error, stack) { + for (final completion in completions) { + completion.completeError(error, stack); + } + } } } - Future _mutate(AppSettings Function(AppSettings) mutation) { + Future _mutate( + AppSettings Function(AppSettings) mutation, { + bool propagateFailure = false, + bool reusePersisted = false, + }) { if (!_loaded) { _pendingInitialMutations.add(mutation); state = mutation(state); + if (propagateFailure) { + final completion = Completer(); + _pendingInitialPersistence.add(completion); + return completion.future; + } return Future.value(); } - return _save(mutation(state)); + return _save( + mutation(state), + propagateFailure: propagateFailure, + reusePersisted: reusePersisted, + ); } - Future _save(AppSettings next) async { + Future _save( + AppSettings next, { + bool propagateFailure = false, + bool reusePersisted = false, + }) async { + if (!ref.mounted) return false; state = next; final json = next.toJson(); + final encoded = _canonicalSettingsJson(json); final previousSave = _lastSave; + Object? failure; + StackTrace? failureStack; final save = () async { await previousSave; try { - await _store.save(json); - } on Object { - // Keep the in-memory preference even when local persistence is unavailable. + // Compare the entire serialized snapshot only after preceding writes + // settle. Failed loads and writes never acknowledge unpersisted state. + if (!reusePersisted || encoded != _persistedSettingsJson) { + await _store.save(json); + _persistedSettingsJson = encoded; + } + return true; + } on Object catch (error, stack) { + failure = error; + failureStack = stack; + return false; } }(); - _lastSave = save; - await save; + // The queue tail always settles successfully; each caller retains its own + // outcome/error without preventing the next admitted publication. + _lastSave = save.then((_) {}); + final succeeded = await save; + if (ref.mounted && + _trackShortcutSaves && + mapEquals(state.shortcutOverrides, next.shortcutOverrides)) { + ref.read(shortcutPersistenceOutcomeProvider.notifier).complete(succeeded); + } + if (propagateFailure && failure != null) { + Error.throwWithStackTrace(failure!, failureStack!); + } + return succeeded; } } +class ShortcutPersistenceOutcome extends Notifier { + @override + bool? build() => null; + void complete(bool saved) => state = saved; +} + +final shortcutPersistenceOutcomeProvider = + NotifierProvider( + ShortcutPersistenceOutcome.new, + ); + final localSettingsStoreProvider = Provider( (ref) => const JsonFileLocalSettingsStore(), ); @@ -1150,3 +1474,17 @@ String? _normalizedStoredGitWorkspacePath(String? value) { } return p.normalize(p.absolute(value)); } + +// Map insertion order is not a preference change. Lists retain their order, +// including the release target's qualified Recent resources. +String _canonicalSettingsJson(Map json) => + jsonEncode(_canonicalSettingsValue(json)); + +Object? _canonicalSettingsValue(Object? value) { + if (value is Map) { + final keys = value.keys.cast().toList()..sort(); + return {for (final key in keys) key: _canonicalSettingsValue(value[key])}; + } + if (value is List) return value.map(_canonicalSettingsValue).toList(); + return value; +} diff --git a/lib/src/app/busymark_app.dart b/lib/src/app/busymark_app.dart index d4e8c5b0..5ae9505e 100644 --- a/lib/src/app/busymark_app.dart +++ b/lib/src/app/busymark_app.dart @@ -1,3 +1,9 @@ +import '../workspace/recent_presentation.dart'; +import '../search/welcome_notes_action.dart'; +import '../nextcloud_notes/application/nextcloud_connection.dart'; +import '../nextcloud_notes/application/notes_navigation.dart'; +import '../nextcloud_notes/presentation/notes_workspace_ui.dart'; +import '../nextcloud_notes/presentation/nextcloud_settings.dart'; import '../references/reference_controller.dart'; import 'dart:async'; @@ -27,12 +33,18 @@ import '../workspace/workspace_tab_actions.dart'; import 'app_router.dart'; import 'app_locale.dart'; import 'app_settings.dart'; -import 'command_palette.dart'; -import 'quick_open.dart'; +import '../search/search_session.dart'; +import '../search/search_activation.dart'; +import '../workspace/recent_navigation.dart'; import 'command_registry.dart'; +import 'command_shortcuts.dart'; +import 'keymap_controller.dart'; +import 'path_label.dart'; +import 'shortcut_capture.dart'; import 'app_theme.dart'; import 'busymark_dialogs.dart'; import 'busymark_design.dart'; +import 'busymark_search_field.dart'; import 'busymark_glyphs.dart'; import 'busymark_toast.dart'; import 'localization.dart'; @@ -40,75 +52,145 @@ import 'system_accent.dart'; import 'window_control_service.dart'; import 'linux/linux_window_host.dart'; +final _busyMarkDefaultCommandRegistryProvider = + Provider((ref) { + final commandIntents = { + for (final id in BusyMarkCommandCatalog.notesWorkspaceCommandIds) + id: _NotesWorkspaceActionIntent(id), + BusyMarkCommandIds.newNote: const _NotesActionIntent( + WelcomeNotesAction.create, + ), + BusyMarkCommandIds.browseNotes: const _NotesActionIntent( + WelcomeNotesAction.browse, + ), + BusyMarkCommandIds.newDocument: const _NewWorkspaceIntent(), + BusyMarkCommandIds.open: const _OpenWorkspaceIntent(), + BusyMarkCommandIds.save: const _SaveActiveIntent(), + BusyMarkCommandIds.export: const _ExportIntent(), + BusyMarkCommandIds.fullScreen: const _ToggleFullScreenIntent(), + BusyMarkCommandIds.back: const _BackIntent(), + BusyMarkCommandIds.keyboardShortcuts: const _KeyboardShortcutsIntent(), + BusyMarkCommandIds.syntaxReference: const _SyntaxReferenceIntent(), + BusyMarkCommandIds.settings: const _SettingsIntent(), + BusyMarkCommandIds.nextTab: const _NextTabIntent(), + BusyMarkCommandIds.previousTab: const _PreviousTabIntent(), + BusyMarkCommandIds.closeTab: const _CloseTabIntent(), + BusyMarkCommandIds.closeAllTabs: const _CloseAllTabsIntent(), + BusyMarkCommandIds.toggleSidebar: const _ToggleSidebarIntent(), + BusyMarkCommandIds.clipboardHistory: + const _OpenClipboardHistoryIntent(), + BusyMarkCommandIds.localHistory: const _OpenLocalHistoryIntent(), + BusyMarkCommandIds.viewEditor: const _DocumentViewModeIntent( + DocumentViewModePreference.editor, + ), + BusyMarkCommandIds.viewSource: const _DocumentViewModeIntent( + DocumentViewModePreference.source, + ), + BusyMarkCommandIds.viewReading: const _DocumentViewModeIntent( + DocumentViewModePreference.preview, + ), + BusyMarkCommandIds.viewSplit: const _DocumentViewModeIntent( + DocumentViewModePreference.split, + ), + }; + final defaults = BusyMarkCommandCatalog.create( + executions: { + for (final id in [ + BusyMarkCommandIds.sidebarFiles, + BusyMarkCommandIds.sidebarToc, + BusyMarkCommandIds.sidebarOutline, + BusyMarkCommandIds.sidebarGit, + ]) + id: () => ref + .read(workspaceNavigationCommandProvider.notifier) + .request(id), + for (final entry in searchCommandDestinations.entries) + entry.key: () { + final search = ref.read(searchSessionProvider.notifier); + search.openCategory(entry.value); + }, + BusyMarkCommandIds.elementProperties: () => ref + .read(workspaceControllerProvider.notifier) + .toggleElementProperties + ?.call(), + BusyMarkCommandIds.references: () => + ref.read(referencePanelRequestProvider.notifier).request(), + for (final entry in commandIntents.entries) + entry.key: () { + final target = rootNavigatorKey.currentContext; + if (target != null) { + Actions.maybeInvoke(target, entry.value); + } + }, + }, + enabled: { + for (final id in BusyMarkCommandCatalog.notesWorkspaceCommandIds) + id: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).workspace?.isRemote == + true, + BusyMarkCommandIds.sidebarFiles: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).workspace != null, + BusyMarkCommandIds.sidebarToc: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref + .read(workspaceControllerProvider) + .workspace + ?.writersideProject != + null, + BusyMarkCommandIds.sidebarOutline: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).activeBuffer != null, + BusyMarkCommandIds.sidebarGit: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(gitControllerProvider).repositoryInfo != null, + for (final id in [ + BusyMarkCommandIds.elementProperties, + BusyMarkCommandIds.references, + BusyMarkCommandIds.viewEditor, + BusyMarkCommandIds.viewSource, + BusyMarkCommandIds.viewReading, + BusyMarkCommandIds.viewSplit, + BusyMarkCommandIds.nextTab, + BusyMarkCommandIds.previousTab, + BusyMarkCommandIds.closeTab, + BusyMarkCommandIds.closeAllTabs, + ]) + id: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).workspace != null, + for (final entry in searchCommandDestinations.entries) + entry.key: () => ref + .read(searchSessionProvider.notifier) + .categories + .contains(entry.value), + BusyMarkCommandIds.save: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).workspace != null, + BusyMarkCommandIds.export: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + canExportWorkspace(ref.read(workspaceControllerProvider)), + BusyMarkCommandIds.clipboardHistory: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).workspace != null, + BusyMarkCommandIds.localHistory: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).workspace != null, + BusyMarkCommandIds.findLocalHistory: () => + ref.read(searchSessionProvider.notifier).workspaceVisible && + ref.read(workspaceControllerProvider).workspace != null, + }, + ); + return defaults; + }); + final busyMarkCommandRegistryProvider = Provider(( ref, ) { - final commandIntents = { - BusyMarkCommandIds.newDocument: const _NewWorkspaceIntent(), - BusyMarkCommandIds.open: const _OpenWorkspaceIntent(), - BusyMarkCommandIds.save: const _SaveActiveIntent(), - BusyMarkCommandIds.export: const _ExportIntent(), - BusyMarkCommandIds.fullScreen: const _ToggleFullScreenIntent(), - BusyMarkCommandIds.back: const _BackIntent(), - BusyMarkCommandIds.search: const _OpenSearchIntent(), - BusyMarkCommandIds.keyboardShortcuts: const _KeyboardShortcutsIntent(), - BusyMarkCommandIds.commandPalette: const _CommandPaletteIntent(), - BusyMarkCommandIds.quickOpen: const _QuickOpenIntent(), - BusyMarkCommandIds.syntaxReference: const _SyntaxReferenceIntent(), - BusyMarkCommandIds.settings: const _SettingsIntent(), - BusyMarkCommandIds.nextTab: const _NextTabIntent(), - BusyMarkCommandIds.previousTab: const _PreviousTabIntent(), - BusyMarkCommandIds.closeTab: const _CloseTabIntent(), - BusyMarkCommandIds.closeAllTabs: const _CloseAllTabsIntent(), - BusyMarkCommandIds.toggleSidebar: const _ToggleSidebarIntent(), - BusyMarkCommandIds.clipboardHistory: const _OpenClipboardHistoryIntent(), - BusyMarkCommandIds.localHistory: const _OpenLocalHistoryIntent(), - BusyMarkCommandIds.findLocalHistory: const _FindLocalHistoryIntent(), - BusyMarkCommandIds.viewEditor: const _DocumentViewModeIntent( - DocumentViewModePreference.editor, - ), - BusyMarkCommandIds.viewSource: const _DocumentViewModeIntent( - DocumentViewModePreference.source, - ), - BusyMarkCommandIds.viewReading: const _DocumentViewModeIntent( - DocumentViewModePreference.preview, - ), - BusyMarkCommandIds.viewSplit: const _DocumentViewModeIntent( - DocumentViewModePreference.split, - ), - }; - return BusyMarkCommandCatalog.create( - executions: { - BusyMarkCommandIds.elementProperties: () => ref - .read(workspaceControllerProvider.notifier) - .toggleElementProperties - ?.call(), - BusyMarkCommandIds.references: () => - ref.read(referencePanelRequestProvider.notifier).request(), - for (final entry in commandIntents.entries) - entry.key: () { - final target = rootNavigatorKey.currentContext; - if (target != null) { - Actions.maybeInvoke(target, entry.value); - } - }, - }, - enabled: { - BusyMarkCommandIds.quickOpen: () => - ref.read(workspaceControllerProvider).workspace != null, - BusyMarkCommandIds.save: () => - ref.read(workspaceControllerProvider).workspace != null, - BusyMarkCommandIds.export: () => - canExportWorkspace(ref.read(workspaceControllerProvider)), - BusyMarkCommandIds.search: () => - ref.read(workspaceControllerProvider).workspace != null, - BusyMarkCommandIds.clipboardHistory: () => - ref.read(workspaceControllerProvider).workspace != null, - BusyMarkCommandIds.localHistory: () => - ref.read(workspaceControllerProvider).workspace != null, - BusyMarkCommandIds.findLocalHistory: () => - ref.read(workspaceControllerProvider).workspace != null, - }, + return effectiveCommandRegistry( + ref.watch(_busyMarkDefaultCommandRegistryProvider), + ref.watch(appSettingsControllerProvider.select((s) => s.shortcutOverrides)), ); }); @@ -168,7 +250,7 @@ class BusyMarkApp extends ConsumerWidget { ], supportedLocales: AppLocalizations.supportedLocales, builder: (context, child) { - final commandRegistry = ref.read(busyMarkCommandRegistryProvider); + final commandRegistry = ref.watch(busyMarkCommandRegistryProvider); final headerBarDefaults = _nativeHeaderBarDefaults( context, settings, @@ -178,8 +260,9 @@ class BusyMarkApp extends ConsumerWidget { final appContent = HeaderBarConfigurationDefaults( configuration: headerBarDefaults, child: _BusyMarkWindowLifecycle( - child: Shortcuts( + child: BusyMarkCommandShortcuts( shortcuts: commandRegistry.shortcutIntents( + excludedIds: searchCommandDestinations.keys.toSet(), scopes: const { BusyMarkCommandScope.application, BusyMarkCommandScope.documentView, @@ -188,12 +271,74 @@ class BusyMarkApp extends ConsumerWidget { ), child: Actions( actions: { + _NotesWorkspaceActionIntent: + CallbackAction<_NotesWorkspaceActionIntent>( + onInvoke: (intent) { + final target = rootNavigatorKey.currentContext; + final workspace = ref + .read(workspaceControllerProvider) + .workspace; + if (target == null || + !ref + .read(searchSessionProvider.notifier) + .workspaceVisible || + workspace?.isRemote != true) { + return null; + } + switch (intent.commandId) { + case BusyMarkCommandIds.syncNotes: + unawaited( + ref + .read(workspaceControllerProvider.notifier) + .refreshNextcloudNotes(), + ); + case BusyMarkCommandIds.exportNotes: + final repository = ref + .read(nextcloudNotesRepositoryProvider) + .value; + if (repository == null) return null; + unawaited( + showNotesExport(target, ref, [ + for (final note in repository.notes) + if (note.accountId == + workspace!.nextcloudAccountId && + !isNotesRecovery(note)) + note.localId, + ]), + ); + case BusyMarkCommandIds.importNotesFolder: + unawaited(showNotesImport(target, ref)); + case BusyMarkCommandIds.importNotesFile: + unawaited( + showNotesImport(target, ref, folder: false), + ); + } + return null; + }, + ), BusyMarkCommandIntent: CallbackAction( onInvoke: (intent) { + if (searchCommandDestinations.containsKey( + intent.commandId, + ) && + (rootNavigatorKey.currentState?.canPop() ?? false)) { + return null; + } unawaited(commandRegistry.execute(intent.commandId)); return null; }, ), + _NotesActionIntent: CallbackAction<_NotesActionIntent>( + onInvoke: (intent) { + final target = rootNavigatorKey.currentContext; + if (target != null) { + unawaited( + performWelcomeNotesAction(target, ref, intent.action), + ); + } + return null; + }, + ), _NewWorkspaceIntent: CallbackAction<_NewWorkspaceIntent>( onInvoke: (intent) { final navigatorContext = rootNavigatorKey.currentContext; @@ -261,27 +406,11 @@ class BusyMarkApp extends ConsumerWidget { final navigatorContext = rootNavigatorKey.currentContext; if (navigatorContext != null) { - showBusyMarkKeyboardShortcutsDialog( - navigatorContext, - ); + openKeyboardShortcutsSettings(navigatorContext); } return null; }, ), - _CommandPaletteIntent: CallbackAction<_CommandPaletteIntent>( - onInvoke: (intent) { - final navigatorContext = rootNavigatorKey.currentContext; - if (navigatorContext != null) { - unawaited( - showBusyMarkCommandPalette( - navigatorContext, - commandRegistry, - ), - ); - } - return null; - }, - ), _SettingsIntent: CallbackAction<_SettingsIntent>( onInvoke: (intent) { final navigatorContext = rootNavigatorKey.currentContext; @@ -326,15 +455,6 @@ class BusyMarkApp extends ConsumerWidget { return null; }, ), - _FindLocalHistoryIntent: - CallbackAction<_FindLocalHistoryIntent>( - onInvoke: (intent) { - ref - .read(localHistoryFindRequestProvider.notifier) - .request(); - return null; - }, - ), _NextTabIntent: CallbackAction<_NextTabIntent>( onInvoke: (intent) { final navigatorContext = rootNavigatorKey.currentContext; @@ -385,25 +505,6 @@ class BusyMarkApp extends ConsumerWidget { return null; }, ), - _QuickOpenIntent: CallbackAction<_QuickOpenIntent>( - onInvoke: (_) { - final target = rootNavigatorKey.currentContext; - if (target != null) unawaited(showQuickOpen(target, ref)); - return null; - }, - ), - _OpenSearchIntent: CallbackAction<_OpenSearchIntent>( - onInvoke: (intent) { - if (ref.read(workspaceControllerProvider).workspace != - null) { - final notifier = ref.read( - workspaceSearchOpenRequestProvider.notifier, - ); - notifier.request(); - } - return null; - }, - ), _ToggleSidebarIntent: CallbackAction<_ToggleSidebarIntent>( onInvoke: (intent) { _toggleSidebar( @@ -488,6 +589,20 @@ class BusyMarkApp extends ConsumerWidget { final recentWorkspaces = ref .read(appSettingsControllerProvider) .recentWorkspaces; + final container = ProviderScope.containerOf(context, listen: false); + final cachedAccounts = container.exists(nextcloudNotesRepositoryProvider) + ? ref.read(nextcloudNotesRepositoryProvider).value?.accounts + : null; + final connectedAccount = container.exists(nextcloudConnectionProvider) + ? ref.read(nextcloudConnectionProvider).account + : null; + final accountLabels = { + for (final account in cachedAccounts ?? []) + account.id: '${account.loginName} · ${account.server.host}', + if (connectedAccount != null) + connectedAccount.id: + '${connectedAccount.loginName} · ${connectedAccount.server.host}', + }; final choice = await showBusyMarkModalDialog<_OpenChooserChoice>( context, headerBarService: headerBar.isAvailable ? headerBar : null, @@ -529,9 +644,27 @@ class BusyMarkApp extends ConsumerWidget { BusyMarkActionRow( title: busyMarkLtrIsolateFor( dialogContext, - _displayPath(recent.path), + recentResourceTitle(dialogContext, recent), ), - subtitle: busyMarkLtrIsolateFor(dialogContext, recent.path), + tooltip: recent.accountId == null + ? recent.path + : accountLabels[recent.accountId], + subtitleWidget: recent.accountId == null + ? BusyMarkPathLabel( + path: recentResourceLocation( + dialogContext, + recent, + recentResources: recentWorkspaces, + ), + style: Theme.of(dialogContext).textTheme.bodySmall, + ) + : Text( + recentResourceLocation( + dialogContext, + recent, + accountLabel: accountLabels[recent.accountId], + ), + ), leading: const Icon(BusyMarkGlyphs.history), trailing: Icon( BusyMarkGlyphs.forwardFor( @@ -540,7 +673,7 @@ class BusyMarkApp extends ConsumerWidget { ), onTap: () => Navigator.pop( dialogContext, - _OpenRecentWorkspace(recent.path), + _OpenRecentResource(recent), ), ), ], @@ -551,10 +684,14 @@ class BusyMarkApp extends ConsumerWidget { if (!context.mounted || choice == null) { return; } + if (choice case _OpenRecentResource(:final resource)) { + await openRecentResource(context, ref, resource); + return; + } final path = switch (choice) { _OpenMarkdownFile() => await _chooseMarkdownFile(ref), _OpenWorkspaceFolder() => await _chooseWorkspaceFolder(ref), - _OpenRecentWorkspace(:final path) => path, + _OpenRecentResource() => null, }; if (path == null || path.isEmpty || !context.mounted) { return; @@ -802,11 +939,6 @@ class BusyMarkApp extends ConsumerWidget { return p.extension(lastPath).isEmpty ? lastPath : p.dirname(lastPath); } - String _displayPath(String path) { - final name = p.basename(path); - return name.isEmpty ? path : name; - } - void _toggleSidebar(WidgetRef ref, {required bool allowWithoutWorkspace}) { final workspace = ref.read(workspaceControllerProvider).workspace; if (workspace == null && !allowWithoutWorkspace) { @@ -846,8 +978,8 @@ class BusyMarkApp extends ConsumerWidget { final textDirection = Directionality.maybeOf(context) ?? TextDirection.ltr; BusyMarkCommand command(String id) => commandRegistry[id]!; String label(String id) => command(id).label(context); - String shortcut(String id) => command(id).shortcut!.label; - String accelerator(String id) => command(id).shortcut!.gtkAccelerator!; + String shortcut(String id) => command(id).shortcut?.label ?? ''; + String accelerator(String id) => command(id).shortcut?.gtkAccelerator ?? ''; final labels = HeaderBarLabels( editor: label(BusyMarkCommandIds.viewEditor), source: label(BusyMarkCommandIds.viewSource), @@ -863,7 +995,7 @@ class BusyMarkApp extends ConsumerWidget { splitShortcut: shortcut(BusyMarkCommandIds.viewSplit), splitGtkAccelerator: accelerator(BusyMarkCommandIds.viewSplit), search: material.searchFieldLabel, - searchShortcut: shortcut(BusyMarkCommandIds.search), + searchShortcut: '', refresh: l10n.validate, menu: l10n.mainMenu, sidebar: settings.sidebarVisible ? l10n.hideSidebar : l10n.showSidebar, @@ -880,11 +1012,6 @@ class BusyMarkApp extends ConsumerWidget { settings: label(BusyMarkCommandIds.settings), settingsShortcut: shortcut(BusyMarkCommandIds.settings), settingsGtkAccelerator: accelerator(BusyMarkCommandIds.settings), - keyboardShortcuts: label(BusyMarkCommandIds.keyboardShortcuts), - keyboardShortcutsShortcut: shortcut(BusyMarkCommandIds.keyboardShortcuts), - keyboardShortcutsGtkAccelerator: accelerator( - BusyMarkCommandIds.keyboardShortcuts, - ), syntaxReference: label(BusyMarkCommandIds.syntaxReference), syntaxReferenceShortcut: shortcut(BusyMarkCommandIds.syntaxReference), syntaxReferenceGtkAccelerator: accelerator( @@ -988,13 +1115,20 @@ class _BusyMarkWindowLifecycleState if (!context.mounted) { return; } + final controller = ref.read(workspaceControllerProvider.notifier); + await controller.waitForWorkspaceOpening(); + if (!mounted || !context.mounted) return; + if (!await confirmNotesSettingsDeparture(context) || + !mounted || + !context.mounted) { + return; + } final settings = ref.read(appSettingsControllerProvider); var workspace = ref.read(workspaceControllerProvider); if (settings.autoSave && workspace.hasUnsavedChanges) { await ref.read(workspaceControllerProvider.notifier).saveAll(); workspace = ref.read(workspaceControllerProvider); } - final controller = ref.read(workspaceControllerProvider.notifier); await _windowControlService.handleCloseRequest( hasUnsavedChanges: workspace.hasUnsavedChanges, confirmCloseWithUnsavedChanges: settings.confirmCloseWithUnsavedChanges, @@ -1106,41 +1240,85 @@ class _BusyMarkSearchShortcutHandlerState } bool _handleKeyEvent(KeyEvent event) { + // Capture owns the event in FocusManager's early handler, including the + // release that ends suppression. Yield here so native key delivery can + // reach that handler instead of treating it as an application command. + if (BusyMarkShortcutCapture.suppressing) return false; if (event is! KeyDownEvent) { return false; } - final workspaceOpen = - ref.read(workspaceControllerProvider).workspace != null; - if (!workspaceOpen) { - return false; - } - final keyboard = HardwareKeyboard.instance; - final commands = - BusyMarkCommandRegistryScope.read(context) ?? - BusyMarkCommandCatalog.metadata; - if (commands.shortcutAccepts(BusyMarkCommandIds.search, event, keyboard)) { - if (rootNavigatorKey.currentState?.canPop() ?? false) { + if (rootNavigatorKey.currentState?.canPop() ?? false) return false; + final focused = FocusManager.instance.primaryFocus?.context; + if (focused != null) { + final editable = focused.findAncestorStateOfType(); + if (editable?.widget.controller.value.composing.isValid == true) { return false; } - ref.read(workspaceSearchOpenRequestProvider.notifier).request(); + } + if (event.logicalKey == LogicalKeyboardKey.escape && + ref.read(searchSessionProvider).active) { + ref.read(searchSessionProvider.notifier).close(); return true; } + final keyboard = HardwareKeyboard.instance; + // Search delivery precedes Flutter's focus hierarchy. Let the query's + // DefaultTextEditingShortcuts consume local editing before any reassigned + // application search command, just as it does for other application actions. + if (FocusManager.instance.primaryFocus is BusyMarkSearchFocusNode && + BusyMarkLocalTextEditingShortcuts.bindings.keys.any( + (a) => a.accepts(event, keyboard), + )) { + return false; + } + if (ref.read(searchSessionProvider.notifier).categories.isEmpty) { + return false; + } + final commands = ref.read(busyMarkCommandRegistryProvider); if (commands.shortcutAccepts( - BusyMarkCommandIds.textEscape, + BusyMarkCommandIds.sidebarOutline, event, keyboard, )) { - if (rootNavigatorKey.currentState?.canPop() ?? false) { - return false; + unawaited(commands.execute(BusyMarkCommandIds.sidebarOutline)); + return true; + } + for (final entry in searchCommandDestinations.entries) { + if (commands.shortcutAccepts(entry.key, event, keyboard)) { + unawaited(commands.execute(entry.key)); + return true; } - ref.read(workspaceSearchCloseRequestProvider.notifier).request(); - return false; } return false; } @override - Widget build(BuildContext context) => widget.child; + Widget build(BuildContext context) { + ref.listen( + headerBarSearchEventsProvider, + (_, next) => next.whenData((event) { + final controller = ref.read(searchSessionProvider.notifier); + if (!ref.read(searchSessionProvider).active && + (event is HeaderBarSearchQueryChanged || + event is HeaderBarSearchSubmitted)) { + controller.open(); + } + switch (event) { + case HeaderBarSearchQueryChanged(:final query): + controller.query(query); + case HeaderBarSearchSubmitted(:final query): + controller.query(query); + unawaited(activateSelectedSearchResult(context, ref)); + case HeaderBarSearchCleared(): + controller.query(''); + case HeaderBarSearchEscapePressed(): + controller.close(); + case HeaderBarSearchFocusChanged(): + break; + } + }), + ); + return widget.child; + } } class _NewWorkspaceIntent extends Intent { @@ -1163,10 +1341,10 @@ final class _OpenWorkspaceFolder extends _OpenChooserChoice { const _OpenWorkspaceFolder(); } -final class _OpenRecentWorkspace extends _OpenChooserChoice { - const _OpenRecentWorkspace(this.path); +final class _OpenRecentResource extends _OpenChooserChoice { + const _OpenRecentResource(this.resource); - final String path; + final RecentResource resource; } sealed class _NewChooserChoice { @@ -1201,10 +1379,6 @@ class _KeyboardShortcutsIntent extends Intent { const _KeyboardShortcutsIntent(); } -class _CommandPaletteIntent extends Intent { - const _CommandPaletteIntent(); -} - class _SettingsIntent extends Intent { const _SettingsIntent(); } @@ -1229,10 +1403,6 @@ class _CloseAllTabsIntent extends Intent { const _CloseAllTabsIntent(); } -class _OpenSearchIntent extends Intent { - const _OpenSearchIntent(); -} - class _OpenClipboardHistoryIntent extends Intent { const _OpenClipboardHistoryIntent(); } @@ -1241,10 +1411,6 @@ class _OpenLocalHistoryIntent extends Intent { const _OpenLocalHistoryIntent(); } -class _FindLocalHistoryIntent extends Intent { - const _FindLocalHistoryIntent(); -} - class _ToggleSidebarIntent extends Intent { const _ToggleSidebarIntent(); } @@ -1255,6 +1421,12 @@ class _DocumentViewModeIntent extends Intent { final DocumentViewModePreference mode; } -class _QuickOpenIntent extends Intent { - const _QuickOpenIntent(); +class _NotesActionIntent extends Intent { + const _NotesActionIntent(this.action); + final WelcomeNotesAction action; +} + +class _NotesWorkspaceActionIntent extends Intent { + const _NotesWorkspaceActionIntent(this.commandId); + final String commandId; } diff --git a/lib/src/app/busymark_design.dart b/lib/src/app/busymark_design.dart index 4cc14af5..d5365454 100644 --- a/lib/src/app/busymark_design.dart +++ b/lib/src/app/busymark_design.dart @@ -10,6 +10,8 @@ import 'package:yaru/yaru.dart'; import '../platform/native_menu_service.dart'; import '../platform/gtk_header_icon_service.dart'; import 'busymark_glyphs.dart'; +import 'path_label.dart'; +import 'localization.dart'; /// Resolve an explicit six-digit RGB color chosen in a document/export control. /// This does not inherit the application's theme accent. @@ -1507,6 +1509,7 @@ List? _busyMarkNativeMenuEntries( iconName: BusyMarkGlyphs.nativeMenuIconName(item.icon), iconColorArgb: item.iconColor?.toARGB32(), shortcut: item.shortcut, + gtkAccelerator: item.gtkAccelerator, enabled: item.enabled, checkable: item.trailingCheck, selected: item.trailingCheck && item.checked, @@ -1835,6 +1838,7 @@ class BusyMarkPopupMenuItem extends PopupMenuItem { IconData? icon, Color? iconColor, String? shortcut, + this.gtkAccelerator, super.enabled = true, bool checked = false, bool trailingCheck = false, @@ -1869,6 +1873,7 @@ class BusyMarkPopupMenuItem extends PopupMenuItem { final IconData? icon; final Color? iconColor; final String? shortcut; + final String? gtkAccelerator; final bool checked; final bool trailingCheck; final bool mutuallyExclusive; @@ -1971,6 +1976,7 @@ class BusyMarkMenuButton extends StatefulWidget { this.enabled = true, this.nativeMenuService = const NativeMenuService(), this.fallbackMenuWidth, + this.selectionForSession, }); final String tooltip; @@ -1981,6 +1987,10 @@ class BusyMarkMenuButton extends StatefulWidget { final NativeMenuService nativeMenuService; final double? fallbackMenuWidth; + /// Captures the reviewed targets once, for every input path that opens a menu. + /// The returned callback belongs to this opening, even if the trigger rebuilds. + final ValueChanged Function()? selectionForSession; + @override State> createState() => _BusyMarkMenuButtonState(); } @@ -2071,7 +2081,7 @@ class _BusyMarkMenuButtonState extends State> { return; } final items = List>.unmodifiable(widget.items); - final onSelected = widget.onSelected; + final onSelected = widget.selectionForSession?.call() ?? widget.onSelected; final session = BusyMarkMenuSession(); _activeMenuSession = session; setState(() => _open = true); @@ -2132,12 +2142,16 @@ class BusyMarkPopupSelectorOption { required this.label, this.icon, this.iconColor, + this.shortcut, + this.gtkAccelerator, }); final T value; final String label; final IconData? icon; final Color? iconColor; + final String? shortcut; + final String? gtkAccelerator; } /// The shared desktop selector used by Settings-style control rows. @@ -2155,7 +2169,7 @@ class BusyMarkPopupSelector extends StatelessWidget { this.buttonMaxWidth = BusyMarkSizes.languageButtonMaxWidth, this.leading, this.fullWidth = false, - this.contentPadding, + this.maxLabelLines = 1, }); final T? value; @@ -2169,10 +2183,12 @@ class BusyMarkPopupSelector extends StatelessWidget { final double buttonMaxWidth; final Widget? leading; final bool fullWidth; - final EdgeInsetsGeometry? contentPadding; - + final int maxLabelLines; @override Widget build(BuildContext context) { + // Inherit borderless button geometry and interaction states while keeping + // the neutral control foreground rather than an action-link accent. + final neutralButtonStyle = Theme.of(context).outlinedButtonTheme.style; final selectorEnabled = enabled && options.isNotEmpty; final fallbackMenuWidth = (fullWidth ? popupMaxWidth : buttonMaxWidth) .clamp(popupMinWidth, popupMaxWidth); @@ -2194,6 +2210,8 @@ class BusyMarkPopupSelector extends StatelessWidget { label: option.label, icon: option.icon, iconColor: option.iconColor, + shortcut: option.shortcut, + gtkAccelerator: option.gtkAccelerator, checked: option.value == value, trailingCheck: true, ), @@ -2204,16 +2222,13 @@ class BusyMarkPopupSelector extends StatelessWidget { message: tooltip, child: Semantics( expanded: trigger.isOpen, - child: BusyMarkPushButton.standard( + child: TextButton( + style: ButtonStyle( + foregroundColor: neutralButtonStyle?.foregroundColor, + iconColor: neutralButtonStyle?.iconColor, + ), onPressed: trigger.onPressed, focusNode: trigger.focusNode, - style: Theme.of(context).outlinedButtonTheme.style - ?.copyWith( - side: const WidgetStatePropertyAll(BorderSide.none), - padding: contentPadding == null - ? null - : WidgetStatePropertyAll(contentPadding), - ), child: Row( mainAxisSize: fullWidth ? MainAxisSize.max @@ -2227,9 +2242,9 @@ class BusyMarkPopupSelector extends StatelessWidget { Expanded( child: Text( label, - maxLines: 1, + maxLines: maxLabelLines, overflow: TextOverflow.ellipsis, - softWrap: false, + softWrap: maxLabelLines > 1, ), ) else @@ -2245,9 +2260,9 @@ class BusyMarkPopupSelector extends StatelessWidget { ), child: Text( label, - maxLines: 1, + maxLines: maxLabelLines, overflow: TextOverflow.ellipsis, - softWrap: false, + softWrap: maxLabelLines > 1, ), ), ), @@ -2767,8 +2782,14 @@ class BusyMarkSidebarRecordRow extends StatefulWidget { required this.onTap, this.title, this.subtitle, + this.pathSubtitle = false, this.content, this.selected = false, + this.compact = false, + this.focusNode, + this.geometryKey, + this.onFocusChange, + this.trailing, this.onDoubleTap, this.semanticsLabel, this.tooltip, @@ -2776,12 +2797,14 @@ class BusyMarkSidebarRecordRow extends StatefulWidget { this.menuItemsBuilder, this.onMenuSelected, this.onMenuOpening, + this.menuSelectionForSession, }) : assert( (title == null) != (content == null), 'Supply either the standard title or custom content.', ), assert( - (menuItemsBuilder == null) == (onMenuSelected == null), + (menuItemsBuilder == null) == + (onMenuSelected == null && menuSelectionForSession == null), 'Menu items and selection handling must be supplied together.', ), assert( @@ -2792,9 +2815,16 @@ class BusyMarkSidebarRecordRow extends StatefulWidget { final IconData icon; final String? title; final String? subtitle; + final bool pathSubtitle; final Widget? content; - final bool selected; - final VoidCallback onTap; + final bool selected, compact; + final FocusNode? focusNode; + + /// Optional measured anchor; the public row key remains its resource identity. + final GlobalKey? geometryKey; + final ValueChanged? onFocusChange; + final Widget? trailing; + final VoidCallback? onTap; final VoidCallback? onDoubleTap; final String? semanticsLabel; final String? tooltip; @@ -2803,6 +2833,9 @@ class BusyMarkSidebarRecordRow extends StatefulWidget { final ValueChanged? onMenuSelected; final VoidCallback? onMenuOpening; + /// Freezes this opening's targets independently of later row rebuilds. + final ValueChanged Function()? menuSelectionForSession; + @override State> createState() => _BusyMarkSidebarRecordRowState(); @@ -2816,7 +2849,8 @@ class _BusyMarkSidebarRecordRowState DateTime? _lastTapAt; bool get _hasMenu => - widget.menuItemsBuilder != null && widget.onMenuSelected != null; + widget.menuItemsBuilder != null && + (widget.onMenuSelected != null || widget.menuSelectionForSession != null); @override void initState() { @@ -2841,7 +2875,7 @@ class _BusyMarkSidebarRecordRowState void _handleTap() { final onDoubleTap = widget.onDoubleTap; if (onDoubleTap == null) { - widget.onTap(); + widget.onTap?.call(); return; } final now = DateTime.now(); @@ -2849,7 +2883,7 @@ class _BusyMarkSidebarRecordRowState final doubleTap = previous != null && now.difference(previous) <= kDoubleTapTimeout; _lastTapAt = doubleTap ? null : now; - widget.onTap(); + widget.onTap?.call(); if (doubleTap) onDoubleTap(); } @@ -2857,12 +2891,15 @@ class _BusyMarkSidebarRecordRowState if (!_hasMenu || _contextMenuOpen) return; final rowContext = _rowKey.currentContext; if (rowContext == null) return; + final items = widget.menuItemsBuilder!(rowContext); + if (items.isEmpty) return; widget.onMenuOpening?.call(); + final onSelected = + widget.menuSelectionForSession?.call() ?? widget.onMenuSelected!; setState(() => _contextMenuOpen = true); T? action; try { - final items = widget.menuItemsBuilder!(rowContext); action = position == null ? await showBusyMarkMenu( context: rowContext, @@ -2878,7 +2915,7 @@ class _BusyMarkSidebarRecordRowState } finally { if (mounted) setState(() => _contextMenuOpen = false); } - if (mounted && action != null) widget.onMenuSelected!(action); + if (mounted && action != null) onSelected(action); } @override @@ -2888,79 +2925,100 @@ class _BusyMarkSidebarRecordRowState final customContent = widget.content; final menuItems = widget.menuItemsBuilder?.call(context); final trailing = menuItems == null || menuItems.isEmpty - ? null + ? widget.trailing : BusyMarkMenuButton( tooltip: widget.menuTooltip!, items: menuItems, - onSelected: widget.onMenuSelected!, + onSelected: widget.onMenuSelected ?? (_) {}, + selectionForSession: () { + widget.onMenuOpening?.call(); + return widget.menuSelectionForSession?.call() ?? + widget.onMenuSelected!; + }, triggerBuilder: (context, trigger) => trigger.anchor( child: BusyMarkCompactIconButton( tooltip: widget.menuTooltip!, icon: BusyMarkGlyphs.menuVertical, focusNode: trigger.focusNode, - onPressed: trigger.onPressed == null - ? null - : () { - widget.onMenuOpening?.call(); - trigger.onPressed!(); - }, + onPressed: trigger.onPressed, ), ), ); - final content = ConstrainedBox( - constraints: BoxConstraints( - minHeight: BusyMarkSizes.sidebarTreeRowHeight * 2, - ), - child: Row( - children: [ - const SizedBox(width: BusyMarkSpacing.sm), - Icon( - widget.icon, - size: BusyMarkSizes.iconSm, - color: colors.mutedForeground, - ), - const SizedBox(width: BusyMarkSpacing.sm), - Expanded( - child: Padding( - padding: customContent == null - ? EdgeInsets.zero - : const EdgeInsets.symmetric(vertical: BusyMarkSpacing.sm), - child: - customContent ?? - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.title!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colors.foreground, - ), - ), - if (subtitle != null && subtitle.isNotEmpty) ...[ - const SizedBox(height: BusyMarkSpacing.xxs), - Text( - subtitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: colors.mutedForeground), - ), - ], - ], + final customRow = customContent == null + ? null + : ConstrainedBox( + constraints: BoxConstraints( + minHeight: widget.compact + ? BusyMarkSizes.sidebarTreeRowHeight + : BusyMarkSizes.sidebarTreeRowHeight * 2, + ), + child: Row( + children: [ + const SizedBox(width: BusyMarkSpacing.sm), + Icon( + widget.icon, + size: BusyMarkSizes.iconSm, + color: colors.mutedForeground, + ), + const SizedBox(width: BusyMarkSpacing.sm), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + vertical: widget.compact + ? BusyMarkSpacing.xs + : BusyMarkSpacing.sm, + ), + child: customContent, ), + ), + if (trailing != null) ...[ + const SizedBox(width: BusyMarkSpacing.xs), + trailing, + ], + const SizedBox(width: BusyMarkSpacing.sm), + ], ), - ), - if (trailing != null) ...[ - const SizedBox(width: BusyMarkSpacing.xs), - trailing, - ], - const SizedBox(width: BusyMarkSpacing.sm), - ], - ), - ); + ); + // Standard identities use the same native row layout as other BusyMark + // lists. Yaru owns their typography, content spacing and minimum extent. + final tile = customRow == null + ? YaruListTile.square( + leading: Icon( + widget.icon, + size: BusyMarkSizes.iconSm, + color: colors.mutedForeground, + ), + title: Text( + widget.title!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: subtitle == null || subtitle.isEmpty + ? null + : _busyMarkGroupedRowSubtitle( + context, + widget.pathSubtitle + ? BusyMarkPathLabel(path: subtitle) + : Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + trailing: trailing, + focusNode: widget.focusNode ?? _rowFocusNode, + onFocusChange: widget.onFocusChange, + hoverColor: busyMarkRowHoverColor(context), + onTap: widget.onTap == null ? null : _handleTap, + ) + : InkWell( + hoverColor: busyMarkRowHoverColor(context), + focusNode: widget.focusNode ?? _rowFocusNode, + onFocusChange: widget.onFocusChange, + canRequestFocus: true, + onTap: widget.onTap == null ? null : _handleTap, + child: customRow, + ); final row = KeyedSubtree( key: _rowKey, child: Focus( @@ -2975,17 +3033,14 @@ class _BusyMarkSidebarRecordRowState : BusyMarkLinuxPalette.transparent, borderRadius: BorderRadius.circular(BusyMarkRadius.md), clipBehavior: Clip.antiAlias, - child: InkWell( - hoverColor: busyMarkRowHoverColor(context), - focusNode: _rowFocusNode, - onTap: _handleTap, + child: GestureDetector( onSecondaryTapUp: !_hasMenu ? null : (details) { - _rowFocusNode.requestFocus(); + (widget.focusNode ?? _rowFocusNode).requestFocus(); unawaited(_showContextMenu(details.globalPosition)); }, - child: content, + child: tile, ), ), ), @@ -2996,17 +3051,21 @@ class _BusyMarkSidebarRecordRowState selected: widget.selected, button: true, label: widget.semanticsLabel, - onTap: _handleTap, + enabled: widget.onTap != null, + onTap: widget.onTap == null ? null : _handleTap, child: row, ); final tooltip = widget.tooltip; - return tooltip == null || tooltip.isEmpty + final presented = tooltip == null || tooltip.isEmpty ? semanticRow : Tooltip( message: tooltip, excludeFromSemantics: true, child: semanticRow, ); + return widget.geometryKey == null + ? presented + : KeyedSubtree(key: widget.geometryKey, child: presented); } } @@ -3138,7 +3197,10 @@ class BusyMarkGroupedList extends StatelessWidget { if (title != null) ...[ Padding( padding: padding, - child: Text(title!, style: busyMarkSectionHeaderStyle(context)), + child: Semantics( + header: true, + child: Text(title!, style: busyMarkSectionHeaderStyle(context)), + ), ), if (description != null && description!.isNotEmpty) ...[ const SizedBox(height: BusyMarkSpacing.xs), @@ -4470,3 +4532,334 @@ class SectionLabel extends StatelessWidget { ); } } + +typedef BusyMarkSidebarTreeMenuRequest = + void Function(BuildContext anchorContext, Offset globalPosition); + +class BusyMarkSidebarRowSurface extends StatelessWidget { + const BusyMarkSidebarRowSurface({ + super.key, + required this.child, + required this.enabled, + required this.clickable, + this.selected = false, + this.focusNode, + this.onKeyEvent, + this.onTap, + this.onDoubleTap, + this.onSecondaryTapUp, + this.onHoverChanged, + this.onFocusChange, + }); + + final Widget child; + final bool enabled; + final bool clickable; + final bool selected; + final FocusNode? focusNode; + final FocusOnKeyEventCallback? onKeyEvent; + final VoidCallback? onTap; + final VoidCallback? onDoubleTap; + final GestureTapUpCallback? onSecondaryTapUp; + final ValueChanged? onHoverChanged; + final ValueChanged? onFocusChange; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: onHoverChanged == null ? null : (_) => onHoverChanged!(true), + onExit: onHoverChanged == null ? null : (_) => onHoverChanged!(false), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: BusyMarkStroke.hairline), + child: Material( + color: selected + ? busyMarkSelectedBackground(context) + : BusyMarkLinuxPalette.transparent, + borderRadius: BorderRadius.circular(BusyMarkRadius.md), + clipBehavior: Clip.antiAlias, + child: Focus( + onKeyEvent: onKeyEvent, + child: InkWell( + focusNode: focusNode, + hoverColor: clickable + ? busyMarkRowHoverColor(context) + : BusyMarkLinuxPalette.transparent, + onTap: enabled ? onTap : null, + onDoubleTap: enabled ? onDoubleTap : null, + onSecondaryTapUp: enabled ? onSecondaryTapUp : null, + onFocusChange: onFocusChange, + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: BusyMarkSizes.sidebarTreeRowHeight, + ), + child: child, + ), + ), + ), + ), + ), + ); + } +} + +class BusyMarkSidebarTreeRow extends StatefulWidget { + const BusyMarkSidebarTreeRow({ + super.key, + required this.title, + required this.depth, + this.icon, + this.leading, + this.trailing, + this.tooltip, + this.focusNode, + this.traverseWithArrows = false, + this.onFocusChange, + this.hasChildren = false, + this.onNavigationKeyEvent, + this.expanded = false, + this.selected = false, + this.enabled = true, + this.muted = false, + this.compactHierarchyIndent = false, + this.vcsColor, + this.onToggle, + this.onTap, + this.onDoubleTap, + this.onMenuRequested, + }); + + final String title; + final int depth; + final IconData? icon; + final Widget? leading; + final Widget? trailing; + final String? tooltip; + final FocusNode? focusNode; + final bool traverseWithArrows; + final ValueChanged? onFocusChange; + + /// Owners can keep selection and reveal policy instead of generic traversal. + final FocusOnKeyEventCallback? onNavigationKeyEvent; + final bool hasChildren; + final bool expanded; + final bool selected; + final bool enabled; + final bool muted; + final bool compactHierarchyIndent; + final BusyMarkVcsFileColor? vcsColor; + final VoidCallback? onToggle; + final VoidCallback? onTap; + final VoidCallback? onDoubleTap; + final BusyMarkSidebarTreeMenuRequest? onMenuRequested; + + @override + State createState() => BusyMarkSidebarTreeRowState(); +} + +class BusyMarkSidebarTreeRowState extends State { + var _hovered = false; + var _focused = false; + + void showMenuFromKeyboard() { + final onMenuRequested = widget.onMenuRequested; + if (!widget.enabled || onMenuRequested == null) { + return; + } + onMenuRequested(context, _busyMarkSidebarTreeMenuAnchor(context)); + } + + KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { + final navigation = widget.enabled + ? widget.onNavigationKeyEvent?.call(node, event) + : null; + if (navigation != null && navigation != KeyEventResult.ignored) { + return navigation; + } + if (widget.enabled && event is KeyDownEvent) { + final rtl = Directionality.of(context) == TextDirection.rtl; + final expand = rtl + ? LogicalKeyboardKey.arrowLeft + : LogicalKeyboardKey.arrowRight; + final collapse = rtl + ? LogicalKeyboardKey.arrowRight + : LogicalKeyboardKey.arrowLeft; + if (widget.hasChildren && + ((event.logicalKey == expand && !widget.expanded) || + (event.logicalKey == collapse && widget.expanded))) { + widget.onToggle?.call(); + return KeyEventResult.handled; + } + if (widget.traverseWithArrows && + event.logicalKey == LogicalKeyboardKey.arrowDown) { + node.nextFocus(); + return KeyEventResult.handled; + } + if (widget.traverseWithArrows && + event.logicalKey == LogicalKeyboardKey.arrowUp) { + node.previousFocus(); + return KeyEventResult.handled; + } + } + if (!widget.enabled || + widget.onMenuRequested == null || + !isBusyMarkContextMenuKeyEvent(event)) { + return KeyEventResult.ignored; + } + showMenuFromKeyboard(); + return KeyEventResult.handled; + } + + @override + Widget build(BuildContext context) { + final colors = BusyMarkSurfaceColors.of(context); + final direction = Directionality.of(context); + final clickable = + widget.enabled && + (widget.onTap != null || widget.onMenuRequested != null); + final vcsForeground = widget.vcsColor == null + ? null + : busyMarkVcsFileStatusColor(context, widget.vcsColor!); + final foreground = !widget.enabled || widget.muted + ? colors.disabledForeground + : vcsForeground ?? + (widget.selected ? colors.foreground : colors.mutedForeground); + final titleColor = !widget.enabled || widget.muted + ? colors.disabledForeground + : vcsForeground ?? colors.foreground; + final titleStyle = Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: titleColor); + final menuVisible = + widget.enabled && + widget.onMenuRequested != null && + (widget.selected || _hovered || _focused); + final depthBase = widget.compactHierarchyIndent + ? 0.0 + : BusyMarkSizes.sidebarTreeDepthBase; + final expanderWidth = widget.compactHierarchyIndent + ? BusyMarkSizes.sidebarTreeArrow + : BusyMarkSizes.sidebarTreeControl; + final expanderGap = widget.compactHierarchyIndent + ? 0.0 + : BusyMarkSpacing.xs; + final surface = BusyMarkSidebarRowSurface( + focusNode: widget.focusNode, + enabled: widget.enabled, + clickable: clickable, + selected: widget.selected, + onKeyEvent: _handleKeyEvent, + onTap: widget.onTap, + onDoubleTap: widget.onDoubleTap, + onSecondaryTapUp: widget.onMenuRequested == null + ? null + : (details) => + widget.onMenuRequested!(context, details.globalPosition), + onHoverChanged: (hovered) => setState(() => _hovered = hovered), + onFocusChange: (focused) { + setState(() => _focused = focused); + widget.onFocusChange?.call(focused); + }, + child: Row( + children: [ + SizedBox( + width: + depthBase + widget.depth * BusyMarkSizes.sidebarTreeDepthIndent, + ), + SizedBox.square( + dimension: expanderWidth, + child: widget.hasChildren + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.enabled + ? widget.onToggle ?? widget.onTap + : null, + child: AnimatedRotation( + turns: widget.expanded + ? direction == TextDirection.rtl + ? -0.25 + : 0.25 + : 0, + duration: MediaQuery.disableAnimationsOf(context) + ? Duration.zero + : BusyMarkMotion.sidebarExpand, + child: Icon( + BusyMarkGlyphs.collapsedTreeArrowFor(direction), + size: BusyMarkSizes.sidebarTreeArrow, + color: foreground, + ), + ), + ) + : const SizedBox.shrink(), + ), + SizedBox(width: expanderGap), + if (widget.leading != null || widget.icon != null) ...[ + SizedBox.square( + dimension: BusyMarkSizes.sidebarTreeControl, + child: Center( + child: + widget.leading ?? + Icon( + widget.icon, + size: BusyMarkSizes.iconSm, + color: foreground, + ), + ), + ), + const SizedBox(width: BusyMarkSpacing.sm), + ], + Expanded( + child: Text( + widget.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: titleStyle, + ), + ), + if (widget.trailing != null) widget.trailing!, + if (widget.onMenuRequested != null) + SizedBox( + width: BusyMarkSizes.iconButton, + child: Center( + child: menuVisible + ? Builder( + builder: (buttonContext) => BusyMarkCompactIconButton( + tooltip: context.l10n.actions, + icon: BusyMarkGlyphs.menuVertical, + onPressed: () => widget.onMenuRequested!( + buttonContext, + _busyMarkSidebarTreeMenuAnchor(buttonContext), + ), + ), + ) + : const SizedBox.shrink(), + ), + ) + else + const SizedBox(width: BusyMarkSpacing.xs), + ], + ), + ); + return Semantics( + selected: widget.selected, + expanded: widget.hasChildren ? widget.expanded : null, + enabled: widget.enabled, + child: widget.tooltip == null || widget.tooltip!.isEmpty + ? surface + : Tooltip(message: widget.tooltip!, child: surface), + ); + } +} + +Offset _busyMarkSidebarTreeMenuAnchor(BuildContext context) { + final renderObject = context.findRenderObject(); + if (renderObject is! RenderBox || !renderObject.hasSize) { + return Offset.zero; + } + final direction = Directionality.of(context); + return renderObject.localToGlobal( + Offset( + direction == TextDirection.rtl ? 0 : renderObject.size.width, + renderObject.size.height, + ), + ); +} diff --git a/lib/src/app/busymark_dialogs.dart b/lib/src/app/busymark_dialogs.dart index 5a22af42..854b7e16 100644 --- a/lib/src/app/busymark_dialogs.dart +++ b/lib/src/app/busymark_dialogs.dart @@ -1,6 +1,8 @@ +import 'command_registry.dart'; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -9,8 +11,6 @@ import '../../l10n/generated/app_localizations.dart'; import '../platform/linux_header_bar_service.dart'; import 'app_metadata.dart'; import 'busymark_dialog_identity.dart'; -import 'busymark_shortcuts.dart'; -import 'command_registry.dart'; import 'busymark_design.dart'; import 'busymark_glyphs.dart'; import 'localization.dart'; @@ -77,19 +77,6 @@ class _DismissBusyMarkModalIntent extends Intent { const _DismissBusyMarkModalIntent(); } -final _busyMarkModalShortcuts = { - for (final command in BusyMarkCommandCatalog.metadata.commands) - if (command.shortcut != null && - command.scope != BusyMarkCommandScope.tree && - command.scope != BusyMarkCommandScope.textEditing) - command.shortcut!.activator: const DoNothingAndStopPropagationIntent(), - BusyMarkCommandCatalog - .metadata[BusyMarkCommandIds.textEscape]! - .shortcut! - .activator: - const _DismissBusyMarkModalIntent(), -}; - /// Prevents application and workspace shortcuts from escaping a modal surface. /// /// Use this around modal UI that is not presented by @@ -146,7 +133,23 @@ class _BusyMarkModalShortcutBoundaryState @override Widget build(BuildContext context) { return Shortcuts( - shortcuts: _busyMarkModalShortcuts, + shortcuts: { + ...(BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .shortcutIntents( + scopes: const { + BusyMarkCommandScope.application, + BusyMarkCommandScope.documentView, + BusyMarkCommandScope.editor, + BusyMarkCommandScope.wysiwyg, + BusyMarkCommandScope.sidebar, + BusyMarkCommandScope.sourceSymbol, + }, + intentFor: (_) => const DoNothingAndStopPropagationIntent(), + ), + const SingleActivator(LogicalKeyboardKey.escape): + const _DismissBusyMarkModalIntent(), + }, child: Actions( actions: >{ _DismissBusyMarkModalIntent: @@ -161,7 +164,7 @@ class _BusyMarkModalShortcutBoundaryState focusNode: _fallbackFocusNode, skipTraversal: true, includeSemantics: false, - child: widget.child, + child: DefaultTextEditingShortcuts(child: widget.child), ), ), ); @@ -457,591 +460,6 @@ Future _openApacheLicense() async { await launchUrl(_apacheLicenseUri, mode: LaunchMode.externalApplication); } -void showBusyMarkKeyboardShortcutsDialog(BuildContext context) { - final registry = - BusyMarkCommandRegistryScope.maybeOf(context) ?? - BusyMarkCommandCatalog.metadata; - final headerBar = LinuxHeaderBarService.instance; - unawaited( - showBusyMarkModalDialog( - context, - headerBarService: headerBar.isAvailable ? headerBar : null, - builder: (context) { - final grouped = >{}; - final shortcutLabels = {}; - for (final command in registry.commands) { - final shortcut = command.shortcut; - if (shortcut == null || !shortcutLabels.add(shortcut.label)) { - continue; - } - grouped.putIfAbsent(command.category(context), () => []).add(command); - } - return _BusyMarkInfoDialog( - title: context.l10n.keyboardShortcuts, - icon: BusyMarkGlyphs.keyboard, - maxWidth: 460, - children: [ - for (final entry in grouped.entries) - BusyMarkGroupedList( - title: entry.key, - filled: true, - children: [ - for (final command in entry.value) - BusyMarkActionRow( - title: command.label(context), - subtitle: command.description?.call(context), - leading: const Icon(BusyMarkGlyphs.keyboard), - trailing: _KeyboardShortcutBadge(command.shortcut!.label), - ), - ], - ), - ], - ); - }, - ), - ); -} - -// Retained temporarily as a layout reference while every shortcut consumer is -// migrated to the registry-backed presentation above. -void showLegacyBusyMarkKeyboardShortcutsDialog(BuildContext context) { - final headerBar = LinuxHeaderBarService.instance; - unawaited( - showBusyMarkModalDialog( - context, - headerBarService: headerBar.isAvailable ? headerBar : null, - builder: (context) => _BusyMarkInfoDialog( - title: context.l10n.keyboardShortcuts, - icon: BusyMarkGlyphs.keyboard, - maxWidth: 460, - children: [ - BusyMarkGroupedList( - title: context.l10n.shortcutGroupGeneral, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.shortcutNewDocument, - subtitle: context.l10n.shortcutNewDocumentDescription, - leading: const Icon(BusyMarkGlyphs.newDocument), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.newDocument, - ), - ), - BusyMarkActionRow( - title: context.l10n.open, - subtitle: context.l10n.shortcutOpenDescription, - leading: const Icon(BusyMarkGlyphs.folderOpen), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.open, - ), - ), - BusyMarkActionRow( - title: context.l10n.save, - subtitle: context.l10n.shortcutSaveDescription, - leading: const Icon(BusyMarkGlyphs.save), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.save, - ), - ), - BusyMarkActionRow( - title: context.l10n.export, - leading: const Icon(BusyMarkGlyphs.exportPdf), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.export, - ), - ), - BusyMarkActionRow( - title: context.l10n.fullScreen, - leading: const Icon(BusyMarkGlyphs.fullScreen), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.fullScreen, - ), - ), - BusyMarkActionRow( - title: context.l10n.back, - leading: Icon( - BusyMarkGlyphs.backFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.back, - ), - ), - BusyMarkActionRow( - title: context.l10n.search, - subtitle: context.l10n.shortcutSearchDescription, - leading: const Icon(BusyMarkGlyphs.search), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.search, - ), - ), - BusyMarkActionRow( - title: context.l10n.keyboardShortcuts, - subtitle: context.l10n.shortcutKeyboardShortcutsDescription, - leading: const Icon(BusyMarkGlyphs.keyboard), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.keyboardShortcuts, - ), - ), - BusyMarkActionRow( - title: context.l10n.syntaxReference, - subtitle: context.l10n.shortcutSyntaxReferenceDescription, - leading: const Icon(BusyMarkGlyphs.markdownFile), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.syntaxReference, - ), - ), - BusyMarkActionRow( - title: context.l10n.settings, - subtitle: context.l10n.shortcutSettingsDescription, - leading: const Icon(BusyMarkGlyphs.settings), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.settings, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.tabs, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.shortcutNextTab, - subtitle: context.l10n.shortcutNextTabDescription, - leading: const Icon(BusyMarkGlyphs.tab), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.nextTab, - ), - ), - BusyMarkActionRow( - title: context.l10n.shortcutPreviousTab, - subtitle: context.l10n.shortcutPreviousTabDescription, - leading: const Icon(BusyMarkGlyphs.tab), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.previousTab, - ), - ), - BusyMarkActionRow( - title: context.l10n.shortcutCloseTab, - subtitle: context.l10n.shortcutCloseTabDescription, - leading: const Icon(BusyMarkGlyphs.clear), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.closeTab, - ), - ), - BusyMarkActionRow( - title: context.l10n.shortcutCloseAllTabs, - subtitle: context.l10n.shortcutCloseAllTabsDescription, - leading: const Icon(BusyMarkGlyphs.clearAll), - trailing: const _KeyboardShortcutBadge( - BusyMarkAppShortcutLabels.closeAllTabs, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.viewMode, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.editor, - leading: const Icon(BusyMarkGlyphs.editorView), - trailing: const _KeyboardShortcutBadge( - BusyMarkDocumentViewShortcutLabels.editor, - ), - ), - BusyMarkActionRow( - title: context.l10n.source, - leading: const Icon(BusyMarkGlyphs.sourceView), - trailing: const _KeyboardShortcutBadge( - BusyMarkDocumentViewShortcutLabels.source, - ), - ), - BusyMarkActionRow( - title: context.l10n.reading, - leading: const Icon(BusyMarkGlyphs.previewView), - trailing: const _KeyboardShortcutBadge( - BusyMarkDocumentViewShortcutLabels.reading, - ), - ), - BusyMarkActionRow( - title: context.l10n.split, - leading: const Icon(BusyMarkGlyphs.splitView), - trailing: const _KeyboardShortcutBadge( - BusyMarkDocumentViewShortcutLabels.split, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.shortcutGroupTextEditing, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.selectAll, - subtitle: context.l10n.shortcutSelectAllDescription, - leading: const Icon(BusyMarkGlyphs.selectAll), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.selectAll, - ), - ), - BusyMarkActionRow( - title: context.l10n.cut, - subtitle: context.l10n.shortcutCutDescription, - leading: const Icon(BusyMarkGlyphs.cut), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.cut, - ), - ), - BusyMarkActionRow( - title: context.l10n.copy, - subtitle: context.l10n.shortcutCopyDescription, - leading: const Icon(BusyMarkGlyphs.copy), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.copy, - ), - ), - BusyMarkActionRow( - title: context.l10n.paste, - subtitle: context.l10n.shortcutPasteDescription, - leading: const Icon(BusyMarkGlyphs.paste), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.paste, - ), - ), - BusyMarkActionRow( - title: context.l10n.clipboardPastePlainText, - subtitle: context.l10n.shortcutPastePlainTextDescription, - leading: const Icon(BusyMarkGlyphs.paste), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.pastePlainText, - ), - ), - BusyMarkActionRow( - title: context.l10n.undo, - subtitle: context.l10n.shortcutUndoDescription, - leading: Icon( - BusyMarkGlyphs.undoFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.undo, - ), - ), - BusyMarkActionRow( - title: context.l10n.redo, - subtitle: context.l10n.shortcutRedoDescription, - leading: Icon( - BusyMarkGlyphs.redoFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.redo, - ), - ), - BusyMarkActionRow( - title: context.l10n.shortcutInsertIndentation, - subtitle: context.l10n.shortcutInsertIndentationDescription, - leading: Icon( - BusyMarkGlyphs.indentFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.insertIndentation, - ), - ), - BusyMarkActionRow( - title: context.l10n.shortcutOutdentSource, - subtitle: context.l10n.shortcutOutdentSourceDescription, - leading: Icon( - BusyMarkGlyphs.outdentFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.outdentSource, - ), - ), - BusyMarkActionRow( - title: context.l10n.shortcutEscape, - subtitle: context.l10n.shortcutEscapeDescription, - leading: const Icon(BusyMarkGlyphs.clear), - trailing: const _KeyboardShortcutBadge( - BusyMarkTextEditingShortcutLabels.escape, - ), - ), - BusyMarkActionRow( - title: context.l10n.aiRefineWithAi, - leading: const Icon(BusyMarkGlyphs.ai), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.refineWithAi, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.shortcutGroupFormatting, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.bold, - subtitle: context.l10n.shortcutBoldDescription, - leading: const Icon(BusyMarkGlyphs.bold), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.bold, - ), - ), - BusyMarkActionRow( - title: context.l10n.italic, - subtitle: context.l10n.shortcutItalicDescription, - leading: const Icon(BusyMarkGlyphs.italic), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.italic, - ), - ), - BusyMarkActionRow( - title: context.l10n.underline, - subtitle: context.l10n.shortcutUnderlineDescription, - leading: const Icon(BusyMarkGlyphs.underline), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.underline, - ), - ), - BusyMarkActionRow( - title: context.l10n.link, - subtitle: context.l10n.shortcutLinkDescription, - leading: const Icon(BusyMarkGlyphs.link), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.link, - ), - ), - BusyMarkActionRow( - title: context.l10n.inlineCode, - subtitle: context.l10n.shortcutInlineCodeDescription, - leading: const Icon(BusyMarkGlyphs.code), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.inlineCode, - ), - ), - BusyMarkActionRow( - title: context.l10n.strikethrough, - subtitle: context.l10n.shortcutStrikethroughDescription, - leading: const Icon(BusyMarkGlyphs.strikethrough), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.strikethrough, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.shortcutGroupBlocks, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.paragraph, - subtitle: context.l10n.shortcutParagraphDescription, - leading: const Icon(BusyMarkGlyphs.paragraph), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.paragraph, - ), - ), - BusyMarkActionRow( - title: context.l10n.heading1, - subtitle: context.l10n.shortcutHeading1Description, - leading: const Icon(BusyMarkGlyphs.heading), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.heading1, - ), - ), - BusyMarkActionRow( - title: context.l10n.heading2, - subtitle: context.l10n.shortcutHeading2Description, - leading: const Icon(BusyMarkGlyphs.heading), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.heading2, - ), - ), - BusyMarkActionRow( - title: context.l10n.heading3, - subtitle: context.l10n.shortcutHeading3Description, - leading: const Icon(BusyMarkGlyphs.heading), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.heading3, - ), - ), - BusyMarkActionRow( - title: context.l10n.heading4, - subtitle: context.l10n.shortcutHeading4Description, - leading: const Icon(BusyMarkGlyphs.heading), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.heading4, - ), - ), - BusyMarkActionRow( - title: context.l10n.heading5, - subtitle: context.l10n.shortcutHeading5Description, - leading: const Icon(BusyMarkGlyphs.heading), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.heading5, - ), - ), - BusyMarkActionRow( - title: context.l10n.heading6, - subtitle: context.l10n.shortcutHeading6Description, - leading: const Icon(BusyMarkGlyphs.heading), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.heading6, - ), - ), - BusyMarkActionRow( - title: context.l10n.blockquote, - leading: const Icon(BusyMarkGlyphs.blockquote), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.blockquote, - ), - ), - BusyMarkActionRow( - title: context.l10n.codeBlock, - leading: const Icon(BusyMarkGlyphs.code), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.codeBlock, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.shortcutGroupLists, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.numberedList, - subtitle: context.l10n.shortcutNumberedListDescription, - leading: const Icon(BusyMarkGlyphs.orderedList), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.orderedList, - ), - ), - BusyMarkActionRow( - title: context.l10n.bulletedList, - subtitle: context.l10n.shortcutBulletedListDescription, - leading: const Icon(BusyMarkGlyphs.unorderedList), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.unorderedList, - ), - ), - BusyMarkActionRow( - title: context.l10n.checklist, - subtitle: context.l10n.shortcutChecklistDescription, - leading: const Icon(BusyMarkGlyphs.checklist), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.taskList, - ), - ), - BusyMarkActionRow( - title: context.l10n.indentListItem, - leading: Icon( - BusyMarkGlyphs.indentFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.indent, - ), - ), - BusyMarkActionRow( - title: context.l10n.outdentListItem, - leading: Icon( - BusyMarkGlyphs.outdentFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.outdent, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.insert, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.image, - leading: const Icon(BusyMarkGlyphs.image), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.image, - ), - ), - BusyMarkActionRow( - title: context.l10n.hardLineBreak, - leading: const Icon(BusyMarkGlyphs.hardBreak), - trailing: const _KeyboardShortcutBadge( - BusyMarkEditorShortcutLabels.hardLineBreak, - ), - ), - ], - ), - BusyMarkGroupedList( - title: context.l10n.shortcutGroupSidebar, - filled: true, - children: [ - BusyMarkActionRow( - title: context.l10n.toggleSidebar, - leading: const Icon(BusyMarkGlyphs.sidebar), - trailing: const _KeyboardShortcutBadge( - BusyMarkSidebarShortcutLabels.toggleSidebar, - ), - ), - BusyMarkActionRow( - title: context.l10n.files, - leading: const Icon(BusyMarkGlyphs.documentOpen), - trailing: const _KeyboardShortcutBadge( - BusyMarkSidebarShortcutLabels.files, - ), - ), - BusyMarkActionRow( - title: context.l10n.toc, - leading: const Icon(BusyMarkGlyphs.orderedList), - trailing: const _KeyboardShortcutBadge( - BusyMarkSidebarShortcutLabels.toc, - ), - ), - BusyMarkActionRow( - title: context.l10n.outline, - leading: Icon( - BusyMarkGlyphs.indentFor(Directionality.of(context)), - ), - trailing: const _KeyboardShortcutBadge( - BusyMarkSidebarShortcutLabels.outline, - ), - ), - BusyMarkActionRow( - title: context.l10n.git, - leading: const Icon(BusyMarkGlyphs.branch), - trailing: const _KeyboardShortcutBadge( - BusyMarkSidebarShortcutLabels.git, - ), - ), - BusyMarkActionRow( - title: context.l10n.localHistoryEllipsis, - leading: const Icon(BusyMarkGlyphs.sidebarLocalHistory), - trailing: const _KeyboardShortcutBadge( - BusyMarkSidebarShortcutLabels.localHistory, - ), - ), - BusyMarkActionRow( - title: context.l10n.clipboardHistory, - leading: const Icon(BusyMarkGlyphs.copy), - trailing: const _KeyboardShortcutBadge( - BusyMarkSidebarShortcutLabels.clipboardHistory, - ), - ), - BusyMarkActionRow( - title: context.l10n.delete, - subtitle: context.l10n.shortcutDeleteTreeItemDescription, - leading: const Icon(BusyMarkGlyphs.delete), - trailing: const _KeyboardShortcutBadge( - BusyMarkTreeShortcutLabels.deleteSelection, - ), - ), - ], - ), - ], - ), - ), - ); -} - void showBusyMarkSyntaxReferenceDialog(BuildContext context) { final headerBar = LinuxHeaderBarService.instance; unawaited( @@ -2595,46 +2013,6 @@ class _SyntaxReferenceValue extends StatelessWidget { } } -class _BusyMarkInfoDialog extends StatelessWidget { - const _BusyMarkInfoDialog({ - required this.title, - required this.icon, - required this.children, - this.maxWidth = 460, - }); - - final String title; - final IconData icon; - final List children; - final double maxWidth; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return BusyMarkInformationalDialog( - closeLabel: context.l10n.close, - maxWidth: maxWidth, - maxHeight: 560, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BusyMarkDialogIdentity( - visual: Icon( - icon, - size: BusyMarkDialogIdentity.visualExtent, - color: colorScheme.primary, - ), - title: title, - ), - const SizedBox(height: BusyMarkSpacing.lg), - ...children, - ], - ), - ); - } -} - class _BusyMarkAboutDialog extends StatelessWidget { const _BusyMarkAboutDialog(); @@ -2756,38 +2134,3 @@ class _AboutVersionTag extends StatelessWidget { ); } } - -class _KeyboardShortcutBadge extends StatelessWidget { - const _KeyboardShortcutBadge(this.label); - - final String label; - - @override - Widget build(BuildContext context) { - final colors = BusyMarkSurfaceColors.of(context); - return DecoratedBox( - decoration: BoxDecoration( - color: colors.control, - borderRadius: BorderRadius.circular(BusyMarkRadius.sm), - border: Border.all(color: colors.subtleBorder), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: BusyMarkSpacing.sm, - vertical: BusyMarkSpacing.xxs, - ), - child: Directionality( - textDirection: TextDirection.ltr, - child: Text( - label, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - fontFamily: BusyMarkTypography.monoFontFamily, - fontFamilyFallback: BusyMarkTypography.monoFontFamilyFallback, - color: colors.foreground, - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/app/busymark_main_menu.dart b/lib/src/app/busymark_main_menu.dart index 00b13ac8..904d4677 100644 --- a/lib/src/app/busymark_main_menu.dart +++ b/lib/src/app/busymark_main_menu.dart @@ -13,9 +13,6 @@ enum BusyMarkMainMenuAction { export, fullScreen, settings, - keyboardShortcuts, - commandPalette, - quickOpen, syntaxReference, reportIssue, aboutBusyMark, @@ -34,9 +31,8 @@ class BusyMarkMainMenuButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l10n = context.l10n; - final commands = - BusyMarkCommandRegistryScope.maybeOf(context) ?? - BusyMarkCommandCatalog.metadata; + final boundCommands = BusyMarkCommandRegistryScope.maybeOf(context); + final commands = boundCommands ?? BusyMarkCommandCatalog.metadata; BusyMarkCommand command(String id) => commands[id]!; final fullScreen = ref.watch( windowControlServiceProvider.select((service) => service.isFullScreen), @@ -50,6 +46,9 @@ class BusyMarkMainMenuButton extends ConsumerWidget { label: command(BusyMarkCommandIds.export).label(context), icon: BusyMarkGlyphs.exportPdf, shortcut: command(BusyMarkCommandIds.export).shortcut?.label, + gtkAccelerator: command( + BusyMarkCommandIds.export, + ).shortcut?.gtkAccelerator, enabled: canExport, ), BusyMarkPopupMenuItem( @@ -57,6 +56,9 @@ class BusyMarkMainMenuButton extends ConsumerWidget { label: command(BusyMarkCommandIds.fullScreen).label(context), icon: BusyMarkGlyphs.fullScreen, shortcut: command(BusyMarkCommandIds.fullScreen).shortcut?.label, + gtkAccelerator: command( + BusyMarkCommandIds.fullScreen, + ).shortcut?.gtkAccelerator, checked: fullScreen, trailingCheck: true, mutuallyExclusive: false, @@ -66,32 +68,18 @@ class BusyMarkMainMenuButton extends ConsumerWidget { label: command(BusyMarkCommandIds.settings).label(context), icon: BusyMarkGlyphs.settings, shortcut: command(BusyMarkCommandIds.settings).shortcut?.label, - ), - BusyMarkPopupMenuItem( - value: BusyMarkMainMenuAction.keyboardShortcuts, - label: command(BusyMarkCommandIds.keyboardShortcuts).label(context), - icon: BusyMarkGlyphs.keyboard, - shortcut: command( - BusyMarkCommandIds.keyboardShortcuts, - ).shortcut?.label, - ), - BusyMarkPopupMenuItem( - value: BusyMarkMainMenuAction.quickOpen, - label: command(BusyMarkCommandIds.quickOpen).label(context), - icon: BusyMarkGlyphs.search, - shortcut: command(BusyMarkCommandIds.quickOpen).shortcut?.label, - ), - BusyMarkPopupMenuItem( - value: BusyMarkMainMenuAction.commandPalette, - label: command(BusyMarkCommandIds.commandPalette).label(context), - icon: BusyMarkGlyphs.search, - shortcut: command(BusyMarkCommandIds.commandPalette).shortcut?.label, + gtkAccelerator: command( + BusyMarkCommandIds.settings, + ).shortcut?.gtkAccelerator, ), BusyMarkPopupMenuItem( value: BusyMarkMainMenuAction.syntaxReference, label: command(BusyMarkCommandIds.syntaxReference).label(context), icon: BusyMarkGlyphs.markdownFile, shortcut: command(BusyMarkCommandIds.syntaxReference).shortcut?.label, + gtkAccelerator: command( + BusyMarkCommandIds.syntaxReference, + ).shortcut?.gtkAccelerator, ), BusyMarkPopupMenuItem( value: BusyMarkMainMenuAction.reportIssue, @@ -109,11 +97,6 @@ class BusyMarkMainMenuButton extends ConsumerWidget { BusyMarkMainMenuAction.export => BusyMarkCommandIds.export, BusyMarkMainMenuAction.fullScreen => BusyMarkCommandIds.fullScreen, BusyMarkMainMenuAction.settings => BusyMarkCommandIds.settings, - BusyMarkMainMenuAction.keyboardShortcuts => - BusyMarkCommandIds.keyboardShortcuts, - BusyMarkMainMenuAction.quickOpen => BusyMarkCommandIds.quickOpen, - BusyMarkMainMenuAction.commandPalette => - BusyMarkCommandIds.commandPalette, BusyMarkMainMenuAction.syntaxReference => BusyMarkCommandIds.syntaxReference, BusyMarkMainMenuAction.reportIssue || diff --git a/lib/src/app/busymark_search_field.dart b/lib/src/app/busymark_search_field.dart index 6909182a..119e5008 100644 --- a/lib/src/app/busymark_search_field.dart +++ b/lib/src/app/busymark_search_field.dart @@ -2,6 +2,48 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:yaru/yaru.dart'; +/// Identifies local query input to the application's early keyboard route. +/// No ancestor lookup is needed while focus detaches during navigation. +class BusyMarkSearchFocusNode extends FocusNode { + BusyMarkSearchFocusNode() + : super(debugLabel: 'BusyMarkSearchField text entry'); +} + +/// Keep input editing local without shadowing application navigation (for +/// example Alt+Left) with a second complete platform shortcut map. +class BusyMarkLocalTextEditingShortcuts extends StatelessWidget { + const BusyMarkLocalTextEditingShortcuts({super.key, required this.child}); + + final Widget child; + static const bindings = { + SingleActivator(LogicalKeyboardKey.keyA, control: true): + SelectAllTextIntent(SelectionChangedCause.keyboard), + SingleActivator(LogicalKeyboardKey.keyC, control: true): + CopySelectionTextIntent.copy, + SingleActivator(LogicalKeyboardKey.insert, control: true): + CopySelectionTextIntent.copy, + SingleActivator(LogicalKeyboardKey.keyX, control: true): + CopySelectionTextIntent.cut(SelectionChangedCause.keyboard), + SingleActivator(LogicalKeyboardKey.delete, shift: true): + CopySelectionTextIntent.cut(SelectionChangedCause.keyboard), + SingleActivator(LogicalKeyboardKey.keyV, control: true): PasteTextIntent( + SelectionChangedCause.keyboard, + ), + SingleActivator(LogicalKeyboardKey.insert, shift: true): PasteTextIntent( + SelectionChangedCause.keyboard, + ), + SingleActivator(LogicalKeyboardKey.keyZ, control: true): UndoTextIntent( + SelectionChangedCause.keyboard, + ), + SingleActivator(LogicalKeyboardKey.keyZ, control: true, shift: true): + RedoTextIntent(SelectionChangedCause.keyboard), + }; + + @override + Widget build(BuildContext context) => + Shortcuts(shortcuts: bindings, child: child); +} + /// Flutter counterpart to BusyMark's native Linux `GtkSearchEntry`. /// /// The native entry cannot be embedded in Flutter-owned layouts. This uses the @@ -17,6 +59,7 @@ class BusyMarkSearchField extends StatefulWidget { this.focusRequest = 0, this.onChanged, this.onSubmitted, + this.onMoveSelection, this.onClear, this.onEscape, this.clearButtonSemanticLabel, @@ -32,6 +75,7 @@ class BusyMarkSearchField extends StatefulWidget { final ValueChanged? onChanged; final ValueChanged? onSubmitted; + final ValueChanged? onMoveSelection; final VoidCallback? onClear; final VoidCallback? onEscape; final String? clearButtonSemanticLabel; @@ -44,7 +88,7 @@ class BusyMarkSearchField extends StatefulWidget { class _BusyMarkSearchFieldState extends State { late TextEditingController _controller; late bool _textIsEmpty; - final _focusNode = FocusNode(debugLabel: 'BusyMarkSearchField text entry'); + final _focusNode = BusyMarkSearchFocusNode(); @override void initState() { @@ -114,6 +158,27 @@ class _BusyMarkSearchFieldState extends State { @override Widget build(BuildContext context) { + _focusNode.onKeyEvent = (_, event) { + if ((event is! KeyDownEvent && event is! KeyRepeatEvent) || + _controller.value.composing.isValid) { + return KeyEventResult.ignored; + } + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.enter && + widget.onSubmitted != null) { + widget.onSubmitted!(_controller.text); + return KeyEventResult.handled; + } + if ((event.logicalKey == LogicalKeyboardKey.arrowDown || + event.logicalKey == LogicalKeyboardKey.arrowUp) && + widget.onMoveSelection != null) { + widget.onMoveSelection!( + event.logicalKey == LogicalKeyboardKey.arrowDown ? 1 : -1, + ); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }; final theme = Theme.of(context); final clearLabel = widget.clearButtonSemanticLabel ?? @@ -121,7 +186,8 @@ class _BusyMarkSearchFieldState extends State { return Focus( onKeyEvent: (_, event) { if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { + event.logicalKey == LogicalKeyboardKey.escape && + !_controller.value.composing.isValid) { if (widget.clearOnEscape) _clear(); widget.onEscape?.call(); return KeyEventResult.handled; @@ -130,32 +196,36 @@ class _BusyMarkSearchFieldState extends State { }, child: SizedBox( height: kYaruTitleBarItemHeight, - child: TextField( - controller: _controller, - focusNode: _focusNode, - autofocus: widget.autofocus, - textInputAction: TextInputAction.search, - textAlignVertical: TextAlignVertical.center, - cursorWidth: 1, - onChanged: widget.onChanged, - onSubmitted: widget.onSubmitted, - decoration: InputDecoration( - hintText: widget.hintText, - filled: true, - fillColor: theme.colorScheme.surface, - prefixIcon: const Icon(YaruIcons.search), - prefixIconConstraints: const BoxConstraints.tightFor( - width: kYaruTitleBarItemHeight, - ), - suffixIcon: _textIsEmpty - ? null - : IconButton( - tooltip: clearLabel, - onPressed: _clear, - icon: const Icon(YaruIcons.edit_clear), - ), - suffixIconConstraints: const BoxConstraints.tightFor( - width: kYaruTitleBarItemHeight, + // Query editing stays local even if an application command acquires + // one of the platform's ordinary text-editing combinations. + child: BusyMarkLocalTextEditingShortcuts( + child: TextField( + controller: _controller, + focusNode: _focusNode, + autofocus: widget.autofocus, + textInputAction: TextInputAction.search, + textAlignVertical: TextAlignVertical.center, + cursorWidth: 1, + onChanged: widget.onChanged, + onSubmitted: widget.onSubmitted, + decoration: InputDecoration( + hintText: widget.hintText, + filled: true, + fillColor: theme.colorScheme.surface, + prefixIcon: const Icon(YaruIcons.search), + prefixIconConstraints: const BoxConstraints.tightFor( + width: kYaruTitleBarItemHeight, + ), + suffixIcon: _textIsEmpty + ? null + : IconButton( + tooltip: clearLabel, + onPressed: _clear, + icon: const Icon(YaruIcons.edit_clear), + ), + suffixIconConstraints: const BoxConstraints.tightFor( + width: kYaruTitleBarItemHeight, + ), ), ), ), diff --git a/lib/src/app/busymark_shortcuts.dart b/lib/src/app/busymark_shortcuts.dart index 6409c3be..3c4a95b3 100644 --- a/lib/src/app/busymark_shortcuts.dart +++ b/lib/src/app/busymark_shortcuts.dart @@ -1,3 +1,4 @@ +import 'shortcut_binding.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -5,8 +6,8 @@ class BusyMarkShortcutDefinition { const BusyMarkShortcutDefinition({ required this.label, required this.activator, - this.gtkAccelerator, - }); + String? gtkAccelerator, + }) : _gtkAccelerator = gtkAccelerator; final String label; final ShortcutActivator activator; @@ -15,7 +16,15 @@ class BusyMarkShortcutDefinition { /// /// This is deliberately separate from [label]: GTK parses strings such as /// `comma`, then formats the visible label for the current desktop. - final String? gtkAccelerator; + final String? _gtkAccelerator; + String? get gtkAccelerator => + _gtkAccelerator ?? + switch (activator) { + final SingleActivator value => BusyMarkKeyBinding.fromActivator( + value, + ).gtkAccelerator, + _ => null, + }; } enum BusyMarkAppShortcutAction { @@ -48,12 +57,15 @@ abstract final class BusyMarkAppShortcuts { static const fullScreenLabel = 'F11'; static const backLabel = 'Alt+Left'; static const searchLabel = 'Ctrl+F'; - static const keyboardShortcutsLabel = 'Ctrl+Alt+K'; - static const quickOpenLabel = 'Ctrl+P'; - static const quickOpenGtkAccelerator = 'p'; + static const quickOpenLabel = 'Ctrl+Shift+D'; + static const quickOpenGtkAccelerator = 'd'; static const quickOpen = BusyMarkShortcutDefinition( label: quickOpenLabel, - activator: SingleActivator(LogicalKeyboardKey.keyP, control: true), + activator: SingleActivator( + LogicalKeyboardKey.keyD, + control: true, + shift: true, + ), gtkAccelerator: quickOpenGtkAccelerator, ); @@ -73,7 +85,6 @@ abstract final class BusyMarkAppShortcuts { static const fullScreenGtkAccelerator = 'F11'; static const backGtkAccelerator = 'Left'; static const searchGtkAccelerator = 'f'; - static const keyboardShortcutsGtkAccelerator = 'k'; static const commandPaletteGtkAccelerator = 'p'; static const syntaxReferenceGtkAccelerator = 'm'; static const settingsGtkAccelerator = 's'; @@ -126,15 +137,6 @@ abstract final class BusyMarkAppShortcuts { activator: SingleActivator(LogicalKeyboardKey.keyF, control: true), gtkAccelerator: searchGtkAccelerator, ); - static const keyboardShortcuts = BusyMarkShortcutDefinition( - label: keyboardShortcutsLabel, - activator: SingleActivator( - LogicalKeyboardKey.keyK, - control: true, - alt: true, - ), - gtkAccelerator: keyboardShortcutsGtkAccelerator, - ); static const commandPalette = BusyMarkShortcutDefinition( label: commandPaletteLabel, activator: SingleActivator( @@ -205,7 +207,6 @@ abstract final class BusyMarkAppShortcuts { BusyMarkAppShortcutAction.fullScreen: fullScreen, BusyMarkAppShortcutAction.back: back, BusyMarkAppShortcutAction.search: search, - BusyMarkAppShortcutAction.keyboardShortcuts: keyboardShortcuts, BusyMarkAppShortcutAction.commandPalette: commandPalette, BusyMarkAppShortcutAction.quickOpen: quickOpen, BusyMarkAppShortcutAction.syntaxReference: syntaxReference, @@ -218,72 +219,6 @@ abstract final class BusyMarkAppShortcuts { }; } -abstract final class BusyMarkAppShortcutLabels { - const BusyMarkAppShortcutLabels._(); - - static const newDocument = BusyMarkAppShortcuts.newDocumentLabel; - static const open = BusyMarkAppShortcuts.openLabel; - static const save = BusyMarkAppShortcuts.saveLabel; - static const export = BusyMarkAppShortcuts.exportLabel; - static const fullScreen = BusyMarkAppShortcuts.fullScreenLabel; - static const back = BusyMarkAppShortcuts.backLabel; - static const search = BusyMarkAppShortcuts.searchLabel; - static const keyboardShortcuts = BusyMarkAppShortcuts.keyboardShortcutsLabel; - static const commandPalette = BusyMarkAppShortcuts.commandPaletteLabel; - static const syntaxReference = BusyMarkAppShortcuts.syntaxReferenceLabel; - static const settings = BusyMarkAppShortcuts.settingsLabel; - static const nextTab = BusyMarkAppShortcuts.nextTabLabel; - static const previousTab = BusyMarkAppShortcuts.previousTabLabel; - static const closeTab = BusyMarkAppShortcuts.closeTabLabel; - static const closeAllTabs = BusyMarkAppShortcuts.closeAllTabsLabel; -} - -abstract final class BusyMarkAppShortcutActivators { - const BusyMarkAppShortcutActivators._(); - - static ShortcutActivator get newDocument => - BusyMarkAppShortcuts.newDocument.activator; - static ShortcutActivator get open => BusyMarkAppShortcuts.open.activator; - static ShortcutActivator get save => BusyMarkAppShortcuts.save.activator; - static ShortcutActivator get export => BusyMarkAppShortcuts.export.activator; - static ShortcutActivator get fullScreen => - BusyMarkAppShortcuts.fullScreen.activator; - static ShortcutActivator get back => BusyMarkAppShortcuts.back.activator; - static ShortcutActivator get search => BusyMarkAppShortcuts.search.activator; - static ShortcutActivator get keyboardShortcuts => - BusyMarkAppShortcuts.keyboardShortcuts.activator; - static ShortcutActivator get commandPalette => - BusyMarkAppShortcuts.commandPalette.activator; - static ShortcutActivator get syntaxReference => - BusyMarkAppShortcuts.syntaxReference.activator; - static ShortcutActivator get settings => - BusyMarkAppShortcuts.settings.activator; - static ShortcutActivator get nextTab => - BusyMarkAppShortcuts.nextTab.activator; - static ShortcutActivator get previousTab => - BusyMarkAppShortcuts.previousTab.activator; - static ShortcutActivator get closeTab => - BusyMarkAppShortcuts.closeTab.activator; - static ShortcutActivator get closeAllTabs => - BusyMarkAppShortcuts.closeAllTabs.activator; - static ShortcutActivator get toggleSidebar => - BusyMarkAppShortcuts.toggleSidebar.activator; -} - -abstract final class BusyMarkAppShortcutGtkAccelerators { - const BusyMarkAppShortcutGtkAccelerators._(); - - static const search = BusyMarkAppShortcuts.searchGtkAccelerator; - static const export = BusyMarkAppShortcuts.exportGtkAccelerator; - static const fullScreen = BusyMarkAppShortcuts.fullScreenGtkAccelerator; - static const back = BusyMarkAppShortcuts.backGtkAccelerator; - static const keyboardShortcuts = - BusyMarkAppShortcuts.keyboardShortcutsGtkAccelerator; - static const syntaxReference = - BusyMarkAppShortcuts.syntaxReferenceGtkAccelerator; - static const settings = BusyMarkAppShortcuts.settingsGtkAccelerator; -} - enum BusyMarkDocumentViewShortcutAction { editor, source, reading, split } abstract final class BusyMarkDocumentViewShortcuts { @@ -345,37 +280,6 @@ abstract final class BusyMarkDocumentViewShortcuts { }; } -abstract final class BusyMarkDocumentViewShortcutLabels { - const BusyMarkDocumentViewShortcutLabels._(); - - static const editor = BusyMarkDocumentViewShortcuts.editorLabel; - static const source = BusyMarkDocumentViewShortcuts.sourceLabel; - static const reading = BusyMarkDocumentViewShortcuts.readingLabel; - static const split = BusyMarkDocumentViewShortcuts.splitLabel; -} - -abstract final class BusyMarkDocumentViewShortcutActivators { - const BusyMarkDocumentViewShortcutActivators._(); - - static ShortcutActivator get editor => - BusyMarkDocumentViewShortcuts.editor.activator; - static ShortcutActivator get source => - BusyMarkDocumentViewShortcuts.source.activator; - static ShortcutActivator get reading => - BusyMarkDocumentViewShortcuts.reading.activator; - static ShortcutActivator get split => - BusyMarkDocumentViewShortcuts.split.activator; -} - -abstract final class BusyMarkDocumentViewShortcutGtkAccelerators { - const BusyMarkDocumentViewShortcutGtkAccelerators._(); - - static const editor = BusyMarkDocumentViewShortcuts.editorGtkAccelerator; - static const source = BusyMarkDocumentViewShortcuts.sourceGtkAccelerator; - static const reading = BusyMarkDocumentViewShortcuts.readingGtkAccelerator; - static const split = BusyMarkDocumentViewShortcuts.splitGtkAccelerator; -} - enum BusyMarkTextEditingShortcutAction { selectAll, cut, @@ -467,48 +371,6 @@ abstract final class BusyMarkTextEditingShortcuts { }; } -abstract final class BusyMarkTextEditingShortcutLabels { - const BusyMarkTextEditingShortcutLabels._(); - - static const selectAll = BusyMarkTextEditingShortcuts.selectAllLabel; - static const cut = BusyMarkTextEditingShortcuts.cutLabel; - static const copy = BusyMarkTextEditingShortcuts.copyLabel; - static const paste = BusyMarkTextEditingShortcuts.pasteLabel; - static const pastePlainText = - BusyMarkTextEditingShortcuts.pastePlainTextLabel; - static const undo = BusyMarkTextEditingShortcuts.undoLabel; - static const redo = BusyMarkTextEditingShortcuts.redoLabel; - static const insertIndentation = - BusyMarkTextEditingShortcuts.insertIndentationLabel; - static const outdentSource = BusyMarkTextEditingShortcuts.outdentSourceLabel; - static const escape = BusyMarkTextEditingShortcuts.escapeLabel; -} - -abstract final class BusyMarkTextEditingShortcutActivators { - const BusyMarkTextEditingShortcutActivators._(); - - static ShortcutActivator get selectAll => - BusyMarkTextEditingShortcuts.selectAll.activator; - static ShortcutActivator get cut => - BusyMarkTextEditingShortcuts.cut.activator; - static ShortcutActivator get copy => - BusyMarkTextEditingShortcuts.copy.activator; - static ShortcutActivator get paste => - BusyMarkTextEditingShortcuts.paste.activator; - static ShortcutActivator get pastePlainText => - BusyMarkTextEditingShortcuts.pastePlainText.activator; - static ShortcutActivator get undo => - BusyMarkTextEditingShortcuts.undo.activator; - static ShortcutActivator get redo => - BusyMarkTextEditingShortcuts.redo.activator; - static ShortcutActivator get insertIndentation => - BusyMarkTextEditingShortcuts.insertIndentation.activator; - static ShortcutActivator get outdentSource => - BusyMarkTextEditingShortcuts.outdentSource.activator; - static ShortcutActivator get escape => - BusyMarkTextEditingShortcuts.escape.activator; -} - enum BusyMarkEditorShortcutAction { refineWithAi, copyPlainText, @@ -770,111 +632,6 @@ abstract final class BusyMarkEditorShortcuts { }; } -abstract final class BusyMarkEditorShortcutLabels { - const BusyMarkEditorShortcutLabels._(); - - static const refineWithAi = BusyMarkEditorShortcuts.refineWithAiLabel; - static const copyPlainText = BusyMarkEditorShortcuts.copyPlainTextLabel; - static const textStyle = BusyMarkEditorShortcuts.textStyleLabel; - static const bold = BusyMarkEditorShortcuts.boldLabel; - static const italic = BusyMarkEditorShortcuts.italicLabel; - static const underline = BusyMarkEditorShortcuts.underlineLabel; - static const strikethrough = BusyMarkEditorShortcuts.strikethroughLabel; - static const inlineCode = BusyMarkEditorShortcuts.inlineCodeLabel; - static const link = BusyMarkEditorShortcuts.linkLabel; - static const paragraph = BusyMarkEditorShortcuts.paragraphLabel; - static const heading1 = BusyMarkEditorShortcuts.heading1Label; - static const heading2 = BusyMarkEditorShortcuts.heading2Label; - static const heading3 = BusyMarkEditorShortcuts.heading3Label; - static const heading4 = BusyMarkEditorShortcuts.heading4Label; - static const heading5 = BusyMarkEditorShortcuts.heading5Label; - static const heading6 = BusyMarkEditorShortcuts.heading6Label; - static const orderedList = BusyMarkEditorShortcuts.orderedListLabel; - static const unorderedList = BusyMarkEditorShortcuts.unorderedListLabel; - static const taskList = BusyMarkEditorShortcuts.taskListLabel; - static const indent = BusyMarkEditorShortcuts.indentLabel; - static const outdent = BusyMarkEditorShortcuts.outdentLabel; - static const blockquote = BusyMarkEditorShortcuts.blockquoteLabel; - static const codeBlock = BusyMarkEditorShortcuts.codeBlockLabel; - static const image = BusyMarkEditorShortcuts.imageLabel; - static const thematicBreak = BusyMarkEditorShortcuts.thematicBreakLabel; - static const hardLineBreak = BusyMarkEditorShortcuts.hardLineBreakLabel; -} - -abstract final class BusyMarkEditorShortcutActivators { - const BusyMarkEditorShortcutActivators._(); - - static Map intentMap( - Intent Function(BusyMarkEditorShortcutAction action) intentFor, - ) { - return { - for (final entry in BusyMarkEditorShortcuts.definitions.entries) - entry.value.activator: intentFor(entry.key), - }; - } - - static BusyMarkEditorShortcutAction? actionForKeyEvent( - KeyEvent event, - HardwareKeyboard keyboard, - ) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return null; - } - for (final entry in BusyMarkEditorShortcuts.definitions.entries) { - if (entry.value.activator.accepts(event, keyboard)) { - return entry.key; - } - } - return null; - } - - static ShortcutActivator get refineWithAi => - BusyMarkEditorShortcuts.refineWithAi.activator; - static ShortcutActivator get bold => BusyMarkEditorShortcuts.bold.activator; - static ShortcutActivator get italic => - BusyMarkEditorShortcuts.italic.activator; - static ShortcutActivator get underline => - BusyMarkEditorShortcuts.underline.activator; - static ShortcutActivator get link => BusyMarkEditorShortcuts.link.activator; - static ShortcutActivator get inlineCode => - BusyMarkEditorShortcuts.inlineCode.activator; - static ShortcutActivator get strikethrough => - BusyMarkEditorShortcuts.strikethrough.activator; - static ShortcutActivator get paragraph => - BusyMarkEditorShortcuts.paragraph.activator; - static ShortcutActivator get heading1 => - BusyMarkEditorShortcuts.heading1.activator; - static ShortcutActivator get heading2 => - BusyMarkEditorShortcuts.heading2.activator; - static ShortcutActivator get heading3 => - BusyMarkEditorShortcuts.heading3.activator; - static ShortcutActivator get heading4 => - BusyMarkEditorShortcuts.heading4.activator; - static ShortcutActivator get heading5 => - BusyMarkEditorShortcuts.heading5.activator; - static ShortcutActivator get heading6 => - BusyMarkEditorShortcuts.heading6.activator; - static ShortcutActivator get orderedList => - BusyMarkEditorShortcuts.orderedList.activator; - static ShortcutActivator get unorderedList => - BusyMarkEditorShortcuts.unorderedList.activator; - static ShortcutActivator get taskList => - BusyMarkEditorShortcuts.taskList.activator; - static ShortcutActivator get indent => - BusyMarkEditorShortcuts.indent.activator; - static ShortcutActivator get outdent => - BusyMarkEditorShortcuts.outdent.activator; - static ShortcutActivator get blockquote => - BusyMarkEditorShortcuts.blockquote.activator; - static ShortcutActivator get codeBlock => - BusyMarkEditorShortcuts.codeBlock.activator; - static ShortcutActivator get image => BusyMarkEditorShortcuts.image.activator; - static ShortcutActivator get thematicBreak => - BusyMarkEditorShortcuts.thematicBreak.activator; - static ShortcutActivator get hardLineBreak => - BusyMarkEditorShortcuts.hardLineBreak.activator; -} - enum BusyMarkSidebarShortcutAction { files, toc, @@ -931,66 +688,6 @@ abstract final class BusyMarkSidebarShortcuts { }; } -abstract final class BusyMarkSidebarShortcutLabels { - const BusyMarkSidebarShortcutLabels._(); - - static const toggleSidebar = BusyMarkAppShortcuts.toggleSidebarLabel; - static const files = BusyMarkSidebarShortcuts.filesLabel; - static const toc = BusyMarkSidebarShortcuts.tocLabel; - static const outline = BusyMarkSidebarShortcuts.outlineLabel; - static const git = BusyMarkSidebarShortcuts.gitLabel; - static const localHistory = BusyMarkSidebarShortcuts.localHistoryLabel; - static const clipboardHistory = - BusyMarkSidebarShortcuts.clipboardHistoryLabel; -} - -abstract final class BusyMarkSidebarShortcutActivators { - const BusyMarkSidebarShortcutActivators._(); - - static ShortcutActivator get toggleSidebar => - BusyMarkSidebarShortcuts.toggleSidebar.activator; - static ShortcutActivator get files => - BusyMarkSidebarShortcuts.files.activator; - static ShortcutActivator get toc => BusyMarkSidebarShortcuts.toc.activator; - static ShortcutActivator get outline => - BusyMarkSidebarShortcuts.outline.activator; - static ShortcutActivator get git => BusyMarkSidebarShortcuts.git.activator; - static ShortcutActivator get localHistory => - BusyMarkSidebarShortcuts.localHistory.activator; - static ShortcutActivator get clipboardHistory => - BusyMarkSidebarShortcuts.clipboardHistory.activator; - - static BusyMarkSidebarShortcutAction? actionForKeyEvent( - KeyEvent event, - HardwareKeyboard keyboard, - ) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return null; - } - if (!keyboard.isControlPressed || - keyboard.isShiftPressed || - keyboard.isAltPressed || - keyboard.isMetaPressed) { - return null; - } - return switch (event.logicalKey) { - LogicalKeyboardKey.digit1 || - LogicalKeyboardKey.numpad1 => BusyMarkSidebarShortcutAction.files, - LogicalKeyboardKey.digit2 || - LogicalKeyboardKey.numpad2 => BusyMarkSidebarShortcutAction.toc, - LogicalKeyboardKey.digit3 || - LogicalKeyboardKey.numpad3 => BusyMarkSidebarShortcutAction.outline, - LogicalKeyboardKey.digit4 || - LogicalKeyboardKey.numpad4 => BusyMarkSidebarShortcutAction.git, - LogicalKeyboardKey.digit5 || - LogicalKeyboardKey.numpad5 => BusyMarkSidebarShortcutAction.localHistory, - LogicalKeyboardKey.digit6 || LogicalKeyboardKey.numpad6 => - BusyMarkSidebarShortcutAction.clipboardHistory, - _ => null, - }; - } -} - enum BusyMarkTreeShortcutAction { deleteSelection } abstract final class BusyMarkTreeShortcuts { @@ -1008,16 +705,3 @@ abstract final class BusyMarkTreeShortcuts { BusyMarkTreeShortcutAction.deleteSelection: deleteSelection, }; } - -abstract final class BusyMarkTreeShortcutLabels { - const BusyMarkTreeShortcutLabels._(); - - static const deleteSelection = BusyMarkTreeShortcuts.deleteSelectionLabel; -} - -abstract final class BusyMarkTreeShortcutActivators { - const BusyMarkTreeShortcutActivators._(); - - static ShortcutActivator get deleteSelection => - BusyMarkTreeShortcuts.deleteSelection.activator; -} diff --git a/lib/src/app/command_palette.dart b/lib/src/app/command_palette.dart deleted file mode 100644 index 131985f2..00000000 --- a/lib/src/app/command_palette.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import '../platform/linux_header_bar_service.dart'; -import 'busymark_design.dart'; -import 'busymark_dialogs.dart'; -import 'busymark_glyphs.dart'; -import 'command_registry.dart'; -import 'localization.dart'; - -Future showBusyMarkCommandPalette( - BuildContext context, - BusyMarkCommandRegistry registry, -) async { - final headerBar = LinuxHeaderBarService.instance; - final commandTarget = FocusManager.instance.primaryFocus?.context; - await showBusyMarkModalDialog( - context, - headerBarService: headerBar.isAvailable ? headerBar : null, - builder: (context) => _BusyMarkCommandPalette( - registry: registry, - commandTarget: commandTarget, - ), - ); -} - -class _BusyMarkCommandPalette extends StatefulWidget { - const _BusyMarkCommandPalette({ - required this.registry, - required this.commandTarget, - }); - - final BusyMarkCommandRegistry registry; - final BuildContext? commandTarget; - - @override - State<_BusyMarkCommandPalette> createState() => - _BusyMarkCommandPaletteState(); -} - -class _BusyMarkCommandPaletteState extends State<_BusyMarkCommandPalette> { - var _query = ''; - - List get _commands { - final query = _query.trim().toLowerCase(); - return widget.registry.visibleCommands().where((command) { - if (query.isEmpty) { - return true; - } - return command.label(context).toLowerCase().contains(query) || - command.category(context).toLowerCase().contains(query) || - command.id.toLowerCase().contains(query); - }).toList(); - } - - @override - Widget build(BuildContext context) { - final commands = _commands; - return BusyMarkDialogShell( - title: context.l10n.commandPalette, - maxWidth: BusyMarkSizes.dialogCompact, - children: [ - BusyMarkGroupedList( - filled: true, - children: [ - BusyMarkGroupedTextEntry( - label: context.l10n.commandPaletteHint, - autofocus: true, - onChanged: (value) => setState(() => _query = value), - onSubmitted: (_) { - if (commands.isNotEmpty && _enabled(commands.first)) { - _execute(commands.first); - } - }, - ), - ], - ), - const SizedBox(height: BusyMarkSpacing.md), - if (commands.isEmpty) - Padding( - padding: const EdgeInsets.all(BusyMarkSpacing.lg), - child: Text( - context.l10n.commandPaletteEmpty, - textAlign: TextAlign.center, - ), - ) - else - BusyMarkGroupedList( - filled: true, - children: [ - for (final command in commands) - BusyMarkActionRow( - title: command.label(context), - subtitle: _enabled(command) - ? command.category(context) - : command.disabledReason?.call(context) ?? - command.category(context), - leading: const Icon(BusyMarkGlyphs.search), - trailing: command.shortcut == null - ? null - : Text(command.shortcut!.label), - enabled: _enabled(command), - onTap: _enabled(command) ? () => _execute(command) : null, - ), - ], - ), - ], - ); - } - - void _execute(BusyMarkCommand command) { - Navigator.pop(context); - unawaited( - Future.microtask( - () => - widget.registry.executeInContext(command.id, widget.commandTarget), - ), - ); - } - - bool _enabled(BusyMarkCommand command) { - return widget.registry.canExecuteInContext( - command.id, - widget.commandTarget, - ); - } -} diff --git a/lib/src/app/command_presentation.dart b/lib/src/app/command_presentation.dart new file mode 100644 index 00000000..674bb42b --- /dev/null +++ b/lib/src/app/command_presentation.dart @@ -0,0 +1,37 @@ +import 'package:flutter/widgets.dart'; + +import 'command_registry.dart'; + +/// Preserve the reference's category order, then include its previously omitted +/// unbound categories. Overrides never change the grouping order. +List commandCategoryOrder(BuildContext context) { + final commands = BusyMarkCommandCatalog.metadata.commands; + return { + for (final c in commands) + if (c.shortcut != null && c.scope != BusyMarkCommandScope.sourceSymbol) + c.category(context), + for (final c in commands) c.category(context), + }.toList(growable: false); +} + +int? commandMatchRank({ + required String name, + required String category, + String? description, + required String id, + required String query, +}) { + final q = query.trim().toLowerCase(); + if (q.isEmpty) return 0; + final title = name.toLowerCase(); + if (title == q) return 0; + if (title.startsWith(q)) return 1; + final tokens = q.split(RegExp(r'\s+')); + final words = title.split(RegExp(r'[\s\p{P}]+', unicode: true)); + if (tokens.every((t) => words.any((w) => w.startsWith(t)))) return 2; + if (title.contains(q)) return 3; + final text = + '$title ${category.toLowerCase()} ${description?.toLowerCase() ?? ''}'; + if (tokens.every(text.contains)) return 4; + return id.toLowerCase().contains(q) ? 5 : null; +} diff --git a/lib/src/app/command_registry.dart b/lib/src/app/command_registry.dart index 2fa9cc70..8309a05f 100644 --- a/lib/src/app/command_registry.dart +++ b/lib/src/app/command_registry.dart @@ -1,9 +1,13 @@ +import '../editor/wysiwyg/wysiwyg_commands.dart'; +import '../markdown/busymark_document.dart'; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'busymark_shortcuts.dart'; +import 'shortcut_binding.dart'; +import 'shortcut_capture.dart'; import 'localization.dart'; typedef BusyMarkCommandCallback = FutureOr Function(); @@ -28,10 +32,12 @@ class BusyMarkContextCommandAction BusyMarkContextCommandAction({ required this.isCommandEnabled, required this.onCommand, + this.captureTarget, }); final bool Function(String commandId) isCommandEnabled; final void Function(String commandId) onCommand; + final BusyMarkContextCommandSnapshot Function()? captureTarget; @override bool isEnabled(BusyMarkContextCommandIntent intent) { @@ -45,6 +51,14 @@ class BusyMarkContextCommandAction } } +/// Editors capture their actual selection before the query receives focus. +/// Validation reads that editor again; it never restores a different selection. +class BusyMarkContextCommandSnapshot { + const BusyMarkContextCommandSnapshot(this._validate); + final bool Function() _validate; + bool get isCurrent => _validate(); +} + enum BusyMarkCommandScope { application, documentView, @@ -52,8 +66,12 @@ enum BusyMarkCommandScope { editor, sidebar, tree, + sourceSymbol, + wysiwyg, } +enum BusyMarkCommandResourceContext { any, localWorkspace, notesWorkspace } + @immutable class BusyMarkCommand { const BusyMarkCommand({ @@ -62,6 +80,8 @@ class BusyMarkCommand { required this.category, required this.scope, this.shortcut, + this.defaultYieldsToOverrides = false, + this.resourceContext = BusyMarkCommandResourceContext.any, this.description, this.disabledReason, this.execute, @@ -74,6 +94,24 @@ class BusyMarkCommand { final BusyMarkCommandLabel category; final BusyMarkCommandScope scope; final BusyMarkShortcutDefinition? shortcut; + final bool defaultYieldsToOverrides; + final BusyMarkCommandResourceContext resourceContext; + + BusyMarkCommand withShortcut(BusyMarkShortcutDefinition? value) => + BusyMarkCommand( + id: id, + label: label, + category: category, + scope: scope, + shortcut: value, + defaultYieldsToOverrides: defaultYieldsToOverrides, + resourceContext: resourceContext, + description: description, + disabledReason: disabledReason, + execute: execute, + enabled: enabled, + visible: visible, + ); final BusyMarkCommandDescription? description; final BusyMarkCommandDescription? disabledReason; final BusyMarkCommandCallback? execute; @@ -127,9 +165,155 @@ class BusyMarkCommandRegistry { : commands = List.unmodifiable(commands) { _validate(this.commands); _byId = {for (final command in this.commands) command.id: command}; + for (final c in this.commands) { + _aliases[c.id] = _defaultAliases(c); + } + _compileKeyCandidates(); } final List commands; + final Map> _aliases = {}; + final Map<(BusyMarkCommandScope, LogicalKeyboardKey), List> + _keyCandidates = {}; + void _compileKeyCandidates() { + _keyCandidates.clear(); + for (final c in commands) { + for (final activator in activators(c.id)) { + for (final trigger in activator.triggers ?? []) { + (_keyCandidates[(c.scope, trigger)] ??= []).add(c.id); + } + } + } + } + + final Map _replacedDefaults = {}; + final Map> _retiredBindings = {}; + final Map> _blockedBindings = {}; + List _blockedFor(Set scopes) { + final key = scopes.fold(0, (int bits, scope) => bits | 1 << scope.index); + return _blockedBindings.putIfAbsent(key, () { + final receivers = { + ...scopes, + BusyMarkCommandScope.application, + BusyMarkCommandScope.documentView, + BusyMarkCommandScope.sidebar, + }; + bool replaced(BusyMarkCommand old, ShortcutActivator retired) => + commands.any( + (c) => + receivers.contains(c.scope) && + contextsOverlap(old, c) && + activators(c.id).any( + (a) => a is SingleActivator && retired is SingleActivator + ? BusyMarkKeyBinding.fromActivator(a) == + BusyMarkKeyBinding.fromActivator(retired) + : a == retired, + ), + ); + return [ + for (final c in _replacedDefaults.values) + if (scopes.contains(c.scope)) + for (final a in _retiredBindings[c.id] ?? []) + if (!replaced(c, a)) a, + ]; + }); + } + + Map blockedDefaultIntents( + Set scopes, + ) => { + for (final a in _blockedFor(scopes)) + a: const DoNothingAndStopPropagationIntent(), + }; + bool blocksReplacedDefault( + KeyEvent event, + HardwareKeyboard keyboard, + Set scopes, + ) => _blockedFor(scopes).any((a) => a.accepts(event, keyboard)); + List activators(String id) => [ + if (_byId[id]?.shortcut case final value?) value.activator, + ...?_aliases[id], + ]; + + BusyMarkCommandRegistry withOverrides( + Map overrides, + ) { + final result = BusyMarkCommandRegistry([ + for (final command in commands) + if (!overrides.containsKey(command.id)) + command + else + command.withShortcut(switch (overrides[command.id]) { + final binding? => BusyMarkShortcutDefinition( + label: binding.label, + activator: binding.activator, + gtkAccelerator: binding.gtkAccelerator, + ), + _ => null, + }), + ]); + result._aliases.clear(); + for (final command in result.commands) { + if (overrides.containsKey(command.id)) { + result._replacedDefaults[command.id] = this[command.id]!; + result._retiredBindings[command.id] = activators(command.id); + } else { + result._aliases[command.id] = _defaultAliases(command); + } + } + result._compileKeyCandidates(); + return result; + } + + Map bindingIntents(String id, Intent intent) => { + for (final activator in activators(id)) activator: intent, + }; + + /// Actual receiving contexts overlap across scopes. Tree browsing is the one + /// disjoint document context; line movement edits the Source .tree buffer. + static bool contextsOverlap(BusyMarkCommand a, BusyMarkCommand b) { + if (a.resourceContext != BusyMarkCommandResourceContext.any && + b.resourceContext != BusyMarkCommandResourceContext.any && + a.resourceContext != b.resourceContext) { + return false; + } + bool global(BusyMarkCommand c) => { + BusyMarkCommandScope.application, + BusyMarkCommandScope.documentView, + BusyMarkCommandScope.sidebar, + }.contains(c.scope); + bool treeOnly(BusyMarkCommand c) => + c.id == BusyMarkCommandIds.treeDeleteSelection || + c.id == BusyMarkCommandIds.treeRenameSelection; + if (global(a) || global(b)) return true; + bool sourceOnly(BusyMarkCommand c) => + c.scope == BusyMarkCommandScope.sourceSymbol || + c.id == BusyMarkCommandIds.treeMoveLineUp || + c.id == BusyMarkCommandIds.treeMoveLineDown; + if (a.scope == BusyMarkCommandScope.wysiwyg && sourceOnly(b) || + b.scope == BusyMarkCommandScope.wysiwyg && sourceOnly(a)) { + return false; + } + if (treeOnly(a) || treeOnly(b)) { + return treeOnly(a) && treeOnly(b); + } + return true; + } + + List conflicts(String id, BusyMarkKeyBinding binding) { + final command = this[id]; + if (command == null) return const []; + return [ + for (final other in commands) + if (other.id != id && + contextsOverlap(command, other) && + activators(other.id).whereType().any( + (a) => BusyMarkKeyBinding.fromActivator(a) == binding, + )) + other, + ]; + } + late final Map _byId; BusyMarkCommand? operator [](String id) => _byId[id]; @@ -153,7 +337,7 @@ class BusyMarkCommandRegistry { if (command == null || command.execute == null) { return false; } - if (!_isContextual(command) || contextTarget == null) { + if (!isContextual(command) || contextTarget == null) { return command.enabled(); } final action = Actions.maybeFind( @@ -167,7 +351,7 @@ class BusyMarkCommandRegistry { if (command == null || !canExecuteInContext(id, contextTarget)) { return false; } - if (_isContextual(command) && contextTarget != null) { + if (isContextual(command) && contextTarget != null) { Actions.maybeInvoke(contextTarget, BusyMarkContextCommandIntent(id)); return true; } @@ -175,15 +359,46 @@ class BusyMarkCommandRegistry { return true; } - bool _isContextual(BusyMarkCommand command) { + bool isContextual(BusyMarkCommand command) { return command.scope == BusyMarkCommandScope.editor || + command.scope == BusyMarkCommandScope.wysiwyg || command.scope == BusyMarkCommandScope.textEditing || + command.scope == BusyMarkCommandScope.sourceSymbol || + command.scope == BusyMarkCommandScope.tree || command.id == BusyMarkCommandIds.treeMoveLineUp || command.id == BusyMarkCommandIds.treeMoveLineDown; } + bool requiresDocumentTarget(BusyMarkCommand command) => + isContextual(command) || + command.scope == BusyMarkCommandScope.documentView || + const { + BusyMarkCommandIds.search, + BusyMarkCommandIds.save, + BusyMarkCommandIds.export, + BusyMarkCommandIds.closeTab, + BusyMarkCommandIds.elementProperties, + BusyMarkCommandIds.references, + BusyMarkCommandIds.sidebarOutline, + }.contains(command.id); + + bool requiresWorkspaceTarget(BusyMarkCommand command) => + command.scope == BusyMarkCommandScope.sidebar || + const { + BusyMarkCommandIds.localHistory, + BusyMarkCommandIds.findLocalHistory, + BusyMarkCommandIds.syncNotes, + BusyMarkCommandIds.exportNotes, + BusyMarkCommandIds.importNotesFolder, + BusyMarkCommandIds.importNotesFile, + BusyMarkCommandIds.nextTab, + BusyMarkCommandIds.previousTab, + BusyMarkCommandIds.closeAllTabs, + }.contains(command.id); + bool shortcutAccepts(String id, KeyEvent event, HardwareKeyboard keyboard) { - return _byId[id]?.shortcut?.activator.accepts(event, keyboard) ?? false; + return !BusyMarkShortcutCapture.suppressing && + activators(id).any((a) => a.accepts(event, keyboard)); } String? matchingCommandId( @@ -191,11 +406,9 @@ class BusyMarkCommandRegistry { HardwareKeyboard keyboard, { required BusyMarkCommandScope scope, }) { - for (final command in commands) { - if (command.scope == scope && - command.shortcut?.activator.accepts(event, keyboard) == true) { - return command.id; - } + if (BusyMarkShortcutCapture.suppressing) return null; + for (final id in _keyCandidates[(scope, event.logicalKey)] ?? []) { + if (shortcutAccepts(id, event, keyboard)) return id; } return null; } @@ -203,17 +416,21 @@ class BusyMarkCommandRegistry { Map shortcutIntents({ required Set scopes, required Intent Function(String commandId) intentFor, + Set excludedIds = const {}, }) { return { for (final command in commands) - if (scopes.contains(command.scope) && command.shortcut != null) - command.shortcut!.activator: intentFor(command.id), + if (scopes.contains(command.scope) && + command.shortcut != null && + !excludedIds.contains(command.id)) + for (final activator in activators(command.id)) + activator: intentFor(command.id), }; } static void _validate(List commands) { final ids = {}; - final shortcuts = <(BusyMarkCommandScope, ShortcutActivator), String>{}; + final shortcuts = <(BusyMarkCommandScope, Object), List>{}; for (final command in commands) { if (!RegExp( r'^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*)+$', @@ -231,13 +448,22 @@ class BusyMarkCommandRegistry { if (shortcut == null) { continue; } - final key = (command.scope, shortcut.activator); - if (shortcuts[key] case final existing?) { - throw BusyMarkCommandRegistryValidationException( - 'Shortcut conflict in ${command.scope.name}: $existing and ${command.id}', - ); + final activator = shortcut.activator; + final key = ( + command.scope, + activator is SingleActivator + ? BusyMarkKeyBinding.fromActivator(activator) + : activator, + ); + final existing = shortcuts[key] ??= []; + for (final other in existing) { + if (contextsOverlap(command, other)) { + throw BusyMarkCommandRegistryValidationException( + 'Shortcut conflict in ${command.scope.name}: ${other.id} and ${command.id}', + ); + } } - shortcuts[key] = command.id; + existing.add(command); } } } @@ -250,6 +476,16 @@ abstract final class BusyMarkCommandIds { static const fullScreen = 'view.fullScreen'; static const back = 'navigation.back'; static const search = 'search.find'; + static const searchWorkspace = 'search.workspace'; + static const searchHeadings = 'search.headings'; + static const searchTopics = 'search.topics'; + static const searchRecent = 'search.recent'; + static const newNote = 'notes.create'; + static const browseNotes = 'notes.browse'; + static const syncNotes = 'notes.sync'; + static const exportNotes = 'notes.export'; + static const importNotesFolder = 'notes.importFolder'; + static const importNotesFile = 'notes.importFile'; static const keyboardShortcuts = 'help.keyboardShortcuts'; static const quickOpen = 'workspace.quickOpen'; static const commandPalette = 'view.commandPalette'; @@ -286,12 +522,22 @@ abstract final class BusyMarkCommandIds { static const clipboardHistory = 'sidebar.clipboardHistory'; static const localHistory = 'history.local'; static const findLocalHistory = 'history.findLocal'; + static const sourceDeclaration = 'source.declaration'; + static const sourceUsages = 'source.usages'; + static const sourceRename = 'source.rename'; + static const treeRenameSelection = 'tree.renameSelection'; static const treeDeleteSelection = 'tree.deleteSelection'; static const treeMoveLineUp = 'tree.moveLineUp'; static const treeMoveLineDown = 'tree.moveLineDown'; } abstract final class BusyMarkCommandCatalog { + static const notesWorkspaceCommandIds = { + BusyMarkCommandIds.syncNotes, + BusyMarkCommandIds.exportNotes, + BusyMarkCommandIds.importNotesFolder, + BusyMarkCommandIds.importNotesFile, + }; static final BusyMarkCommandRegistry metadata = create(); static BusyMarkCommandRegistry create({ @@ -305,11 +551,15 @@ abstract final class BusyMarkCommandCatalog { required BusyMarkCommandLabel category, required BusyMarkCommandScope scope, BusyMarkShortcutDefinition? shortcut, + bool defaultYieldsToOverrides = false, BusyMarkCommandDescription? description, }) { final contextual = scope == BusyMarkCommandScope.editor || + scope == BusyMarkCommandScope.wysiwyg || scope == BusyMarkCommandScope.textEditing || + scope == BusyMarkCommandScope.sourceSymbol || + scope == BusyMarkCommandScope.tree || id == BusyMarkCommandIds.treeMoveLineUp || id == BusyMarkCommandIds.treeMoveLineDown; return BusyMarkCommand( @@ -318,6 +568,18 @@ abstract final class BusyMarkCommandCatalog { category: category, scope: scope, shortcut: shortcut, + defaultYieldsToOverrides: defaultYieldsToOverrides, + resourceContext: notesWorkspaceCommandIds.contains(id) + ? BusyMarkCommandResourceContext.notesWorkspace + : scope == BusyMarkCommandScope.sourceSymbol || + const { + BusyMarkCommandIds.treeDeleteSelection, + BusyMarkCommandIds.treeRenameSelection, + BusyMarkCommandIds.treeMoveLineUp, + BusyMarkCommandIds.treeMoveLineDown, + }.contains(id) + ? BusyMarkCommandResourceContext.localWorkspace + : BusyMarkCommandResourceContext.any, description: description, disabledReason: (context) => context.l10n.commandUnavailableInContext, execute: @@ -333,6 +595,141 @@ abstract final class BusyMarkCommandCatalog { } final commands = [ + command( + id: BusyMarkCommandIds.sourceDeclaration, + label: (c) => c.l10n.goToDeclaration, + category: (c) => 'Writerside', + scope: BusyMarkCommandScope.sourceSymbol, + shortcut: const BusyMarkShortcutDefinition( + label: 'Ctrl+B', + activator: SingleActivator(LogicalKeyboardKey.keyB, control: true), + ), + ), + command( + id: BusyMarkCommandIds.sourceUsages, + label: (c) => c.l10n.findUsages, + category: (c) => 'Writerside', + scope: BusyMarkCommandScope.sourceSymbol, + shortcut: const BusyMarkShortcutDefinition( + label: 'Alt+F7', + activator: SingleActivator(LogicalKeyboardKey.f7, alt: true), + ), + ), + command( + id: BusyMarkCommandIds.sourceRename, + label: (c) => c.l10n.rename, + category: (c) => 'Writerside', + scope: BusyMarkCommandScope.sourceSymbol, + shortcut: const BusyMarkShortcutDefinition( + label: 'Shift+F6', + activator: SingleActivator(LogicalKeyboardKey.f6, shift: true), + ), + ), + command( + id: BusyMarkCommandIds.newNote, + label: (c) => c.l10n.nextcloudNewNote, + category: (c) => c.l10n.nextcloudNotes, + scope: BusyMarkCommandScope.application, + ), + command( + id: BusyMarkCommandIds.browseNotes, + label: (c) => c.l10n.browseNotes, + category: (c) => c.l10n.nextcloudNotes, + scope: BusyMarkCommandScope.application, + ), + command( + id: BusyMarkCommandIds.syncNotes, + label: (c) => c.l10n.notesSync, + category: (c) => c.l10n.nextcloudNotes, + scope: BusyMarkCommandScope.application, + ), + command( + id: BusyMarkCommandIds.exportNotes, + label: (c) => c.l10n.notesExport, + category: (c) => c.l10n.nextcloudNotes, + scope: BusyMarkCommandScope.application, + ), + command( + id: BusyMarkCommandIds.importNotesFolder, + label: (c) => '${c.l10n.notesImport} · ${c.l10n.folder}', + category: (c) => c.l10n.nextcloudNotes, + scope: BusyMarkCommandScope.application, + ), + command( + id: BusyMarkCommandIds.importNotesFile, + label: (c) => '${c.l10n.notesImport} · ${c.l10n.fileTypeMarkdown}', + category: (c) => c.l10n.nextcloudNotes, + scope: BusyMarkCommandScope.application, + ), + for (final entry in writersideCommands.entries) + command( + id: entry.key, + label: (context) => writersideCommandLabel(context, entry.value), + category: (context) => 'Writerside', + scope: BusyMarkCommandScope.wysiwyg, + ), + command( + id: BusyMarkCommandIds.searchWorkspace, + label: (context) => context.l10n.searchInWorkspace, + category: (c) => c.l10n.search, + scope: BusyMarkCommandScope.application, + shortcut: const BusyMarkShortcutDefinition( + label: 'Ctrl+Shift+F', + activator: SingleActivator( + LogicalKeyboardKey.keyF, + control: true, + shift: true, + ), + gtkAccelerator: 'f', + ), + ), + command( + id: BusyMarkCommandIds.searchHeadings, + label: (c) => c.l10n.searchHeadingsCommand, + category: (c) => c.l10n.search, + scope: BusyMarkCommandScope.application, + defaultYieldsToOverrides: true, + shortcut: const BusyMarkShortcutDefinition( + label: 'Ctrl+Shift+H', + activator: SingleActivator( + LogicalKeyboardKey.keyH, + control: true, + shift: true, + ), + gtkAccelerator: 'h', + ), + ), + command( + id: BusyMarkCommandIds.searchTopics, + label: (context) => context.l10n.searchTopicsCommand, + category: (c) => c.l10n.search, + scope: BusyMarkCommandScope.application, + shortcut: const BusyMarkShortcutDefinition( + label: 'Ctrl+Shift+T', + activator: SingleActivator( + LogicalKeyboardKey.keyT, + control: true, + shift: true, + ), + gtkAccelerator: 't', + ), + ), + command( + id: BusyMarkCommandIds.searchRecent, + label: (context) => context.l10n.searchRecentCommand, + category: (c) => c.l10n.search, + scope: BusyMarkCommandScope.application, + defaultYieldsToOverrides: true, + shortcut: const BusyMarkShortcutDefinition( + label: 'Ctrl+Shift+R', + activator: SingleActivator( + LogicalKeyboardKey.keyR, + control: true, + shift: true, + ), + gtkAccelerator: 'r', + ), + ), command( id: BusyMarkCommandIds.elementProperties, label: (context) => context.l10n.wsElementProperties, @@ -352,6 +749,8 @@ abstract final class BusyMarkCommandCatalog { category: (context) => _appCategory(context, action), scope: BusyMarkCommandScope.application, shortcut: BusyMarkAppShortcuts.definitions[action], + defaultYieldsToOverrides: + action == BusyMarkAppShortcutAction.quickOpen, description: (context) => _appDescription(context, action), ), for (final action in BusyMarkDocumentViewShortcutAction.values) @@ -445,6 +844,16 @@ abstract final class BusyMarkCommandCatalog { shortcut: BusyMarkSidebarShortcuts.definitions[action], description: (context) => _sidebarDescription(context, action), ), + command( + id: BusyMarkCommandIds.treeRenameSelection, + label: (c) => c.l10n.rename, + category: (c) => c.l10n.shortcutGroupSidebar, + scope: BusyMarkCommandScope.tree, + shortcut: const BusyMarkShortcutDefinition( + label: 'Shift+F6', + activator: SingleActivator(LogicalKeyboardKey.f6, shift: true), + ), + ), for (final action in BusyMarkTreeShortcutAction.values) command( id: 'tree.${action.name}', @@ -457,8 +866,18 @@ abstract final class BusyMarkCommandCatalog { ), command( id: BusyMarkCommandIds.findLocalHistory, + defaultYieldsToOverrides: true, + shortcut: const BusyMarkShortcutDefinition( + label: 'Ctrl+Shift+L', + activator: SingleActivator( + LogicalKeyboardKey.keyL, + control: true, + shift: true, + ), + gtkAccelerator: 'l', + ), label: (context) => context.l10n.findLocalHistoryEllipsis, - category: (context) => context.l10n.shortcutGroupSidebar, + category: (context) => context.l10n.search, scope: BusyMarkCommandScope.application, description: (context) => context.l10n.findLocalHistoryDescription, ), @@ -499,11 +918,12 @@ abstract final class BusyMarkCommandCatalog { BusyMarkAppShortcutAction.export => context.l10n.export, BusyMarkAppShortcutAction.fullScreen => context.l10n.fullScreen, BusyMarkAppShortcutAction.back => context.l10n.back, - BusyMarkAppShortcutAction.search => context.l10n.search, - BusyMarkAppShortcutAction.quickOpen => context.l10n.quickOpen, + BusyMarkAppShortcutAction.search => context.l10n.searchInDocument, + BusyMarkAppShortcutAction.quickOpen => context.l10n.searchDocumentsCommand, BusyMarkAppShortcutAction.keyboardShortcuts => context.l10n.keyboardShortcuts, - BusyMarkAppShortcutAction.commandPalette => context.l10n.commandPalette, + BusyMarkAppShortcutAction.commandPalette => + context.l10n.searchActionsCommand, BusyMarkAppShortcutAction.syntaxReference => context.l10n.syntaxReference, BusyMarkAppShortcutAction.settings => context.l10n.settings, BusyMarkAppShortcutAction.nextTab => context.l10n.shortcutNextTab, @@ -517,6 +937,9 @@ abstract final class BusyMarkCommandCatalog { BuildContext context, BusyMarkAppShortcutAction action, ) => switch (action) { + BusyMarkAppShortcutAction.search || + BusyMarkAppShortcutAction.quickOpen || + BusyMarkAppShortcutAction.commandPalette => context.l10n.search, BusyMarkAppShortcutAction.nextTab || BusyMarkAppShortcutAction.previousTab || BusyMarkAppShortcutAction.closeTab || @@ -764,3 +1187,105 @@ abstract final class BusyMarkCommandCatalog { } } } + +/// The same typed commands used by the consolidated Writerside toolbar. +final writersideCommands = { + for (final command in BusyWritersideInsertCommand.values) + writersideToolbarCommandId(BusyWritersideToolbarInsert(command)): + BusyWritersideToolbarInsert(command), + for (final style in BusyAdmonitionStyle.values) + writersideToolbarCommandId(BusyWritersideToolbarAdmonition(style)): + BusyWritersideToolbarAdmonition(style), + for (final command in [ + BusyWysiwygInlineCommand.uiControl, + BusyWysiwygInlineCommand.filePath, + BusyWysiwygInlineCommand.uiPath, + BusyWysiwygInlineCommand.shortcut, + ]) + writersideToolbarCommandId(BusyWritersideToolbarSemantic(command)): + BusyWritersideToolbarSemantic(command), +}; +String writersideToolbarCommandId( + BusyWritersideToolbarCommand value, +) => switch (value) { + BusyWritersideToolbarInsert(:final command) => + 'writerside.insert${command.name[0].toUpperCase()}${command.name.substring(1)}', + BusyWritersideToolbarAdmonition(:final style) => 'writerside.${style.name}', + BusyWritersideToolbarSemantic(:final command) => 'writerside.${command.name}', +}; +String writersideCommandLabel( + BuildContext context, + BusyWritersideToolbarCommand command, +) => switch (command) { + BusyWritersideToolbarInsert(:final command) => switch (command) { + BusyWritersideInsertCommand.procedure => context.l10n.procedure, + BusyWritersideInsertCommand.tabs => context.l10n.tabs, + BusyWritersideInsertCommand.definitionList => context.l10n.wsDefinitionList, + BusyWritersideInsertCommand.include => context.l10n.wsIncludeContent, + BusyWritersideInsertCommand.variable => context.l10n.wsVariableReference, + BusyWritersideInsertCommand.video => context.l10n.video, + BusyWritersideInsertCommand.tldr => context.l10n.wsTldr, + BusyWritersideInsertCommand.convertListToProcedure => + context.l10n.wsConvertList, + }, + BusyWritersideToolbarAdmonition(:final style) => switch (style) { + BusyAdmonitionStyle.tip => context.l10n.tip, + BusyAdmonitionStyle.note => context.l10n.note, + BusyAdmonitionStyle.warning => context.l10n.warning, + BusyAdmonitionStyle.quote => context.l10n.quote, + }, + BusyWritersideToolbarSemantic(:final command) => switch (command) { + BusyWysiwygInlineCommand.uiControl => context.l10n.wsUiControl, + BusyWysiwygInlineCommand.filePath => context.l10n.wsFilePath, + BusyWysiwygInlineCommand.uiPath => context.l10n.wsUiPath, + _ => context.l10n.keyboardShortcuts, + }, +}; + +final _keypadAliases = { + LogicalKeyboardKey.digit0: LogicalKeyboardKey.numpad0, + LogicalKeyboardKey.digit1: LogicalKeyboardKey.numpad1, + LogicalKeyboardKey.digit2: LogicalKeyboardKey.numpad2, + LogicalKeyboardKey.digit3: LogicalKeyboardKey.numpad3, + LogicalKeyboardKey.digit4: LogicalKeyboardKey.numpad4, + LogicalKeyboardKey.digit5: LogicalKeyboardKey.numpad5, + LogicalKeyboardKey.digit6: LogicalKeyboardKey.numpad6, +}; + +List _defaultAliases(BusyMarkCommand command) { + final shortcut = command.shortcut; + if (shortcut == null) return const []; + final aliases = [ + ...?const >{ + BusyMarkCommandIds.textCopy: [ + SingleActivator(LogicalKeyboardKey.insert, control: true), + ], + BusyMarkCommandIds.textCut: [ + SingleActivator(LogicalKeyboardKey.delete, shift: true), + ], + BusyMarkCommandIds.textPaste: [ + SingleActivator(LogicalKeyboardKey.insert, shift: true), + ], + }[command.id], + ]; + final a = shortcut.activator; + if (a is SingleActivator && + (command.scope == BusyMarkCommandScope.sidebar || + command.id == BusyMarkCommandIds.localHistory || + command.id == BusyMarkCommandIds.clipboardHistory)) { + final key = _keypadAliases[a.trigger]; + if (key != null) { + aliases.add( + SingleActivator( + key, + control: a.control, + shift: a.shift, + alt: a.alt, + meta: a.meta, + includeRepeats: false, + ), + ); + } + } + return aliases; +} diff --git a/lib/src/app/command_shortcuts.dart b/lib/src/app/command_shortcuts.dart new file mode 100644 index 00000000..de723243 --- /dev/null +++ b/lib/src/app/command_shortcuts.dart @@ -0,0 +1,86 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'command_registry.dart'; + +/// A disabled command still owns its combination in this context. +/// It must not fall through to a different platform text-editing operation. +class BusyMarkCommandShortcuts extends StatefulWidget { + const BusyMarkCommandShortcuts({ + super.key, + required this.shortcuts, + required this.child, + this.isComposing, + }); + final Map shortcuts; + final Widget child; + final bool Function()? isComposing; + @override + State createState() => + _BusyMarkCommandShortcutsState(); +} + +class _BusyMarkCommandShortcutsState extends State { + late final _CommandShortcutManager _manager = _CommandShortcutManager( + widget.shortcuts, + widget.isComposing, + ); + @override + void didUpdateWidget(BusyMarkCommandShortcuts oldWidget) { + super.didUpdateWidget(oldWidget); + _manager.update(widget.shortcuts, widget.isComposing); + } + + @override + void dispose() { + _manager.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => + Shortcuts.manager(manager: _manager, child: widget.child); +} + +class _CommandShortcutManager extends ShortcutManager { + _CommandShortcutManager( + Map bindings, + bool Function()? isComposing, + ) { + update(bindings, isComposing); + } + bool Function()? _isComposing; + final _commandKeys = >{}; + void update( + Map bindings, + bool Function()? isComposing, + ) { + _isComposing = isComposing; + shortcuts = bindings; + _commandKeys.clear(); + for (final entry in bindings.entries) { + if (entry.value is! BusyMarkContextCommandIntent && + entry.value is! BusyMarkCommandIntent) { + continue; + } + for (final key in entry.key.triggers ?? []) { + (_commandKeys[key] ??= []).add(entry.key); + } + } + } + + @override + KeyEventResult handleKeypress(BuildContext context, KeyEvent event) { + if (_isComposing?.call() ?? false) { + return KeyEventResult.skipRemainingHandlers; + } + final result = super.handleKeypress(context, event); + if (result == KeyEventResult.ignored && + (_commandKeys[event.logicalKey] ?? const []).any( + (a) => a.accepts(event, HardwareKeyboard.instance), + )) { + return KeyEventResult.handled; + } + return result; + } +} diff --git a/lib/src/app/keyboard_shortcut_badge.dart b/lib/src/app/keyboard_shortcut_badge.dart new file mode 100644 index 00000000..0a360d93 --- /dev/null +++ b/lib/src/app/keyboard_shortcut_badge.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'busymark_design.dart'; + +class BusyMarkKeyboardShortcutBadge extends StatelessWidget { + const BusyMarkKeyboardShortcutBadge(this.label, {super.key}); + + final String label; + + @override + Widget build(BuildContext context) { + final colors = BusyMarkSurfaceColors.of(context); + return DecoratedBox( + decoration: BoxDecoration( + color: colors.control, + borderRadius: BorderRadius.circular(BusyMarkRadius.sm), + border: Border.all(color: colors.subtleBorder), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: BusyMarkSpacing.sm, + vertical: BusyMarkSpacing.xxs, + ), + child: Directionality( + textDirection: TextDirection.ltr, + child: Text( + // Let a narrow/scaled badge wrap between modifiers, rather than + // splitting a name such as Shift. Identity never comes from text. + label.replaceAll('+', '+\u200B'), + semanticsLabel: label, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontFamily: BusyMarkTypography.monoFontFamily, + fontFamilyFallback: BusyMarkTypography.monoFontFamilyFallback, + color: colors.foreground, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/app/keymap_controller.dart b/lib/src/app/keymap_controller.dart new file mode 100644 index 00000000..5f139518 --- /dev/null +++ b/lib/src/app/keymap_controller.dart @@ -0,0 +1,269 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'app_settings.dart'; +import 'command_registry.dart'; +import 'shortcut_binding.dart'; + +enum KeymapUpdateStatus { saved, failed, changed, conflict, invalid } + +class KeymapUpdate { + const KeymapUpdate(this.status, [this.conflicts = const []]); + final KeymapUpdateStatus status; + final List conflicts; +} + +/// Validation and publication are synchronous after hydration. A stale editor +/// can never publish its old copy of the entire map over a newer assignment. +class KeymapController extends Notifier { + @override + KeymapUpdateStatus build() { + ref.listen(shortcutPersistenceOutcomeProvider, (_, saved) { + if (saved != null) { + state = saved ? KeymapUpdateStatus.saved : KeymapUpdateStatus.failed; + } + }); + return KeymapUpdateStatus.saved; + } + + int _generation = 0; + + Future update( + String commandId, + BusyMarkKeyBinding? binding, { + required Map expected, + bool reset = false, + Set reassign = const {}, + }) async { + final settings = ref.read(appSettingsControllerProvider.notifier); + await settings.waitUntilLoaded(); + if (!ref.mounted) return const KeymapUpdate(KeymapUpdateStatus.changed); + final current = ref.read(appSettingsControllerProvider).shortcutOverrides; + if (!mapEquals(current, expected)) { + return const KeymapUpdate(KeymapUpdateStatus.changed); + } + final defaults = BusyMarkCommandCatalog.metadata; + final command = defaults[commandId]; + if (command == null || !shortcutConfigurable(command)) { + return const KeymapUpdate(KeymapUpdateStatus.invalid); + } + final candidate = reset ? defaultBinding(command) : binding; + if (!reset && + candidate != null && + !shortcutAssignmentAllowed(command, candidate)) { + return const KeymapUpdate(KeymapUpdateStatus.invalid); + } + final effective = effectiveCommandRegistry(defaults, current); + final candidates = reset + ? defaults + .activators(commandId) + .whereType() + .map(BusyMarkKeyBinding.fromActivator) + : [if (candidate != null) candidate]; + final conflicts = { + for (final value in candidates) + for (final other in effective.conflicts(commandId, value)) + other.id: other, + }.values.toList(growable: false); + if (!setEquals(conflicts.map((c) => c.id).toSet(), reassign) && + conflicts.isNotEmpty) { + return KeymapUpdate(KeymapUpdateStatus.conflict, conflicts); + } + final next = validatedShortcutOverrides(defaults, current); + for (final c in conflicts) { + next[c.id] = null; + } + if (reset) { + next.remove(commandId); + } else { + next[commandId] = binding; + } + final generation = ++_generation; + final saved = await settings.saveShortcutOverrides(next); + if (ref.mounted && generation == _generation) { + state = saved ? KeymapUpdateStatus.saved : KeymapUpdateStatus.failed; + } + return KeymapUpdate( + saved ? KeymapUpdateStatus.saved : KeymapUpdateStatus.failed, + ); + } + + Future resetAll() async { + final settings = ref.read(appSettingsControllerProvider.notifier); + await settings.waitUntilLoaded(); + final generation = ++_generation; + final saved = await settings.saveShortcutOverrides(const {}); + if (ref.mounted && generation == _generation) { + state = saved ? KeymapUpdateStatus.saved : KeymapUpdateStatus.failed; + } + return saved; + } + + Future retry() async { + final generation = ++_generation; + final saved = await ref + .read(appSettingsControllerProvider.notifier) + .retryShortcutSave(); + if (ref.mounted && generation == _generation) { + state = saved ? KeymapUpdateStatus.saved : KeymapUpdateStatus.failed; + } + } +} + +final keymapControllerProvider = + NotifierProvider( + KeymapController.new, + ); + +BusyMarkKeyBinding? defaultBinding(BusyMarkCommand command) => + switch (command.shortcut?.activator) { + final SingleActivator a => BusyMarkKeyBinding.fromActivator(a), + _ => null, + }; + +/// These are local input/traversal operations, not global assignments. Their +/// existing context-specific behavior remains local to each editor/widget. +bool shortcutConfigurable(BusyMarkCommand command) => !const { + BusyMarkCommandIds.textInsertIndentation, + BusyMarkCommandIds.textOutdentSource, + BusyMarkCommandIds.textEscape, +}.contains(command.id); + +/// Invalid, unknown or conflicting persisted records are ignored individually. +/// Defaults are never mutated. No unvalidated entry becomes executable. +BusyMarkCommandRegistry effectiveCommandRegistry( + BusyMarkCommandRegistry defaults, + Map overrides, +) { + final valid = validatedShortcutOverrides(defaults, overrides); + final deferred = deferredDefaultConflicts(defaults, valid); + return valid.isEmpty && deferred.isEmpty + ? defaults + : defaults.withOverrides({ + ...valid, + for (final id in deferred.keys) id: null, + }); +} + +/// Newly supplied defaults must not take a previously saved user binding. +/// This is derived runtime state, never an extra persisted override. Settings +/// exposes these conflicts and resolves them through the usual transaction. +Map> deferredDefaultConflicts( + BusyMarkCommandRegistry defaults, + Map overrides, +) { + final result = >{}; + for (final command in defaults.commands) { + final binding = defaultBinding(command); + if (!command.defaultYieldsToOverrides || + overrides.containsKey(command.id) || + binding == null) { + continue; + } + final conflicts = []; + for (final entry in overrides.entries) { + final other = defaults[entry.key]; + if (other != null && + entry.value == binding && + shortcutAssignmentAllowed(other, binding) && + BusyMarkCommandRegistry.contextsOverlap(command, other)) { + conflicts.add(other); + } + } + if (conflicts.isNotEmpty) result[command.id] = conflicts; + } + return result; +} + +/// Resolve all assignments together: a later override may free an earlier +/// command's default. Conflicting loaded custom records are rejected together, +/// then checked again against any defaults restored by that rejection. +Map validatedShortcutOverrides( + BusyMarkCommandRegistry defaults, + Map overrides, +) { + final valid = {}; + for (final e in overrides.entries) { + final command = defaults[e.key]; + if (command != null && + shortcutConfigurable(command) && + (e.value == null || shortcutAssignmentAllowed(command, e.value!))) { + valid[e.key] = e.value; + } + } + while (true) { + final deferred = deferredDefaultConflicts(defaults, valid); + final bindings = >{}; + for (final command in defaults.commands) { + final effective = valid.containsKey(command.id) + ? [if (valid[command.id] case final value?) value] + : [ + if (!deferred.containsKey(command.id)) + for (final a + in defaults + .activators(command.id) + .whereType()) + BusyMarkKeyBinding.fromActivator(a), + ]; + for (final binding in effective) { + (bindings[binding] ??= []).add(command); + } + } + final rejected = {}; + for (final commands in bindings.values) { + for (var i = 0; i < commands.length; i++) { + for (var j = i + 1; j < commands.length; j++) { + final a = commands[i], b = commands[j]; + if (a.id == b.id || !BusyMarkCommandRegistry.contextsOverlap(a, b)) { + continue; + } + if (valid[a.id] != null) rejected.add(a.id); + if (valid[b.id] != null) rejected.add(b.id); + } + } + } + if (rejected.isEmpty) return valid; + for (final id in rejected) { + valid.remove(id); + } + } +} + +bool shortcutAssignmentAllowed( + BusyMarkCommand command, + BusyMarkKeyBinding binding, +) { + if (BusyMarkKeyBinding.fromJson(binding.toJson()) != binding) return false; + if (BusyMarkKeyBinding.modifierKeys.contains(binding.key)) return false; + if (binding == defaultBinding(command)) return true; + final key = binding.key; + if (key == LogicalKeyboardKey.escape || + key == LogicalKeyboardKey.contextMenu || + key == LogicalKeyboardKey.f10 && + binding.shift && + !binding.control && + !binding.alt && + !binding.meta || + binding.control && + !binding.alt && + !binding.meta && + { + LogicalKeyboardKey.space, + LogicalKeyboardKey.enter, + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown, + }.contains(key)) { + return false; + } + if (binding.control || binding.alt || binding.meta) return true; + // Preserve local tree deletion; ordinary typing, traversal and dismissal + // cannot become application-wide assignments. + return (command.scope == BusyMarkCommandScope.tree && + binding.key == LogicalKeyboardKey.delete) || + binding.key.keyId >= LogicalKeyboardKey.f1.keyId && + binding.key.keyId <= LogicalKeyboardKey.f24.keyId; +} diff --git a/lib/src/app/linux/linux_page_frame.dart b/lib/src/app/linux/linux_page_frame.dart index 7cd74deb..8900120b 100644 --- a/lib/src/app/linux/linux_page_frame.dart +++ b/lib/src/app/linux/linux_page_frame.dart @@ -18,6 +18,7 @@ class LinuxPageFrame extends StatefulWidget { this.sidebarBody, this.sidebarAvailable = false, this.sidebarExpanded = false, + this.sidebarOverlay = false, this.sidebarTransitionGeneration = 0, }); @@ -27,6 +28,7 @@ class LinuxPageFrame extends StatefulWidget { final Widget? sidebarBody; final bool sidebarAvailable; final bool sidebarExpanded; + final bool sidebarOverlay; final int sidebarTransitionGeneration; @override @@ -40,6 +42,9 @@ class _LinuxPageFrameState extends State animationBehavior: AnimationBehavior.preserve, value: _targetVisible ? 1 : 0, ); + final _bodyKey = GlobalKey(); + final _headerKey = GlobalKey(); + final _sidebarBodyKey = GlobalKey(); late int _generation; bool _disableAnimations = false; @@ -120,6 +125,10 @@ class _LinuxPageFrameState extends State final oppositeControlInset = sidebarAtLeft ? metrics.rightControlInset : metrics.leftControlInset; + final retainedSidebar = KeyedSubtree( + key: _sidebarBodyKey, + child: widget.sidebarBody ?? const SizedBox.shrink(), + ); final sidebar = _hasSidebar ? SizedBox( width: BusyMarkSizes.sidebarWidth, @@ -136,15 +145,18 @@ class _LinuxPageFrameState extends State child: widget.sidebarHeader!, ), ), - Expanded(child: widget.sidebarBody!), + Expanded(child: retainedSidebar), ], ), ), ) : null; final main = _LinuxFrameMain( - header: ColoredBox(color: colors.headerbarFlat, child: widget.header), - body: widget.body, + header: KeyedSubtree( + key: _headerKey, + child: ColoredBox(color: colors.headerbarFlat, child: widget.header), + ), + body: KeyedSubtree(key: _bodyKey, child: widget.body), ); return AnimatedBuilder( animation: _controller, @@ -165,6 +177,48 @@ class _LinuxPageFrameState extends State ? oppositeControlInset : sidebarSideOverlap; final sidebarInteractive = _targetVisible && _controller.value == 1; + final sidebarViewport = sidebar == null + ? null + : DecoratedBox( + decoration: BoxDecoration( + border: BorderDirectional( + end: BorderSide(color: colors.sidebarBorder), + ), + ), + child: sidebar, + ); + if (widget.sidebarOverlay) { + return Stack( + children: [ + Positioned.fill( + child: Column( + children: [ + SizedBox( + height: BusyMarkLinuxWindowMetrics.headerHeight, + child: LinuxPageHeaderInsetsScope( + leftObstruction: metrics.leftControlInset, + rightObstruction: metrics.rightControlInset, + child: (mainChild! as _LinuxFrameMain).header, + ), + ), + Expanded(child: (mainChild as _LinuxFrameMain).body), + ], + ), + ), + if (_targetVisible && sidebarViewport != null) + PositionedDirectional( + start: 0, + top: BusyMarkLinuxWindowMetrics.headerHeight, + bottom: 0, + width: BusyMarkSizes.sidebarWidth, + child: ColoredBox( + color: colors.sidebar, + child: retainedSidebar, + ), + ), + ], + ); + } return Row( children: [ if (sidebar != null) diff --git a/lib/src/app/path_label.dart b/lib/src/app/path_label.dart new file mode 100644 index 00000000..db6bb9f0 --- /dev/null +++ b/lib/src/app/path_label.dart @@ -0,0 +1,67 @@ +import 'package:flutter/widgets.dart'; +import 'package:path/path.dart' as p; + +/// One measured elision of a location, retaining its trailing path components. +/// Full navigation identities and accessible descriptions belong to the row. +class BusyMarkPathLabel extends StatelessWidget { + const BusyMarkPathLabel({required this.path, this.style, super.key}); + final String path; + final TextStyle? style; + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) { + final effectiveStyle = DefaultTextStyle.of(context).style.merge(style); + final scaler = MediaQuery.textScalerOf(context); + bool fits(String text) { + final painter = TextPainter( + text: TextSpan(text: text, style: effectiveStyle), + textDirection: TextDirection.ltr, + textScaler: scaler, + maxLines: 1, + )..layout(); + final result = painter.width <= constraints.maxWidth; + painter.dispose(); + return result; + } + + var label = path; + if (!fits(label)) { + final prefix = p.isAbsolute(path) + ? '${p.rootPrefix(path)}…${p.separator}' + : path.startsWith('~${p.separator}') + ? '~${p.separator}…${p.separator}' + : '…${p.separator}'; + final parts = p + .split(path) + .where((part) => part != p.rootPrefix(path) && part != '~') + .toList(); + for (var i = 1; i < parts.length; i++) { + label = '$prefix${p.joinAll(parts.skip(i))}'; + if (fits(label)) break; + } + if (!fits(label)) { + final end = (parts.lastOrNull ?? path).characters.toList(); + var low = 0, high = end.length; + while (low < high) { + final mid = (low + high) ~/ 2; + if (fits('$prefix${end.skip(mid).join()}')) { + high = mid; + } else { + low = mid + 1; + } + } + label = '$prefix${end.skip(low).join()}'; + } + } + return Text( + label, + style: style, + maxLines: 1, + softWrap: false, + textDirection: TextDirection.ltr, + overflow: TextOverflow.clip, + ); + }, + ); +} diff --git a/lib/src/app/quick_open.dart b/lib/src/app/quick_open.dart index 345e365d..1f9aa37c 100644 --- a/lib/src/app/quick_open.dart +++ b/lib/src/app/quick_open.dart @@ -1,18 +1,6 @@ import 'dart:isolate'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path/path.dart' as p; - -import '../nextcloud_notes/application/nextcloud_connection.dart'; -import '../nextcloud_notes/application/notes_navigation.dart'; import '../nextcloud_notes/domain/notes_search.dart'; -import '../search/workspace_search_scope.dart'; -import '../workspace/workspace_controller.dart'; -import 'busymark_design.dart'; -import 'busymark_dialogs.dart'; -import 'localization.dart'; class QuickOpenDocument { const QuickOpenDocument({ @@ -59,187 +47,10 @@ List rankQuickOpen( return ranked.take(limit).map((r) => r.document).toList(); } -Future showQuickOpen(BuildContext context, WidgetRef ref) async { - final workspace = ref.read(workspaceControllerProvider).workspace; - if (workspace == null) return; - final previous = FocusManager.instance.primaryFocus; - final documents = []; - if (workspace.isRemote) { - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - documents.addAll( - repository.notes - .where( - (n) => - n.accountId == workspace.nextcloudAccountId && - !isNotesRecovery(n), - ) - .map( - (n) => QuickOpenDocument( - title: n.title, - path: n.category, - localId: n.localId, - ), - ), - ); - } else { - documents.addAll( - workspace.files - .where(isSearchableWorkspaceDocument) - .map( - (f) => QuickOpenDocument( - title: p.basenameWithoutExtension(f.relativePath), - path: f.relativePath, - filePath: f.absolutePath, - ), - ), - ); - } - if (!context.mounted) return; - final result = await showBusyMarkModalDialog( - context, - builder: (_) => QuickOpenDialog(documents: documents), - ); - if (!ref.context.mounted || - ref.read(workspaceControllerProvider).workspace?.id != workspace.id) { - return; - } - if (result == null) { - if (previous?.context?.mounted == true) previous!.requestFocus(); - return; - } - final controller = ref.read(workspaceControllerProvider.notifier); - if (result.localId != null) { - await controller.openNextcloudNote(result.localId!); - } else { - await controller.openActiveFile(result.filePath!); - } -} - -class QuickOpenDialog extends StatefulWidget { - const QuickOpenDialog({super.key, required this.documents}); - final List documents; - @override - State createState() => _QuickOpenDialogState(); -} - -class _QuickOpenDialogState extends State { - late final FocusNode _focus = FocusNode(onKeyEvent: _key); - final _scroll = ScrollController(); - List _results = []; - int _selected = 0, _generation = 0; - bool _loading = false; - @override - void initState() { - super.initState(); - _search(''); - } - - Future _search(String query) async { - final generation = ++_generation; - setState(() => _loading = true); - final documents = widget.documents; - final results = await _rankQuickOpenAsync(documents, query); - if (!mounted || generation != _generation) return; - setState(() { - _results = results; - _selected = 0; - _loading = false; - }); - if (_scroll.hasClients) _scroll.jumpTo(0); - } - - KeyEventResult _key(FocusNode node, KeyEvent event) { - if (event is! KeyDownEvent) return KeyEventResult.ignored; - if (event.logicalKey == LogicalKeyboardKey.escape) { - Navigator.pop(context); - return KeyEventResult.handled; - } - if (event.logicalKey == LogicalKeyboardKey.enter) { - _open(); - return KeyEventResult.handled; - } - if (event.logicalKey == LogicalKeyboardKey.arrowDown || - event.logicalKey == LogicalKeyboardKey.arrowUp) { - if (_results.isNotEmpty) { - setState( - () => _selected = - (_selected + - (event.logicalKey == LogicalKeyboardKey.arrowDown - ? 1 - : -1)) - .clamp(0, _results.length - 1), - ); - if (_scroll.hasClients) { - _scroll.animateTo( - (_selected * 64.0).clamp(0, _scroll.position.maxScrollExtent), - duration: const Duration(milliseconds: 100), - curve: Curves.easeOut, - ); - } - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - void _open() { - if (!_loading && _results.isNotEmpty) { - Navigator.pop(context, _results[_selected]); - } - } - - @override - void dispose() { - _generation++; - _focus.dispose(); - _scroll.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => BusyMarkDialogShell( - title: context.l10n.quickOpen, - maxWidth: 640, - children: [ - TextField( - autofocus: true, - focusNode: _focus, - decoration: InputDecoration(labelText: context.l10n.quickOpen), - onChanged: _search, - ), - if (_loading) const LinearProgressIndicator(), - SizedBox( - height: 340, - child: _results.isEmpty - ? Center(child: Text(context.l10n.noResults)) - : ListView.builder( - controller: _scroll, - itemCount: _results.length, - itemExtent: 64, - itemBuilder: (context, index) { - final document = _results[index]; - return ListTile( - selected: index == _selected, - title: Text( - document.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - document.path, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - onTap: () => Navigator.pop(context, document), - ); - }, - ), - ), - ], - ); -} - -Future> _rankQuickOpenAsync( +/// A top-level isolate boundary prevents a search controller's subscriptions +/// and futures from being captured by a ranking closure. +Future> rankQuickOpenAsync( List documents, - String query, -) => Isolate.run(() => rankQuickOpen(documents, query)); + String query, { + int limit = 100, +}) => Isolate.run(() => rankQuickOpen(documents, query, limit: limit)); diff --git a/lib/src/app/shortcut_binding.dart b/lib/src/app/shortcut_binding.dart new file mode 100644 index 00000000..5e7f0522 --- /dev/null +++ b/lib/src/app/shortcut_binding.dart @@ -0,0 +1,200 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// A layout-aware logical key, independent of localized presentation text. +@immutable +class BusyMarkKeyBinding { + const BusyMarkKeyBinding( + this.key, { + this.control = false, + this.shift = false, + this.alt = false, + this.meta = false, + }); + final LogicalKeyboardKey key; + final bool control, shift, alt, meta; + + static final modifierKeys = { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.controlRight, + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.shiftRight, + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.altRight, + LogicalKeyboardKey.metaLeft, + LogicalKeyboardKey.metaRight, + LogicalKeyboardKey.control, + LogicalKeyboardKey.shift, + LogicalKeyboardKey.alt, + LogicalKeyboardKey.meta, + LogicalKeyboardKey.altGraph, + LogicalKeyboardKey.compose, + }; + + static BusyMarkKeyBinding? fromJson(Object? value) { + if (value is! Map) return null; + final id = value['key']; + if (id is! int || id < 0 || id > 0x2ffffffff) return null; + for (final name in ['control', 'shift', 'alt', 'meta']) { + if (value[name] != null && value[name] is! bool) return null; + } + final known = LogicalKeyboardKey.findKeyByKeyId(id); + if (known == null && + !(id >= 0x20 && + id <= 0x10ffff && + !(id >= 0x7f && id <= 0x9f) && + !(id >= 0xd800 && id <= 0xdfff))) { + return null; + } + final key = known ?? LogicalKeyboardKey(id); + if (modifierKeys.contains(key) || key == LogicalKeyboardKey.unidentified) { + return null; + } + return BusyMarkKeyBinding( + key, + control: value['control'] == true, + shift: value['shift'] == true, + alt: value['alt'] == true, + meta: value['meta'] == true, + ); + } + + factory BusyMarkKeyBinding.fromActivator(SingleActivator value) => + BusyMarkKeyBinding( + value.trigger, + control: value.control, + shift: value.shift, + alt: value.alt, + meta: value.meta, + ); + + static BusyMarkKeyBinding? fromEvent(KeyEvent event) { + if (event is! KeyDownEvent || + modifierKeys.contains(event.logicalKey) || + event.logicalKey == LogicalKeyboardKey.unidentified) { + return null; + } + final keyboard = HardwareKeyboard.instance; + final binding = BusyMarkKeyBinding( + event.logicalKey, + control: keyboard.isControlPressed, + shift: keyboard.isShiftPressed, + alt: keyboard.isAltPressed, + meta: keyboard.isMetaPressed, + ); + return fromJson(binding.toJson()); + } + + SingleActivator get activator => SingleActivator( + key, + control: control, + shift: shift, + alt: alt, + meta: meta, + includeRepeats: false, + ); + Map toJson() => { + 'key': key.keyId, + 'control': control, + 'shift': shift, + 'alt': alt, + 'meta': meta, + }; + String get label => [ + if (control) 'Ctrl', + if (alt) 'Alt', + if (shift) 'Shift', + if (meta) 'Super', + key == LogicalKeyboardKey.escape ? 'Esc' : key.keyLabel, + ].join('+'); + + /// Native metadata uses GDK names for functional keys. Unicode scalar + /// notation is resolved through GDK by the existing menu bridge. + String get gtkAccelerator { + final name = + _gtkNames[key] ?? + (key.keyId < 0x100000000 + ? (!RegExp(r'^[a-zA-Z0-9]$').hasMatch(key.keyLabel) + ? 'U+${key.keyId.toRadixString(16).toUpperCase().padLeft(4, '0')}' + : key.keyLabel.toLowerCase()) + : key.keyLabel.replaceAll(' ', '_')); + return '${control ? '' : ''}${alt ? '' : ''}' + '${shift ? '' : ''}${meta ? '' : ''}$name'; + } + + bool get hasKnownSystemConflict => + meta || + (alt && {LogicalKeyboardKey.tab, LogicalKeyboardKey.f4}.contains(key)) || + (control && + alt && + (key == LogicalKeyboardKey.delete || + key.keyId >= LogicalKeyboardKey.f1.keyId && + key.keyId <= LogicalKeyboardKey.f12.keyId)); + + @override + bool operator ==(Object other) => + other is BusyMarkKeyBinding && + key == other.key && + control == other.control && + shift == other.shift && + alt == other.alt && + meta == other.meta; + @override + int get hashCode => Object.hash(key, control, shift, alt, meta); +} + +final _gtkNames = { + LogicalKeyboardKey.escape: 'Escape', + LogicalKeyboardKey.enter: 'Return', + LogicalKeyboardKey.arrowLeft: 'Left', + LogicalKeyboardKey.arrowRight: 'Right', + LogicalKeyboardKey.arrowUp: 'Up', + LogicalKeyboardKey.arrowDown: 'Down', + LogicalKeyboardKey.pageUp: 'Page_Up', + LogicalKeyboardKey.pageDown: 'Page_Down', + LogicalKeyboardKey.backspace: 'BackSpace', + LogicalKeyboardKey.space: 'space', + LogicalKeyboardKey.comma: 'comma', + LogicalKeyboardKey.period: 'period', + LogicalKeyboardKey.slash: 'slash', + LogicalKeyboardKey.backslash: 'backslash', + LogicalKeyboardKey.semicolon: 'semicolon', + LogicalKeyboardKey.quote: 'apostrophe', + LogicalKeyboardKey.bracketLeft: 'bracketleft', + LogicalKeyboardKey.bracketRight: 'bracketright', + LogicalKeyboardKey.minus: 'minus', + LogicalKeyboardKey.equal: 'equal', + LogicalKeyboardKey.backquote: 'grave', + LogicalKeyboardKey.numpad0: 'KP_0', + LogicalKeyboardKey.numpad1: 'KP_1', + LogicalKeyboardKey.numpad2: 'KP_2', + LogicalKeyboardKey.numpad3: 'KP_3', + LogicalKeyboardKey.numpad4: 'KP_4', + LogicalKeyboardKey.numpad5: 'KP_5', + LogicalKeyboardKey.numpad6: 'KP_6', + LogicalKeyboardKey.numpad7: 'KP_7', + LogicalKeyboardKey.numpad8: 'KP_8', + LogicalKeyboardKey.numpad9: 'KP_9', + LogicalKeyboardKey.numpadEnter: 'KP_Enter', + LogicalKeyboardKey.numpadAdd: 'KP_Add', + LogicalKeyboardKey.numpadSubtract: 'KP_Subtract', + LogicalKeyboardKey.numpadMultiply: 'KP_Multiply', + LogicalKeyboardKey.numpadDivide: 'KP_Divide', + LogicalKeyboardKey.numpadDecimal: 'KP_Decimal', +}; + +/// Missing entries use defaults; null entries explicitly unassign a command. +Map readShortcutOverrides(Object? value) { + if (value is! Map) return const {}; + return Map.unmodifiable({ + for (final entry in value.entries) + if (entry.key is String && + RegExp( + r'^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*)+$', + ).hasMatch(entry.key as String)) + if (entry.value == null) + entry.key as String: null + else if (BusyMarkKeyBinding.fromJson(entry.value) case final binding?) + entry.key as String: binding, + }); +} diff --git a/lib/src/app/shortcut_capture.dart b/lib/src/app/shortcut_capture.dart new file mode 100644 index 00000000..eb31dacc --- /dev/null +++ b/lib/src/app/shortcut_capture.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'shortcut_binding.dart'; + +/// Intercepts before the focus tree and also gates hardware-level dispatch. +/// After capture closes, held keys must be released before normal dispatch. +abstract final class BusyMarkShortcutCapture { + static Object? _owner; + static void Function(BusyMarkKeyBinding)? _onCandidate; + static VoidCallback? _onCancel; + static bool _installed = false; + static bool get suppressing => _installed; + + static void begin( + Object owner, { + required void Function(BusyMarkKeyBinding) onCandidate, + required VoidCallback onCancel, + }) { + if (_owner != null && !identical(_owner, owner)) _onCancel?.call(); + _owner = owner; + _onCandidate = onCandidate; + _onCancel = onCancel; + if (!_installed) { + _installed = true; + FocusManager.instance.addEarlyKeyEventHandler(_handle); + HardwareKeyboard.instance.addHandler(_observeRelease); + } + } + + static void end(Object owner) { + if (!identical(_owner, owner)) return; + _owner = null; + _onCandidate = null; + _onCancel = null; + _releaseIfIdle(); + } + + static void _releaseIfIdle() { + if (_owner == null && + HardwareKeyboard.instance.logicalKeysPressed.isEmpty && + _installed) { + FocusManager.instance.removeEarlyKeyEventHandler(_handle); + HardwareKeyboard.instance.removeHandler(_observeRelease); + _installed = false; + } + } + + // Native delivery can omit the focus phase of an already handled release. + // Observe releases independently of that phase so capture cannot keep command + // dispatch suppressed after its owner and all held keys have gone away. + static bool _observeRelease(KeyEvent event) { + if (event is KeyUpEvent && _owner == null) { + scheduleMicrotask(_releaseIfIdle); + } + return false; + } + + static KeyEventResult _handle(KeyEvent event) { + final focused = FocusManager.instance.primaryFocus?.context; + final composing = + focused + ?.findAncestorStateOfType() + ?.widget + .controller + .value + .composing + .isValid == + true; + if (_owner != null && event is KeyDownEvent && !composing) { + if (event.logicalKey == LogicalKeyboardKey.escape) { + _onCancel?.call(); + } else if (BusyMarkKeyBinding.fromEvent(event) case final binding?) { + _onCandidate?.call(binding); + } + } + // The event that releases the last captured key is still consumed. + _releaseIfIdle(); + return KeyEventResult.handled; + } +} diff --git a/lib/src/clipboard/clipboard_history_panel.dart b/lib/src/clipboard/clipboard_history_panel.dart index 69a10b1c..3aeeeb21 100644 --- a/lib/src/clipboard/clipboard_history_panel.dart +++ b/lib/src/clipboard/clipboard_history_panel.dart @@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/busymark_design.dart'; import '../app/busymark_glyphs.dart'; -import '../app/busymark_search_field.dart'; import '../app/busymark_toast.dart'; import '../app/localization.dart'; import 'clipboard_history_controller.dart'; @@ -24,7 +23,6 @@ class ClipboardHistoryPanel extends ConsumerStatefulWidget { } class _ClipboardHistoryPanelState extends ConsumerState { - final _searchController = TextEditingController(); final _listScrollController = ScrollController(); final _listFocusNode = FocusNode(debugLabel: 'Clipboard History list'); final _entryKeys = {}; @@ -46,7 +44,6 @@ class _ClipboardHistoryPanelState extends ConsumerState { @override void dispose() { - _searchController.dispose(); _listScrollController.dispose(); _listFocusNode.dispose(); super.dispose(); @@ -57,14 +54,11 @@ class _ClipboardHistoryPanelState extends ConsumerState { final state = ref.watch(clipboardHistoryControllerProvider); final controller = ref.read(clipboardHistoryControllerProvider.notifier); final registry = ref.watch(clipboardInsertionRegistryProvider); - final query = _searchController.text.toLowerCase(); final current = state.currentClipboard; final visible = [ - if (current != null && _matches(current, query)) current, + if (current != null) current, for (final entry in state.entries) - if ((current == null || !current.equivalentTo(entry)) && - _matches(entry, query)) - entry, + if (current == null || !current.equivalentTo(entry)) entry, ]; if (_selectedId != null && !visible.any((entry) => entry.id == _selectedId)) { @@ -86,17 +80,13 @@ class _ClipboardHistoryPanelState extends ConsumerState { BusyMarkSpacing.sm, ), child: Row( - textDirection: TextDirection.ltr, children: [ Expanded( - child: BusyMarkSearchField( - controller: _searchController, - hintText: context.l10n.clipboardHistorySearchHint, - onChanged: (_) => setState(() {}), - onEscape: widget.onEscape, + child: Text( + context.l10n.clipboardHistory, + style: Theme.of(context).textTheme.bodyMedium, ), ), - const SizedBox(width: BusyMarkSpacing.sm), BusyMarkHeaderPopupMenuButton<_ClipboardHistoryAction>( key: const ValueKey('clipboard-history-actions-menu'), tooltip: context.l10n.actions, @@ -147,9 +137,7 @@ class _ClipboardHistoryPanelState extends ConsumerState { Expanded( child: visible.isEmpty ? _PanelEmpty( - text: query.isNotEmpty - ? context.l10n.clipboardNoMatches - : state.clipboardAvailable + text: state.clipboardAvailable ? context.l10n.clipboardNoItems : context.l10n.clipboardUnavailable, ) @@ -201,17 +189,6 @@ class _ClipboardHistoryPanelState extends ConsumerState { ); } - bool _matches(BusyMarkClipboardPayload payload, String query) { - if (query.isEmpty) return true; - return [ - payload.text, - payload.sourceText, - payload.imageDisplayName, - payload.origin?.documentName, - payload.origin?.documentPath, - ].whereType().any((value) => value.toLowerCase().contains(query)); - } - KeyEventResult _handleKey( KeyEvent event, List visible, @@ -222,9 +199,8 @@ class _ClipboardHistoryPanelState extends ConsumerState { widget.onEscape?.call(); return KeyEventResult.handled; } - // Key events bubble through this Focus even while the search field owns - // primary focus. List navigation, activation, and especially deletion must - // never consume text-editing keys from that descendant. + // Descendant controls and popups own their keys. List navigation, + // activation and deletion require primary focus on the list itself. if (!_listFocusNode.hasPrimaryFocus) return KeyEventResult.ignored; if (visible.isEmpty) return KeyEventResult.ignored; final index = visible.indexWhere((entry) => entry.id == _selectedId); @@ -377,7 +353,6 @@ class _ClipboardEntryTile extends StatelessWidget { busyMarkLtrIsolateFor(context, originPath), maxLines: 1, overflow: TextOverflow.ellipsis, - textDirection: TextDirection.ltr, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: colors.mutedForeground), diff --git a/lib/src/core/linux_private_permissions.dart b/lib/src/core/linux_private_permissions.dart new file mode 100644 index 00000000..221b6c3e --- /dev/null +++ b/lib/src/core/linux_private_permissions.dart @@ -0,0 +1,163 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +typedef _OpenNative = Int32 Function(Pointer, Int32); +typedef _OpenDart = int Function(Pointer, int); +typedef _FchmodNative = Int32 Function(Int32, Uint32); +typedef _FchmodDart = int Function(int, int); +typedef _ChmodNative = Int32 Function(Pointer, Uint32); +typedef _ChmodDart = int Function(Pointer, int); +typedef _CloseNative = Int32 Function(Int32); +typedef _CloseDart = int Function(int); +typedef _StatxNative = + Int32 Function(Int32, Pointer, Int32, Uint32, Pointer); +typedef _StatxDart = int Function(int, Pointer, int, int, Pointer); +typedef _ErrnoNative = Pointer Function(); +typedef _ErrnoDart = Pointer Function(); + +/// Restricts an existing Linux state inode without launching a subprocess. +/// State directories may be configured through symbolic links. Files reject +/// final-component links; protection always applies to the opened inode, even +/// if another process replaces the pathname in the meantime. +final class LinuxPrivatePermissions { + LinuxPrivatePermissions._() : _library = 'libc.so.6'; + + /// Native barrier/error fixture only; production always selects libc. + LinuxPrivatePermissions.forTesting({required String nativeLibrary}) + : _library = nativeLibrary; + + static final instance = LinuxPrivatePermissions._(); + final String _library; + + // At most one native worker is active per calling isolate. UI transaction + // queues await admission/completion; no operation is dropped or cancelled. + // Isolate.run retires its worker after each operation, including failures, + // so an idle permission service cannot keep application shutdown alive. + static Future _tail = Future.value(); + + Future restrict(String path, {required bool directory}) async { + if (path.contains('\u0000')) { + throw FileSystemException('Invalid application state path', path); + } + final operation = _tail.then( + (_) => _restrictOffIsolate(path, directory, _library), + ); + _tail = operation.then((_) {}, onError: (Object _, StackTrace _) {}); + await operation; + } +} + +// Explicit inputs prevent a closure from transferring a service, transaction, +// open descriptor or UI object. Construction, allocation, open/fchmod/close +// and error/errno handling all execute together on the worker isolate. +Future _restrictOffIsolate(String path, bool directory, String library) => + Isolate.run( + () => _NativePermissions(library).restrict(path, directory: directory), + debugName: 'busymark-private-permissions', + ); + +final class _NativePermissions { + _NativePermissions(String library) { + final libc = DynamicLibrary.open(library); + _open = libc.lookupFunction<_OpenNative, _OpenDart>('open'); + _fchmod = libc.lookupFunction<_FchmodNative, _FchmodDart>('fchmod'); + _chmod = libc.lookupFunction<_ChmodNative, _ChmodDart>('chmod'); + _close = libc.lookupFunction<_CloseNative, _CloseDart>('close'); + _statx = libc.lookupFunction<_StatxNative, _StatxDart>('statx'); + _errno = libc.lookupFunction<_ErrnoNative, _ErrnoDart>('__errno_location'); + } + + late final _OpenDart _open; + late final _FchmodDart _fchmod; + late final _ChmodDart _chmod; + late final _CloseDart _close; + late final _StatxDart _statx; + late final _ErrnoDart _errno; + + void restrict(String path, {required bool directory}) { + if (path.contains('\u0000')) { + throw FileSystemException('Invalid application state path', path); + } + // Linux UAPI: O_DIRECTORY follows a configured directory link and pins its + // target. Files use O_NOFOLLOW. O_NONBLOCK prevents an unexpected FIFO from + // blocking the worker; O_CLOEXEC keeps descriptors out of child processes. + final typeFlags = directory ? 0x10000 : 0x20000; + final nativePath = path.toNativeUtf8(); + var descriptor = -1; + var pathDescriptor = false; + int? error; + try { + descriptor = _open(nativePath, 0x800 | 0x80000 | typeFlags); + if (descriptor < 0 && _errno().value == 13) { + // chmod does not require read permission on an owned inode. A creation + // mask can remove read access even from a just-written staging file. + // Pin the inode without I/O; validate O_PATH symlinks below before use. + descriptor = _open(nativePath, 0x200000 | 0x80000 | typeFlags); + pathDescriptor = true; + } + if (descriptor < 0) error = _errno().value; + } finally { + malloc.free(nativePath); + } + if (error != null) _fail(path, error); + try { + if (pathDescriptor) { + // O_DIRECTORY already guarantees a directory. O_PATH | O_NOFOLLOW + // permits holding a file symlink itself, so inspect that descriptor. + if (!directory) _rejectHeldSymlink(descriptor, path); + // fchmod rejects O_PATH descriptors. This procfs magic link resolves + // the held inode, not the original (possibly replaced) pathname. Keep + // the descriptor open until chmod completes; unavailable procfs fails + // the transaction rather than falling back to an unpinned path. + final heldPath = '/proc/self/fd/$descriptor'.toNativeUtf8(); + try { + if (_chmod(heldPath, directory ? 448 : 384) != 0) { + error = _errno().value; + } + } finally { + malloc.free(heldPath); + } + } else if (_fchmod(descriptor, directory ? 448 : 384) != 0) { + error = _errno().value; + } + } finally { + if (_close(descriptor) != 0) error ??= _errno().value; + } + if (error != null) _fail(path, error); + } + + void _rejectHeldSymlink(int descriptor, String path) { + // O_PATH | O_NOFOLLOW can open a symlink itself. AT_EMPTY_PATH inspects the + // held inode, never a replaced pathname. Use the fixed Linux statx ABI, + // as in LinuxFileIdentity, rather than architecture-dependent struct stat. + final empty = ''.toNativeUtf8(); + final buffer = calloc(256); + try { + // AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW, STATX_TYPE. + if (_statx(descriptor, empty, 0x1100, 1, buffer) != 0) { + _fail(path, _errno().value); + } + final data = ByteData.sublistView(buffer.asTypedList(256)); + if (data.getUint32(0, Endian.host) & 1 == 0) { + _fail(path, 95); // EOPNOTSUPP: no authoritative inode type. + } + if (data.getUint16(0x1c, Endian.host) & 0xf000 == 0xa000) { + _fail(path, 40); // ELOOP, as for O_NOFOLLOW open. + } + } finally { + calloc.free(buffer); + malloc.free(empty); + } + } + + Never _fail(String path, int code) => throw FileSystemException( + 'Could not restrict application state permissions', + path, + OSError('Native permission operation failed', code), + ); +} diff --git a/lib/src/editor/editor_text_context_menu.dart b/lib/src/editor/editor_text_context_menu.dart index 2123f6b7..fa4b150f 100644 --- a/lib/src/editor/editor_text_context_menu.dart +++ b/lib/src/editor/editor_text_context_menu.dart @@ -248,6 +248,7 @@ class _BusyMarkEditorTextContextMenuState value: spellingFallback, label: command?.label(context) ?? '', shortcut: command?.shortcut?.label, + gtkAccelerator: command?.shortcut?.gtkAccelerator, ), ); items.add(const PopupMenuDivider(height: BusyMarkSpacing.sm)); @@ -265,6 +266,7 @@ class _BusyMarkEditorTextContextMenuState label: command.label(context), icon: BusyMarkGlyphs.paste, shortcut: command.shortcut?.label, + gtkAccelerator: command.shortcut?.gtkAccelerator, ), ); } @@ -277,6 +279,7 @@ class _BusyMarkEditorTextContextMenuState label: command.label(context), icon: BusyMarkGlyphs.paste, shortcut: command.shortcut?.label, + gtkAccelerator: command.shortcut?.gtkAccelerator, ), ); } @@ -304,6 +307,7 @@ class _BusyMarkEditorTextContextMenuState label: commands[commandId]!.label(context), icon: _iconFor(item.type), shortcut: commands[commandId]!.shortcut?.label, + gtkAccelerator: commands[commandId]!.shortcut?.gtkAccelerator, enabled: callback != null, ), ); @@ -328,6 +332,7 @@ class _BusyMarkEditorTextContextMenuState label: command.label(context), icon: BusyMarkGlyphs.copy, shortcut: command.shortcut?.label, + gtkAccelerator: command.shortcut?.gtkAccelerator, ), ); } @@ -344,6 +349,9 @@ class _BusyMarkEditorTextContextMenuState icon: BusyMarkGlyphs.ai, shortcut: commands[BusyMarkCommandIds.editorRefineWithAi]?.shortcut?.label, + gtkAccelerator: commands[BusyMarkCommandIds.editorRefineWithAi] + ?.shortcut + ?.gtkAccelerator, ), ); } @@ -360,6 +368,7 @@ class _BusyMarkEditorTextContextMenuState label: command.label(context), icon: entry.$2, shortcut: command.shortcut?.label, + gtkAccelerator: command.shortcut?.gtkAccelerator, enabled: command.enabled(), ), ); diff --git a/lib/src/editor/source/source_editor.dart b/lib/src/editor/source/source_editor.dart index 5f1141af..0ad226d7 100644 --- a/lib/src/editor/source/source_editor.dart +++ b/lib/src/editor/source/source_editor.dart @@ -1,3 +1,5 @@ +import '../../app/command_shortcuts.dart'; +import '../../app/busymark_search_field.dart'; import '../link_destination.dart'; import '../wysiwyg/writerside_editing_adapter.dart'; import '../../assets/document_media_context.dart'; @@ -83,15 +85,18 @@ class BusyMarkSourceEditor extends StatefulWidget { required this.editorFontSize, required this.wordWrap, required this.searchActive, + this.searchControlsVisible = true, required this.searchOptions, - required this.onSearchOptionsChanged, + this.searchSnapshot, + this.onNavigateSearch, + this.onCancelSearchNavigation, this.searchReplacement = '', this.onSearchReplacementChanged, required this.onChanged, this.onTransactionalChanged, this.onUndo, this.onRedo, - required this.onOpenSearch, + this.onOpenSearch, required this.onCloseSearch, this.onVisibleLineChanged, this.onAiEdit, @@ -128,15 +133,18 @@ class BusyMarkSourceEditor extends StatefulWidget { final double editorFontSize; final bool wordWrap; final bool searchActive; + final bool searchControlsVisible; final SourceSearchOptions searchOptions; - final ValueChanged onSearchOptionsChanged; + final SourceSearchSnapshot? searchSnapshot; + final int? Function(SourceSearchNavigationRequest)? onNavigateSearch; + final ValueChanged? onCancelSearchNavigation; final String searchReplacement; final ValueChanged? onSearchReplacementChanged; final BusyMarkSourceChanged onChanged; final BusyMarkSourceTransactionalChanged? onTransactionalChanged; final TextEditingValue? Function()? onUndo; final TextEditingValue? Function()? onRedo; - final VoidCallback onOpenSearch; + final VoidCallback? onOpenSearch; final VoidCallback onCloseSearch; final ValueChanged? onVisibleLineChanged; final BusyMarkAiEditCallback? onAiEdit; @@ -175,6 +183,7 @@ class BusyMarkSourceEditorState extends State { late final ScrollController _horizontalScrollController; late UndoHistoryController _undoController; final _sourceEditorKey = GlobalKey(); + final _searchPanelKey = GlobalKey(); final _foldedRegionKeys = {}; final _searchController = SourceSearchController(); final _searchWorker = SourceSearchWorker(); @@ -182,6 +191,7 @@ class BusyMarkSourceEditorState extends State { bool _applyingSearchReplacement = false; ({int start, int end})? _requestedSearchRange; bool _searchNavigationFailed = false; + _SharedSearchNavigation? _pendingSharedSearchNavigation; final _intrinsicWidthCache = SourceIntrinsicWidthCache(); final _lineLayoutCache = SourceLineLayoutCache(); final _autocompleteProvider = const SourceAutocompleteProvider(); @@ -357,6 +367,7 @@ class BusyMarkSourceEditorState extends State { widget.clipboardInsertionRegistry?.register(_historyInsertionTarget); } if (widget.text != oldWidget.text || + widget.searchActive != oldWidget.searchActive || widget.searchOptions != oldWidget.searchOptions || widget.searchReplacement != oldWidget.searchReplacement) { _replacementWorker.cancel(); @@ -364,6 +375,14 @@ class BusyMarkSourceEditorState extends State { final path = widget.documentId ?? widget.filePath ?? ''; final pathChanged = path != _lastPath; final languageChanged = widget.language != oldWidget.language; + if (pathChanged || + languageChanged || + widget.searchActive != oldWidget.searchActive || + widget.searchOptions != oldWidget.searchOptions || + (_pendingSharedSearchNavigation != null && + widget.text != _pendingSharedSearchNavigation!.request.source)) { + _cancelSharedSearchNavigation(); + } var authoritativeDocumentChanged = false; if (pathChanged) { authoritativeDocumentChanged = true; @@ -392,6 +411,18 @@ class BusyMarkSourceEditorState extends State { _requestedSearchRange = null; _searchNavigationFailed = false; _syncSearchOptions(); + } else if (widget.searchSnapshot?.result != + oldWidget.searchSnapshot?.result || + widget.searchSnapshot?.source != oldWidget.searchSnapshot?.source) { + _syncSearchOptions(); + } + if (widget.searchSnapshot?.navigationRequest != + oldWidget.searchSnapshot?.navigationRequest || + widget.searchSnapshot?.appliedNavigation != + oldWidget.searchSnapshot?.appliedNavigation || + widget.searchSnapshot?.searching != + oldWidget.searchSnapshot?.searching) { + _syncSearchOptions(); } if (widget.wordWrap && !oldWidget.wordWrap) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -419,6 +450,12 @@ class BusyMarkSourceEditorState extends State { } void _handleControllerActivity() { + final pending = _pendingSharedSearchNavigation; + if (pending != null && + (pending.request.source != _controller.fullText || + pending.selection != _controller.fullSelection)) { + _cancelSharedSearchNavigation(); + } _scheduleSessionPublication(); if (widget.wordWrap || _horizontalCaretScheduled) { return; @@ -499,6 +536,7 @@ class BusyMarkSourceEditorState extends State { required int startOffset, required int endOffset, }) { + _cancelSharedSearchNavigation(); _unfoldSourceRange(startOffset, endOffset); final start = startOffset.clamp(0, _controller.fullText.length).toInt(); final end = endOffset.clamp(start, _controller.fullText.length).toInt(); @@ -520,6 +558,12 @@ class BusyMarkSourceEditorState extends State { result.firstMatchIndex + localIndex, ); _controller.setSearchResult(_searchController.result); + if (widget.searchSnapshot != null && widget.onNavigateSearch != null) { + _requestSharedSearchNavigation( + matchIndex: result.firstMatchIndex + localIndex, + reveal: false, + ); + } setState(() {}); } else if (start < end) { _scheduleSearch( @@ -531,14 +575,35 @@ class BusyMarkSourceEditorState extends State { _searchController.setCurrentMatchIndex(null); _controller.setSearchResult(_searchController.result); } + final source = _controller.fullText; + final documentId = widget.documentId ?? widget.filePath; + final options = widget.searchOptions; + final sourceRevision = widget.searchSnapshot?.sourceRevision; + final sessionRevision = widget.searchSnapshot?.sessionRevision; + bool current() => + mounted && + _controller.fullText == source && + (widget.documentId ?? widget.filePath) == documentId && + widget.searchOptions == options && + widget.searchSnapshot?.sourceRevision == sourceRevision && + widget.searchSnapshot?.sessionRevision == sessionRevision && + _controller.fullSelection == + TextSelection(baseOffset: start, extentOffset: end); WidgetsBinding.instance.addPostFrameCallback((_) { - _animateScrollToLine(line); + if (!current()) return; + _animateSearchToLine(line, current); WidgetsBinding.instance.addPostFrameCallback((_) { - _jumpScrollToLine(line); + if (current()) { + _jumpScrollToLine(line); + _ensureSearchCaretVisible(); + } }); unawaited( Future.delayed(BusyMarkMotion.previewSearchDelay, () { - _jumpScrollToLine(line); + if (current()) { + _jumpScrollToLine(line); + _ensureSearchCaretVisible(); + } }), ); }); @@ -577,6 +642,11 @@ class BusyMarkSourceEditorState extends State { } final keyboard = HardwareKeyboard.instance; final key = event.logicalKey; + final unmodified = + !keyboard.isControlPressed && + !keyboard.isAltPressed && + !keyboard.isMetaPressed && + !keyboard.isShiftPressed; if (_autocompleteSuggestions.isNotEmpty) { if (key == LogicalKeyboardKey.arrowDown) { _moveAutocompleteSelection(1); @@ -586,7 +656,8 @@ class BusyMarkSourceEditorState extends State { _moveAutocompleteSelection(-1); return KeyEventResult.handled; } - if (key == LogicalKeyboardKey.enter || key == LogicalKeyboardKey.tab) { + if (unmodified && + (key == LogicalKeyboardKey.enter || key == LogicalKeyboardKey.tab)) { _applyAutocomplete(_autocompleteSuggestions[_autocompleteSelection]); return KeyEventResult.handled; } @@ -601,22 +672,20 @@ class BusyMarkSourceEditorState extends State { _showAutocomplete(); return KeyEventResult.handled; } - if (widget.onSymbolAction != null) { - final action = key == LogicalKeyboardKey.keyB && keyboard.isControlPressed - ? SourceSymbolAction.declaration - : key == LogicalKeyboardKey.f7 && keyboard.isAltPressed - ? SourceSymbolAction.usages - : key == LogicalKeyboardKey.f6 && keyboard.isShiftPressed - ? SourceSymbolAction.rename - : null; - if (action != null) { - widget.onSymbolAction!(action, _controller.fullSelection.extentOffset); - return KeyEventResult.handled; - } - } final commands = BusyMarkCommandRegistryScope.read(context) ?? BusyMarkCommandCatalog.metadata; + if (widget.onSymbolAction != null) { + for (final entry in sourceSymbolCommands.entries) { + if (commands.shortcutAccepts(entry.value, event, keyboard)) { + widget.onSymbolAction!( + entry.key, + _controller.fullSelection.extentOffset, + ); + return KeyEventResult.handled; + } + } + } if (widget.filePath?.toLowerCase().endsWith('.tree') == true) { final up = commands.shortcutAccepts( BusyMarkCommandIds.treeMoveLineUp, @@ -633,8 +702,9 @@ class BusyMarkSourceEditorState extends State { return KeyEventResult.handled; } } - if (commands.shortcutAccepts(BusyMarkCommandIds.search, event, keyboard)) { - widget.onOpenSearch(); + if (widget.onOpenSearch != null && + commands.shortcutAccepts(BusyMarkCommandIds.search, event, keyboard)) { + widget.onOpenSearch!(); return KeyEventResult.handled; } if (commands.shortcutAccepts( @@ -703,7 +773,7 @@ class BusyMarkSourceEditorState extends State { _outdentSelection(); return KeyEventResult.handled; } - if (key == LogicalKeyboardKey.enter && !keyboard.isShiftPressed) { + if (key == LogicalKeyboardKey.enter && unmodified) { _applyFullEditingValue(SourceCommands.smartEnter(_fullEditingValue())); return KeyEventResult.handled; } @@ -775,21 +845,72 @@ class BusyMarkSourceEditorState extends State { key: _sourceEditorKey, child: KeyedSubtree( key: ValueKey(widget.documentId ?? widget.filePath), - child: Shortcuts( - shortcuts: - (BusyMarkCommandRegistryScope.maybeOf(context) ?? - BusyMarkCommandCatalog.metadata) - .shortcutIntents( - scopes: const {BusyMarkCommandScope.editor}, - intentFor: BusyMarkContextCommandIntent.new, - ), + child: BusyMarkCommandShortcuts( + isComposing: () => _controller.value.composing.isValid, + shortcuts: { + // Other text-editing bindings are dispatched by the + // source input handler. Select All uses the existing + // full-source action, including folded content. + for (final activator + in (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .activators(BusyMarkCommandIds.textSelectAll)) + activator: const BusyMarkContextCommandIntent( + BusyMarkCommandIds.textSelectAll, + ), + ...(BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .blockedDefaultIntents({ + BusyMarkCommandScope.textEditing, + BusyMarkCommandScope.editor, + if (widget.onSymbolAction != null) + BusyMarkCommandScope.sourceSymbol, + if (widget.filePath?.toLowerCase().endsWith( + '.tree', + ) == + true) + BusyMarkCommandScope.tree, + }), + ...(BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .shortcutIntents( + scopes: { + BusyMarkCommandScope.editor, + if (widget.onSymbolAction != null) + BusyMarkCommandScope.sourceSymbol, + }, + intentFor: BusyMarkContextCommandIntent.new, + ), + }, child: Actions( actions: { BusyMarkContextCommandIntent: BusyMarkContextCommandAction( + captureTarget: () { + final source = _controller.fullText; + final selection = _controller.fullSelection; + return BusyMarkContextCommandSnapshot( + () => + mounted && + !_hasActiveComposition && + _controller.fullText == source && + _controller.fullSelection == selection, + ); + }, isCommandEnabled: (commandId) => !_hasActiveComposition && - (commandId == + (!DocumentReadOnlyScope.of(context) || + { + BusyMarkCommandIds.textCopy, + BusyMarkCommandIds.textSelectAll, + BusyMarkCommandIds.editorCopyPlainText, + BusyMarkCommandIds.checkSpelling, + }.contains(commandId)) && + ((widget.onSymbolAction != null && + sourceSymbolCommands.containsValue( + commandId, + )) || + commandId == BusyMarkCommandIds.checkSpelling || commandId.startsWith('editor.') || (widget.filePath?.toLowerCase().endsWith( @@ -801,12 +922,53 @@ class BusyMarkSourceEditorState extends State { BusyMarkCommandIds.treeMoveLineDown, }.contains(commandId)) || { + BusyMarkCommandIds.textSelectAll, + BusyMarkCommandIds.textUndo, + BusyMarkCommandIds.textRedo, BusyMarkCommandIds.textCopy, BusyMarkCommandIds.textCut, BusyMarkCommandIds.textPaste, BusyMarkCommandIds.textPastePlainText, }.contains(commandId)), onCommand: (commandId) { + final symbol = sourceSymbolCommands.entries + .where((e) => e.value == commandId) + .firstOrNull; + if (symbol != null && + widget.onSymbolAction != null) { + widget.onSymbolAction!( + symbol.key, + _controller.fullSelection.extentOffset, + ); + return; + } + if (commandId == + BusyMarkCommandIds.textSelectAll) { + _controller.fullSelection = TextSelection( + baseOffset: 0, + extentOffset: _controller.fullText.length, + ); + return; + } + if (commandId == BusyMarkCommandIds.textUndo || + commandId == BusyMarkCommandIds.textRedo) { + final undo = + commandId == BusyMarkCommandIds.textUndo; + final callback = undo + ? widget.onUndo + : widget.onRedo; + if (callback != null) { + final value = callback(); + if (value != null) { + _applyOwnedUndoValue(value); + } + } else if (undo) { + _undoController.undo(); + } else { + _undoController.redo(); + } + return; + } if (commandId == BusyMarkCommandIds.checkSpelling) { widget.onCheckSpelling?.call(); @@ -940,19 +1102,28 @@ class BusyMarkSourceEditorState extends State { ), label: switch (action) { SourceSymbolAction.declaration => - 'Go to Declaration', + context.l10n.goToDeclaration, SourceSymbolAction.usages => - 'Find Usages', + context.l10n.findUsages, SourceSymbolAction.rename => context.l10n.rename, }, - shortcut: switch (action) { - SourceSymbolAction.declaration => - 'Ctrl+B', - SourceSymbolAction.usages => 'Alt+F7', - SourceSymbolAction.rename => - 'Shift+F6', - }, + shortcut: + (BusyMarkCommandRegistryScope.read( + context, + ) ?? + BusyMarkCommandCatalog + .metadata)[sourceSymbolCommands[action]!] + ?.shortcut + ?.label, + gtkAccelerator: + (BusyMarkCommandRegistryScope.read( + context, + ) ?? + BusyMarkCommandCatalog + .metadata)[sourceSymbolCommands[action]!] + ?.shortcut + ?.gtkAccelerator, ), ], onRefineWithAi: widget.onAiEdit == null @@ -990,32 +1161,22 @@ class BusyMarkSourceEditorState extends State { left: BusyMarkSpacing.sm, child: _SourceLargeFileBanner(), ), - if (widget.searchActive) + if (widget.searchActive && widget.searchControlsVisible) Positioned( top: BusyMarkSpacing.sm, right: BusyMarkSpacing.sm, child: _SourceSearchPanel( + key: _searchPanelKey, result: _searchController.result, + truncated: widget.searchSnapshot?.truncated ?? false, onPrevious: _previousSearchMatch, onNext: _nextSearchMatch, - onToggleCaseSensitive: () => _updateSearchOptions( - widget.searchOptions.copyWith( - caseSensitive: !widget.searchOptions.caseSensitive, - ), - ), - onToggleWholeWord: () => _updateSearchOptions( - widget.searchOptions.copyWith( - wholeWord: !widget.searchOptions.wholeWord, - ), - ), - onToggleRegex: () => _updateSearchOptions( - widget.searchOptions.copyWith( - regex: !widget.searchOptions.regex, - ), - ), replacement: widget.searchReplacement, canReplaceCurrent: - _requestedSearchRange == null && !_searchNavigationFailed, + !DocumentReadOnlyScope.of(context) && + _requestedSearchRange == null && + !_searchNavigationFailed, + canReplaceAll: !DocumentReadOnlyScope.of(context), onReplacementChanged: widget.onSearchReplacementChanged ?? (_) {}, onReplaceCurrent: () => unawaited(_replaceCurrentSearchMatch()), @@ -1135,15 +1296,71 @@ class BusyMarkSourceEditorState extends State { } final options = widget.searchOptions; final previousResult = _searchController.result; + final shared = widget.searchSnapshot; + if (shared != null) { + if (revealCurrentAfterRefresh && + widget.onNavigateSearch != null && + (minimumFullOffset != null || currentIndex != null)) { + _requestSharedSearchNavigation( + matchIndex: currentIndex, + minimumFullOffset: minimumFullOffset, + ); + _searchController.stageOptions(options); + _controller.setSearchResult(_searchController.result); + return; + } + _searchController.stageOptions(options); + if (shared.source != _controller.fullText || + shared.result.options != options) { + _controller.setSearchResult(_searchController.result); + return; + } + final document = _controller.document; + // Only project the authoritative window through the local folding map. + final projected = SourceSearchResult( + options: options, + totalMatchCount: shared.result.totalMatchCount, + firstMatchIndex: shared.result.firstMatchIndex, + currentMatchIndex: shared.result.currentMatchIndex, + matches: [ + for (final match in shared.result.matches) + if (match.fullEnd <= document.fullText.length) + _projectSearchMatch(document, match), + ], + ); + _searchController.acceptResult(projected); + final requested = _requestedSearchRange; + if (requested != null && !shared.searching) { + final index = projected.matches.indexWhere( + (match) => + match.fullStart == requested.start && + match.fullEnd == requested.end, + ); + _searchController.setCurrentMatchIndex( + index < 0 ? null : projected.firstMatchIndex + index, + ); + _searchNavigationFailed = index < 0; + _requestedSearchRange = null; + } else if (widget.onNavigateSearch == null) { + _searchController.setCurrentMatchIndex( + currentIndex ?? + (previousResult.options == options + ? previousResult.currentMatchIndex + : null), + ); + } + _controller.setSearchResult(_searchController.result); + _applySharedSearchNavigation(shared); + return; + } final requestedFirstMatchIndex = firstMatchIndex ?? (previousResult.options == options ? previousResult.firstMatchIndex : 0); - final invalidRegex = sourceSearchOptionsHaveInvalidRegex(options); - _searchController.stageOptions(options, invalidRegex: invalidRegex); + _searchController.stageOptions(options); _controller.setSearchResult(_searchController.result); - if (options.query.isEmpty || invalidRegex) { + if (options.query.isEmpty) { return; } _searchDebounce = Timer(const Duration(milliseconds: 120), () { @@ -1214,6 +1431,96 @@ class BusyMarkSourceEditorState extends State { }); } + SourceSearchMatch _projectSearchMatch( + SourceDocument document, + SourceSearchMatch match, + ) { + final visible = document.fullRangeToVisibleRange( + match.fullStart, + match.fullEnd, + ); + return SourceSearchMatch( + fullStart: match.fullStart, + fullEnd: match.fullEnd, + visibleStart: visible.range.start, + visibleEnd: visible.range.end, + hidden: + visible.clippedByHiddenRange || + visible.range.start == visible.range.end, + ); + } + + void _requestSharedSearchNavigation({ + int? matchIndex, + int? minimumFullOffset, + bool reveal = true, + }) { + final request = SourceSearchNavigationRequest( + documentId: widget.documentId ?? widget.filePath, + source: _controller.fullText, + options: widget.searchOptions, + matchIndex: matchIndex, + minimumFullOffset: minimumFullOffset, + reveal: reveal, + ); + final id = widget.onNavigateSearch?.call(request); + if (id != null && reveal) { + _pendingSharedSearchNavigation = _SharedSearchNavigation( + id, + request, + _controller.fullSelection, + ); + } + } + + void _cancelSharedSearchNavigation() { + final pending = _pendingSharedSearchNavigation; + _pendingSharedSearchNavigation = null; + if (pending != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) widget.onCancelSearchNavigation?.call(pending.id); + }); + } + } + + void _applySharedSearchNavigation(SourceSearchSnapshot snapshot) { + final pending = _pendingSharedSearchNavigation; + if (pending == null) return; + if (snapshot.navigationRequest != pending.id) { + _cancelSharedSearchNavigation(); + return; + } + if (snapshot.searching || + snapshot.appliedNavigation != pending.id || + pending.scheduled) { + return; + } + pending.scheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + pending.scheduled = false; + if (!mounted || + !identical(_pendingSharedSearchNavigation, pending) || + !widget.searchActive || + widget.searchSnapshot?.navigationRequest != pending.id || + widget.searchSnapshot?.appliedNavigation != pending.id || + pending.request.documentId != + (widget.documentId ?? widget.filePath) || + pending.request.options != widget.searchOptions || + pending.request.source != _controller.fullText || + pending.selection != _controller.fullSelection) { + return; + } + _pendingSharedSearchNavigation = null; + if (!_searchNavigationFailed) { + _revealSearchMatch(_searchController.result.currentMatch); + final index = _searchController.result.currentMatchIndex; + if (index != null) { + _requestSharedSearchNavigation(matchIndex: index, reveal: false); + } + } + }); + } + void _refreshSearch({ int? currentIndex, int? firstMatchIndex, @@ -1232,10 +1539,6 @@ class BusyMarkSourceEditorState extends State { ); } - void _updateSearchOptions(SourceSearchOptions options) { - widget.onSearchOptionsChanged(options); - } - void _nextSearchMatch() { _requestedSearchRange = null; _searchNavigationFailed = false; @@ -1266,6 +1569,10 @@ class BusyMarkSourceEditorState extends State { } void _selectSearchMatchIndex(int index, {required bool loadPreviousWindow}) { + if (widget.searchSnapshot != null && widget.onNavigateSearch != null) { + _requestSharedSearchNavigation(matchIndex: index); + return; + } final result = _searchController.result; final storedEnd = result.firstMatchIndex + result.matches.length; if (index >= result.firstMatchIndex && index < storedEnd) { @@ -1284,7 +1591,7 @@ class BusyMarkSourceEditorState extends State { } Future _replaceCurrentSearchMatch({bool findNext = false}) async { - if (_searchController.result.invalidRegex || + if (DocumentReadOnlyScope.of(context) || _requestedSearchRange != null || _searchNavigationFailed) { return; @@ -1300,6 +1607,7 @@ class BusyMarkSourceEditorState extends State { final document = _controller.document; final options = widget.searchOptions; final replacement = widget.searchReplacement; + final selection = _controller.fullSelection; final preview = await _replacementWorker.previewText( source: document.fullText, options: options, @@ -1308,12 +1616,13 @@ class BusyMarkSourceEditorState extends State { targetEnd: currentMatch.fullEnd, ); if (!mounted || + DocumentReadOnlyScope.of(context) || preview == null || !identical(document, _controller.document) || options != widget.searchOptions || replacement != widget.searchReplacement || + selection != _controller.fullSelection || _searchController.result.currentMatchIndex != currentIndex || - preview.invalidRegex || preview.matches.isEmpty) { return; } @@ -1345,6 +1654,7 @@ class BusyMarkSourceEditorState extends State { } Future _replaceAllSearchMatches() async { + if (DocumentReadOnlyScope.of(context)) return; final document = _controller.document; final options = widget.searchOptions; final replacement = widget.searchReplacement; @@ -1354,11 +1664,11 @@ class BusyMarkSourceEditorState extends State { replacement: replacement, ); if (!mounted || + DocumentReadOnlyScope.of(context) || preview == null || !identical(document, _controller.document) || options != widget.searchOptions || replacement != widget.searchReplacement || - preview.invalidRegex || preview.matches.isEmpty) { return; } @@ -1404,11 +1714,87 @@ class BusyMarkSourceEditorState extends State { _focusNode.requestFocus(); _controller.setSearchResult(_searchController.result); setState(() {}); + final source = _controller.fullText; + final documentId = widget.documentId ?? widget.filePath; + final options = widget.searchOptions; + final selection = _controller.fullSelection; + final sourceRevision = widget.searchSnapshot?.sourceRevision; + final sessionRevision = widget.searchSnapshot?.sessionRevision; WidgetsBinding.instance.addPostFrameCallback((_) { - _animateScrollToLine(line); + if (!mounted || + source != _controller.fullText || + documentId != (widget.documentId ?? widget.filePath) || + options != widget.searchOptions || + sourceRevision != widget.searchSnapshot?.sourceRevision || + sessionRevision != widget.searchSnapshot?.sessionRevision || + !widget.searchActive || + selection != _controller.fullSelection) { + return; + } + _animateSearchToLine( + line, + () => + mounted && + source == _controller.fullText && + documentId == (widget.documentId ?? widget.filePath) && + options == widget.searchOptions && + sourceRevision == widget.searchSnapshot?.sourceRevision && + sessionRevision == widget.searchSnapshot?.sessionRevision && + selection == _controller.fullSelection, + ); }); } + void _animateSearchToLine(int line, bool Function() current) { + if (!_scrollController.hasClients || !current()) return; + unawaited( + _scrollController + .animateTo( + _scrollOffsetForLine(line), + duration: BusyMarkMotion.scroll, + curve: Curves.easeOutCubic, + ) + .then((_) { + if (current()) _ensureSearchCaretVisible(); + }), + ); + } + + void _ensureSearchCaretVisible() { + if (!mounted || !_scrollController.hasClients) return; + final root = _sourceEditorKey.currentContext?.findRenderObject(); + final editable = root == null ? null : _findSourceRenderEditable(root); + if (editable == null || !_controller.selection.isValid) return; + final caret = editable.getLocalRectForCaret( + TextPosition(offset: _controller.selection.extentOffset), + ); + final position = _scrollController.position; + const margin = BusyMarkSpacing.sm; + final height = math.min(editable.size.height, position.viewportDimension); + var topMargin = margin; + final panel = _searchPanelKey.currentContext?.findRenderObject(); + if (panel is RenderBox && panel.hasSize) { + final panelBounds = + editable.globalToLocal(panel.localToGlobal(Offset.zero)) & panel.size; + if (caret.right > panelBounds.left && caret.left < panelBounds.right) { + topMargin = math.max( + margin, + math.min(panelBounds.bottom + margin, height - caret.height - margin), + ); + } + } + final delta = caret.top < topMargin + ? caret.top - topMargin + : caret.bottom > height - margin + ? caret.bottom - height + margin + : 0.0; + if (delta != 0) { + position.jumpTo( + (position.pixels + delta).clamp(0.0, position.maxScrollExtent), + ); + } + } + void _recomputeFoldRegions({bool resetCollapsed = false}) { final next = _controller.sourceFeaturesDegraded ? const [] @@ -3430,16 +3816,24 @@ class _CollapsedSpellingPainter extends CustomPainter { oldDelegate.textScaler != textScaler; } +class _SharedSearchNavigation { + _SharedSearchNavigation(this.id, this.request, this.selection); + final int id; + final SourceSearchNavigationRequest request; + final TextSelection selection; + bool scheduled = false; +} + class _SourceSearchPanel extends StatefulWidget { const _SourceSearchPanel({ + super.key, required this.result, + this.truncated = false, required this.onPrevious, required this.onNext, - required this.onToggleCaseSensitive, - required this.onToggleWholeWord, - required this.onToggleRegex, required this.replacement, required this.canReplaceCurrent, + this.canReplaceAll = true, required this.onReplacementChanged, required this.onReplaceCurrent, required this.onReplaceAndFindNext, @@ -3448,13 +3842,12 @@ class _SourceSearchPanel extends StatefulWidget { }); final SourceSearchResult result; + final bool truncated; final VoidCallback onPrevious; final VoidCallback onNext; - final VoidCallback onToggleCaseSensitive; - final VoidCallback onToggleWholeWord; - final VoidCallback onToggleRegex; final String replacement; final bool canReplaceCurrent; + final bool canReplaceAll; final ValueChanged onReplacementChanged; final VoidCallback onReplaceCurrent; final VoidCallback onReplaceAndFindNext; @@ -3467,6 +3860,7 @@ class _SourceSearchPanel extends StatefulWidget { class _SourceSearchPanelState extends State<_SourceSearchPanel> { late final TextEditingController _replacementController; + final _replacementFocus = BusyMarkSearchFocusNode(); @override void initState() { @@ -3486,6 +3880,7 @@ class _SourceSearchPanelState extends State<_SourceSearchPanel> { @override void dispose() { _replacementController.dispose(); + _replacementFocus.dispose(); super.dispose(); } @@ -3493,9 +3888,7 @@ class _SourceSearchPanelState extends State<_SourceSearchPanel> { Widget build(BuildContext context) { final colors = BusyMarkSurfaceColors.of(context); final result = widget.result; - final status = result.invalidRegex - ? context.l10n.sourceSearchInvalidRegex - : result.totalMatchCount == 0 + final status = result.totalMatchCount == 0 ? '0 / 0' : result.currentMatchIndex == null ? '– / ${result.totalMatchCount}' @@ -3510,23 +3903,14 @@ class _SourceSearchPanelState extends State<_SourceSearchPanel> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (result.hasZeroLengthMatches) - Text( - context.l10n.sourceSearchZeroLengthUnsupported, - style: Theme.of(context).textTheme.labelSmall, - ), Row( mainAxisSize: MainAxisSize.min, children: [ Text( - status, - textDirection: result.invalidRegex - ? Directionality.of(context) - : TextDirection.ltr, + widget.truncated ? '$status+' : status, + textDirection: TextDirection.ltr, style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: result.invalidRegex - ? Theme.of(context).colorScheme.error - : colors.mutedForeground, + color: colors.mutedForeground, fontFeatures: const [FontFeature.tabularFigures()], ), ), @@ -3543,24 +3927,6 @@ class _SourceSearchPanelState extends State<_SourceSearchPanel> { icon: YaruIcons.pan_down, onPressed: result.totalMatchCount == 0 ? null : widget.onNext, ), - _SearchOptionButton( - label: 'Aa', - tooltip: context.l10n.sourceSearchCaseSensitive, - selected: result.options.caseSensitive, - onPressed: widget.onToggleCaseSensitive, - ), - _SearchOptionButton( - label: 'W', - tooltip: context.l10n.sourceSearchWholeWord, - selected: result.options.wholeWord, - onPressed: widget.onToggleWholeWord, - ), - _SearchOptionButton( - label: '.*', - tooltip: context.l10n.sourceSearchRegex, - selected: result.options.regex, - onPressed: widget.onToggleRegex, - ), _SearchPanelIconButton( tooltip: context.l10n.close, icon: YaruIcons.window_close, @@ -3575,16 +3941,19 @@ class _SourceSearchPanelState extends State<_SourceSearchPanel> { child: Row( children: [ Expanded( - child: TextField( - key: const ValueKey('source-search-replacement'), - controller: _replacementController, - onChanged: widget.onReplacementChanged, - decoration: InputDecoration( - isDense: true, - hintText: context.l10n.sourceSearchReplacement, - contentPadding: const EdgeInsets.symmetric( - horizontal: BusyMarkSpacing.sm, - vertical: BusyMarkSpacing.xs, + child: BusyMarkLocalTextEditingShortcuts( + child: TextField( + key: const ValueKey('source-search-replacement'), + controller: _replacementController, + focusNode: _replacementFocus, + onChanged: widget.onReplacementChanged, + decoration: InputDecoration( + isDense: true, + hintText: context.l10n.sourceSearchReplacement, + contentPadding: const EdgeInsets.symmetric( + horizontal: BusyMarkSpacing.sm, + vertical: BusyMarkSpacing.xs, + ), ), ), ), @@ -3609,7 +3978,7 @@ class _SourceSearchPanelState extends State<_SourceSearchPanel> { tooltip: context.l10n.sourceSearchReplaceAll, icon: BusyMarkGlyphs.searchUnavailable, onPressed: - result.options.query.isEmpty || result.invalidRegex + !widget.canReplaceAll || result.options.query.isEmpty ? null : widget.onReplaceAll, ), @@ -3645,46 +4014,6 @@ class _SearchPanelIconButton extends StatelessWidget { } } -class _SearchOptionButton extends StatelessWidget { - const _SearchOptionButton({ - required this.label, - required this.tooltip, - required this.selected, - required this.onPressed, - }); - - final String label; - final String tooltip; - final bool selected; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - Widget optionLabel() => Builder( - builder: (context) => Text( - label, - textDirection: TextDirection.ltr, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: IconTheme.of(context).color, - fontWeight: FontWeight.w700, - letterSpacing: 0, - ), - ), - ); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 1), - child: YaruIconButton( - tooltip: tooltip, - iconSize: 28, - isSelected: selected, - onPressed: onPressed, - icon: optionLabel(), - selectedIcon: optionLabel(), - ), - ); - } -} - class _SourceAutocompletePopup extends StatelessWidget { const _SourceAutocompletePopup({ required this.suggestions, @@ -3964,3 +4293,9 @@ String _collapsedLineText(String text) { } return '$trimmed ...'; } + +const sourceSymbolCommands = { + SourceSymbolAction.declaration: BusyMarkCommandIds.sourceDeclaration, + SourceSymbolAction.usages: BusyMarkCommandIds.sourceUsages, + SourceSymbolAction.rename: BusyMarkCommandIds.sourceRename, +}; diff --git a/lib/src/editor/source/source_search.dart b/lib/src/editor/source/source_search.dart index 82d37974..68ec1d16 100644 --- a/lib/src/editor/source/source_search.dart +++ b/lib/src/editor/source/source_search.dart @@ -10,44 +10,18 @@ import 'source_hidden_ranges.dart'; /// navigation. const int sourceInteractiveSearchMatchLimit = 2048; +/// The literal query also identifies asynchronous matching/navigation requests. class SourceSearchOptions { - const SourceSearchOptions({ - this.query = '', - this.caseSensitive = false, - this.wholeWord = false, - this.regex = false, - }); + const SourceSearchOptions({this.query = ''}); final String query; - final bool caseSensitive; - final bool wholeWord; - final bool regex; - - SourceSearchOptions copyWith({ - String? query, - bool? caseSensitive, - bool? wholeWord, - bool? regex, - }) { - return SourceSearchOptions( - query: query ?? this.query, - caseSensitive: caseSensitive ?? this.caseSensitive, - wholeWord: wholeWord ?? this.wholeWord, - regex: regex ?? this.regex, - ); - } @override - bool operator ==(Object other) { - return other is SourceSearchOptions && - other.query == query && - other.caseSensitive == caseSensitive && - other.wholeWord == wholeWord && - other.regex == regex; - } + bool operator ==(Object other) => + other is SourceSearchOptions && other.query == query; @override - int get hashCode => Object.hash(query, caseSensitive, wholeWord, regex); + int get hashCode => query.hashCode; } class SourceSearchMatch { @@ -75,8 +49,6 @@ class SourceSearchResult { this.currentMatchIndex, int? totalMatchCount, this.firstMatchIndex = 0, - this.invalidRegex = false, - this.hasZeroLengthMatches = false, }) : totalMatchCount = totalMatchCount ?? matches.length; static final empty = SourceSearchResult( @@ -93,8 +65,6 @@ class SourceSearchResult { /// The complete-result index represented by [matches.first]. final int firstMatchIndex; - final bool invalidRegex; - final bool hasZeroLengthMatches; SourceSearchMatch? get currentMatch { final index = currentMatchIndex; @@ -115,8 +85,6 @@ class SourceSearchResult { currentMatchIndex: currentMatchIndex, totalMatchCount: totalMatchCount, firstMatchIndex: firstMatchIndex, - invalidRegex: invalidRegex, - hasZeroLengthMatches: hasZeroLengthMatches, ); } @@ -127,8 +95,6 @@ class SourceSearchResult { other.currentMatchIndex == currentMatchIndex && other.totalMatchCount == totalMatchCount && other.firstMatchIndex == firstMatchIndex && - other.invalidRegex == invalidRegex && - other.hasZeroLengthMatches == hasZeroLengthMatches && _matchesEqual(other.matches, matches); } @@ -138,12 +104,50 @@ class SourceSearchResult { currentMatchIndex, totalMatchCount, firstMatchIndex, - invalidRegex, - hasZeroLengthMatches, Object.hashAll(matches.map(_matchHash)), ); } +/// A source editor projects the window's matches through its folding map. +/// It does not start another search while this snapshot is supplied. +class SourceSearchSnapshot { + const SourceSearchSnapshot({ + required this.source, + required this.result, + this.truncated = false, + this.searching = false, + this.navigationRequest = 0, + this.appliedNavigation = 0, + this.sourceRevision, + this.sessionRevision = 0, + }); + final String source; + final SourceSearchResult result; + final bool truncated; + final bool searching; + final int navigationRequest, appliedNavigation; + final int? sourceRevision; + final int sessionRevision; +} + +/// A navigation request for the window's existing search session. The source +/// and identity must still match the active buffer before resolving an offset. +class SourceSearchNavigationRequest { + const SourceSearchNavigationRequest({ + required this.documentId, + required this.source, + required this.options, + this.matchIndex, + this.minimumFullOffset, + this.reveal = true, + }); + final String? documentId; + final String source; + final SourceSearchOptions options; + final int? matchIndex, minimumFullOffset; + final bool reveal; +} + class SourceSearchController { SourceSearchController({ SourceSearchOptions options = const SourceSearchOptions(), @@ -159,19 +163,10 @@ class SourceSearchController { int get version => _version; - void setOptions(SourceSearchOptions options, SourceDocument document) { - _options = options; - refresh(document); - } - - void stageOptions(SourceSearchOptions options, {bool invalidRegex = false}) { + void stageOptions(SourceSearchOptions options) { _options = options; _version++; - _result = SourceSearchResult( - options: options, - matches: const [], - invalidRegex: invalidRegex, - ); + _result = SourceSearchResult(options: options, matches: const []); } void acceptResult(SourceSearchResult result) { @@ -253,10 +248,11 @@ class SourceSearchController { } } -/// Runs potentially expensive regular expressions away from Flutter's UI +/// Runs literal matching and full-document counting away from Flutter's UI /// isolate. Starting a newer request kills the preceding worker immediately. class SourceSearchWorker { Isolate? _isolate; + SendPort? _sessionPort; ReceivePort? _receivePort; Completer? _completer; var _generation = 0; @@ -269,49 +265,65 @@ class SourceSearchWorker { int? minimumFullOffset, int maximumMatches = sourceInteractiveSearchMatchLimit, bool stopAfterMaximumMatches = false, + bool reuseWorker = false, }) { - cancel(); + final reuse = reuseWorker && _sessionPort != null && _completer == null; + if (!reuse) cancel(); final generation = ++_generation; final receivePort = ReceivePort(); final completer = Completer(); _receivePort = receivePort; _completer = completer; + late final Map request; receivePort.listen((message) { if (generation != _generation || completer.isCompleted) { return; } + if (message is SendPort && reuseWorker) { + _sessionPort = message; + message.send([receivePort.sendPort, request]); + return; + } if (message is Map) { completer.complete(_decodeSearchResult(message, options)); } else { completer.complete(null); } - _releaseWorker(kill: true); + if (reuseWorker) { + _receivePort?.close(); + _receivePort = null; + _completer = null; + } else { + _releaseWorker(kill: true); + } }); - final request = { + request = { 'text': document.fullText, 'hidden': [ for (final range in document.hiddenRanges.ranges) [range.start, range.end, range.key], ], 'query': options.query, - 'caseSensitive': options.caseSensitive, - 'wholeWord': options.wholeWord, - 'regex': options.regex, 'currentMatchIndex': currentMatchIndex, 'firstMatchIndex': firstMatchIndex, 'minimumFullOffset': minimumFullOffset, 'maximumMatches': maximumMatches, 'stopAfterMaximumMatches': stopAfterMaximumMatches, }; + if (reuse) { + _sessionPort!.send([receivePort.sendPort, request]); + return completer.future; + } Isolate.spawn>( - _sourceSearchWorkerMain, + reuseWorker ? _sourceSearchSessionMain : _sourceSearchWorkerMain, [receivePort.sendPort, request], debugName: 'BusyMark source search', onError: receivePort.sendPort, onExit: receivePort.sendPort, ) .then((isolate) { - if (generation != _generation || completer.isCompleted) { + if (generation != _generation || + completer.isCompleted && !reuseWorker) { isolate.kill(priority: Isolate.immediate); } else { _isolate = isolate; @@ -342,23 +354,34 @@ class SourceSearchWorker { _isolate?.kill(priority: Isolate.immediate); } _isolate = null; + _sessionPort = null; _receivePort?.close(); _receivePort = null; _completer = null; } } +// One explicitly scoped workspace query can reuse its worker for sequential +// files. Cancelling or changing the query still kills the isolate immediately. +void _sourceSearchSessionMain(List payload) { + final requests = ReceivePort(); + (payload[0] as SendPort).send(requests.sendPort); + requests.listen((request) { + final message = (request as List); + try { + _sourceSearchWorkerMain(message); + } on Object { + (message[0] as SendPort).send(null); + } + }); +} + void _sourceSearchWorkerMain(List payload) { final sendPort = payload[0] as SendPort; final request = payload[1] as Map; final text = request['text']! as String; final hidden = request['hidden']! as List; - final options = SourceSearchOptions( - query: request['query']! as String, - caseSensitive: request['caseSensitive']! as bool, - wholeWord: request['wholeWord']! as bool, - regex: request['regex']! as bool, - ); + final options = SourceSearchOptions(query: request['query']! as String); final document = SourceDocument( fullText: text, hiddenRanges: SourceHiddenRanges( @@ -383,8 +406,6 @@ void _sourceSearchWorkerMain(List payload) { stopAfterMaximumMatches: request['stopAfterMaximumMatches']! as bool, ); sendPort.send({ - 'invalidRegex': result.invalidRegex, - 'hasZeroLengthMatches': result.hasZeroLengthMatches, 'currentMatchIndex': result.currentMatchIndex, 'totalMatchCount': result.totalMatchCount, 'firstMatchIndex': result.firstMatchIndex, @@ -421,28 +442,9 @@ SourceSearchResult _decodeSearchResult( currentMatchIndex: payload['currentMatchIndex'] as int?, totalMatchCount: payload['totalMatchCount']! as int, firstMatchIndex: payload['firstMatchIndex']! as int, - invalidRegex: payload['invalidRegex']! as bool, - hasZeroLengthMatches: payload['hasZeroLengthMatches']! as bool, ); } -bool sourceSearchOptionsHaveInvalidRegex(SourceSearchOptions options) { - if (!options.regex || options.query.isEmpty) { - return false; - } - try { - RegExp( - options.query, - caseSensitive: options.caseSensitive, - multiLine: true, - unicode: true, - ); - return false; - } on FormatException { - return true; - } -} - class SourceWorkspaceSearchFileResult { const SourceWorkspaceSearchFileResult({ required this.filePath, @@ -493,28 +495,7 @@ SourceSearchResult searchSourceDocument( return SourceSearchResult(options: options, matches: const []); } - late final Iterable<({int start, int end})> rawMatches; - if (options.regex) { - try { - rawMatches = - RegExp( - query, - caseSensitive: options.caseSensitive, - multiLine: true, - unicode: true, - ) - .allMatches(document.fullText) - .map((match) => (start: match.start, end: match.end)); - } on FormatException { - return SourceSearchResult( - options: options, - matches: const [], - invalidRegex: true, - ); - } - } else { - rawMatches = _plainMatches(document.fullText, query, options.caseSensitive); - } + final rawMatches = _plainMatches(document.fullText, query); final requestedFirstMatchIndex = firstMatchIndex < 0 ? 0 : firstMatchIndex; final matchLimit = maximumMatches?.clamp(0, 0x7fffffff).toInt(); @@ -522,12 +503,7 @@ SourceSearchResult searchSourceDocument( var totalMatchCount = 0; var storedFirstMatchIndex = requestedFirstMatchIndex; var foundOffsetWindow = minimumFullOffset == null; - var hasZeroLengthMatches = false; for (final match in rawMatches) { - if (match.start == match.end) { - hasZeroLengthMatches = true; - continue; - } if (!sourceSearchRangeHasSafeBoundaries( document.fullText, match.start, @@ -535,10 +511,6 @@ SourceSearchResult searchSourceDocument( )) { continue; } - if (options.wholeWord && - !_isWholeWord(document.fullText, match.start, match.end)) { - continue; - } final matchIndex = totalMatchCount++; if (!foundOffsetWindow) { if (match.start < minimumFullOffset!) { @@ -585,7 +557,6 @@ SourceSearchResult searchSourceDocument( : null, totalMatchCount: totalMatchCount, firstMatchIndex: storedFirstMatchIndex, - hasZeroLengthMatches: hasZeroLengthMatches, ); } @@ -605,11 +576,10 @@ bool sourceSearchRangeHasSafeBoundaries(String source, int start, int end) { Iterable<({int start, int end})> _plainMatches( String source, String query, - bool caseSensitive, ) sync* { final expression = RegExp( RegExp.escape(query), - caseSensitive: caseSensitive, + caseSensitive: false, unicode: true, ); for (final match in expression.allMatches(source)) { @@ -617,47 +587,6 @@ Iterable<({int start, int end})> _plainMatches( } } -bool _isWholeWord(String source, int start, int end) { - return !_isUnicodeWordCharacter(_characterBefore(source, start)) && - !_isUnicodeWordCharacter(_characterAt(source, end)); -} - -final _unicodeWordCharacter = RegExp(r'^[\p{L}\p{N}\p{M}_]$', unicode: true); - -bool _isUnicodeWordCharacter(String? character) { - if (character == null) { - return false; - } - return _unicodeWordCharacter.hasMatch(character); -} - -String? _characterBefore(String source, int offset) { - if (offset <= 0 || source.isEmpty) { - return null; - } - final end = offset.clamp(0, source.length).toInt(); - var start = end - 1; - if (_isLowSurrogate(source.codeUnitAt(start)) && - start > 0 && - _isHighSurrogate(source.codeUnitAt(start - 1))) { - start--; - } - return source.substring(start, end); -} - -String? _characterAt(String source, int offset) { - if (offset < 0 || offset >= source.length) { - return null; - } - var end = offset + 1; - if (_isHighSurrogate(source.codeUnitAt(offset)) && - end < source.length && - _isLowSurrogate(source.codeUnitAt(end))) { - end++; - } - return source.substring(offset, end); -} - bool _isHighSurrogate(int unit) => unit >= 0xD800 && unit <= 0xDBFF; bool _isLowSurrogate(int unit) => unit >= 0xDC00 && unit <= 0xDFFF; diff --git a/lib/src/editor/wysiwyg/writerside_editing_adapter.dart b/lib/src/editor/wysiwyg/writerside_editing_adapter.dart index 506b6f33..fdc23ecf 100644 --- a/lib/src/editor/wysiwyg/writerside_editing_adapter.dart +++ b/lib/src/editor/wysiwyg/writerside_editing_adapter.dart @@ -370,6 +370,12 @@ class WritersideEditingAdapter { filePath: filePath, source: source, ); + return fromAuthoredXml(authored); + } + + /// Projects a validated, unresolved authored tree without parsing it again. + /// Callers must match the tree's path and source to the editor revision. + BusyDocument? fromAuthoredXml(WritersideDocument authored) { if (!authored.isWellFormed || authored.rootElement?.name != 'topic' || authored.nodes.whereType().length != 1) { @@ -377,9 +383,9 @@ class WritersideEditingAdapter { } var index = 0; return BusyDocument( - filePath: filePath, + filePath: authored.filePath, mode: MarkdownMode.writersideMarkdown, - source: source, + source: authored.source, authoredXml: authored, title: authored.rootElement?.attributes['title'], blocks: [ diff --git a/lib/src/editor/wysiwyg/writerside_properties.dart b/lib/src/editor/wysiwyg/writerside_properties.dart index 460d66ec..bfb38b2e 100644 --- a/lib/src/editor/wysiwyg/writerside_properties.dart +++ b/lib/src/editor/wysiwyg/writerside_properties.dart @@ -1083,12 +1083,19 @@ class BusyMarkWritersideProperties extends StatelessWidget { BusyMarkCommandCatalog.metadata; return Shortcuts( shortcuts: { + ...commands.blockedDefaultIntents(const { + BusyMarkCommandScope.textEditing, + }), if (!readOnly && onUndo != null) - commands['text.undo']!.shortcut!.activator: - const _PropertyHistoryIntent(false), + ...commands.bindingIntents( + 'text.undo', + const _PropertyHistoryIntent(false), + ), if (!readOnly && onRedo != null) - commands['text.redo']!.shortcut!.activator: - const _PropertyHistoryIntent(true), + ...commands.bindingIntents( + 'text.redo', + const _PropertyHistoryIntent(true), + ), }, child: Actions( actions: { diff --git a/lib/src/editor/wysiwyg/wysiwyg_document_controller.dart b/lib/src/editor/wysiwyg/wysiwyg_document_controller.dart index 1da726b6..0159079d 100644 --- a/lib/src/editor/wysiwyg/wysiwyg_document_controller.dart +++ b/lib/src/editor/wysiwyg/wysiwyg_document_controller.dart @@ -60,6 +60,20 @@ BusyBlock busyMarkWysiwygImmutableBlockSnapshot(BusyBlock block) { enum BusyWysiwygReplacementScope { fieldContent, documentRange } +class _BlockPosition { + const _BlockPosition(this.block, this.parent, this.rootIndex); + final BusyBlock block; + final _BlockPosition? parent; + final int rootIndex; +} + +class _HeadingPosition { + const _HeadingPosition(this.block, this.parent, this.level); + final BusyBlock block; + final _HeadingPosition? parent; + final int level; +} + class BusyMarkWysiwygDocumentController extends ChangeNotifier { BusyMarkWysiwygDocumentController({ required BusyDocument document, @@ -72,6 +86,40 @@ class BusyMarkWysiwygDocumentController extends ChangeNotifier { BusyDocument _document; final BusyMarkMarkdownSerializer _serializer; int _generatedBlockIndex = 0; + BusyDocument? _indexedDocument; + final _positions = {}; + final _headingsByRoot = <_HeadingPosition?>[]; + + void _ensureBlockIndex() { + if (identical(_indexedDocument, _document)) return; + _positions.clear(); + _headingsByRoot.clear(); + void visit(BusyBlock block, _BlockPosition? parent, int rootIndex) { + final position = _BlockPosition(block, parent, rootIndex); + // Match the existing depth-first first-match behavior, including models + // with duplicate IDs. Parent links use positions, rather than ID lookups. + _positions.putIfAbsent(block.id, () => position); + for (final child in block.children) { + visit(child, position, rootIndex); + } + } + + _HeadingPosition? heading; + final firstRootIndices = {}; + for (var i = 0; i < _document.blocks.length; i++) { + final block = _document.blocks[i]; + if (block.kind == BusyBlockKind.heading) { + final level = int.tryParse(block.attributes['level'] ?? '') ?? 1; + while (heading != null && heading.level >= level) { + heading = heading.parent; + } + heading = _HeadingPosition(block, heading, level); + } + _headingsByRoot.add(heading); + visit(block, null, firstRootIndices.putIfAbsent(block.id, () => i)); + } + _indexedDocument = _document; + } BusyDocument get document => _document; @@ -215,6 +263,7 @@ class BusyMarkWysiwygDocumentController extends ChangeNotifier { @override void notifyListeners() { + _indexedDocument = null; _document = _ensureEditableDocument(_document); super.notifyListeners(); } @@ -232,30 +281,27 @@ class BusyMarkWysiwygDocumentController extends ChangeNotifier { } List elementPath(String blockId) { - List? find(List blocks) { - for (final block in blocks) { - if (block.id == blockId) return [block]; - final path = find(block.children); - if (path != null) return [block, ...path]; - } - return null; - } - - final path = find(_document.blocks) ?? []; + _ensureBlockIndex(); + final position = _positions[blockId]; + if (position == null) return []; + final reversed = []; + for (var current = position; ;) { + reversed.add(current.block); + final parent = current.parent; + if (parent == null) break; + current = parent; + } + final path = reversed.reversed.toList(); if (!_document.isXmlTopic && path.isNotEmpty) { - final rootIndex = _document.blocks.indexWhere( - (b) => b.id == path.first.id, - ); final headings = []; - for (final candidate in _document.blocks.take(rootIndex + 1)) { - if (candidate.kind != BusyBlockKind.heading) continue; - final level = int.tryParse(candidate.attributes['level'] ?? '') ?? 1; - headings.removeWhere( - (h) => (int.tryParse(h.attributes['level'] ?? '') ?? 1) >= level, - ); - if (candidate.id != blockId) headings.add(candidate); + for ( + var heading = _headingsByRoot[position.rootIndex]; + heading != null; + heading = heading.parent + ) { + if (heading.block.id != blockId) headings.add(heading.block); } - return [...headings, ...path]; + return [...headings.reversed, ...path]; } return path; } @@ -2178,12 +2224,8 @@ class BusyMarkWysiwygDocumentController extends ChangeNotifier { } BusyBlock? blockById(String blockId) { - for (final block in _flatten(_document.blocks)) { - if (block.id == blockId) { - return block; - } - } - return null; + _ensureBlockIndex(); + return _positions[blockId]?.block; } String admonitionTargetId(String blockId) { diff --git a/lib/src/editor/wysiwyg/wysiwyg_editor.dart b/lib/src/editor/wysiwyg/wysiwyg_editor.dart index e18c682b..2bad3006 100644 --- a/lib/src/editor/wysiwyg/wysiwyg_editor.dart +++ b/lib/src/editor/wysiwyg/wysiwyg_editor.dart @@ -1,3 +1,5 @@ +import '../../app/command_shortcuts.dart'; +import 'package:flutter/foundation.dart' show listEquals; import '../../writerside/writerside_effective_properties.dart'; import '../../writerside/writerside_model.dart'; import '../link_destination.dart'; @@ -829,31 +831,51 @@ class BusyMarkWysiwygEditorState extends State { } } final selectedBlockIds = _fullySelectedBlockIds(blocks); - return Shortcuts( + return BusyMarkCommandShortcuts( + isComposing: () => _hasActiveClipboardComposition, shortcuts: { + ...commandRegistry.blockedDefaultIntents(const { + BusyMarkCommandScope.textEditing, + BusyMarkCommandScope.editor, + BusyMarkCommandScope.wysiwyg, + }), ...commandRegistry.shortcutIntents( - scopes: const {BusyMarkCommandScope.editor}, + scopes: const { + BusyMarkCommandScope.editor, + BusyMarkCommandScope.wysiwyg, + }, intentFor: BusyMarkContextCommandIntent.new, ), - commandRegistry[BusyMarkCommandIds.textPaste]!.shortcut!.activator: - const _PasteTextIntent(BusyMarkPasteMode.normal), - commandRegistry[BusyMarkCommandIds.textPastePlainText]! - .shortcut! - .activator: const _PasteTextIntent( - BusyMarkPasteMode.plainText, + ...commandRegistry.bindingIntents( + BusyMarkCommandIds.textPaste, + const _PasteTextIntent(BusyMarkPasteMode.normal), + ), + ...commandRegistry.bindingIntents( + BusyMarkCommandIds.textPastePlainText, + const _PasteTextIntent(BusyMarkPasteMode.plainText), + ), + ...commandRegistry.bindingIntents( + BusyMarkCommandIds.textSelectAll, + const _SelectAllTextIntent(), + ), + ...commandRegistry.bindingIntents( + 'text.undo', + const _UndoEditorIntent(), + ), + ...commandRegistry.bindingIntents( + 'text.redo', + const _RedoEditorIntent(), ), - commandRegistry[BusyMarkCommandIds.textSelectAll]!.shortcut!.activator: - const _SelectAllTextIntent(), - commandRegistry['text.undo']!.shortcut!.activator: - const _UndoEditorIntent(), - commandRegistry['text.redo']!.shortcut!.activator: - const _RedoEditorIntent(), if (blockSelectionActive) - commandRegistry[BusyMarkCommandIds.textCopy]!.shortcut!.activator: - const _CopyBlockSelectionIntent(), + ...commandRegistry.bindingIntents( + BusyMarkCommandIds.textCopy, + const _CopyBlockSelectionIntent(), + ), if (blockSelectionActive) - commandRegistry[BusyMarkCommandIds.textCut]!.shortcut!.activator: - const _CutBlockSelectionIntent(), + ...commandRegistry.bindingIntents( + BusyMarkCommandIds.textCut, + const _CutBlockSelectionIntent(), + ), if (blockSelectionActive) const SingleActivator(LogicalKeyboardKey.backspace): const _DeleteBlockSelectionIntent(), @@ -861,12 +883,35 @@ class BusyMarkWysiwygEditorState extends State { const SingleActivator(LogicalKeyboardKey.delete): const _DeleteBlockSelectionIntent(), if (blockSelectionActive) - commandRegistry['text.escape']!.shortcut!.activator: - const _ClearBlockSelectionIntent(), + ...commandRegistry.bindingIntents( + 'text.escape', + const _ClearBlockSelectionIntent(), + ), }, child: Actions( actions: { BusyMarkContextCommandIntent: BusyMarkContextCommandAction( + captureTarget: () { + final guard = _captureDialogTarget(); + final blockId = _activeBlockId, cellId = _activeCellId; + final target = _activeTextTarget(); + final selection = target?.controller.selection; + final ranges = [ + for (final r in _selectedTextRanges()) + (r.block.id, r.start, r.end), + ]; + return BusyMarkContextCommandSnapshot( + () => + _isDialogTargetCurrent(guard) && + blockId == _activeBlockId && + cellId == _activeCellId && + selection == _activeTextTarget()?.controller.selection && + listEquals(ranges, [ + for (final r in _selectedTextRanges()) + (r.block.id, r.start, r.end), + ]), + ); + }, isCommandEnabled: _canApplyContextCommand, onCommand: _applyContextCommand, ), @@ -4171,7 +4216,8 @@ class BusyMarkWysiwygEditorState extends State { } return KeyEventResult.handled; } - if (commands.shortcutAccepts(BusyMarkCommandIds.search, event, keyboard)) { + if (widget.onOpenSearch != null && + commands.shortcutAccepts(BusyMarkCommandIds.search, event, keyboard)) { widget.onOpenSearch?.call(); return KeyEventResult.handled; } @@ -4201,6 +4247,13 @@ class BusyMarkWysiwygEditorState extends State { _applyEditorShortcutAction(shortcutAction); return KeyEventResult.handled; } + if (commands.blocksReplacedDefault(event, keyboard, const { + BusyMarkCommandScope.editor, + BusyMarkCommandScope.wysiwyg, + BusyMarkCommandScope.textEditing, + })) { + return KeyEventResult.handled; + } if (event is KeyDownEvent && key == LogicalKeyboardKey.enter && keyboard.isControlPressed && @@ -4442,7 +4495,8 @@ class BusyMarkWysiwygEditorState extends State { } return KeyEventResult.handled; } - if (commands.shortcutAccepts(BusyMarkCommandIds.search, event, keyboard)) { + if (widget.onOpenSearch != null && + commands.shortcutAccepts(BusyMarkCommandIds.search, event, keyboard)) { widget.onOpenSearch?.call(); return KeyEventResult.handled; } @@ -5649,6 +5703,33 @@ class BusyMarkWysiwygEditorState extends State { } bool _canApplyContextCommand(String commandId) { + if (DocumentReadOnlyScope.of(context) && + !{ + BusyMarkCommandIds.textSelectAll, + BusyMarkCommandIds.textCopy, + BusyMarkCommandIds.editorCopyPlainText, + BusyMarkCommandIds.checkSpelling, + }.contains(commandId)) { + return false; + } + final writerside = writersideCommands[commandId]; + if (writerside != null) { + if (DocumentReadOnlyScope.of(context) || + !_documentController.document.isWriterside) { + return false; + } + return switch (writerside) { + BusyWritersideToolbarInsert(:final command) => + _documentController.canInsertWriterside(command, _activeBlockId) && + (!{ + BusyWritersideInsertCommand.include, + BusyWritersideInsertCommand.variable, + }.contains(command) || + widget.writersideProjectIndex != null), + BusyWritersideToolbarAdmonition() => _canApplyAdmonitionCommand(), + BusyWritersideToolbarSemantic() => _hasSemanticCommandTarget, + }; + } if ((commandId == BusyMarkCommandIds.textPaste || commandId == BusyMarkCommandIds.textPastePlainText) && _hasActiveClipboardComposition) { @@ -5656,9 +5737,49 @@ class BusyMarkWysiwygEditorState extends State { } if (commandId.startsWith('editor.')) { final name = commandId.substring('editor.'.length); - return BusyMarkEditorShortcutAction.values.any( - (action) => action.name == name, - ); + final action = BusyMarkEditorShortcutAction.values + .where((a) => a.name == name) + .firstOrNull; + if (action == null) return false; + return switch (action) { + BusyMarkEditorShortcutAction.bold || + BusyMarkEditorShortcutAction.italic || + BusyMarkEditorShortcutAction.underline || + BusyMarkEditorShortcutAction.strikethrough || + BusyMarkEditorShortcutAction.inlineCode || + BusyMarkEditorShortcutAction.link => _hasInlineCommandTarget, + BusyMarkEditorShortcutAction.paragraph => _canApplyBlockCommand( + BusyWysiwygBlockCommand.paragraph, + ), + BusyMarkEditorShortcutAction.heading1 => _canApplyBlockCommand( + BusyWysiwygBlockCommand.heading1, + ), + BusyMarkEditorShortcutAction.heading2 => _canApplyBlockCommand( + BusyWysiwygBlockCommand.heading2, + ), + BusyMarkEditorShortcutAction.heading3 => _canApplyBlockCommand( + BusyWysiwygBlockCommand.heading3, + ), + BusyMarkEditorShortcutAction.heading4 => _canApplyBlockCommand( + BusyWysiwygBlockCommand.heading4, + ), + BusyMarkEditorShortcutAction.heading5 => _canApplyBlockCommand( + BusyWysiwygBlockCommand.heading5, + ), + BusyMarkEditorShortcutAction.heading6 => _canApplyBlockCommand( + BusyWysiwygBlockCommand.heading6, + ), + BusyMarkEditorShortcutAction.orderedList => _canApplyBlockCommand( + BusyWysiwygBlockCommand.orderedList, + ), + BusyMarkEditorShortcutAction.unorderedList => _canApplyBlockCommand( + BusyWysiwygBlockCommand.unorderedList, + ), + BusyMarkEditorShortcutAction.blockquote => _canApplyBlockCommand( + BusyWysiwygBlockCommand.blockquote, + ), + _ => true, + }; } if (commandId == BusyMarkCommandIds.checkSpelling) { return widget.onCheckSpelling != null; @@ -5676,6 +5797,30 @@ class BusyMarkWysiwygEditorState extends State { } void _applyContextCommand(String commandId) { + final writerside = writersideCommands[commandId]; + if (writerside != null) { + if (!_canApplyContextCommand(commandId)) return; + switch (writerside) { + case BusyWritersideToolbarInsert(:final command): + unawaited( + _applyWritersideInsert( + command, + _captureDialogTarget(), + _activeBlockId, + ), + ); + case BusyWritersideToolbarAdmonition(:final style): + _applyToolbarCommand(() => _applyAdmonitionCommand(style)); + case BusyWritersideToolbarSemantic(:final command): + if (_selectedTextRanges().isEmpty && + _activeTextTarget()?.controller.selection.isCollapsed == true) { + unawaited(_insertSemanticText(command)); + } else { + _applyToolbarCommand(() => _applyInlineCommand(command)); + } + } + return; + } if (commandId == BusyMarkCommandIds.checkSpelling) { widget.onCheckSpelling?.call(); return; @@ -8057,6 +8202,7 @@ class BusyMarkWysiwygEditorState extends State { label: command.label(context), icon: icon, shortcut: command.shortcut?.label, + gtkAccelerator: command.shortcut?.gtkAccelerator, ); } diff --git a/lib/src/editor/wysiwyg/wysiwyg_toolbar.dart b/lib/src/editor/wysiwyg/wysiwyg_toolbar.dart index 4ee4dfcd..7bee489a 100644 --- a/lib/src/editor/wysiwyg/wysiwyg_toolbar.dart +++ b/lib/src/editor/wysiwyg/wysiwyg_toolbar.dart @@ -1,8 +1,9 @@ +import '../../app/busymark_shortcuts.dart' show BusyMarkShortcutDefinition; import 'package:flutter/material.dart'; import '../../app/busymark_design.dart'; import '../../app/busymark_glyphs.dart'; -import '../../app/busymark_shortcuts.dart'; +import '../../app/command_registry.dart'; import '../../app/localization.dart'; import '../../markdown/busymark_document.dart'; import 'wysiwyg_commands.dart'; @@ -119,7 +120,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.bold, icon: BusyMarkGlyphs.bold, - shortcut: BusyMarkEditorShortcutLabels.bold, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.bold'] + ?.shortcut + ?.label, onPressed: inlineCommandsEnabled ? () => onInlineCommand(BusyWysiwygInlineCommand.bold) : null, @@ -128,7 +133,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.italic, icon: BusyMarkGlyphs.italic, - shortcut: BusyMarkEditorShortcutLabels.italic, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.italic'] + ?.shortcut + ?.label, onPressed: inlineCommandsEnabled ? () => onInlineCommand(BusyWysiwygInlineCommand.italic) : null, @@ -137,7 +146,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.underline, icon: BusyMarkGlyphs.underline, - shortcut: BusyMarkEditorShortcutLabels.underline, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.underline'] + ?.shortcut + ?.label, onPressed: inlineCommandsEnabled ? () => onInlineCommand(BusyWysiwygInlineCommand.underline) : null, @@ -146,7 +159,12 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.strikethrough, icon: BusyMarkGlyphs.strikethrough, - shortcut: BusyMarkEditorShortcutLabels.strikethrough, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog + .metadata)['editor.strikethrough'] + ?.shortcut + ?.label, onPressed: inlineCommandsEnabled ? () => onInlineCommand(BusyWysiwygInlineCommand.strikethrough) @@ -156,7 +174,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.inlineCode, icon: BusyMarkGlyphs.code, - shortcut: BusyMarkEditorShortcutLabels.inlineCode, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.inlineCode'] + ?.shortcut + ?.label, onPressed: inlineCommandsEnabled ? () => onInlineCommand(BusyWysiwygInlineCommand.code) : null, @@ -165,7 +187,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.link, icon: BusyMarkGlyphs.link, - shortcut: BusyMarkEditorShortcutLabels.link, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.link'] + ?.shortcut + ?.label, onPressed: inlineCommandsEnabled ? onLinkCommand : null, ), _button( @@ -178,7 +204,12 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.hardLineBreak, icon: BusyMarkGlyphs.hardBreak, - shortcut: BusyMarkEditorShortcutLabels.hardLineBreak, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog + .metadata)['editor.hardLineBreak'] + ?.shortcut + ?.label, onPressed: inlineCommandsEnabled && lineBreakCommandsEnabled ? onHardBreakCommand : null, @@ -198,7 +229,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.blockquote, icon: BusyMarkGlyphs.blockquote, - shortcut: BusyMarkEditorShortcutLabels.blockquote, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.blockquote'] + ?.shortcut + ?.label, onPressed: _blockCommandEnabled(BusyWysiwygBlockCommand.blockquote) ? () => onBlockCommand(BusyWysiwygBlockCommand.blockquote) @@ -208,7 +243,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.codeBlock, icon: BusyMarkGlyphs.codeBlock, - shortcut: BusyMarkEditorShortcutLabels.codeBlock, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.codeBlock'] + ?.shortcut + ?.label, onPressed: _blockCommandEnabled(BusyWysiwygBlockCommand.codeBlock) ? () => onBlockCommand(BusyWysiwygBlockCommand.codeBlock) : null, @@ -229,7 +268,12 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.thematicBreak, icon: BusyMarkGlyphs.thematicBreak, - shortcut: BusyMarkEditorShortcutLabels.thematicBreak, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog + .metadata)['editor.thematicBreak'] + ?.shortcut + ?.label, onPressed: _blockCommandEnabled(BusyWysiwygBlockCommand.thematicBreak) ? () => onBlockCommand(BusyWysiwygBlockCommand.thematicBreak) @@ -241,7 +285,12 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.unorderedList, icon: BusyMarkGlyphs.unorderedList, - shortcut: BusyMarkEditorShortcutLabels.unorderedList, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog + .metadata)['editor.unorderedList'] + ?.shortcut + ?.label, onPressed: _blockCommandEnabled(BusyWysiwygBlockCommand.unorderedList) ? () => onBlockCommand(BusyWysiwygBlockCommand.unorderedList) @@ -251,7 +300,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.orderedList, icon: BusyMarkGlyphs.orderedList, - shortcut: BusyMarkEditorShortcutLabels.orderedList, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.orderedList'] + ?.shortcut + ?.label, onPressed: _blockCommandEnabled(BusyWysiwygBlockCommand.orderedList) ? () => onBlockCommand(BusyWysiwygBlockCommand.orderedList) @@ -261,7 +314,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.taskList, icon: BusyMarkGlyphs.checkedBox, - shortcut: BusyMarkEditorShortcutLabels.taskList, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.taskList'] + ?.shortcut + ?.label, onPressed: _blockCommandEnabled(BusyWysiwygBlockCommand.taskList) ? () => onBlockCommand(BusyWysiwygBlockCommand.taskList) : null, @@ -269,6 +326,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { _button( context, tooltip: context.l10n.toggleTaskChecked, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.toggleTask'] + ?.shortcut + ?.label, icon: BusyMarkGlyphs.checkedBox, onPressed: onToggleTaskCommand, ), @@ -276,14 +338,22 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.indentListItem, icon: BusyMarkGlyphs.indentFor(direction), - shortcut: BusyMarkEditorShortcutLabels.indent, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.indent'] + ?.shortcut + ?.label, onPressed: onIndentCommand, ), _button( context, tooltip: context.l10n.outdentListItem, icon: BusyMarkGlyphs.outdentFor(direction), - shortcut: BusyMarkEditorShortcutLabels.outdent, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.outdent'] + ?.shortcut + ?.label, onPressed: onOutdentCommand, ), ], @@ -292,7 +362,11 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { context, tooltip: context.l10n.image, icon: BusyMarkGlyphs.image, - shortcut: BusyMarkEditorShortcutLabels.image, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.image'] + ?.shortcut + ?.label, onPressed: _blockCommandEnabled(BusyWysiwygBlockCommand.image) ? onImageCommand : null, @@ -300,12 +374,22 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { _button( context, tooltip: context.l10n.inlineImage, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.inlineImage'] + ?.shortcut + ?.label, icon: BusyMarkGlyphs.inlineImage, onPressed: inlineCommandsEnabled ? onInlineImageCommand : null, ), _button( context, tooltip: context.l10n.table, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.table'] + ?.shortcut + ?.label, icon: BusyMarkGlyphs.table, onPressed: onTableCommand, ), @@ -337,6 +421,14 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { for (final command in BusyWritersideInsertCommand.values) BusyMarkPopupMenuItem( value: BusyWritersideToolbarInsert(command), + shortcut: _writersideShortcut( + context, + BusyWritersideToolbarInsert(command), + )?.label, + gtkAccelerator: _writersideShortcut( + context, + BusyWritersideToolbarInsert(command), + )?.gtkAccelerator, label: busyMarkWritersideInsertLabel(context, command), enabled: isWritersideInsertEnabled?.call(command) ?? true, ), @@ -349,6 +441,14 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { for (final style in BusyAdmonitionStyle.values) BusyMarkPopupMenuItem( value: BusyWritersideToolbarAdmonition(style), + shortcut: _writersideShortcut( + context, + BusyWritersideToolbarAdmonition(style), + )?.label, + gtkAccelerator: _writersideShortcut( + context, + BusyWritersideToolbarAdmonition(style), + )?.gtkAccelerator, label: switch (style) { BusyAdmonitionStyle.tip => context.l10n.tip, BusyAdmonitionStyle.note => context.l10n.note, @@ -370,6 +470,14 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { ]) BusyMarkPopupMenuItem( value: BusyWritersideToolbarSemantic(command), + shortcut: _writersideShortcut( + context, + BusyWritersideToolbarSemantic(command), + )?.label, + gtkAccelerator: _writersideShortcut( + context, + BusyWritersideToolbarSemantic(command), + )?.gtkAccelerator, enabled: inlineCommandsEnabled && semanticCommandsEnabled, label: busyMarkSemanticInlineLabel(context, command), ), @@ -411,7 +519,7 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { onCanceled: onMenuCanceled, tooltip: context.l10n.textStyle, icon: BusyMarkGlyphs.font, - shortcut: BusyMarkEditorShortcutLabels.textStyle, + shortcut: null, transparent: false, elevated: true, foregroundColor: BusyMarkLinuxPalette.white, @@ -421,49 +529,77 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { value: BusyWysiwygBlockCommand.paragraph, label: context.l10n.paragraph, icon: BusyMarkGlyphs.paragraph, - shortcut: BusyMarkEditorShortcutLabels.paragraph, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.paragraph'] + ?.shortcut + ?.label, enabled: _blockCommandEnabled(BusyWysiwygBlockCommand.paragraph), ), BusyMarkPopupMenuItem( value: BusyWysiwygBlockCommand.heading1, label: context.l10n.heading1, icon: BusyMarkGlyphs.heading, - shortcut: BusyMarkEditorShortcutLabels.heading1, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.heading1'] + ?.shortcut + ?.label, enabled: _blockCommandEnabled(BusyWysiwygBlockCommand.heading1), ), BusyMarkPopupMenuItem( value: BusyWysiwygBlockCommand.heading2, label: context.l10n.heading2, icon: BusyMarkGlyphs.heading, - shortcut: BusyMarkEditorShortcutLabels.heading2, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.heading2'] + ?.shortcut + ?.label, enabled: _blockCommandEnabled(BusyWysiwygBlockCommand.heading2), ), BusyMarkPopupMenuItem( value: BusyWysiwygBlockCommand.heading3, label: context.l10n.heading3, icon: BusyMarkGlyphs.heading, - shortcut: BusyMarkEditorShortcutLabels.heading3, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.heading3'] + ?.shortcut + ?.label, enabled: _blockCommandEnabled(BusyWysiwygBlockCommand.heading3), ), BusyMarkPopupMenuItem( value: BusyWysiwygBlockCommand.heading4, label: context.l10n.heading4, icon: BusyMarkGlyphs.heading, - shortcut: BusyMarkEditorShortcutLabels.heading4, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.heading4'] + ?.shortcut + ?.label, enabled: _blockCommandEnabled(BusyWysiwygBlockCommand.heading4), ), BusyMarkPopupMenuItem( value: BusyWysiwygBlockCommand.heading5, label: context.l10n.heading5, icon: BusyMarkGlyphs.heading, - shortcut: BusyMarkEditorShortcutLabels.heading5, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.heading5'] + ?.shortcut + ?.label, enabled: _blockCommandEnabled(BusyWysiwygBlockCommand.heading5), ), BusyMarkPopupMenuItem( value: BusyWysiwygBlockCommand.heading6, label: context.l10n.heading6, icon: BusyMarkGlyphs.heading, - shortcut: BusyMarkEditorShortcutLabels.heading6, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)['editor.heading6'] + ?.shortcut + ?.label, enabled: _blockCommandEnabled(BusyWysiwygBlockCommand.heading6), ), ], @@ -506,3 +642,13 @@ class BusyMarkWysiwygToolbar extends StatelessWidget { }); } } + +BusyMarkShortcutDefinition? _writersideShortcut( + BuildContext context, + BusyWritersideToolbarCommand command, +) => + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)[writersideToolbarCommandId( + command, + )] + ?.shortcut; diff --git a/lib/src/local_history/local_history_comparison_view.dart b/lib/src/local_history/local_history_comparison_view.dart index aa3a676e..45ffe977 100644 --- a/lib/src/local_history/local_history_comparison_view.dart +++ b/lib/src/local_history/local_history_comparison_view.dart @@ -27,13 +27,18 @@ typedef LocalHistoryComparisonComputer = /// Read-only rendering of the shared source-comparison model. Refactoring /// previews reuse this instead of maintaining a second diff presentation. class SourceComparisonView extends StatelessWidget { - const SourceComparisonView({super.key, required this.comparison}); + const SourceComparisonView({super.key, required this.comparison}) + : _sliver = false; + + const SourceComparisonView.sliver({super.key, required this.comparison}) + : _sliver = true; final SourceComparison comparison; + final bool _sliver; @override Widget build(BuildContext context) => - _UnifiedComparisonPane(comparison: comparison); + _UnifiedComparisonPane(comparison: comparison, sliver: _sliver); } final localHistoryComparisonComputerProvider = @@ -56,6 +61,9 @@ class _LocalHistoryComparisonViewState extends ConsumerState { Future<_ComparisonSnapshot?>? _future; _ComparisonRequest? _request; + bool _recovering = false; + final _recoveredNotes = {}; + String? _recoveryError; @override Widget build(BuildContext context) { @@ -120,6 +128,9 @@ class _LocalHistoryComparisonViewState return _ComparisonBody( snapshot: data, remoteRecovery: document.remoteNote != null, + recoveryCreated: _recoveredNotes.containsKey(revision.summary.id), + recoveryBusy: _recovering, + recoveryError: _recoveryError, onClose: () => ref .read(localHistoryControllerProvider.notifier) .clearComparison(), @@ -247,13 +258,40 @@ class _LocalHistoryComparisonViewState LocalHistoryDocument document, LocalHistoryRevision revision, ) async { - if (!await confirmSafeToContinue(context, ref) || !mounted) return; - await ref - .read(workspaceControllerProvider.notifier) - .recoverNextcloudHistoryRevision( - document: document, - revision: revision, + if (_recovering) return; + setState(() { + _recovering = true; + _recoveryError = null; + }); + try { + final controller = ref.read(workspaceControllerProvider.notifier); + final existing = _recoveredNotes[revision.summary.id]; + if (existing != null) { + if (!await controller.openNextcloudNote(existing) && mounted) { + setState( + () => _recoveryError = context.l10n.notesRecoveryCreatedNotOpened, + ); + } + return; + } + if (!await confirmSafeToContinue(context, ref) || !mounted) return; + final result = await controller.recoverNextcloudHistoryRevision( + document: document, + revision: revision, + ); + if (result.noteId != null) { + _recoveredNotes[revision.summary.id] = result.noteId!; + } + if (mounted && !result.opened) { + setState( + () => _recoveryError = result.created + ? context.l10n.notesRecoveryCreatedNotOpened + : context.l10n.notesOperationFailed, ); + } + } finally { + if (mounted) setState(() => _recovering = false); + } } Future _restoreMissing( @@ -389,6 +427,9 @@ class _ComparisonBody extends StatelessWidget { required this.onRestoreOriginal, required this.onRestoreNewLocation, this.remoteRecovery = false, + this.recoveryCreated = false, + this.recoveryBusy = false, + this.recoveryError, }); final _ComparisonSnapshot snapshot; @@ -398,6 +439,8 @@ class _ComparisonBody extends StatelessWidget { final VoidCallback? onRestoreOriginal; final VoidCallback? onRestoreNewLocation; final bool remoteRecovery; + final bool recoveryCreated, recoveryBusy; + final String? recoveryError; @override Widget build(BuildContext context) { @@ -446,9 +489,11 @@ class _ComparisonBody extends StatelessWidget { actions: [ if (onRestoreOriginal != null) OutlinedButton( - onPressed: onRestoreOriginal, + onPressed: recoveryBusy ? null : onRestoreOriginal, child: Text( - remoteRecovery + recoveryCreated + ? context.l10n.open + : remoteRecovery ? context.l10n.nextcloudNewNote : context.l10n.localHistoryRestoreOriginalLocation, ), @@ -460,6 +505,12 @@ class _ComparisonBody extends StatelessWidget { ), ], ), + if (recoveryBusy) const LinearProgressIndicator(), + if (recoveryError != null) + BusyMarkStatusBox( + message: recoveryError!, + kind: BusyMarkStatusKind.warning, + ), if (comparison.simplified) _ComparisonNotice( text: context.l10n.localHistoryComparisonSimplified, @@ -527,9 +578,10 @@ class _ComparisonBody extends StatelessWidget { } class _UnifiedComparisonPane extends StatelessWidget { - const _UnifiedComparisonPane({required this.comparison}); + const _UnifiedComparisonPane({required this.comparison, this.sliver = false}); final SourceComparison comparison; + final bool sliver; @override Widget build(BuildContext context) { @@ -595,6 +647,12 @@ class _UnifiedComparisonPane extends StatelessWidget { if (rows.isEmpty) { rows.add(_UnifiedSourceChunk(source: comparison.currentInput.source)); } + if (sliver) { + return SliverPadding( + padding: const EdgeInsets.all(BusyMarkSpacing.md), + sliver: SliverList.list(children: rows), + ); + } return ListView( padding: const EdgeInsets.all(BusyMarkSpacing.md), children: rows, diff --git a/lib/src/local_history/local_history_controller.dart b/lib/src/local_history/local_history_controller.dart index 2e4fc7e8..bf3df938 100644 --- a/lib/src/local_history/local_history_controller.dart +++ b/lib/src/local_history/local_history_controller.dart @@ -53,11 +53,6 @@ final localHistoryOpenRequestProvider = LocalHistoryOpenRequestController.new, ); -final localHistoryFindRequestProvider = - NotifierProvider( - LocalHistoryOpenRequestController.new, - ); - class LocalHistoryOpenRequestController extends Notifier { @override int build() => 0; @@ -68,29 +63,23 @@ class LocalHistoryOpenRequestController extends Notifier { class LocalHistoryState { const LocalHistoryState({ this.snapshot = const LocalHistorySnapshot(), + this.searchRevision = 0, this.loading = false, this.selectedDocumentId, this.selectedRevisionId, this.selectedRevision, - this.searchQuery = '', - this.searching = false, - this.searchMatches = const {}, - this.documentSearchMatches = const {}, - this.findingDocuments = false, + this.browsingDocuments = false, this.inspectingRetainedDocument = false, this.warning, }); final LocalHistorySnapshot snapshot; + final int searchRevision; final bool loading; final String? selectedDocumentId; final String? selectedRevisionId; final LocalHistoryRevision? selectedRevision; - final String searchQuery; - final bool searching; - final Set searchMatches; - final Set documentSearchMatches; - final bool findingDocuments; + final bool browsingDocuments; final bool inspectingRetainedDocument; final LocalHistoryWarning? warning; @@ -103,24 +92,19 @@ class LocalHistoryState { ? const [] : snapshot.revisionsFor(selectedDocumentId!); - bool revisionVisible(LocalHistoryRevisionSummary revision) => - searchQuery.isEmpty || searchMatches.contains(revision.id); - LocalHistoryState copyWith({ LocalHistorySnapshot? snapshot, + int? searchRevision, bool? loading, Object? selectedDocumentId = _unset, Object? selectedRevisionId = _unset, Object? selectedRevision = _unset, - String? searchQuery, - bool? searching, - Set? searchMatches, - Set? documentSearchMatches, - bool? findingDocuments, + bool? browsingDocuments, bool? inspectingRetainedDocument, Object? warning = _unset, }) => LocalHistoryState( snapshot: snapshot ?? this.snapshot, + searchRevision: searchRevision ?? this.searchRevision, loading: loading ?? this.loading, selectedDocumentId: identical(selectedDocumentId, _unset) ? this.selectedDocumentId @@ -131,11 +115,7 @@ class LocalHistoryState { selectedRevision: identical(selectedRevision, _unset) ? this.selectedRevision : selectedRevision as LocalHistoryRevision?, - searchQuery: searchQuery ?? this.searchQuery, - searching: searching ?? this.searching, - searchMatches: searchMatches ?? this.searchMatches, - documentSearchMatches: documentSearchMatches ?? this.documentSearchMatches, - findingDocuments: findingDocuments ?? this.findingDocuments, + browsingDocuments: browsingDocuments ?? this.browsingDocuments, inspectingRetainedDocument: inspectingRetainedDocument ?? this.inspectingRetainedDocument, warning: identical(warning, _unset) @@ -418,7 +398,6 @@ class LocalHistoryController extends Notifier { var _historyGeneration = 0; var _snapshotGeneration = 0; var _loadGeneration = 0; - var _searchGeneration = 0; var _scopeGeneration = 0; var _pathOperationSerial = 0; Future Function()? _persistDurableState; @@ -606,6 +585,12 @@ class LocalHistoryController extends Notifier { final generation = ++_loadGeneration; if (ref.mounted) state = state.copyWith(loading: true); try { + // Defaults are presentation state while settings hydrate. In particular, + // the default retention age must not delete history before a longer saved + // policy is known. A newer authoritative refresh supersedes this one + // before maintenance starts, rather than reusing its eventual snapshot. + await ref.read(appSettingsControllerProvider.notifier).waitUntilLoaded(); + if (!ref.mounted || generation != _loadGeneration) return false; await _store.prune(policy, _clock()); final snapshot = await _store.load(); if (!ref.mounted || generation != _loadGeneration) return false; @@ -3840,17 +3825,12 @@ class LocalHistoryController extends Notifier { final generation = ++_scopeGeneration; _normalBrowsingScope = LocalHistoryBufferSnapshot.fromBuffer(buffer); _loadGeneration++; - _searchGeneration++; state = state.copyWith( loading: true, selectedDocumentId: null, selectedRevisionId: null, selectedRevision: null, - searchQuery: '', - searching: false, - searchMatches: const {}, - documentSearchMatches: const {}, - findingDocuments: false, + browsingDocuments: false, inspectingRetainedDocument: false, ); _showCaptureFailure(); @@ -3874,7 +3854,7 @@ class LocalHistoryController extends Notifier { } else { state = state.copyWith( loading: false, - findingDocuments: false, + browsingDocuments: false, inspectingRetainedDocument: false, ); } @@ -3897,17 +3877,12 @@ class LocalHistoryController extends Notifier { _scopeGeneration++; _normalBrowsingScope = null; _loadGeneration++; - _searchGeneration++; state = state.copyWith( loading: false, selectedDocumentId: null, selectedRevisionId: null, selectedRevision: null, - searchQuery: '', - searching: false, - searchMatches: const {}, - documentSearchMatches: const {}, - findingDocuments: false, + browsingDocuments: false, inspectingRetainedDocument: false, ); _showCaptureFailure(); @@ -3928,39 +3903,29 @@ class LocalHistoryController extends Notifier { return; } _loadGeneration++; - _searchGeneration++; state = state.copyWith( loading: false, selectedDocumentId: documentId, selectedRevisionId: null, selectedRevision: null, - searchQuery: '', - searching: false, - searchMatches: const {}, - documentSearchMatches: const {}, - findingDocuments: false, + browsingDocuments: false, inspectingRetainedDocument: inspectingRetainedDocument, ); _showCaptureFailure(); } - /// Opens the store-wide discovery surface used by Find in Local History. + /// Browses retained documents without filtering the revision timeline. /// No current editor is implied: closed, renamed, untitled, and deleted /// documents remain first-class results. - void beginDocumentSearch() { + void browseDocuments() { _scopeGeneration++; _loadGeneration++; - _searchGeneration++; state = state.copyWith( loading: false, selectedDocumentId: null, selectedRevisionId: null, selectedRevision: null, - searchQuery: '', - searching: false, - searchMatches: const {}, - documentSearchMatches: const {}, - findingDocuments: true, + browsingDocuments: true, inspectingRetainedDocument: false, ); _showCaptureFailure(); @@ -3990,7 +3955,22 @@ class LocalHistoryController extends Notifier { if (document != null) selectDocument(document.id); } - Future selectRevision(String revisionId) async { + Future readRecoveryHistory() => _store.load(); + + /// Reads recovery content without changing History or editor navigation. + Future readRecoveryRevision( + String documentId, + String revisionId, + ) async { + final revision = await _store.readRevision(revisionId); + return revision?.summary.documentId == documentId ? revision : null; + } + + Future selectRevision( + String revisionId, { + bool Function()? acceptSelection, + }) async { + if (acceptSelection?.call() == false) return; final documentId = state.selectedDocumentId; final summary = state.snapshot.revisions .where((revision) => revision.id == revisionId) @@ -4011,6 +3991,14 @@ class LocalHistoryController extends Notifier { (revision != null && revision.summary.documentId != documentId)) { return; } + if (acceptSelection?.call() == false) { + state = state.copyWith( + loading: false, + selectedRevisionId: null, + selectedRevision: null, + ); + return; + } state = state.copyWith( selectedRevision: revision, loading: false, @@ -4025,6 +4013,14 @@ class LocalHistoryController extends Notifier { state.selectedRevisionId != revisionId) { return; } + if (acceptSelection?.call() == false) { + state = state.copyWith( + loading: false, + selectedRevisionId: null, + selectedRevision: null, + ); + return; + } state = state.copyWith( loading: false, warning: LocalHistoryWarning( @@ -4113,11 +4109,8 @@ class LocalHistoryController extends Notifier { return false; } - Future search(String query) async { - await _runSearch(query, clearMatches: true); - } - Future clearDocument(String documentId) async { + _invalidateFindRequests(); final durableBeforeClear = _durableStateFingerprint(); final document = state.snapshot.documents .where((candidate) => candidate.id == documentId) @@ -4266,7 +4259,6 @@ class LocalHistoryController extends Notifier { } _checkpointTimers.remove(_pathRemapOwner)?.cancel(); _loadGeneration++; - _searchGeneration++; _documentIdsByBuffer.removeWhere((_, value) => value == documentId); final retiredPromotions = _pendingUntitledPromotions.values .where((promotion) => promotion.documentId == documentId) @@ -4387,6 +4379,7 @@ class LocalHistoryController extends Notifier { } Future clearAll() async { + _invalidateFindRequests(); final durableBeforeClear = _durableStateFingerprint(); final affectedBuffers = { ..._documentIdsByBuffer.keys, @@ -4394,7 +4387,6 @@ class LocalHistoryController extends Notifier { }; _historyGeneration++; _loadGeneration++; - _searchGeneration++; _cancelCheckpoints(clearTransitions: false); _captureFailures.clear(); _documentIdsByBuffer.clear(); @@ -4727,6 +4719,16 @@ class LocalHistoryController extends Notifier { (requireProtection || retainedProtection ? _newPathOperationId() : null); + // Keep the accepted generations across hydration: disabling recording or + // clearing history while settings load must cancel this queued capture. + if (!_operationIsCurrent( + snapshot.bufferId, + captureHistoryGeneration, + captureBufferGeneration, + )) { + return !requireProtection; + } + await ref.read(appSettingsControllerProvider.notifier).waitUntilLoaded(); if (!_operationIsCurrent( snapshot.bufferId, captureHistoryGeneration, @@ -5966,7 +5968,6 @@ class LocalHistoryController extends Notifier { _clearEpochLoaded = true; _storeWarning = warning; _snapshotGeneration++; - _searchGeneration++; if (capturedBufferId != null && _normalBrowsingScope?.bufferId == capturedBufferId && capturedSnapshot != null && @@ -5975,7 +5976,7 @@ class LocalHistoryController extends Notifier { } var selectedDocumentId = state.selectedDocumentId; - if (!state.findingDocuments && !state.inspectingRetainedDocument) { + if (!state.browsingDocuments && !state.inspectingRetainedDocument) { final scope = _normalBrowsingScope; if (scope != null) { selectedDocumentId = _documentIdsByBuffer[scope.bufferId]; @@ -6040,106 +6041,88 @@ class LocalHistoryController extends Notifier { selectedRevision = null; } - final query = state.searchQuery; state = state.copyWith( snapshot: snapshot, + searchRevision: state.searchRevision + 1, loading: false, selectedDocumentId: selectedDocumentId, selectedRevisionId: selectedRevisionId, selectedRevision: selectedRevision, warning: effectiveWarning, ); - if (query.isNotEmpty) { - await _runSearch(query, clearMatches: false); - } else { - final revisionIds = snapshot.revisions - .map((revision) => revision.id) - .toSet(); - final documentIds = snapshot.documents - .map((document) => document.id) - .toSet(); - state = state.copyWith( - searchMatches: state.searchMatches.intersection(revisionIds), - documentSearchMatches: state.documentSearchMatches.intersection( - documentIds, - ), - ); - } } - Future _runSearch(String query, {required bool clearMatches}) async { - final generation = ++_searchGeneration; - final snapshotGeneration = _snapshotGeneration; - final documentId = state.selectedDocumentId; - final findingDocuments = state.findingDocuments; - state = state.copyWith( - searchQuery: query, - searching: query.isNotEmpty, - searchMatches: clearMatches ? const {} : state.searchMatches, - documentSearchMatches: clearMatches - ? const {} - : state.documentSearchMatches, - ); - if (query.isEmpty) { - state = state.copyWith( - searching: false, - searchMatches: const {}, - documentSearchMatches: const {}, - ); - return; - } + void _invalidateFindRequests() { + _snapshotGeneration++; + state = state.copyWith(searchRevision: state.searchRevision + 1); + } + + /// Searches a retained snapshot without binding an editor, capturing a + /// revision, selecting a timeline or replacing an existing comparison. + Future findRetained({ + required String query, + String? documentId, + int maximumResults = 80, + bool Function()? isCancelled, + void Function(LocalHistorySearchPage page)? onProgress, + }) async { final snapshot = state.snapshot; - final summaries = findingDocuments - ? snapshot.revisions - : documentId == null - ? const [] - : snapshot.revisionsFor(documentId); - final matches = {}; - final documentMatches = {}; - final needle = query.toLowerCase(); + final version = _snapshotGeneration; bool current() => ref.mounted && - generation == _searchGeneration && - snapshotGeneration == _snapshotGeneration && - state.searchQuery == query && - state.selectedDocumentId == documentId && - state.findingDocuments == findingDocuments; - try { - if (findingDocuments) { - for (final document in snapshot.documents) { - if (document.displayName.toLowerCase().contains(needle) || - (document.currentPath?.toLowerCase().contains(needle) ?? false) || - document.historicalPaths.any( - (path) => path.toLowerCase().contains(needle), - )) { - documentMatches.add(document.id); - } + version == _snapshotGeneration && + !(isCancelled?.call() ?? false); + final documents = { + for (final document in snapshot.documents) + if (documentId == null || document.id == documentId) + document.id: document, + }; + final matches = []; + final needle = query.toLowerCase(); + LocalHistorySearchPage page({bool truncated = false}) => + LocalHistorySearchPage( + List.unmodifiable(matches.take(maximumResults)), + truncated: truncated, + ); + final orderedDocuments = documents.values.toList() + ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); + if (documentId == null) { + for (final document in orderedDocuments) { + if (!current()) return null; + if (document.displayName.toLowerCase().contains(needle) || + (document.currentPath?.toLowerCase().contains(needle) ?? false) || + document.historicalPaths.any( + (path) => path.toLowerCase().contains(needle), + )) { + matches.add(LocalHistoryDocumentMatch(document)); + if (matches.length > maximumResults) return page(truncated: true); } } - for (final summary in summaries) { - final revision = await _store.readRevision(summary.id); - if (!current()) return; - if (revision?.source.toLowerCase().contains(needle) == true) { - matches.add(summary.id); - documentMatches.add(summary.documentId); - } + if (matches.isNotEmpty) onProgress?.call(page()); + if (needle.isEmpty) return current() ? page() : null; + } + final summaries = + snapshot.revisions + .where((r) => documents.containsKey(r.documentId)) + .toList() + ..sort((a, b) => b.capturedAt.compareTo(a.capturedAt)); + for (final summary in summaries) { + if (!current()) return null; + // Empty scoped queries can use summaries without reading any bodies. + final revision = needle.isEmpty + ? null + : await _store.readRevision(summary.id); + if (!current()) return null; + if (needle.isEmpty || + revision?.source.toLowerCase().contains(needle) == true) { + matches.add( + LocalHistoryRevisionMatch(documents[summary.documentId]!, summary), + ); + if (matches.length > maximumResults) return page(truncated: true); + onProgress?.call(page()); } - if (!current()) return; - state = state.copyWith( - searching: false, - searchMatches: matches, - documentSearchMatches: documentMatches, - ); - } on Object catch (error) { - if (!current()) return; - state = state.copyWith( - searching: false, - warning: LocalHistoryWarning( - LocalHistoryWarningKind.revisionRead, - detail: error.toString(), - ), - ); } + return current() ? page() : null; } void _setWarning(LocalHistoryWarningKind kind, [String? detail]) { diff --git a/lib/src/local_history/local_history_models.dart b/lib/src/local_history/local_history_models.dart index 096f379b..a44c9d4f 100644 --- a/lib/src/local_history/local_history_models.dart +++ b/lib/src/local_history/local_history_models.dart @@ -201,6 +201,32 @@ class LocalHistoryRevision { bool get isIntact => sourceChecksum(source) == summary.checksum; } +/// Search returns navigation metadata only. Revision bodies remain in the store. +sealed class LocalHistorySearchMatch { + const LocalHistorySearchMatch(this.document); + final LocalHistoryDocument document; + String get identity; +} + +final class LocalHistoryDocumentMatch extends LocalHistorySearchMatch { + const LocalHistoryDocumentMatch(super.document); + @override + String get identity => 'history-document:${document.id}'; +} + +final class LocalHistoryRevisionMatch extends LocalHistorySearchMatch { + const LocalHistoryRevisionMatch(super.document, this.revision); + final LocalHistoryRevisionSummary revision; + @override + String get identity => 'history-revision:${document.id}:${revision.id}'; +} + +class LocalHistorySearchPage { + const LocalHistorySearchPage(this.matches, {this.truncated = false}); + final List matches; + final bool truncated; +} + @immutable class LocalHistorySnapshot { const LocalHistorySnapshot({ diff --git a/lib/src/local_history/local_history_panel.dart b/lib/src/local_history/local_history_panel.dart index 50040ba8..05ee9f4f 100644 --- a/lib/src/local_history/local_history_panel.dart +++ b/lib/src/local_history/local_history_panel.dart @@ -7,7 +7,6 @@ import 'package:path/path.dart' as p; import '../app/busymark_design.dart'; import '../app/busymark_glyphs.dart'; -import '../app/busymark_search_field.dart'; import '../app/busymark_dialogs.dart'; import '../app/localization.dart'; import '../workspace/workspace_controller.dart'; @@ -15,33 +14,23 @@ import 'local_history_controller.dart'; import 'local_history_models.dart'; class LocalHistoryPanel extends ConsumerStatefulWidget { - const LocalHistoryPanel({this.focusSearchRequest = 0, super.key}); - - final int focusSearchRequest; + const LocalHistoryPanel({super.key}); @override ConsumerState createState() => _LocalHistoryPanelState(); } class _LocalHistoryPanelState extends ConsumerState { - final _searchController = TextEditingController(); - @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _selectActive()); } - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - Future _selectActive({bool preserveLookup = true}) async { final history = ref.read(localHistoryControllerProvider); if (preserveLookup && - (history.findingDocuments || history.inspectingRetainedDocument)) { + (history.browsingDocuments || history.inspectingRetainedDocument)) { return; } final buffer = ref.read(workspaceControllerProvider).activeBuffer; @@ -58,12 +47,6 @@ class _LocalHistoryPanelState extends ConsumerState { @override Widget build(BuildContext context) { final state = ref.watch(localHistoryControllerProvider); - if (_searchController.text != state.searchQuery) { - _searchController.value = TextEditingValue( - text: state.searchQuery, - selection: TextSelection.collapsed(offset: state.searchQuery.length), - ); - } final controller = ref.read(localHistoryControllerProvider.notifier); final workspaceState = ref.watch(workspaceControllerProvider); final activeBuffer = workspaceState.activeBuffer; @@ -80,7 +63,7 @@ class _LocalHistoryPanelState extends ConsumerState { if (previous == next) return; if (next.bufferId == null) { final history = ref.read(localHistoryControllerProvider); - if (!history.findingDocuments && + if (!history.browsingDocuments && !history.inspectingRetainedDocument) { controller.clearDocumentScope(); } @@ -92,26 +75,8 @@ class _LocalHistoryPanelState extends ConsumerState { } }, ); - ref.listen( - localHistoryControllerProvider.select((value) => value.searchQuery), - (previous, next) { - if (_searchController.text == next) return; - _searchController.value = TextEditingValue( - text: next, - selection: TextSelection.collapsed(offset: next.length), - ); - }, - ); - final documents = [...state.snapshot.documents] + final visibleDocuments = [...state.snapshot.documents] ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); - final visibleDocuments = - state.findingDocuments && state.searchQuery.isNotEmpty - ? documents - .where( - (document) => state.documentSearchMatches.contains(document.id), - ) - .toList(growable: false) - : documents; final selected = state.selectedDocument; final activeHistoryDocumentId = activeBuffer == null ? null @@ -128,91 +93,61 @@ class _LocalHistoryPanelState extends ConsumerState { state.inspectingRetainedDocument || selectedMatchesActive; final visibleRevisions = selectedMatchesVisibleScope ? state.selectedRevisions - .where(state.revisionVisible) - .toList(growable: false) : const []; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (state.findingDocuments || state.inspectingRetainedDocument) - _LocalHistoryHeader( - selectedDocument: selected, - findingDocuments: state.findingDocuments, - inspectingRetainedDocument: state.inspectingRetainedDocument, - onBack: () => unawaited(_selectActive(preserveLookup: false)), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: BusyMarkSpacing.md, - vertical: BusyMarkSpacing.xs, - ), - child: Row( - textDirection: TextDirection.ltr, - children: [ - Expanded( - child: BusyMarkSearchField( - controller: _searchController, - focusRequest: widget.focusSearchRequest, - hintText: context.l10n.localHistorySearchHint, - onChanged: (value) => unawaited(controller.search(value)), - ), + _LocalHistoryHeader( + selectedDocument: selected, + browsingDocuments: state.browsingDocuments, + inspectingRetainedDocument: state.inspectingRetainedDocument, + onBack: () => unawaited(_selectActive(preserveLookup: false)), + actions: BusyMarkHeaderPopupMenuButton<_HistoryAction>( + key: const ValueKey('local-history-actions-menu'), + tooltip: context.l10n.actions, + icon: BusyMarkGlyphs.menuVertical, + transparent: true, + borderRadius: BusyMarkRadius.nativeHeaderButton, + highlightWhenOpen: false, + itemBuilder: (context) => [ + BusyMarkPopupMenuItem( + value: _HistoryAction.refresh, + label: MaterialLocalizations.of( + context, + ).refreshIndicatorSemanticLabel, + icon: BusyMarkGlyphs.refresh, + enabled: !state.loading, ), - const SizedBox(width: BusyMarkSpacing.sm), - BusyMarkHeaderPopupMenuButton<_HistoryAction>( - key: const ValueKey('local-history-actions-menu'), - tooltip: context.l10n.actions, - icon: BusyMarkGlyphs.menuVertical, - transparent: true, - borderRadius: BusyMarkRadius.nativeHeaderButton, - highlightWhenOpen: false, - itemBuilder: (context) => [ - BusyMarkPopupMenuItem( - value: _HistoryAction.refresh, - label: MaterialLocalizations.of( - context, - ).refreshIndicatorSemanticLabel, - icon: BusyMarkGlyphs.refresh, - enabled: !state.loading, - ), - BusyMarkPopupMenuItem( - value: _HistoryAction.find, - label: context.l10n.findLocalHistoryEllipsis, - icon: BusyMarkGlyphs.search, - ), - const PopupMenuDivider(height: BusyMarkSpacing.sm), - BusyMarkPopupMenuItem( - value: _HistoryAction.clearDocument, - label: context.l10n.localHistoryClearDocument, - icon: BusyMarkGlyphs.delete, - enabled: - selectedMatchesVisibleScope && - state.selectedRevisions.isNotEmpty, - ), - BusyMarkPopupMenuItem( - value: _HistoryAction.clearAll, - label: context.l10n.localHistoryClearAll, - icon: BusyMarkGlyphs.clearAll, - enabled: state.snapshot.revisions.isNotEmpty, - ), - ], - onSelected: (action) { - switch (action) { - case _HistoryAction.refresh: - unawaited(controller.refresh()); - case _HistoryAction.find: - controller.beginDocumentSearch(); - case _HistoryAction.clearDocument: - final documentId = selected?.id; - if (documentId != null) { - unawaited(_confirmClear(action, documentId)); - } - case _HistoryAction.clearAll: - unawaited(_confirmClear(action, null)); - } - }, + const PopupMenuDivider(height: BusyMarkSpacing.sm), + BusyMarkPopupMenuItem( + value: _HistoryAction.clearDocument, + label: context.l10n.localHistoryClearDocument, + icon: BusyMarkGlyphs.delete, + enabled: + selectedMatchesVisibleScope && + state.selectedRevisions.isNotEmpty, + ), + BusyMarkPopupMenuItem( + value: _HistoryAction.clearAll, + label: context.l10n.localHistoryClearAll, + icon: BusyMarkGlyphs.clearAll, + enabled: state.snapshot.revisions.isNotEmpty, ), ], + onSelected: (action) { + switch (action) { + case _HistoryAction.refresh: + unawaited(controller.refresh()); + case _HistoryAction.clearDocument: + final documentId = selected?.id; + if (documentId != null) { + unawaited(_confirmClear(action, documentId)); + } + case _HistoryAction.clearAll: + unawaited(_confirmClear(action, null)); + } + }, ), ), if (state.warning != null) @@ -233,17 +168,11 @@ class _LocalHistoryPanelState extends ConsumerState { ), ), Expanded( - child: state.findingDocuments + child: state.browsingDocuments ? visibleDocuments.isEmpty - ? _HistoryEmpty( - text: documents.isEmpty - ? context.l10n.localHistoryNoDocuments - : context.l10n.localHistoryNoMatches, - ) + ? _HistoryEmpty(text: context.l10n.localHistoryNoDocuments) : ListView.builder( - key: const ValueKey( - 'local-history-document-search-results', - ), + key: const ValueKey('local-history-documents'), padding: BusyMarkInsets.sidebarList, itemCount: visibleDocuments.length, itemBuilder: (context, index) { @@ -274,11 +203,7 @@ class _LocalHistoryPanelState extends ConsumerState { : activeBuffer == null && !state.inspectingRetainedDocument ? _HistoryEmpty(text: context.l10n.localHistoryNoDocuments) : visibleRevisions.isEmpty - ? _HistoryEmpty( - text: state.searchQuery.isEmpty - ? context.l10n.localHistoryNoRevisions - : context.l10n.localHistoryNoMatches, - ) + ? _HistoryEmpty(text: context.l10n.localHistoryNoRevisions) : ListView.builder( key: ValueKey( 'local-history-revisions-${selected?.id ?? 'none'}', @@ -392,74 +317,79 @@ typedef _LocalHistoryEditorScope = ({ String? displayName, }); -enum _HistoryAction { refresh, find, clearDocument, clearAll } +enum _HistoryAction { refresh, clearDocument, clearAll } class _LocalHistoryHeader extends StatelessWidget { const _LocalHistoryHeader({ required this.selectedDocument, - required this.findingDocuments, + required this.browsingDocuments, required this.inspectingRetainedDocument, required this.onBack, + required this.actions, }); final LocalHistoryDocument? selectedDocument; - final bool findingDocuments; + final bool browsingDocuments; final bool inspectingRetainedDocument; final VoidCallback onBack; + final Widget actions; @override Widget build(BuildContext context) { final selectedPath = selectedDocument?.currentPath ?? selectedDocument?.historicalPaths.lastOrNull; - final title = findingDocuments - ? context.l10n.findLocalHistoryEllipsis - : selectedDocument?.displayName ?? context.l10n.localHistoryNoDocuments; - final path = inspectingRetainedDocument ? selectedPath : null; + final contextTitle = browsingDocuments + ? context.l10n.searchDocumentsCategory + : selectedDocument?.displayName; + final retained = browsingDocuments || inspectingRetainedDocument; return Padding( - key: const ValueKey('local-history-context-header'), - padding: const EdgeInsets.fromLTRB( + key: const ValueKey('local-history-panel-header'), + padding: const EdgeInsetsDirectional.fromSTEB( BusyMarkSpacing.md, BusyMarkSpacing.sm, + BusyMarkSpacing.md, BusyMarkSpacing.sm, - BusyMarkSpacing.xs, ), child: Row( children: [ - BusyMarkHeaderIconButton( - tooltip: context.l10n.back, - icon: BusyMarkGlyphs.backFor(Directionality.of(context)), - transparent: true, - onPressed: onBack, - ), - const SizedBox(width: BusyMarkSpacing.xs), + if (retained) ...[ + BusyMarkHeaderIconButton( + tooltip: context.l10n.back, + icon: BusyMarkGlyphs.backFor(Directionality.of(context)), + transparent: true, + onPressed: onBack, + ), + const SizedBox(width: BusyMarkSpacing.xs), + ], Expanded( - child: Tooltip( - message: path == null - ? title - : busyMarkLtrIsolateFor(context, path), - child: Row( - children: [ - Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.localHistory, + style: Theme.of(context).textTheme.bodyMedium, + ), + if (contextTitle != null) + Tooltip( + message: selectedPath ?? contextTitle, child: Text( - busyMarkLtrIsolateFor(context, title), + contextTitle, + key: const ValueKey('local-history-document-context'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - if (inspectingRetainedDocument && - selectedDocument?.deleted == true) ...[ - const SizedBox(width: BusyMarkSpacing.xs), - Text( - context.l10n.localHistoryDeleted, style: Theme.of(context).textTheme.bodySmall, ), - ], - ], - ), + ), + ], ), ), + if (inspectingRetainedDocument && selectedDocument?.deleted == true) + Text( + context.l10n.localHistoryDeleted, + style: Theme.of(context).textTheme.bodySmall, + ), + actions, ], ), ); diff --git a/lib/src/local_history/local_history_store.dart b/lib/src/local_history/local_history_store.dart index 8fe98cb7..477e01b8 100644 --- a/lib/src/local_history/local_history_store.dart +++ b/lib/src/local_history/local_history_store.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/foundation.dart' show listEquals, visibleForTesting; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; @@ -357,14 +357,13 @@ class FileLocalHistoryStore implements LocalHistoryStore { await _deleteFileBestEffort(target); throw const LocalHistoryCaptureCancelled(); } - final retained = await _applyRetention( + await _applyRetention( root, index, policy, revisionId, request.capturedAt.toUtc(), ); - if (!identical(retained, index)) await _publishIndex(root, retained); return LocalHistoryCaptureResult(document: document, revision: summary); }); }); @@ -394,14 +393,7 @@ class FileLocalHistoryStore implements LocalHistoryStore { (root) => _withFileLock(root, () async { policy.validate(); final index = await _loadIndexUnlocked(root, repair: true); - final retained = await _applyRetention( - root, - index, - policy, - null, - now.toUtc(), - ); - if (!identical(retained, index)) await _publishIndex(root, retained); + await _applyRetention(root, index, policy, null, now.toUtc()); }), ); @@ -627,10 +619,12 @@ class FileLocalHistoryStore implements LocalHistoryStore { Future _serialized(Future Function(Directory root) operation) { final completer = Completer(); + _queue = _queue.then((_) async { try { final root = await _rootDirectory(); await root.create(recursive: true); + completer.complete( await _serializedForRoot(root, () => operation(root)), ); @@ -678,6 +672,7 @@ class FileLocalHistoryStore implements LocalHistoryStore { break; } on FileSystemException catch (error) { if (!_isLockContention(error)) rethrow; + final remaining = _lockBudget - deadline.elapsed; if (remaining <= Duration.zero) throw LocalHistoryLockTimeout(error); await Future.delayed( @@ -1077,7 +1072,7 @@ class FileLocalHistoryStore implements LocalHistoryStore { } } - Future<_Index> _applyRetention( + Future _applyRetention( Directory root, _Index index, LocalHistoryPolicy policy, @@ -1111,7 +1106,7 @@ class FileLocalHistoryStore implements LocalHistoryStore { if (retainedDocumentIds.contains(document.id)) document, ]; if (removed.isEmpty && documents.length == index.documents.length) { - return index; + return; } final retained = index.copyWith( documents: documents, @@ -1127,22 +1122,46 @@ class FileLocalHistoryStore implements LocalHistoryStore { _revisionFile(root, revision.documentId, revision.id), ); } - return retained; + // The retained index is already durable before deletion. Republishing the + // identical index afterward adds two flushes without another state change. } Future _publishIndex(Directory root, _Index index) async { - await _publishReplacingFile( - File(p.join(root.path, 'tombstones.json')), - const JsonEncoder.withIndent( - ' ', - ).convert(index.tombstones.toList()..sort()), - ); + final journal = File(p.join(root.path, 'tombstones.json')); + final tombstones = const JsonEncoder.withIndent( + ' ', + ).convert(index.tombstones.toList()..sort()); + // Every caller holds the store's interprocess lock. Reuse only the exact + // regular journal already on disk; never cache completed authority across + // transactions. New tombstones still commit before the index or deletion. + if (!await _journalMatches(journal, tombstones)) { + await _publishReplacingFile(journal, tombstones); + } await _publishReplacingFile( File(p.join(root.path, 'index.json')), const JsonEncoder.withIndent(' ').convert(index.toJson()), ); } + Future _journalMatches(File journal, String content) async { + try { + if (await FileSystemEntity.type(journal.path, followLinks: false) != + FileSystemEntityType.file) { + return false; + } + final expected = utf8.encode(content); + if (await journal.length() != expected.length) return false; + final matches = listEquals(await journal.readAsBytes(), expected); + return matches && + await FileSystemEntity.type(journal.path, followLinks: false) == + FileSystemEntityType.file; + } on FileSystemException { + // A missing/unreadable journal cannot be reused. Attempt the same atomic + // publication as before; any publication failure still propagates. + return false; + } + } + Future _publishNewFile(File target, String content) async { if (await target.exists()) { throw LocalHistoryStorageException('Revision identity collision.'); diff --git a/lib/src/markdown/document_outline.dart b/lib/src/markdown/document_outline.dart index 9e26daa4..c4152574 100644 --- a/lib/src/markdown/document_outline.dart +++ b/lib/src/markdown/document_outline.dart @@ -83,3 +83,34 @@ extension BusyDocumentOutline on BusyDocument { return List.unmodifiable(headings); } } + +/// A projection of the existing model; browsing keeps the complete tree. +class DocumentOutlineMatch { + const DocumentOutlineMatch(this.heading, this.ancestors); + final DocumentOutlineHeading heading; + final List ancestors; +} + +/// Match headings in document order, retaining ancestor context and the exact +/// source/editor identities. This never parses or changes the outline. +List matchDocumentOutline( + List headings, + String query, { + int maximumResults = 80, +}) { + final normalized = query.trim().toLowerCase(); + if (normalized.isEmpty || maximumResults <= 0) return const []; + final matches = []; + final ancestors = []; + for (final heading in headings) { + while (ancestors.isNotEmpty && ancestors.last.level >= heading.level) { + ancestors.removeLast(); + } + if (heading.text.toLowerCase().contains(normalized)) { + matches.add(DocumentOutlineMatch(heading, List.unmodifiable(ancestors))); + if (matches.length >= maximumResults) break; + } + ancestors.add(heading); + } + return List.unmodifiable(matches); +} diff --git a/lib/src/markdown/markdown_parser.dart b/lib/src/markdown/markdown_parser.dart index 8c0f87a4..97b89135 100644 --- a/lib/src/markdown/markdown_parser.dart +++ b/lib/src/markdown/markdown_parser.dart @@ -24,7 +24,32 @@ import '../writerside/writerside_schema.dart'; // resolve inside the opened workspace. Targets outside that root, unsupported // file types, and oversized files are not inspected. const int _maxLocalReferenceTargetBytes = 2 * 1024 * 1024; -const int _backgroundParseThresholdBytes = 64 * 1024; +// Release measurements put structured 4 Ki code-unit inputs beyond a frame +// budget, with about 1–3 ms of worker overhead. Count UTF-16 code units (the +// parser's offsets), rather than doing a UTF-8 encoding just for this decision. +const int _backgroundParseThresholdCodeUnits = 4 * 1024; + +typedef _MarkdownParseInput = ({ + String filePath, + String source, + MarkdownMode mode, + String? workspaceRoot, + bool preserveHtmlSemantics, +}); + +// Keep the Isolate.run closure outside the parser method: it owns only this +// explicit input, never the parser receiver or caller's controller graph. +Future _parseInWorker(_MarkdownParseInput input) => + Isolate.run( + () => MarkdownParser(preserveHtmlSemantics: input.preserveHtmlSemantics) + .parse( + filePath: input.filePath, + source: input.source, + mode: input.mode, + workspaceRoot: input.workspaceRoot, + validateLocalReferences: false, + ), + ); class MarkdownParser { const MarkdownParser({this.preserveHtmlSemantics = false}); @@ -78,7 +103,7 @@ class MarkdownParser { Map sourceOverrides = const {}, }) async { final parsed = - source.length < _backgroundParseThresholdBytes || + source.length < _backgroundParseThresholdCodeUnits || runtimeType != MarkdownParser ? parse( filePath: filePath, @@ -87,15 +112,13 @@ class MarkdownParser { workspaceRoot: workspaceRoot, validateLocalReferences: false, ) - : await Isolate.run( - () => parse( - filePath: filePath, - source: source, - mode: mode, - workspaceRoot: workspaceRoot, - validateLocalReferences: false, - ), - ); + : await _parseInWorker(( + filePath: filePath, + source: source, + mode: mode, + workspaceRoot: workspaceRoot, + preserveHtmlSemantics: preserveHtmlSemantics, + )); if (!validateLocalReferences) { return parsed; } diff --git a/lib/src/nextcloud_notes/application/nextcloud_connection.dart b/lib/src/nextcloud_notes/application/nextcloud_connection.dart index 88e6148d..ee653502 100644 --- a/lib/src/nextcloud_notes/application/nextcloud_connection.dart +++ b/lib/src/nextcloud_notes/application/nextcloud_connection.dart @@ -1,3 +1,4 @@ +import '../../app/app_settings.dart'; import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -15,6 +16,7 @@ import '../data/notes_store.dart'; import '../data/server_uri.dart'; import '../domain/notes_models.dart'; import 'notes_repository.dart'; +import 'notes_navigation.dart'; final nextcloudHttpClientProvider = Provider((ref) { final client = http.Client(); @@ -111,6 +113,8 @@ final nextcloudNotesRepositoryProvider = FutureProvider(( enum NextcloudConnectionPhase { idle, + starting, + cancelling, awaitingBrowser, verifying, connected, @@ -122,13 +126,26 @@ class NextcloudConnectionState { this.account, this.phase = NextcloudConnectionPhase.idle, this.error, + this.removedWithWarning = false, + this.failure, + this.removalFailed = false, }); final NextcloudAccount? account; final NextcloudConnectionPhase phase; final String? error; + final bool removedWithWarning; + final Object? failure; + final bool removalFailed; + DateTime? get retryNotBefore => switch (failure) { + NextcloudCapabilityException(:final retryNotBefore) => retryNotBefore, + NotesException(:final retryNotBefore) => retryNotBefore, + _ => null, + }; bool get busy => + phase == NextcloudConnectionPhase.starting || + phase == NextcloudConnectionPhase.cancelling || phase == NextcloudConnectionPhase.awaitingBrowser || phase == NextcloudConnectionPhase.verifying || phase == NextcloudConnectionPhase.removing; @@ -185,14 +202,17 @@ class NextcloudConnectionController extends Notifier { } Future _connect(String serverUrl, {NextcloudAccount? previous}) async { - if (state.busy) return false; + if (state.busy || + (state.retryNotBefore?.isAfter(DateTime.now()) ?? false)) { + return false; + } final generation = ++_generation; final cancellation = NextcloudLoginCancellation(); _cancellation = cancellation; var retainedAccount = previous ?? state.account; state = NextcloudConnectionState( account: retainedAccount, - phase: NextcloudConnectionPhase.awaitingBrowser, + phase: NextcloudConnectionPhase.starting, ); NextcloudLoginCredentials? credentials; String? accountId; @@ -211,6 +231,7 @@ class NextcloudConnectionController extends Notifier { retainedAccount = repository.accounts.first; throw const NextcloudCredentialException( 'Remove the connected Nextcloud account before connecting another account.', + code: NextcloudCredentialFailure.alreadyConnected, ); } if (previous != null) { @@ -220,6 +241,16 @@ class NextcloudConnectionController extends Notifier { credentials = await loginFlow.authenticate( server, cancellation: cancellation, + onAwaitingBrowser: () { + if (!_disposed && + generation == _generation && + !cancellation.isCancelled) { + state = NextcloudConnectionState( + account: retainedAccount, + phase: NextcloudConnectionPhase.awaitingBrowser, + ); + } + }, ); cancellation.throwIfCancelled(); if (previous != null && @@ -227,6 +258,7 @@ class NextcloudConnectionController extends Notifier { previous.loginName != credentials.loginName)) { throw const NextcloudCredentialException( 'Sign in to the same Nextcloud account when reconnecting. Remove this account first to connect a different account.', + code: NextcloudCredentialFailure.identityMismatch, ); } state = NextcloudConnectionState( @@ -295,6 +327,9 @@ class NextcloudConnectionController extends Notifier { phase: retainedAccount == null ? NextcloudConnectionPhase.idle : NextcloudConnectionPhase.connected, + failure: const NextcloudCredentialException( + 'Desktop keyring cleanup failed.', + ), error: 'Nextcloud setup failed and BusyMark could not clean up the desktop keyring. Unlock the keyring and reconnect.', ); @@ -316,16 +351,15 @@ class NextcloudConnectionController extends Notifier { phase: retainedAccount == null ? NextcloudConnectionPhase.idle : NextcloudConnectionPhase.connected, + failure: error, error: switch (error) { NextcloudLoginException(code: NextcloudLoginFailure.cancelled) => null, NextcloudLoginException(:final message) => message, NextcloudCapabilityException(:final message) => message, NextcloudCredentialException(:final message) => message, - FormatException(:final message) => - message == 'Enter a valid HTTPS Nextcloud server URL.' - ? message - : 'Enter a valid Nextcloud server URL.', + NextcloudServerUrlException(:final message) => message, + FormatException() => 'Enter a valid Nextcloud server URL.', _ => 'BusyMark could not finish connecting to Nextcloud Notes.', }, ); @@ -335,7 +369,17 @@ class NextcloudConnectionController extends Notifier { } } - void cancel() => _cancellation?.cancel(); + void cancel() { + final cancellation = _cancellation; + if (cancellation == null || cancellation.isCancelled) return; + cancellation.cancel(); + if (!_disposed) { + state = NextcloudConnectionState( + account: state.account, + phase: NextcloudConnectionPhase.cancelling, + ); + } + } /// Remote revocation is best effort; keyring and cache removal are required. Future disconnect() async { @@ -366,8 +410,13 @@ class NextcloudConnectionController extends Notifier { account.id, beforeRemove: () => secrets.delete(account.id), ); + ref.invalidate(notesNavigationProvider(account.id)); + await ref + .read(appSettingsControllerProvider.notifier) + .removeRecentAccount(account.id); if (!_disposed) { state = NextcloudConnectionState( + removedWithWarning: !revoked, error: revoked ? null : 'The local Nextcloud account was removed. BusyMark could not confirm app-password revocation; remove its password in Nextcloud Security settings.', @@ -379,6 +428,8 @@ class NextcloudConnectionController extends Notifier { state = NextcloudConnectionState( account: account, phase: NextcloudConnectionPhase.connected, + failure: error, + removalFailed: true, error: error is NextcloudCredentialException ? error.message : 'BusyMark could not remove the local Nextcloud account data. Try again.', diff --git a/lib/src/nextcloud_notes/application/notes_navigation.dart b/lib/src/nextcloud_notes/application/notes_navigation.dart index 62de3b09..731d223b 100644 --- a/lib/src/nextcloud_notes/application/notes_navigation.dart +++ b/lib/src/nextcloud_notes/application/notes_navigation.dart @@ -1,5 +1,26 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../domain/notes_models.dart'; +/// Account-local browsing state, retained for this application session. +final notesNavigationProvider = + Provider.family( + (ref, id) => NotesNavigationController(), + ); +final notesCreatedProvider = + NotifierProvider< + NotesCreatedController, + ({String accountId, String localId})? + >(NotesCreatedController.new); + +class NotesCreatedController + extends Notifier<({String accountId, String localId})?> { + @override + ({String accountId, String localId})? build() => null; + void created(String accountId, String localId) => + state = (accountId: accountId, localId: localId); +} + enum NotesDestination { all, favorites, uncategorized, category, recovery } enum NotesSort { recent, oldest, titleAscending, titleDescending } @@ -14,10 +35,12 @@ class NotesNavigationController { NotesDestination destination = NotesDestination.all; NotesSort sort = NotesSort.recent; String category = ''; + double scrollOffset = 0; final collapsed = {}; final selected = {}; String? focused; String? anchor; + String? pendingReveal; List visible = const []; Map categoryCounts = const {}; List categoryPaths = const []; @@ -80,11 +103,24 @@ class NotesNavigationController { selected.retainAll(visible.map((n) => n.localId)); if (!visible.any((n) => n.localId == focused)) focused = null; if (!visible.any((n) => n.localId == anchor)) anchor = null; + if (!visible.any((n) => n.localId == pendingReveal)) pendingReveal = null; + } + + void showCreated(NextcloudNote note, Iterable notes) { + destination = note.category.isEmpty + ? NotesDestination.all + : NotesDestination.category; + category = note.category; + collapsed.removeWhere((parent) => note.category.startsWith('$parent/')); + update(notes, note.accountId); + select(note.localId); + pendingReveal = note.localId; } void select(String id, {bool toggle = false, bool range = false}) { final ids = visible.map((n) => n.localId).toList(); if (!ids.contains(id)) return; + if (pendingReveal != id) pendingReveal = null; focused = id; if (range && anchor != null && ids.contains(anchor)) { final a = ids.indexOf(anchor!); @@ -113,10 +149,30 @@ class NotesNavigationController { enum NotesBatchStatus { changed, skipped, conflicted, failed } +enum NotesBatchReason { + unchanged, + readOnly, + reviewChanged, + needsReview, + missing, + storage, + editorNotSaved, + unavailable, +} + class NotesBatchOutcome { - const NotesBatchOutcome(this.localId, this.title, this.status, [this.detail]); + const NotesBatchOutcome( + this.localId, + this.title, + this.status, [ + this.detail, + this.reason, + this.cause, + ]); final String localId; final String title; final NotesBatchStatus status; final String? detail; + final NotesBatchReason? reason; + final Object? cause; } diff --git a/lib/src/nextcloud_notes/application/notes_offline_controller.dart b/lib/src/nextcloud_notes/application/notes_offline_controller.dart index ea2f2c83..08520e2a 100644 --- a/lib/src/nextcloud_notes/application/notes_offline_controller.dart +++ b/lib/src/nextcloud_notes/application/notes_offline_controller.dart @@ -69,6 +69,7 @@ class NotesOfflineController { String target, { bool required = true, bool paused = false, + bool waitForDownloads = true, }) async { await repository.setOfflineRequirement( accountId, @@ -96,13 +97,19 @@ class NotesOfflineController { failures.removeWhere((key, _) => key.startsWith('${note.localId}:')); } } - await reconcile(); + _emit(); + final job = reconcile(); + if (waitForDownloads) { + await job; + } else { + unawaited(job); + } } Future cancelNote(String id) => - setRequirement('pause', id, paused: true); + setRequirement('pause', id, paused: true, waitForDownloads: false); - Future retry([String? noteId]) async { + Future retry([String? noteId, bool waitForDownloads = true]) async { for (final r in requirements.toList()) { if (r['kind'] == 'pause' && (noteId == null || r['target'] == noteId)) { await repository.setOfflineRequirement( @@ -121,9 +128,21 @@ class NotesOfflineController { } } requirements = await repository.store.offlineRequirements(accountId); - _attempted.clear(); - failures.clear(); - await reconcile(); + if (noteId == null) { + _attempted.clear(); + failures.clear(); + } else { + _attempted.removeWhere((key) => key.startsWith('$noteId:')); + failures.removeWhere((key, _) => key.startsWith('$noteId:')); + } + _generation++; + _emit(); + final job = reconcile(); + if (waitForDownloads) { + await job; + } else { + unawaited(job); + } } void _schedule() { @@ -181,7 +200,15 @@ class NotesOfflineController { (n) => n.accountId == accountId && retained(n) && !isNotesRecovery(n), )) { if (_disposed || repository.accountById(accountId) == null) return; - var status = await inspect(note); + NotesOfflineStatus status; + try { + status = await inspect(note); + failures.remove('${note.localId}:inspection'); + } on Object catch (error) { + failures['${note.localId}:inspection'] = error.toString(); + _emit(); + continue; + } if (_disposed) return; statuses[note.localId] = status; _emit(); @@ -200,15 +227,19 @@ class NotesOfflineController { ); if (path == null) failures[key] = 'Attachment unavailable'; } on Object catch (error) { - failures[key] = error.toString(); + if (_active(note)) failures[key] = error.toString(); } if (_disposed || repository.accountById(accountId) == null) { return; } final current = repository.noteById(note.localId); if (current != null) { - status = await inspect(current); - statuses[note.localId] = status; + try { + status = await inspect(current); + statuses[note.localId] = status; + } on Object catch (error) { + failures['${note.localId}:inspection'] = error.toString(); + } _emit(); } } diff --git a/lib/src/nextcloud_notes/application/notes_repository.dart b/lib/src/nextcloud_notes/application/notes_repository.dart index 36e6b04a..4d68217d 100644 --- a/lib/src/nextcloud_notes/application/notes_repository.dart +++ b/lib/src/nextcloud_notes/application/notes_repository.dart @@ -427,6 +427,15 @@ class NotesRepository { : updated.revision == before ? NotesBatchStatus.skipped : NotesBatchStatus.changed, + null, + updated.metadataConflict != null || + updated.syncState == NoteSyncState.conflict + ? (before != original.revision + ? NotesBatchReason.reviewChanged + : NotesBatchReason.needsReview) + : updated.revision == before + ? NotesBatchReason.unchanged + : null, ), ); } on NotesException catch (error) { @@ -441,15 +450,30 @@ class NotesRepository { ? NotesBatchStatus.conflicted : NotesBatchStatus.failed, error.message, + switch (error.code) { + NotesFailureCode.forbidden => + original.readonly + ? NotesBatchReason.readOnly + : NotesBatchReason.unavailable, + NotesFailureCode.missing => NotesBatchReason.missing, + NotesFailureCode.conflict => + noteById(original.localId)?.revision != original.revision + ? NotesBatchReason.reviewChanged + : NotesBatchReason.needsReview, + _ => NotesBatchReason.unavailable, + }, + error, ), ); - } on Object { + } on Object catch (error) { outcomes.add( NotesBatchOutcome( original.localId, original.title, NotesBatchStatus.failed, 'Local transaction failed; successful items remain saved.', + NotesBatchReason.storage, + error, ), ); } @@ -783,10 +807,9 @@ class NotesRepository { for (final field in patch.keys) { if (fresh.toJson()[field] != original.toJson()[field] && fresh.toJson()[field] != patch[field]) { - throw const NotesException( - NotesFailureCode.conflict, + throw const NotesSettingsConflict( + NotesSettingsConflictReason.stale, 'Server settings changed while this form was open. Reload and review them.', - scope: NotesRequestScope.settings, ); } } @@ -817,10 +840,11 @@ class NotesRepository { _notes[a.noteId]?.accountId == id && !{'cached', 'uploaded', 'deleted'}.contains(a.state), ))) { - throw const NotesException( - NotesFailureCode.conflict, + throw NotesSettingsConflict( + hasSettingsBlockers(id) + ? NotesSettingsConflictReason.pendingNotes + : NotesSettingsConflictReason.pendingAttachments, 'Synchronize or resolve pending notes, conflicts and attachment operations before changing the server collection.', - scope: NotesRequestScope.settings, ); } final current = _accounts[id]!.copyWith(settingsAttempt: attempt); @@ -2425,6 +2449,7 @@ class NotesRepository { int? expectedRevision, int? creationCandidateServerId, NotesCreationReview? creationReview, + NoteState? reviewedRemote, }) async { final original = _require(localId); if (expectedRevision != null && original.revision != expectedRevision) { @@ -2505,8 +2530,9 @@ class NotesRepository { 'New local edits appeared while resolving this note. Review them before applying the resolution.', ); } - if (original.remote != null && - jsonEncode(fresh.toJson()) != jsonEncode(original.remote!.toJson())) { + final reviewed = reviewedRemote ?? original.remote; + if (reviewed != null && + jsonEncode(fresh.toJson()) != jsonEncode(reviewed.toJson())) { final changedAgain = current.copyWith( remote: fresh, readonly: fresh.readonly, @@ -2517,11 +2543,7 @@ class NotesRepository { await store.saveNote(changedAgain); _notes[localId] = changedAgain; _notify(); - throw NotesException( - NotesFailureCode.conflict, - changedAgain.errorMessage!, - remote: fresh, - ); + throw NotesConflictReviewChanged(fresh); } final merge = NotesConflictMerge(current, fresh); final merging = resolution == NoteConflictResolution.merge; diff --git a/lib/src/nextcloud_notes/application/notes_search_controller.dart b/lib/src/nextcloud_notes/application/notes_search_controller.dart index 890bdbca..49be522e 100644 --- a/lib/src/nextcloud_notes/application/notes_search_controller.dart +++ b/lib/src/nextcloud_notes/application/notes_search_controller.dart @@ -53,7 +53,6 @@ class NotesSearchController { Future search( String text, { - bool wholeWord = false, int limit = 80, List overlays = const [], }) async { @@ -61,7 +60,11 @@ class NotesSearchController { bool current() => !_disposed && generation == _generation; _publish(const NotesSearchState(loading: true, indexing: true)); try { - final query = NotesSearchQuery(text, wholeWord: wholeWord); + final query = NotesSearchQuery(text); + if (query.terms.isEmpty) { + if (current()) _publish(const NotesSearchState()); + return; + } while (await store.indexStep() != 0) { if (!current()) return; } diff --git a/lib/src/nextcloud_notes/application/notes_settings_controller.dart b/lib/src/nextcloud_notes/application/notes_settings_controller.dart index 316d6046..f602d7b2 100644 --- a/lib/src/nextcloud_notes/application/notes_settings_controller.dart +++ b/lib/src/nextcloud_notes/application/notes_settings_controller.dart @@ -5,6 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../domain/notes_models.dart'; import 'nextcloud_connection.dart'; +enum NotesSettingsFailure { + buffersNotSaved, + operation, + stale, + pendingNotes, + pendingAttachments, +} + enum NotesSettingsPhase { loading, ready, saving, saved, failed } class NotesSettingsState { @@ -15,6 +23,10 @@ class NotesSettingsState { this.phase = NotesSettingsPhase.loading, this.error, this.normalized = false, + this.failure, + this.refreshFailed = false, + this.cause, + this.refreshing = false, }); final String? accountId; final NotesSettings? original; @@ -22,6 +34,10 @@ class NotesSettingsState { final NotesSettingsPhase phase; final String? error; final bool normalized; + final NotesSettingsFailure? failure; + final bool refreshFailed; + final Object? cause; + final bool refreshing; Map get patch => { if (draft != null && original != null && @@ -61,7 +77,12 @@ class NotesSettingsController extends Notifier { return; } final generation = ++_generation; - state = NotesSettingsState(accountId: id); + final previous = state; + state = NotesSettingsState( + accountId: id, + original: previous.original, + draft: previous.draft, + ); try { final repository = await ref.read( nextcloudNotesRepositoryProvider.future, @@ -84,12 +105,16 @@ class NotesSettingsController extends Notifier { draft: settings, phase: NotesSettingsPhase.ready, ); - } on NotesException catch (error) { + } on Object catch (error) { if (_disposed || generation != _generation) return; state = NotesSettingsState( accountId: id, + original: previous.original, + draft: previous.draft, phase: NotesSettingsPhase.failed, - error: error.message, + error: error.toString(), + cause: error, + failure: NotesSettingsFailure.operation, ); } } @@ -105,6 +130,7 @@ class NotesSettingsController extends Notifier { fileSuffix: fileSuffix ?? draft.fileSuffix, ), phase: NotesSettingsPhase.ready, + refreshing: state.refreshing, ); } @@ -141,6 +167,7 @@ class NotesSettingsController extends Notifier { original: original, draft: draft, phase: NotesSettingsPhase.failed, + failure: NotesSettingsFailure.buffersNotSaved, ); } return false; @@ -160,19 +187,53 @@ class NotesSettingsController extends Notifier { original: result, draft: result, phase: NotesSettingsPhase.saved, + refreshing: true, normalized: patch.entries.any((e) => result.toJson()[e.key] != e.value), ); // Checkpoint invalidation is durable before this authoritative read. - await repository.synchronize(id, allowWrites: false); - return true; - } on NotesException catch (error) { + var refreshFailed = false; + try { + await repository.synchronize(id, allowWrites: false); + } on Object { + refreshFailed = true; + } + if (_disposed || generation != _generation) return false; + // The acknowledged response is already the baseline. Refresh owns only + // its progress/warning, never input entered since that acknowledgement. + state = NotesSettingsState( + accountId: state.accountId, + original: state.original, + draft: state.draft, + phase: state.phase, + normalized: state.normalized, + error: state.error, + cause: state.cause, + failure: state.failure, + refreshFailed: refreshFailed, + ); + return state.accountId == id && !state.dirty; + } on Object catch (error) { if (!_disposed && generation == _generation) { state = NotesSettingsState( accountId: id, original: original, draft: draft, phase: NotesSettingsPhase.failed, - error: error.message, + error: error.toString(), + cause: error, + failure: switch (error) { + NotesSettingsConflict(reason: NotesSettingsConflictReason.stale) => + NotesSettingsFailure.stale, + NotesSettingsConflict( + reason: NotesSettingsConflictReason.pendingNotes, + ) => + NotesSettingsFailure.pendingNotes, + NotesSettingsConflict( + reason: NotesSettingsConflictReason.pendingAttachments, + ) => + NotesSettingsFailure.pendingAttachments, + _ => NotesSettingsFailure.operation, + }, ); } return false; diff --git a/lib/src/nextcloud_notes/application/notes_transfer_service.dart b/lib/src/nextcloud_notes/application/notes_transfer_service.dart index 200942bd..5ce11bd8 100644 --- a/lib/src/nextcloud_notes/application/notes_transfer_service.dart +++ b/lib/src/nextcloud_notes/application/notes_transfer_service.dart @@ -14,11 +14,41 @@ import '../domain/notes_models.dart'; import '../domain/notes_search.dart' show normalizeNotesSearch; import 'notes_repository.dart'; +class NotesTransferCancelled extends FileSystemException { + const NotesTransferCancelled() : super('Transfer cancelled.'); +} + +enum NotesTransferFailureReason { + sourceChanged, + sourceUnreadable, + invalidSnapshot, + mediaTooLarge, + unsafeSource, + destinationUnavailable, + exportChanged, + operation, +} + +class NotesTransferException extends FileSystemException { + const NotesTransferException(this.reason, String message, [String path = '']) + : super(message, path); + final NotesTransferFailureReason reason; +} + +NotesTransferFailureReason notesTransferFailureReason(Object? error) => + switch (error) { + NotesTransferException(:final reason) => reason, + FormatException() || + TypeError() => NotesTransferFailureReason.invalidSnapshot, + FileSystemException() => NotesTransferFailureReason.sourceUnreadable, + _ => NotesTransferFailureReason.operation, + }; + class NotesTransferCancellation { bool cancelled = false; void cancel() => cancelled = true; void check() { - if (cancelled) throw const FileSystemException('Transfer cancelled.'); + if (cancelled) throw const NotesTransferCancelled(); } } @@ -69,10 +99,14 @@ class NotesImportOutcome { this.noteId, this.error, this.alreadyImported = false, + this.reason, + this.cause, }); final String item; final String? noteId, error; final bool alreadyImported; + final NotesTransferFailureReason? reason; + final Object? cause; } /// Portable snapshots contain document metadata only. Every note is an @@ -92,6 +126,7 @@ class NotesTransferService { required NotesTransferCancellation cancellation, void Function(int completed, int total)? onProgress, }) async { + onProgress?.call(0, notes.length); final snapshot = notes is NotesLocalSnapshot ? notes : NotesLocalSnapshot( @@ -113,7 +148,8 @@ class NotesTransferService { snapshot.accountGenerations.entries.any( (e) => repository.accountGeneration(e.key) != e.value, )) { - throw const FileSystemException( + throw const NotesTransferException( + NotesTransferFailureReason.exportChanged, 'Export media changed; capture the snapshot again.', ); } @@ -131,6 +167,7 @@ class NotesTransferService { final omissions = []; final directories = {'': ''}; final used = {}; + var didPublish = false; try { for (final note in snapshot) { checkSnapshot(); @@ -212,13 +249,16 @@ class NotesTransferService { throw const FileSystemException('Export destination already exists.'); } await staging.rename(published); + didPublish = true; return NotesExportResult( published, documents.length, List.unmodifiable(omissions), ); } finally { - if (await staging.exists()) await staging.delete(recursive: true); + if (!didPublish && await staging.exists()) { + await staging.delete(recursive: true); + } } } @@ -228,6 +268,7 @@ class NotesTransferService { String category = '', NotesTransferCancellation? cancellation, }) async { + cancellation?.check(); await _safeAbsolute(source); final type = await FileSystemEntity.type(source, followLinks: false); final root = type == FileSystemEntityType.directory @@ -392,6 +433,7 @@ class NotesTransferService { ), ); } + cancellation?.check(); return NotesImportReview(root, items, issues); } @@ -491,6 +533,7 @@ class NotesTransferService { ); final outcomes = []; final selected = review.items.where((i) => i.selected).toList(); + onProgress?.call(0, selected.length); for (final item in selected) { if (cancellation.cancelled) break; try { @@ -513,7 +556,11 @@ class NotesTransferService { final file = File(item.path); if (await file.length() > maximumDocumentBytes || await file.readAsString() != item.content) { - throw const FileSystemException('Document changed after review.'); + throw NotesTransferException( + NotesTransferFailureReason.sourceChanged, + 'Document changed after review.', + item.path, + ); } final media = {}; var total = 0; @@ -524,11 +571,19 @@ class NotesTransferService { final length = await asset.length(); total += length; if (length > maximumManagedAssetBytes || total > 256 * 1024 * 1024) { - throw const FileSystemException('Media exceeds import size limit.'); + throw NotesTransferException( + NotesTransferFailureReason.mediaTooLarge, + 'Media exceeds import size limit.', + entry.value.path, + ); } final bytes = await asset.readAsBytes(); if (sha256.convert(bytes).toString() != entry.value.digest) { - throw const FileSystemException('Media changed after review.'); + throw NotesTransferException( + NotesTransferFailureReason.sourceChanged, + 'Media changed after review.', + entry.value.path, + ); } media[entry.key] = (filename: entry.value.filename, bytes: bytes); } @@ -546,8 +601,17 @@ class NotesTransferService { ); item.importedNoteId = note.localId; outcomes.add(NotesImportOutcome(item.title, noteId: note.localId)); + } on NotesTransferCancelled { + break; } on Object catch (error) { - outcomes.add(NotesImportOutcome(item.title, error: error.toString())); + outcomes.add( + NotesImportOutcome( + item.title, + error: error.toString(), + reason: notesTransferFailureReason(error), + cause: error, + ), + ); } onProgress?.call(outcomes.length, selected.length); } @@ -613,12 +677,18 @@ class NotesTransferService { static Future _within(String root, String path) async { if (!p.isWithin(root, path)) { - throw const FileSystemException('Import path escapes source.'); + throw const NotesTransferException( + NotesTransferFailureReason.unsafeSource, + 'Import path escapes source.', + ); } await _safeAbsolute(path); final resolved = await File(path).resolveSymbolicLinks(); if (!p.isWithin(root, resolved)) { - throw const FileSystemException('Import path escapes source.'); + throw const NotesTransferException( + NotesTransferFailureReason.unsafeSource, + 'Import path escapes source.', + ); } } @@ -626,7 +696,8 @@ class NotesTransferService { var current = p.normalize(p.absolute(path)); while (true) { if (await FileSystemEntity.isLink(current)) { - throw const FileSystemException( + throw const NotesTransferException( + NotesTransferFailureReason.unsafeSource, 'Symbolic links are not allowed in transfers.', ); } diff --git a/lib/src/nextcloud_notes/data/notes_secret_store.dart b/lib/src/nextcloud_notes/data/notes_secret_store.dart index 6494c43e..04afdf5c 100644 --- a/lib/src/nextcloud_notes/data/notes_secret_store.dart +++ b/lib/src/nextcloud_notes/data/notes_secret_store.dart @@ -8,9 +8,19 @@ abstract interface class NextcloudSecretStore { Future delete(String accountId); } +enum NextcloudCredentialFailure { + unavailable, + identityMismatch, + alreadyConnected, +} + class NextcloudCredentialException implements Exception { - const NextcloudCredentialException(this.message); + const NextcloudCredentialException( + this.message, { + this.code = NextcloudCredentialFailure.unavailable, + }); final String message; + final NextcloudCredentialFailure code; @override String toString() => message; } diff --git a/lib/src/nextcloud_notes/data/notes_store.dart b/lib/src/nextcloud_notes/data/notes_store.dart index 0161959f..e2b66c52 100644 --- a/lib/src/nextcloud_notes/data/notes_store.dart +++ b/lib/src/nextcloud_notes/data/notes_store.dart @@ -237,7 +237,6 @@ class NotesStore { await _call('searchChunk', { 'account': accountId, 'query': query.text, - 'wholeWord': query.wholeWord, 'after': after, 'continuation': continuation, 'recovery': recovery, @@ -462,10 +461,7 @@ Object? _execute(Database db, String action, dynamic argument) { return null; case 'searchChunk': final a = argument as Map; - final query = NotesSearchQuery( - a['query'] as String, - wholeWord: a['wholeWord'] as bool, - ); + final query = NotesSearchQuery(a['query'] as String); final expression = query.ftsCandidates; final recovery = a['recovery'] == true; final rows = db.select( diff --git a/lib/src/nextcloud_notes/data/server_uri.dart b/lib/src/nextcloud_notes/data/server_uri.dart index e6ed8b0f..c1c844aa 100644 --- a/lib/src/nextcloud_notes/data/server_uri.dart +++ b/lib/src/nextcloud_notes/data/server_uri.dart @@ -1,3 +1,9 @@ +/// A typed validation failure; presentation must not parse diagnostic text. +class NextcloudServerUrlException extends FormatException { + const NextcloudServerUrlException() + : super('Enter a valid HTTPS Nextcloud server URL.'); +} + /// Validates a Nextcloud installation URL without losing its path prefix. Uri normalizeNextcloudServer(String input) { final value = input.trim(); @@ -17,7 +23,7 @@ Uri normalizeNextcloudServer(String input) { ); return decoded == '.' || decoded == '..'; })) { - throw const FormatException('Enter a valid HTTPS Nextcloud server URL.'); + throw const NextcloudServerUrlException(); } final uri = Uri.tryParse(value); if (uri == null || @@ -38,7 +44,7 @@ Uri normalizeNextcloudServer(String input) { part.contains('\\') || part.contains(RegExp(r'[\x00-\x1f\x7f]')), )) { - throw const FormatException('Enter a valid HTTPS Nextcloud server URL.'); + throw const NextcloudServerUrlException(); } // Preserve the installation prefix. Production never permits plaintext auth. final path = uri.path.replaceFirst(RegExp(r'/+$'), ''); diff --git a/lib/src/nextcloud_notes/domain/notes_models.dart b/lib/src/nextcloud_notes/domain/notes_models.dart index 3dc37ba4..682f8140 100644 --- a/lib/src/nextcloud_notes/domain/notes_models.dart +++ b/lib/src/nextcloud_notes/domain/notes_models.dart @@ -108,6 +108,36 @@ class NotesException implements Exception { String toString() => message; } +/// Consent to resolve applies to the remote snapshot shown by the dialog. +class NotesConflictReviewChanged extends NotesException { + const NotesConflictReviewChanged(NoteState remote) + : super( + NotesFailureCode.conflict, + 'Review the changed server version before resolving this note.', + remote: remote, + ); +} + +class NotesRecoveryResult { + const NotesRecoveryResult({this.noteId, this.opened = false, this.failure}); + final String? noteId; + final bool opened; + final Object? failure; + bool get created => noteId != null; +} + +enum NotesSettingsConflictReason { stale, pendingNotes, pendingAttachments } + +class NotesSettingsConflict extends NotesException { + const NotesSettingsConflict(this.reason, String message) + : super( + NotesFailureCode.conflict, + message, + scope: NotesRequestScope.settings, + ); + final NotesSettingsConflictReason reason; +} + class NextcloudAccount { const NextcloudAccount({ required this.id, diff --git a/lib/src/nextcloud_notes/domain/notes_search.dart b/lib/src/nextcloud_notes/domain/notes_search.dart index e5b3c40b..531a8eeb 100644 --- a/lib/src/nextcloud_notes/domain/notes_search.dart +++ b/lib/src/nextcloud_notes/domain/notes_search.dart @@ -16,7 +16,7 @@ class NotesSearchTerm { /// Only these two field prefixes and quotes are grammar. Everything else, /// including SQL/FTS operators, punctuation and wildcards, is literal text. class NotesSearchQuery { - NotesSearchQuery(this.text, {this.wholeWord = false}) { + NotesSearchQuery(this.text) { if (text.length > 512) throw const FormatException('Search is too long.'); if ('"'.allMatches(text).length.isOdd) { throw const FormatException('Close the quoted phrase.'); @@ -37,7 +37,6 @@ class NotesSearchQuery { } } final String text; - final bool wholeWord; late final List terms; String? get ftsCandidates { final indexable = terms.where((t) => t.literal.runes.length >= 3); @@ -105,48 +104,17 @@ class NotesSearchHit { String notesSearchDigest(String source) => sha256.convert(utf8.encode(source)).toString(); -final _word = RegExp(r'[\p{L}\p{M}\p{N}_]', unicode: true); - -Iterable<({int start, int end})> _ranges( - String normalized, - String term, - bool wholeWord, -) sync* { +Iterable<({int start, int end})> _ranges(String normalized, String term) sync* { var position = 0; while (position <= normalized.length) { final start = normalized.indexOf(term, position); if (start < 0) break; final end = start + term.length; - // Unicode identifier boundaries; punctuation in the query remains literal. - if (!wholeWord || - ((start == 0 || - !_word.hasMatch( - String.fromCharCode(normalized.runesBefore(start)), - )) && - (end == normalized.length || - !_word.hasMatch( - String.fromCharCode(normalized.runesAt(end)), - )))) { - yield (start: start, end: end); - } + yield (start: start, end: end); position = end; } } -extension on String { - int runesBefore(int offset) { - var start = offset - 1; - if (start > 0 && - codeUnitAt(start) >= 0xdc00 && - codeUnitAt(start) <= 0xdfff) { - start--; - } - return substring(start, offset).runes.first; - } - - int runesAt(int offset) => substring(offset).runes.first; -} - /// Shared verifier for indexed durable documents, dirty overlays and stale /// navigation. Offsets map back through grapheme clusters to authored UTF-16. List matchNotesDocument({ @@ -169,15 +137,15 @@ List matchNotesDocument({ for (final term in query.terms) { final contentMatches = (term.field == null - ? _ranges(body, term.literal, query.wholeWord) + ? _ranges(body, term.literal) : const <({int start, int end})>[]) .iterator; final hasContent = contentMatches.moveNext(); final titleMatches = term.field != 'category' - ? _ranges(normalizedTitle, term.literal, query.wholeWord) + ? _ranges(normalizedTitle, term.literal) : <({int start, int end})>[]; final categoryMatches = term.field != 'title' - ? _ranges(normalizedCategory, term.literal, query.wholeWord) + ? _ranges(normalizedCategory, term.literal) : <({int start, int end})>[]; if (!hasContent && titleMatches.isEmpty && categoryMatches.isEmpty) { return []; diff --git a/lib/src/nextcloud_notes/presentation/nextcloud_settings.dart b/lib/src/nextcloud_notes/presentation/nextcloud_settings.dart index a6c7822e..b7fb5185 100644 --- a/lib/src/nextcloud_notes/presentation/nextcloud_settings.dart +++ b/lib/src/nextcloud_notes/presentation/nextcloud_settings.dart @@ -1,3 +1,4 @@ +import '../../search/welcome_notes_action.dart'; import 'dart:async'; import 'package:flutter/material.dart'; @@ -12,6 +13,7 @@ import '../../workspace/workspace_safety.dart'; import '../application/nextcloud_connection.dart'; import '../application/notes_settings_controller.dart'; import 'notes_sidebar.dart' show nextcloudNotesChangesProvider; +import 'notes_messages.dart'; class NextcloudNotesSettings extends ConsumerStatefulWidget { const NextcloudNotesSettings({super.key}); @@ -23,14 +25,34 @@ class NextcloudNotesSettings extends ConsumerStatefulWidget { class _NextcloudNotesSettingsState extends ConsumerState { final _server = TextEditingController(); + late final WelcomeNotesActionController _pendingWelcome; + bool _returningToWelcome = false; + bool _localSaveFailed = false; + Timer? _retryTimer; + DateTime? _retryDeadline; + @override + void initState() { + super.initState(); + _pendingWelcome = ref.read(welcomeNotesActionProvider.notifier); + } + @override void dispose() { + _retryTimer?.cancel(); + if (!_returningToWelcome) Future.microtask(_pendingWelcome.clear); _server.dispose(); super.dispose(); } Future _connect() async { - await ref.read(nextcloudConnectionProvider.notifier).connect(_server.text); + final connected = await ref + .read(nextcloudConnectionProvider.notifier) + .connect(_server.text); + if (mounted && connected && ref.read(welcomeNotesActionProvider) != null) { + _returningToWelcome = true; + context.go('/'); + return; + } if (!mounted) return; final account = ref.read(nextcloudConnectionProvider).account; if (account != null) { @@ -39,6 +61,7 @@ class _NextcloudNotesSettingsState } Future _openAccount(String accountId) async { + if (!await confirmNotesSettingsDeparture(context) || !mounted) return; if (!await confirmSafeToContinue(context, ref) || !mounted) return; final opened = await ref .read(workspaceControllerProvider.notifier) @@ -51,7 +74,24 @@ class _NextcloudNotesSettingsState final connection = ref.watch(nextcloudConnectionProvider); final controller = ref.read(nextcloudConnectionProvider.notifier); ref.watch(nextcloudNotesChangesProvider); - final repository = ref.watch(nextcloudNotesRepositoryProvider).value; + final repositoryState = ref.watch(nextcloudNotesRepositoryProvider); + final repository = repositoryState.value; + final deadline = connection.retryNotBefore; + final throttled = deadline?.isAfter(DateTime.now()) ?? false; + if (_retryDeadline != deadline) { + _retryDeadline = deadline; + _retryTimer?.cancel(); + if (throttled) { + _retryTimer = Timer(deadline!.difference(DateTime.now()), () { + if (mounted) setState(() {}); + }); + } + } + final actionsEnabled = + !connection.busy && + !repositoryState.isLoading && + !repositoryState.hasError; + final signInEnabled = actionsEnabled && !throttled; final account = connection.account == null ? null : repository == null @@ -69,78 +109,144 @@ class _NextcloudNotesSettingsState key: const ValueKey('nextcloud-server'), label: context.l10n.nextcloudServer, controller: _server, - enabled: !connection.busy, + enabled: actionsEnabled, keyboardType: TextInputType.url, + textDirection: TextDirection.ltr, autocorrect: false, enableSuggestions: false, onSubmitted: (_) => - connection.busy ? null : unawaited(_connect()), + signInEnabled ? unawaited(_connect()) : null, ), BusyMarkActionRow( key: const ValueKey('nextcloud-connect'), title: context.l10n.nextcloudConnect, subtitle: context.l10n.nextcloudSignInBrowser, - onTap: connection.busy ? null : () => unawaited(_connect()), + enabled: signInEnabled, + onTap: signInEnabled ? () => unawaited(_connect()) : null, ), ] else ...[ BusyMarkActionRow( title: account.loginName, - subtitle: account.server.toString(), - trailing: Text( - '${context.l10n.nextcloudNotes} ${account.appVersion} · ${account.apiVersion}', + subtitleWidget: Directionality( + textDirection: TextDirection.ltr, + child: SelectableText(account.server.toString()), ), + tooltip: + '${context.l10n.nextcloudNotes} ${account.appVersion} · ${account.apiVersion}', ), BusyMarkActionRow( title: context.l10n.open, - onTap: connection.busy + enabled: actionsEnabled, + onTap: !actionsEnabled ? null : () => unawaited(_openAccount(account.id)), ), BusyMarkActionRow( title: context.l10n.nextcloudReconnect, - onTap: connection.busy + enabled: signInEnabled, + onTap: !signInEnabled ? null - : () => unawaited(controller.reconnect()), + : () async { + if (await confirmNotesSettingsDeparture(context)) { + await controller.reconnect(); + } + }, ), BusyMarkActionRow( title: context.l10n.nextcloudDisconnect, - onTap: connection.busy ? null : () => unawaited(_disconnect()), + destructive: true, + enabled: actionsEnabled, + onTap: actionsEnabled ? () => unawaited(_disconnect()) : null, ), ], if (connection.busy) - BusyMarkActionRow( - title: context.l10n.nextcloudSignInBrowser, - trailing: const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), + Semantics( + liveRegion: true, + child: BusyMarkActionRow( + title: switch (connection.phase) { + NextcloudConnectionPhase.starting => + context.l10n.notesStartingConnection, + NextcloudConnectionPhase.awaitingBrowser => + context.l10n.nextcloudSignInBrowser, + NextcloudConnectionPhase.verifying => + context.l10n.notesVerifyingConnection, + NextcloudConnectionPhase.cancelling => + context.l10n.notesCancellingConnection, + NextcloudConnectionPhase.removing => + context.l10n.notesRemovingConnection, + _ => context.l10n.nextcloudNotes, + }, + trailing: const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), ), - if (connection.phase == NextcloudConnectionPhase.awaitingBrowser) + if ({ + NextcloudConnectionPhase.starting, + NextcloudConnectionPhase.awaitingBrowser, + }.contains(connection.phase)) BusyMarkActionRow( title: context.l10n.cancel, - onTap: controller.cancel, + onTap: () { + ref.read(welcomeNotesActionProvider.notifier).clear(); + controller.cancel(); + }, ), ], ), + if (repositoryState.isLoading && !connection.busy) + LinearProgressIndicator(semanticsLabel: context.l10n.nextcloudNotes), + if (repositoryState.hasError) ...[ + BusyMarkStatusBox( + message: context.l10n.notesLoadFailed, + kind: BusyMarkStatusKind.error, + ), + BusyMarkActionRow( + title: context.l10n.visualizationRetry, + enabled: !connection.busy && !repositoryState.isLoading, + onTap: connection.busy || repositoryState.isLoading + ? null + : () { + ref.invalidate(nextcloudNotesRepositoryProvider); + unawaited(controller.reload()); + }, + ), + ], if (account != null) ...[ _ServerNotesSettings(accountId: account.id), if (repository?.accountError(account.id) case final error?) BusyMarkStatusBox( - message: error.message, + message: localizeNotesFailure(context, error), kind: BusyMarkStatusKind.error, ), ], - if (connection.error != null) + if (_localSaveFailed) BusyMarkStatusBox( - message: connection.error!, + message: context.l10n.notesBuffersNotSaved, kind: BusyMarkStatusKind.error, ), + if (connection.error != null && + !repositoryState.hasError && + !repositoryState.isLoading) + BusyMarkStatusBox( + message: connection.removedWithWarning + ? context.l10n.notesRemovedWarning + : connection.removalFailed + ? context.l10n.notesLocalRemovalFailed + : localizeNotesConnectionFailure(context, connection.failure), + kind: connection.removedWithWarning + ? BusyMarkStatusKind.warning + : BusyMarkStatusKind.error, + ), + if (throttled) Text(notesLocalDate(context, deadline!)), ], ); } Future _disconnect() async { + if (!await confirmNotesSettingsDeparture(context) || !mounted) return; final confirmed = await showBusyMarkModalDialog( context, builder: (context) => BusyMarkDialogShell( @@ -163,7 +269,10 @@ class _NextcloudNotesSettingsState final saved = await ref .read(workspaceControllerProvider.notifier) .saveAll(); - if (!saved.succeeded) return; + if (!saved.succeeded) { + if (mounted) setState(() => _localSaveFailed = true); + return; + } if (!mounted) return; final accountId = ref.read(nextcloudConnectionProvider).account?.id; final removed = await ref @@ -247,6 +356,7 @@ class _ServerNotesSettingsState extends ConsumerState<_ServerNotesSettings> { BusyMarkGroupedTextEntry( key: const ValueKey('notes-settings-suffix'), label: context.l10n.nextcloudFileSuffix, + textDirection: TextDirection.ltr, controller: _suffix, enabled: draft != null && !settings.busy, onChanged: (value) => controller.edit(fileSuffix: value), @@ -254,6 +364,7 @@ class _ServerNotesSettingsState extends ConsumerState<_ServerNotesSettings> { ], ), Text(context.l10n.nextcloudServerSettingsExplanation), + Text(context.l10n.notesPathRelative), if (settings.busy) ...[ Semantics( liveRegion: true, @@ -269,11 +380,28 @@ class _ServerNotesSettingsState extends ConsumerState<_ServerNotesSettings> { : context.l10n.nextcloudSettingsSaving, ), ], - if (settings.error != null) + if (settings.failure != null || settings.error != null) BusyMarkStatusBox( - message: settings.error!, + message: settings.failure == NotesSettingsFailure.buffersNotSaved + ? context.l10n.notesBuffersNotSaved + : settings.failure == NotesSettingsFailure.stale + ? context.l10n.notesSettingsStale + : settings.failure == NotesSettingsFailure.pendingNotes + ? context.l10n.notesSettingsPendingNotes + : settings.failure == NotesSettingsFailure.pendingAttachments + ? context.l10n.notesSettingsPendingAttachments + : localizeNotesFailure(context, settings.cause), kind: BusyMarkStatusKind.error, ), + if (settings.refreshFailed) + BusyMarkStatusBox( + message: context.l10n.notesSettingsRefreshFailed, + kind: BusyMarkStatusKind.warning, + ), + if (settings.refreshing) + LinearProgressIndicator( + semanticsLabel: context.l10n.notesSettingsRefreshing, + ), if (settings.phase == NotesSettingsPhase.saved) Text( settings.normalized @@ -281,7 +409,9 @@ class _ServerNotesSettingsState extends ConsumerState<_ServerNotesSettings> { : context.l10n.nextcloudSettingsSaved, ), if (settings.dirty) Text(context.l10n.closeUnsavedChangesTitle), - Row( + Wrap( + spacing: BusyMarkSpacing.sm, + runSpacing: BusyMarkSpacing.sm, children: [ BusyMarkPushButton.standard( key: const ValueKey('notes-settings-save'), @@ -305,10 +435,12 @@ class _ServerNotesSettingsState extends ConsumerState<_ServerNotesSettings> { child: Text(context.l10n.cancel), ), BusyMarkPushButton.standard( - onPressed: !settings.busy && !settings.dirty - ? () => unawaited(controller.load(widget.accountId)) + onPressed: !settings.busy + ? () => unawaited( + reloadNotesSettings(context, widget.accountId), + ) : null, - child: Text(context.l10n.visualizationRetry), + child: Text(context.l10n.notesSettingsReload), ), ], ), @@ -316,3 +448,79 @@ class _ServerNotesSettingsState extends ConsumerState<_ServerNotesSettings> { ); } } + +/// Every settings departure uses the same decision, including native Back and +/// programmatic route exits. Keep the provider alive until the decision settles. +Future confirmNotesSettingsDeparture(BuildContext context) async { + final container = ProviderScope.containerOf(context, listen: false); + if (!container.exists(notesSettingsProvider)) return true; + final settings = container.read(notesSettingsProvider); + if (settings.phase == NotesSettingsPhase.saving) return false; + if (!settings.dirty) return true; + final decision = await showBusyMarkModalDialog( + context, + builder: (context) => BusyMarkDialogShell( + title: context.l10n.notesSettingsLeave, + actions: [ + BusyMarkDialogButton( + label: context.l10n.cancel, + onPressed: () => Navigator.pop(context), + ), + BusyMarkDialogButton( + label: context.l10n.discard, + destructive: true, + onPressed: () => Navigator.pop(context, 'discard'), + ), + BusyMarkDialogButton( + label: context.l10n.save, + onPressed: () => Navigator.pop(context, 'save'), + ), + ], + children: [Text(context.l10n.notesSettingsLeaveHelp)], + ), + ); + if (!context.mounted) return false; + final controller = container.read(notesSettingsProvider.notifier); + if (decision == 'discard') { + controller.cancel(); + return true; + } + if (decision != 'save') return false; + final saved = await controller.save( + preserveBuffers: () async => + (await container.read(workspaceControllerProvider.notifier).saveAll()) + .succeeded, + ); + return saved && + context.mounted && + !container.read(notesSettingsProvider).dirty && + !container.read(notesSettingsProvider).busy; +} + +Future reloadNotesSettings(BuildContext context, String accountId) async { + final container = ProviderScope.containerOf(context, listen: false); + if (container.read(notesSettingsProvider).dirty) { + final discard = await showBusyMarkModalDialog( + context, + builder: (context) => BusyMarkDialogShell( + title: context.l10n.notesSettingsReload, + actions: [ + BusyMarkDialogButton( + label: context.l10n.cancel, + onPressed: () => Navigator.pop(context, false), + ), + BusyMarkDialogButton( + label: context.l10n.discard, + destructive: true, + onPressed: () => Navigator.pop(context, true), + ), + ], + children: [Text(context.l10n.notesSettingsReloadHelp)], + ), + ); + if (discard != true || !context.mounted) return; + } + await container + .read(notesSettingsProvider.notifier) + .load(accountId, discard: true); +} diff --git a/lib/src/nextcloud_notes/presentation/notes_messages.dart b/lib/src/nextcloud_notes/presentation/notes_messages.dart new file mode 100644 index 00000000..9b1eb596 --- /dev/null +++ b/lib/src/nextcloud_notes/presentation/notes_messages.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; + +import '../../app/localization.dart'; +import '../domain/notes_models.dart'; +import '../data/server_uri.dart'; +import '../data/login_flow.dart'; +import '../data/notes_secret_store.dart'; +import '../data/notes_capabilities.dart'; +import '../application/notes_transfer_service.dart'; +import '../application/notes_navigation.dart'; + +/// Presentation is selected by typed failures, never by persisted English text. +String localizeNotesFailure(BuildContext context, Object? error) { + final l10n = context.l10n; + return switch (error) { + NextcloudServerUrlException() => + '${l10n.nextcloudServer} · HTTPS · ${l10n.notesFailed}', + NextcloudLoginException( + code: NextcloudLoginFailure.network || NextcloudLoginFailure.server, + ) => + l10n.notesUnavailable, + NextcloudLoginException(code: NextcloudLoginFailure.browserUnavailable) => + '${l10n.nextcloudSignInBrowser} ${l10n.visualizationRetry}', + NextcloudCredentialException( + code: NextcloudCredentialFailure.identityMismatch || + NextcloudCredentialFailure.alreadyConnected, + ) => + l10n.notesAccountMismatch, + NextcloudCredentialException() => l10n.notesKeyringUnavailable, + NotesException(code: NotesFailureCode.authentication) => + l10n.nextcloudReconnectRequired, + NotesException(code: NotesFailureCode.forbidden) => l10n.nextcloudReadOnly, + NotesException(code: NotesFailureCode.conflict) => + l10n.notesRecoveryRequired, + NotesException(code: NotesFailureCode.locked) => l10n.nextcloudLocked, + NotesException(code: NotesFailureCode.storageFull) => l10n.notesStorageFull, + NotesException(code: NotesFailureCode.throttled) => l10n.notesThrottled, + NotesException(code: NotesFailureCode.network || NotesFailureCode.server) => + l10n.notesUnavailable, + NotesException(code: NotesFailureCode.rejected) => l10n.notesRejected, + _ => l10n.notesOperationFailed, + }; +} + +String notesLocalDate(BuildContext context, DateTime value) { + final date = value.toLocal(); + final localizations = MaterialLocalizations.of(context); + return '${localizations.formatShortDate(date)} ${localizations.formatTimeOfDay(TimeOfDay.fromDateTime(date))}'; +} + +String localizeNotesTransferFailure( + BuildContext context, + NotesTransferFailureReason reason, { + bool exporting = false, +}) => switch (reason) { + NotesTransferFailureReason.sourceChanged || + NotesTransferFailureReason.exportChanged => + context.l10n.notesTransferSourceChanged, + NotesTransferFailureReason.sourceUnreadable => + context.l10n.notesTransferUnreadable, + NotesTransferFailureReason.invalidSnapshot => + context.l10n.notesTransferInvalidSnapshot, + NotesTransferFailureReason.mediaTooLarge => + context.l10n.notesTransferMediaLimit, + NotesTransferFailureReason.unsafeSource => + context.l10n.notesTransferUnsafeSource, + NotesTransferFailureReason.destinationUnavailable => + context.l10n.notesTransferDestination, + NotesTransferFailureReason.operation => + exporting + ? context.l10n.notesExportOperationFailed + : context.l10n.notesTransferOperationFailed, +}; + +String localizeNotesBatchReason( + BuildContext context, + NotesBatchOutcome outcome, +) => switch (outcome.reason) { + NotesBatchReason.unchanged => context.l10n.notesBatchUnchanged, + NotesBatchReason.readOnly => context.l10n.notesBatchReadOnly, + NotesBatchReason.reviewChanged => context.l10n.notesBatchReviewChanged, + NotesBatchReason.needsReview => context.l10n.notesRecoveryRequired, + NotesBatchReason.missing => context.l10n.notesBatchMissing, + NotesBatchReason.storage => context.l10n.notesBatchStorage, + NotesBatchReason.editorNotSaved => context.l10n.notesBuffersNotSaved, + NotesBatchReason.unavailable => localizeNotesFailure(context, outcome.cause), + null => '', +}; + +String localizeNotesConnectionFailure( + BuildContext context, + Object? error, +) => switch (error) { + NextcloudCredentialException() => localizeNotesFailure(context, error), + NextcloudServerUrlException() => context.l10n.notesAccountServerAddress, + NextcloudCapabilityException(code: NextcloudCapabilityFailure.unsupported) => + context.l10n.notesAccountUnsupported, + NextcloudCapabilityException(code: NextcloudCapabilityFailure.unauthorized) || + NotesException( + code: NotesFailureCode.authentication, + ) => context.l10n.notesAccountAuthorizeAgain, + NextcloudCapabilityException(code: NextcloudCapabilityFailure.forbidden) || + NotesException( + code: NotesFailureCode.forbidden, + ) => context.l10n.notesAccountCheckForbidden, + NextcloudCapabilityException(code: NextcloudCapabilityFailure.throttled) || + NotesException( + code: NotesFailureCode.throttled, + ) => context.l10n.notesAccountThrottled, + NextcloudCapabilityException(code: NextcloudCapabilityFailure.malformed) || + NextcloudLoginException(code: NextcloudLoginFailure.malformedResponse) || + NotesException( + code: NotesFailureCode.invalidResponse, + ) => context.l10n.notesAccountMalformed, + NextcloudLoginException(code: NextcloudLoginFailure.unsafeRedirect) => + context.l10n.notesAccountServerAddress, + NextcloudLoginException(code: NextcloudLoginFailure.timedOut) => + context.l10n.notesAccountTimedOut, + NextcloudLoginException(code: NextcloudLoginFailure.browserUnavailable) => + context.l10n.notesAccountBrowserUnavailable, + NextcloudLoginException(code: NextcloudLoginFailure.cancelled) => + context.l10n.notesCancelled, + NextcloudLoginException( + code: NextcloudLoginFailure.network || NextcloudLoginFailure.server, + ) || + NextcloudCapabilityException(code: NextcloudCapabilityFailure.network) || + NotesException( + code: NotesFailureCode.network || NotesFailureCode.server, + ) => context.l10n.notesAccountNetwork, + NextcloudCapabilityException(code: NextcloudCapabilityFailure.rejected) => + context.l10n.notesAccountRejected, + _ => context.l10n.notesAccountFailed, +}; diff --git a/lib/src/nextcloud_notes/presentation/notes_sidebar.dart b/lib/src/nextcloud_notes/presentation/notes_sidebar.dart index 7cb6a4b4..4406ab34 100644 --- a/lib/src/nextcloud_notes/presentation/notes_sidebar.dart +++ b/lib/src/nextcloud_notes/presentation/notes_sidebar.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'dart:convert'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:yaru/yaru.dart'; +import 'package:go_router/go_router.dart'; import '../../app/busymark_design.dart'; import '../../app/busymark_dialogs.dart'; @@ -12,6 +14,7 @@ import '../../app/busymark_glyphs.dart'; import '../../app/localization.dart'; import '../../assets/asset_ingestion_service.dart'; import '../../comparison/source_comparison.dart'; +import '../../editor/document_text_direction.dart'; import '../../local_history/local_history_comparison_view.dart'; import '../../workspace/workspace_controller.dart'; import '../../workspace/workspace_message.dart'; @@ -19,6 +22,7 @@ import '../../workspace/workspace_safety.dart'; import '../application/nextcloud_connection.dart'; import '../application/notes_navigation.dart'; import 'notes_workspace_ui.dart'; +import 'notes_messages.dart'; import '../application/attachment_markdown.dart'; import '../domain/notes_models.dart'; import '../domain/notes_conflict.dart'; @@ -46,11 +50,11 @@ String nextcloudSyncLabel( NoteSyncState.creationUncertain => context.l10n.nextcloudCreationUncertain, NoteSyncState.deletedRemotely => context.l10n.localHistoryDeleted, NoteSyncState.forbidden => context.l10n.nextcloudReadOnly, - NoteSyncState.storageFull || - NoteSyncState.unavailable || - NoteSyncState.rejected || - NoteSyncState.throttled || - NoteSyncState.recoveryRequired => context.l10n.warning, + NoteSyncState.storageFull => context.l10n.notesStorageFull, + NoteSyncState.unavailable => context.l10n.notesUnavailable, + NoteSyncState.rejected => context.l10n.notesRejected, + NoteSyncState.throttled => context.l10n.notesThrottled, + NoteSyncState.recoveryRequired => context.l10n.notesRecoveryRequired, }; // Legacy response transitions may have left a transient/synchronized state @@ -72,19 +76,35 @@ class NextcloudNotesSidebar extends ConsumerStatefulWidget { final String accountId; @override ConsumerState createState() => - _NextcloudNotesSidebarState(); + NextcloudNotesSidebarState(); } -class _NextcloudNotesSidebarState extends ConsumerState { - final _navigation = NotesNavigationController(); - final _listFocus = FocusNode(); - final _listScroll = ScrollController(); +class NextcloudNotesSidebarState extends ConsumerState { + late NotesNavigationController _navigation; + late final ScrollController _listScroll; + final _rowKeys = {}; + final _noteViewportKey = GlobalKey(); + final _rowFocus = {}; + final _navigationFocus = {}; + List _exportTargets = const []; + String _offlineScope = ''; StreamSubscription? _notesSubscription; bool _ready = false; + bool _creating = false; + Object? _failure; + String? _operationError; @override void initState() { super.initState(); + _navigation = ref.read(notesNavigationProvider(widget.accountId)); + _listScroll = + ScrollController(initialScrollOffset: _navigation.scrollOffset) + ..addListener(() { + if (_listScroll.hasClients) { + _navigation.scrollOffset = _listScroll.offset; + } + }); unawaited(_initializeNavigation()); } @@ -94,21 +114,36 @@ class _NextcloudNotesSidebarState extends ConsumerState { if (oldWidget.accountId != widget.accountId) { unawaited(_notesSubscription?.cancel()); _ready = false; + _navigation = ref.read(notesNavigationProvider(widget.accountId)); unawaited(_initializeNavigation()); } } Future _initializeNavigation() async { final accountId = widget.accountId; - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - if (!mounted || accountId != widget.accountId) return; - _navigation.update(repository.notes, widget.accountId); - _notesSubscription = repository.changes.listen((_) { - if (mounted) { - setState(() => _navigation.update(repository.notes, widget.accountId)); + try { + final repository = await ref.read( + nextcloudNotesRepositoryProvider.future, + ); + if (!mounted || accountId != widget.accountId) return; + _navigation.update(repository.notes, accountId); + _notesSubscription = repository.changes.listen((_) { + if (mounted && accountId == widget.accountId) { + setState(_refreshNavigation); + } + }); + setState(() { + _ready = true; + _failure = null; + }); + } on Object catch (error) { + if (mounted && accountId == widget.accountId) { + setState(() { + _failure = error; + _ready = false; + }); } - }); - setState(() => _ready = true); + } } void _refreshNavigation() { @@ -118,508 +153,760 @@ class _NextcloudNotesSidebarState extends ConsumerState { } } + FocusNode _focusForDestination(Object destination) => + _navigationFocus.putIfAbsent(destination, () => FocusNode()); + + /// Enter Notes through its registered sidebar shortcut without changing the + /// retained viewport. Prefer the previous row when it is still mounted. + void requestNavigationFocus() { + if (_navigation.pendingReveal != null) { + unawaited(_revealPending(requestFocus: true)); + return; + } + final row = _rowFocus[_navigation.focused]; + if (row?.context != null) { + row!.requestFocus(); + } else { + _focusForDestination( + _navigation.destination == NotesDestination.category + ? _navigation.category + : _navigation.destination, + ).requestFocus(); + } + } + + bool get _presented { + var shown = true; + context.visitAncestorElements((element) { + if (element.widget case Offstage(offstage: true)) { + shown = false; + return false; + } + return true; + }); + return shown; + } + + Future _revealPending({bool requestFocus = false}) async { + final id = _navigation.pendingReveal; + if (!mounted || + !_ready || + !_listScroll.hasClients || + id == null || + !_presented) { + return; + } + await _reveal(id, requestFocus: requestFocus); + if (mounted && _presented && _navigation.pendingReveal == id) { + _navigation.pendingReveal = null; + } + } + @override void dispose() { unawaited(_notesSubscription?.cancel()); - _listFocus.dispose(); + for (final node in _rowFocus.values) { + node.dispose(); + } + for (final node in _navigationFocus.values) { + node.dispose(); + } _listScroll.dispose(); super.dispose(); } + Future _reveal(String id, {bool requestFocus = true}) async { + final duration = MediaQuery.disableAnimationsOf(context) + ? Duration.zero + : BusyMarkMotion.fast; + // Seek from the current scroll extent for distant lazy rows, then use the + // mounted row's geometry below. No fixed row height is assumed. + if (_listScroll.hasClients && + _rowKeys[id]?.currentContext == null && + _navigation.visible.length > 1) { + final index = _navigation.visible.indexWhere( + (note) => note.localId == id, + ); + if (index < 0) return; + _listScroll.jumpTo( + _listScroll.position.maxScrollExtent * + index / + (_navigation.visible.length - 1), + ); + await WidgetsBinding.instance.endOfFrame; + } + while (mounted && _listScroll.hasClients && _navigation.focused == id) { + final target = _rowKeys[id]?.currentContext; + if (target != null && target.mounted) { + await Scrollable.ensureVisible( + target, + alignment: .5, + duration: duration, + ); + if (requestFocus && mounted && _navigation.focused == id) { + _rowFocus[id]?.requestFocus(); + } + return; + } + final ids = _navigation.visible.map((n) => n.localId).toList(); + final index = ids.indexOf(id); + if (index < 0) return; + final mountedIndices = [ + for (final entry in _rowKeys.entries) + if (entry.value.currentContext != null) ids.indexOf(entry.key), + ].where((i) => i >= 0).toList()..sort(); + final before = mountedIndices.isNotEmpty && index < mountedIndices.first; + final p = _listScroll.position; + final offset = (p.pixels + p.viewportDimension * (before ? -.8 : .8)) + .clamp(0.0, p.maxScrollExtent); + if (offset == p.pixels) return; + _listScroll.jumpTo(offset); + await WidgetsBinding.instance.endOfFrame; + } + } + KeyEventResult _listKey(FocusNode node, KeyEvent event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; final keyboard = HardwareKeyboard.instance; + if (keyboard.isAltPressed || keyboard.isMetaPressed) { + return KeyEventResult.ignored; + } if (keyboard.isControlPressed && event.logicalKey == LogicalKeyboardKey.keyA) { setState(_navigation.selectAll); return KeyEventResult.handled; } + if (event.logicalKey == LogicalKeyboardKey.escape) { + setState(_navigation.selected.clear); + return KeyEventResult.handled; + } final visible = _navigation.visible; if (visible.isEmpty) return KeyEventResult.ignored; - if (event.logicalKey == LogicalKeyboardKey.arrowDown || - event.logicalKey == LogicalKeyboardKey.arrowUp) { - final index = visible.indexWhere((n) => n.localId == _navigation.focused); - final next = - (index + (event.logicalKey == LogicalKeyboardKey.arrowDown ? 1 : -1)) - .clamp(0, visible.length - 1); - setState( - () => _navigation.select( - visible[next].localId, - range: keyboard.isShiftPressed, - toggle: keyboard.isControlPressed, - ), - ); - if (_listScroll.hasClients) { - unawaited( - _listScroll.animateTo( - (next * 100.0).clamp(0, _listScroll.position.maxScrollExtent), - duration: const Duration(milliseconds: 120), - curve: Curves.easeOut, - ), - ); + final key = event.logicalKey; + final index = visible.indexWhere((n) => n.localId == _navigation.focused); + int? next; + if (key == LogicalKeyboardKey.home) next = 0; + if (key == LogicalKeyboardKey.end) next = visible.length - 1; + if (key == LogicalKeyboardKey.arrowDown) next = index + 1; + if (key == LogicalKeyboardKey.arrowUp) next = index - 1; + if (key == LogicalKeyboardKey.pageDown || + key == LogicalKeyboardKey.pageUp) { + // Count measured visible rows; text scaling determines the page size. + final listBox = _noteViewportKey.currentContext?.findRenderObject(); + if (listBox is! RenderBox || !listBox.hasSize) { + return KeyEventResult.ignored; } - return KeyEventResult.handled; + final origin = listBox.localToGlobal(Offset.zero).dy; + final page = _rowKeys.values + .where((k) { + final box = k.currentContext?.findRenderObject(); + if (box is! RenderBox || !box.hasSize) return false; + final top = box.localToGlobal(Offset.zero).dy; + return top + box.size.height > origin && + top < origin + listBox.size.height; + }) + .length + .clamp(1, visible.length); + next = index + (key == LogicalKeyboardKey.pageDown ? page : -page); } - if (event.logicalKey == LogicalKeyboardKey.space && - _navigation.focused != null) { - setState(() => _navigation.select(_navigation.focused!, toggle: true)); + if (next != null) { + final id = visible[next.clamp(0, visible.length - 1)].localId; + setState(() { + if (keyboard.isControlPressed && !keyboard.isShiftPressed) { + _navigation.focused = id; + } else { + _navigation.select( + id, + range: keyboard.isShiftPressed, + toggle: keyboard.isControlPressed, + ); + } + }); + unawaited(_reveal(id)); return KeyEventResult.handled; } - if (event.logicalKey == LogicalKeyboardKey.enter && - _navigation.focused != null) { - final note = visible.firstWhere((n) => n.localId == _navigation.focused); - unawaited( - isNotesRecovery(note) - ? showNotesRecovery(context, ref, note) - : ref - .read(workspaceControllerProvider.notifier) - .openNextcloudNote(note.localId), - ); + if (key == LogicalKeyboardKey.space && _navigation.focused != null) { + setState(() => _navigation.select(_navigation.focused!, toggle: true)); return KeyEventResult.handled; } return KeyEventResult.ignored; } + Future _create() async { + if (_creating) return; + final category = _navigation.destination == NotesDestination.category + ? _navigation.category + : ''; + setState(() => _creating = true); + final created = await ref + .read(workspaceControllerProvider.notifier) + .createNextcloudNote( + title: context.l10n.nextcloudNewNote, + category: category, + ); + if (!mounted) return; + final id = ref + .read(workspaceControllerProvider) + .activeBuffer + ?.remoteNote + ?.localId; + setState(() { + _creating = false; + if (created && id != null) { + _navigation.destination = category.isEmpty + ? NotesDestination.all + : NotesDestination.category; + _navigation.category = category; + _refreshNavigation(); + _navigation.select(id); + } + }); + if (created && id != null) unawaited(_reveal(id, requestFocus: false)); + } + + List> _noteActions( + BuildContext context, + NextcloudNote note, + ) { + final targets = _navigation.contextTargets(note.localId); + final recovery = isNotesRecovery(note); + return [ + if (!recovery) ...[ + BusyMarkPopupMenuItem( + value: note.favorite ? 'unfavorite' : 'favorite', + label: note.favorite + ? context.l10n.notesRemoveFavorite + : context.l10n.nextcloudFavorites, + ), + BusyMarkPopupMenuItem( + value: 'move', + enabled: !note.readonly && !note.error, + label: context.l10n.notesMove, + ), + ], + if (targets.length == 1) ...[ + if (!recovery) + BusyMarkPopupMenuItem( + value: 'properties', + label: context.l10n.noteProperties, + ), + BusyMarkPopupMenuItem( + value: 'offline', + label: context.l10n.notesOffline, + ), + if (recovery) + BusyMarkPopupMenuItem( + value: 'recovery', + label: context.l10n.notesRecovery, + ), + if (!recovery) ...[ + BusyMarkPopupMenuItem( + value: 'attach', + enabled: !note.readonly && !note.error, + label: context.l10n.nextcloudAddAttachment, + ), + BusyMarkPopupMenuItem( + value: 'copy', + label: context.l10n.nextcloudSaveLocalCopy, + ), + BusyMarkPopupMenuItem( + value: 'delete', + enabled: !note.readonly && !note.error, + label: context.l10n.nextcloudDeleteNote, + ), + ], + ], + ]; + } + + List _captureTargets(NextcloudNote note) { + final repository = ref.read(nextcloudNotesRepositoryProvider).value; + return List.unmodifiable([ + for (final id in _navigation.contextTargets(note.localId)) + if (repository?.noteById(id) != null) repository!.metadataSnapshot(id), + ]); + } + + Future _noteAction( + String action, + List reviewed, + ) async { + if (reviewed.isEmpty) return; + final targets = reviewed.map((s) => s.note.localId).toList(); + final note = reviewed.first.note; + if ({'favorite', 'unfavorite', 'move'}.contains(action)) { + if (reviewed.length == 1 && action != 'move') { + await ref + .read(workspaceControllerProvider.notifier) + .updateNextcloudNoteMetadata( + targets.single, + snapshot: reviewed.single, + favorite: action == 'favorite', + ); + } else { + await showNotesBatch( + context, + ref, + targets, + initialAction: action, + reviewedTargets: reviewed, + ); + } + return; + } + if (!mounted) return; + switch (action) { + case 'properties': + await _editNote(note, reviewed: reviewed.single); + case 'offline': + await showNotesOffline(context, ref, note); + case 'recovery': + await showNotesRecovery(context, ref, note); + case 'attach': + await addNextcloudFileAttachment(context, ref, note.localId); + case 'copy': + await _saveCopy(note); + case 'delete': + await _deleteNote(note); + } + } + + Future _saveCopy(NextcloudNote note) async { + if (await ref + .read(workspaceControllerProvider.notifier) + .openNextcloudNote(note.localId) && + mounted) { + await saveActiveToNewLocation(context, ref); + } + } + + Future _deleteNote(NextcloudNote note) async { + final confirmed = await showBusyMarkModalDialog( + context, + builder: (context) => BusyMarkDialogShell( + title: context.l10n.nextcloudDeleteNote, + actions: [ + BusyMarkDialogButton( + label: context.l10n.cancel, + onPressed: () => Navigator.pop(context, false), + ), + BusyMarkDialogButton( + label: context.l10n.delete, + destructive: true, + onPressed: () => Navigator.pop(context, true), + ), + ], + children: [Text(note.title)], + ), + ); + return confirmed == true && + mounted && + await ref + .read(workspaceControllerProvider.notifier) + .deleteNextcloudNote(note.localId); + } + @override Widget build(BuildContext context) { - final repository = ref.watch(nextcloudNotesRepositoryProvider).value; + ref.watch(notesNavigationProvider(widget.accountId)); + ref.listen(notesCreatedProvider, (_, created) { + if (created == null || created.accountId != widget.accountId) return; + final note = ref + .read(nextcloudNotesRepositoryProvider) + .value + ?.noteById(created.localId); + if (note == null) return; + setState(() { + _refreshNavigation(); + }); + }); + if (_ready && _navigation.pendingReveal != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) unawaited(_revealPending()); + }); + } + final repositoryState = ref.watch(nextcloudNotesRepositoryProvider); + final repository = repositoryState.value; final active = ref.watch( workspaceControllerProvider.select( (s) => s.activeBuffer?.remoteNote?.localId, ), ); - final offline = ref.watch(notesOfflineProvider(widget.accountId)).value; + final offlineController = ref + .watch(notesOfflineProvider(widget.accountId)) + .value; ref.watch(notesOfflineChangesProvider(widget.accountId)); + if (_failure != null || repositoryState.hasError) { + return Column( + children: [ + BusyMarkStatusBox( + message: context.l10n.notesLoadFailed, + kind: BusyMarkStatusKind.error, + ), + BusyMarkPushButton.standard( + onPressed: () { + ref.invalidate(nextcloudNotesRepositoryProvider); + setState(() => _failure = null); + unawaited(_initializeNavigation()); + }, + child: Text(context.l10n.visualizationRetry), + ), + ], + ); + } if (repository == null || !_ready) { return const Center(child: CircularProgressIndicator()); } final categories = _navigation.categoryPaths; final categoryRows = categories .where( - (category) => !_navigation.collapsed.any( - (parent) => category.startsWith('$parent/'), - ), + (category) => + !_navigation.collapsed.any((p) => category.startsWith('$p/')), ) .toList(); final visible = _navigation.visible; - final accountError = repository.accountError(widget.accountId); - String destinationLabel(NotesDestination destination) => - switch (destination) { - NotesDestination.all => context.l10n.notesAll, - NotesDestination.favorites => context.l10n.nextcloudFavorites, - NotesDestination.uncategorized => context.l10n.notesUncategorized, - NotesDestination.recovery => context.l10n.notesRecovery, - NotesDestination.category => _navigation.category, - }; - return Column( - children: [ - if (accountError != null) + final categoryScope = _navigation.destination == NotesDestination.category; + final scope = categoryScope ? _navigation.category : ''; + String label(NotesDestination destination) => switch (destination) { + NotesDestination.all => context.l10n.notesAll, + NotesDestination.favorites => context.l10n.nextcloudFavorites, + NotesDestination.uncategorized => context.l10n.notesUncategorized, + NotesDestination.recovery => context.l10n.notesRecovery, + NotesDestination.category => _navigation.category, + }; + String sortLabel(NotesSort sort) => switch (sort) { + NotesSort.recent => context.l10n.notesRecentlyEdited, + NotesSort.oldest => context.l10n.notesOldestEdited, + NotesSort.titleAscending => context.l10n.notesTitleAscending, + NotesSort.titleDescending => context.l10n.notesTitleDescending, + }; + return LayoutBuilder( + builder: (context, constraints) => Column( + children: [ + if (_operationError != null) + BusyMarkStatusBox( + message: _operationError!, + kind: BusyMarkStatusKind.error, + ), + if (repository.accountError(widget.accountId) case final error?) + BusyMarkBanner( + title: localizeNotesFailure(context, error), + actionLabel: error.code == NotesFailureCode.authentication + ? context.l10n.nextcloudReconnect + : null, + onAction: error.code == NotesFailureCode.authentication + ? () => context.go( + '/settings?returnTo=workspace&page=nextcloudNotes', + ) + : null, + ), Padding( padding: const EdgeInsets.all(BusyMarkSpacing.sm), - child: BusyMarkStatusBox( - message: accountError.message, - kind: BusyMarkStatusKind.warning, - ), - ), - Padding( - padding: const EdgeInsets.all(BusyMarkSpacing.sm), - child: Row( - children: [ - Expanded( - child: BusyMarkPushButton.standard( - onPressed: () => unawaited( - ref - .read(workspaceControllerProvider.notifier) - .createNextcloudNote( - title: context.l10n.nextcloudNewNote, - category: - _navigation.destination == - NotesDestination.category - ? _navigation.category - : '', - ), - ), - child: Text(context.l10n.nextcloudNewNote), + child: Row( + children: [ + BusyMarkHeaderIconButton( + tooltip: context.l10n.nextcloudNewNote, + icon: BusyMarkGlyphs.add, + onPressed: _creating ? null : () => unawaited(_create()), ), - ), - IconButton( - tooltip: context.l10n.tocSynchronize, - icon: const Icon(BusyMarkGlyphs.refresh), - onPressed: () => unawaited( - ref - .read(workspaceControllerProvider.notifier) - .refreshNextcloudNotes(), + const Spacer(), + BusyMarkHeaderIconButton( + tooltip: context.l10n.tocSynchronize, + icon: BusyMarkGlyphs.refresh, + onPressed: repository.isSynchronizing(widget.accountId) + ? null + : () => unawaited( + ref + .read(workspaceControllerProvider.notifier) + .refreshNextcloudNotes(), + ), ), - ), - PopupMenuButton( - tooltip: context.l10n.mainMenu, - onSelected: (action) { - if (action == 'export') { - unawaited( - showNotesExport( - context, - ref, - repository.notes - .where( - (n) => - n.accountId == widget.accountId && - !isNotesRecovery(n) && - (_navigation.destination != - NotesDestination.category || - categoryIncludes( - _navigation.category, - n.category, - )), - ) - .map((n) => n.localId) - .toList(), - ), + BusyMarkHeaderPopupMenuButton( + tooltip: context.l10n.notesActions, + icon: BusyMarkGlyphs.menuVertical, + itemBuilder: (context) { + _offlineScope = scope; + _exportTargets = List.unmodifiable( + repository.notes + .where( + (n) => + n.accountId == widget.accountId && + !isNotesRecovery(n) && + categoryIncludes(scope, n.category), + ) + .map((n) => n.localId), ); - } - if (action == 'import') { - unawaited(showNotesImport(context, ref)); - } - if (action == 'file') { - unawaited(showNotesImport(context, ref, folder: false)); - } - if (action == 'offline' && offline != null) { - unawaited( - offline.setRequirement( - 'category', - _navigation.destination == NotesDestination.category - ? _navigation.category - : '', + final requirement = offlineController?.requirements + .where( + (r) => + r['kind'] == 'category' && r['target'] == scope, + ) + .firstOrNull; + final scopeLabel = scope.isEmpty + ? context.l10n.notesAll + : scope; + final paused = requirement?['paused'] == 1; + return [ + BusyMarkPopupMenuItem( + value: 'export', + label: categoryScope + ? '${context.l10n.notesExportCategory} · $scope' + : context.l10n.notesExportAll, ), - ); - } - if (action == 'remove' && offline != null) { - unawaited( - offline.setRequirement( - 'category', - _navigation.destination == NotesDestination.category - ? _navigation.category - : '', - required: false, + BusyMarkPopupMenuItem( + value: 'import', + label: + '${context.l10n.notesImport} · ${context.l10n.folder}', ), - ); - } - if (action == 'cancel' && offline != null) { - unawaited( - offline.setRequirement( - 'category', - _navigation.destination == NotesDestination.category - ? _navigation.category - : '', - paused: true, + BusyMarkPopupMenuItem( + value: 'file', + label: + '${context.l10n.notesImport} · ${context.l10n.fileTypeMarkdown}', ), - ); - } - if (action == 'retry' && offline != null) { - unawaited( - offline.setRequirement( - 'category', - _navigation.destination == NotesDestination.category - ? _navigation.category - : '', + BusyMarkPopupMenuItem( + value: 'offline', + label: categoryScope + ? '${context.l10n.notesOfflineCategory} · $scope' + : context.l10n.notesOfflineAll, ), - ); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'export', - child: Text(context.l10n.notesExport), - ), - PopupMenuItem( - value: 'import', - child: Text( - '${context.l10n.notesImport} · ${context.l10n.folder}', - ), - ), - PopupMenuItem( - value: 'file', - child: Text( - '${context.l10n.notesImport} · ${context.l10n.fileTypeMarkdown}', - ), - ), - PopupMenuItem( - value: 'offline', - child: Text(context.l10n.notesOffline), - ), - PopupMenuItem( - value: 'remove', - child: Text(context.l10n.notesOfflineRemove), - ), - PopupMenuItem( - value: 'cancel', - child: Text(context.l10n.cancel), - ), - PopupMenuItem( - value: 'retry', - child: Text(context.l10n.visualizationRetry), - ), - ], - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: BusyMarkSpacing.sm), - child: TextField( - readOnly: true, - decoration: InputDecoration( - hintText: context.l10n.search, - prefixIcon: const Icon(BusyMarkGlyphs.search), - ), - onTap: () => unawaited( - showNotesSearch( - context, - ref, - account: widget.accountId, - recovery: _navigation.destination == NotesDestination.recovery, - ), + BusyMarkPopupMenuItem( + value: 'remove', + label: categoryScope + ? '${context.l10n.notesOfflineRemoveCategory} · $scope' + : context.l10n.notesOfflineRemoveAll, + ), + if (requirement != null) + BusyMarkPopupMenuItem( + value: paused ? 'resumeScope' : 'pauseScope', + label: + '${paused ? context.l10n.notesResumeRequirement : context.l10n.notesPauseRequirement} · $scopeLabel', + ), + BusyMarkPopupMenuItem( + value: 'retryScope', + enabled: + offlineController != null && + offlineController.failures.keys.any( + (key) => _exportTargets.any( + (id) => key.startsWith('$id:'), + ), + ) && + (scope.isEmpty || requirement != null), + label: + '${context.l10n.visualizationRetry} · ${context.l10n.notesAttachments} · $scopeLabel', + ), + ]; + }, + onSelected: (action) async { + if (action == 'export') { + await showNotesExport(context, ref, _exportTargets); + } else if (action == 'import' || action == 'file') { + await showNotesImport( + context, + ref, + folder: action == 'import', + ); + } else { + try { + final offline = await ref.read( + notesOfflineProvider(widget.accountId).future, + ); + if (action == 'retryScope' && _offlineScope.isEmpty) { + await offline.retry(); + } else { + await offline.setRequirement( + 'category', + _offlineScope, + required: action != 'remove', + paused: action == 'pauseScope', + ); + } + } on Object catch (error) { + if (mounted) { + setState( + () => _operationError = localizeNotesFailure( + context, + error, + ), + ); + } + } + } + }, + ), + ], ), ), - ), - SizedBox( - height: ((categoryRows.length + 4) * 36.0).clamp(0, 240), - child: ListView.builder( - itemExtent: 36, - itemCount: categoryRows.length + 4, - itemBuilder: (context, index) { - if (index < 4) { - final destination = [ - NotesDestination.all, - NotesDestination.favorites, - NotesDestination.uncategorized, - NotesDestination.recovery, - ][index]; - return ListTile( - dense: true, - minTileHeight: 36, - minVerticalPadding: 0, - selected: _navigation.destination == destination, - title: Text(destinationLabel(destination)), - trailing: Text('${_navigation.counts[destination] ?? 0}'), - onTap: () => setState(() { - _navigation.destination = destination; - _refreshNavigation(); - }), - ); - } - final category = categoryRows[index - 4]; - return ListTile( - dense: true, - minTileHeight: 36, - minVerticalPadding: 0, - contentPadding: EdgeInsets.only( - left: 8 + 12.0 * (category.split('/').length - 1), - right: 12, - ), - selected: - _navigation.destination == NotesDestination.category && - _navigation.category == category, - leading: IconButton( - constraints: const BoxConstraints.tightFor( - width: 24, - height: 24, - ), - padding: EdgeInsets.zero, + SizedBox( + height: (constraints.maxHeight * .36).clamp(0.0, 240.0), + child: ListView.builder( + padding: BusyMarkInsets.sidebarList, + itemCount: categoryRows.length + 4, + itemBuilder: (context, index) { + if (index < 4) { + final destination = [ + NotesDestination.all, + NotesDestination.favorites, + NotesDestination.uncategorized, + NotesDestination.recovery, + ][index]; + return BusyMarkSidebarTreeRow( + traverseWithArrows: true, + key: ValueKey(destination), + focusNode: _focusForDestination(destination), + title: label(destination), + depth: 0, + selected: _navigation.destination == destination, + trailing: Text('${_navigation.counts[destination] ?? 0}'), + onTap: () => setState(() { + _focusForDestination(destination).requestFocus(); + _navigation.destination = destination; + _refreshNavigation(); + }), + ); + } + final category = categoryRows[index - 4]; + return BusyMarkSidebarTreeRow( + traverseWithArrows: true, + key: ValueKey('notes-category-$category'), + focusNode: _focusForDestination(category), + title: category.split('/').last, tooltip: category, - icon: Icon( - _navigation.collapsed.contains(category) - ? BusyMarkGlyphs.collapsedTreeArrowFor( - Directionality.of(context), - ) - : BusyMarkGlyphs.downArrow, + depth: category.split('/').length - 1, + hasChildren: categories.any( + (c) => c.startsWith('$category/'), ), - onPressed: () => setState(() { + expanded: !_navigation.collapsed.contains(category), + selected: categoryScope && category == _navigation.category, + trailing: Text('${_navigation.categoryCounts[category]}'), + onToggle: () => setState(() { if (!_navigation.collapsed.remove(category)) { _navigation.collapsed.add(category); } }), - ), - title: Text( - category.split('/').last, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: Text('${_navigation.categoryCounts[category]}'), - onTap: () => setState(() { - _navigation.destination = NotesDestination.category; - _navigation.category = category; - _refreshNavigation(); - }), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: DropdownButton( - isExpanded: true, - value: _navigation.sort, - items: [ - DropdownMenuItem( - value: NotesSort.recent, - child: Text(context.l10n.notesRecentlyEdited), - ), - DropdownMenuItem( - value: NotesSort.oldest, - child: Text(context.l10n.notesOldestEdited), - ), - DropdownMenuItem( - value: NotesSort.titleAscending, - child: Text(context.l10n.notesTitleAscending), - ), - DropdownMenuItem( - value: NotesSort.titleDescending, - child: Text(context.l10n.notesTitleDescending), - ), - ], - onChanged: (v) => setState(() { - _navigation.sort = v!; - _refreshNavigation(); - }), - ), - ), - if (_navigation.selected.isNotEmpty && - _navigation.destination != NotesDestination.recovery) - TextButton( - onPressed: () => unawaited( - showNotesBatch( - context, - ref, - _navigation.visible - .where((n) => _navigation.selected.contains(n.localId)) - .map((n) => n.localId) - .toList(), - ), - ), - child: Text( - '${context.l10n.notesSelected} (${_navigation.selected.length})', + onTap: () => setState(() { + _focusForDestination(category).requestFocus(); + _navigation.destination = NotesDestination.category; + _navigation.category = category; + _refreshNavigation(); + }), + ); + }, ), ), - if (_navigation.destination == NotesDestination.recovery) Padding( - padding: const EdgeInsets.all(8), - child: Text( - context.l10n.notesRecoveryHelp, - style: Theme.of(context).textTheme.bodySmall, + padding: const EdgeInsets.symmetric(horizontal: BusyMarkSpacing.sm), + child: Row( + children: [ + Expanded( + child: BusyMarkPopupSelector( + value: _navigation.sort, + label: sortLabel(_navigation.sort), + tooltip: context.l10n.notesSort, + fullWidth: true, + options: [ + for (final sort in NotesSort.values) + BusyMarkPopupSelectorOption( + value: sort, + label: sortLabel(sort), + ), + ], + onSelected: (v) => setState(() { + _navigation.sort = v; + _refreshNavigation(); + }), + ), + ), + if (_navigation.selected.length > 1 && + _navigation.destination != NotesDestination.recovery) + BusyMarkCompactIconButton( + tooltip: + '${context.l10n.notesSelected} (${_navigation.selected.length})', + icon: BusyMarkGlyphs.menuVertical, + onPressed: () => unawaited( + showNotesBatch( + context, + ref, + _navigation.selected.toList(), + ), + ), + ), + ], ), ), - Expanded( - child: Focus( - focusNode: _listFocus, - onKeyEvent: _listKey, - child: visible.isEmpty - ? Center(child: Text(context.l10n.noResults)) - : ListView.builder( - controller: _listScroll, - itemExtent: 100, - itemCount: visible.length, - itemBuilder: (context, index) { - final note = visible[index]; - final retained = offline?.statuses[note.localId]; - return GestureDetector( - onSecondaryTapDown: (details) => unawaited( - _contextMenu(note, details.globalPosition), - ), - child: ListTile( + Expanded( + child: Focus( + key: _noteViewportKey, + onKeyEvent: _listKey, + child: visible.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(BusyMarkSpacing.md), + child: Text(switch (_navigation.destination) { + NotesDestination.all => context.l10n.notesEmptyAll, + NotesDestination.favorites => + context.l10n.notesEmptyFavorites, + NotesDestination.category => + context.l10n.notesEmptyCategory, + NotesDestination.uncategorized => + context.l10n.notesEmptyUncategorized, + NotesDestination.recovery => + context.l10n.notesEmptyRecovery, + }), + ), + ) + : ListView.builder( + key: const ValueKey('nextcloud-notes-list'), + findChildIndexCallback: (key) { + if (key is! ValueKey) return null; + final index = visible.indexWhere( + (note) => + key.value == 'nextcloud-note-${note.localId}', + ); + return index < 0 ? null : index; + }, + controller: _listScroll, + padding: BusyMarkInsets.sidebarList, + itemCount: visible.length, + itemBuilder: (context, index) { + final note = visible[index]; + return BusyMarkSidebarRecordRow( key: ValueKey('nextcloud-note-${note.localId}'), - dense: true, - selected: _navigation.selected.contains(note.localId), - shape: _navigation.focused == note.localId - ? Border( - left: BorderSide( - color: Theme.of( - context, - ).colorScheme.primary, - width: 2, - ), - ) - : null, - leading: Checkbox( - value: _navigation.selected.contains(note.localId), - semanticLabel: note.title, - onChanged: (_) => setState( - () => _navigation.select( - note.localId, - toggle: true, - ), - ), - ), - title: Text( - note.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, + geometryKey: _rowKeys.putIfAbsent( + note.localId, + GlobalKey.new, ), - subtitle: Text( - [ - note.category, - MaterialLocalizations.of(context).formatShortDate( - DateTime.fromMicrosecondsSinceEpoch( - note.activityMicros, - ), - ), - if (_metadataNeedsReview(note)) - context.l10n.gitConflicts - else if (note.syncState != NoteSyncState.synced) - nextcloudSyncLabel(context, note.syncState), - if (note.content.contains('busymark-attachment:')) - context.l10n.nextcloudPendingUpload, - if (retained != null) - '${retained.available ? context.l10n.notesAvailableOffline : context.l10n.notesIncomplete} ${retained.availableCount}/${retained.requiredCount}', - ].where((s) => s.isNotEmpty).join(' · '), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (active == note.localId) - Icon( - BusyMarkGlyphs.markdownFile, - size: 14, - semanticLabel: context.l10n.open, - ), - IconButton( - tooltip: note.favorite - ? context.l10n.notesRemoveFavorite - : context.l10n.nextcloudFavorites, - icon: Icon( - note.favorite - ? YaruIcons.star_filled - : YaruIcons.star, - size: 16, - ), - onPressed: () => unawaited( - ref - .read( - workspaceControllerProvider.notifier, - ) - .updateNextcloudNoteMetadata( - note.localId, - snapshot: repository.metadataSnapshot( - note.localId, - ), - favorite: !note.favorite, - ), - ), - ), - IconButton( - tooltip: context.l10n.mainMenu, - icon: const Icon( - BusyMarkGlyphs.menuVertical, - size: 16, - ), - onPressed: () => - unawaited(_contextMenu(note, null)), - ), - ], + focusNode: _rowFocus.putIfAbsent( + note.localId, + () => FocusNode(debugLabel: 'Note ${note.localId}'), ), + onFocusChange: (focused) { + if (focused) _navigation.focused = note.localId; + }, + icon: active == note.localId + ? BusyMarkGlyphs.markdownFile + : note.favorite + ? YaruIcons.star_filled + : YaruIcons.document, + title: note.title, + subtitle: _metadataNeedsReview(note) + ? context.l10n.gitConflicts + : note.syncState != NoteSyncState.synced + ? nextcloudSyncLabel(context, note.syncState) + : note.category, + tooltip: note.title, + semanticsLabel: + '${note.title}${active == note.localId ? ' · ${context.l10n.open}' : ''}', + selected: _navigation.selected.contains(note.localId), + menuTooltip: context.l10n.noteActions, + menuItemsBuilder: (context) => + _noteActions(context, note), + menuSelectionForSession: () { + final reviewed = _captureTargets(note); + return (action) => + unawaited(_noteAction(action, reviewed)); + }, onTap: () { - _listFocus.requestFocus(); + _rowFocus[note.localId]?.requestFocus(); final keyboard = HardwareKeyboard.instance; setState( () => _navigation.select( @@ -642,196 +929,243 @@ class _NextcloudNotesSidebarState extends ConsumerState { ); } }, - onLongPress: () => unawaited(_editNote(note)), - ), - ); - }, - ), - ), - ), - ], - ); - } - - Future _contextMenu(NextcloudNote note, Offset? position) async { - final targets = _navigation.contextTargets(note.localId); - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - final reviewed = [ - for (final id in targets) - if (repository.noteById(id) != null) repository.metadataSnapshot(id), - ]; - if (!mounted) return; - final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; - final point = - position ?? - overlay.localToGlobal( - Offset(overlay.size.width / 2, overlay.size.height / 2), - ); - final action = await showMenu( - context: context, - position: RelativeRect.fromRect( - Rect.fromLTWH(point.dx, point.dy, 1, 1), - Offset.zero & overlay.size, - ), - items: [ - PopupMenuItem( - enabled: false, - child: Text( - targets.length > 1 - ? '${context.l10n.notesSelected} (${targets.length})' - : note.title, - ), - ), - if (!isNotesRecovery(note)) ...[ - PopupMenuItem( - value: 'favorite', - child: Text(context.l10n.nextcloudFavorites), - ), - PopupMenuItem( - value: 'unfavorite', - child: Text(context.l10n.notesRemoveFavorite), - ), - PopupMenuItem(value: 'move', child: Text(context.l10n.notesMove)), - ], - if (targets.length == 1) ...[ - PopupMenuItem(value: 'properties', child: Text(context.l10n.rename)), - PopupMenuItem( - value: 'offline', - child: Text(context.l10n.notesOffline), - ), - if (isNotesRecovery(note)) - PopupMenuItem( - value: 'recovery', - child: Text(context.l10n.notesRecovery), + ); + }, + ), ), + ), ], - ], + ), ); - if (!mounted || action == null) return; - if ({'favorite', 'unfavorite', 'move'}.contains(action)) { - await showNotesBatch( - context, - ref, - targets, - initialAction: action, - reviewedTargets: reviewed, - ); - } - if (action == 'properties') await _editNote(note); - if (!mounted) return; - if (action == 'offline') await showNotesOffline(context, ref, note); - if (!mounted) return; - if (action == 'recovery') await showNotesRecovery(context, ref, note); } - Future _editNote(NextcloudNote note) async { + Future _editNote( + NextcloudNote note, { + NotesMetadataSnapshot? reviewed, + }) async { final repository = await ref.read(nextcloudNotesRepositoryProvider.future); if (!mounted || repository.noteById(note.localId) == null) return; - final snapshot = repository.metadataSnapshot(note.localId); + final snapshot = reviewed ?? repository.metadataSnapshot(note.localId); note = snapshot.note; var title = note.title; var category = note.category; var favorite = note.favorite; - final action = await showBusyMarkModalDialog( + var busy = false; + var attachmentVersion = 0; + String? error; + await showBusyMarkModalDialog( context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => BusyMarkDialogShell( - title: note.title, - actions: [ - BusyMarkDialogButton( - label: context.l10n.cancel, - onPressed: () => Navigator.pop(context), - ), - BusyMarkDialogButton( - label: context.l10n.delete, - destructive: true, - onPressed: () => Navigator.pop(context, 'delete'), - ), - BusyMarkDialogButton( - label: context.l10n.nextcloudSaveLocalCopy, - onPressed: () => Navigator.pop(context, 'copy'), - ), - BusyMarkDialogButton( - label: context.l10n.save, - onPressed: () => Navigator.pop(context, 'save'), - ), - ], - children: [ - BusyMarkGroupedList( - filled: true, - children: [ - BusyMarkGroupedTextEntry( - label: context.l10n.pdfTitlePageTitle, - initialValue: title, - readOnly: note.readonly, - onChanged: (v) => title = v, + barrierDismissible: false, + builder: (dialogContext) => StatefulBuilder( + builder: (context, update) { + Future run(Future Function() action) async { + if (busy) return; + update(() { + busy = true; + error = null; + }); + try { + await action(); + } on Object catch (failure) { + if (context.mounted) { + update(() => error = localizeNotesFailure(context, failure)); + } + } finally { + if (context.mounted) update(() => busy = false); + } + } + + return PopScope( + canPop: !busy, + child: BusyMarkDialogShell( + title: context.l10n.noteProperties, + closable: !busy, + actions: [ + BusyMarkDialogButton( + label: context.l10n.cancel, + onPressed: busy ? null : () => Navigator.pop(context), ), - BusyMarkGroupedTextEntry( - label: context.l10n.syntaxReferenceCategory, - initialValue: category, - readOnly: note.readonly, - onChanged: (v) => category = v, + BusyMarkDialogButton( + label: context.l10n.save, + onPressed: busy + ? null + : () => unawaited( + run(() async { + final previousMessage = ref + .read(workspaceControllerProvider) + .message; + final success = await ref + .read(workspaceControllerProvider.notifier) + .updateNextcloudNoteMetadata( + note.localId, + snapshot: snapshot, + title: + note.readonly || + title == snapshot.note.title + ? null + : title, + category: + note.readonly || + category == snapshot.note.category + ? null + : category, + favorite: favorite == snapshot.note.favorite + ? null + : favorite, + ); + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + final current = repository.noteById(note.localId); + final message = ref + .read(workspaceControllerProvider) + .message; + update( + () => error = current?.metadataConflict != null + ? context.l10n.notesRecoveryRequired + : message != null && + !identical(message, previousMessage) + ? localizeNotesFailure( + context, + message.error, + ) + : context.l10n.notesOperationFailed, + ); + } + }), + ), + ), + ], + children: [ + BusyMarkGroupedList( + filled: true, + children: [ + BusyMarkGroupedTextEntry( + label: context.l10n.pdfTitlePageTitle, + initialValue: title, + textDirection: busyMarkDocumentTextDirection( + text: title, + fallback: Directionality.of(context), + ), + enabled: !busy, + readOnly: note.readonly, + onChanged: (v) => update(() => title = v), + ), + BusyMarkGroupedTextEntry( + label: context.l10n.syntaxReferenceCategory, + initialValue: category, + textDirection: busyMarkDocumentTextDirection( + text: category, + fallback: Directionality.of(context), + ), + enabled: !busy, + readOnly: note.readonly, + onChanged: (v) => update(() => category = v), + ), + BusyMarkSwitchRow( + title: context.l10n.nextcloudFavorites, + value: favorite, + enabled: !busy, + onChanged: (v) => update(() => favorite = v), + ), + BusyMarkActionRow( + title: context.l10n.nextcloudAddAttachment, + enabled: !busy && !note.readonly && !note.error, + onTap: busy || note.readonly || note.error + ? null + : () => unawaited( + run(() async { + await addNextcloudFileAttachment( + context, + ref, + note.localId, + ); + if (context.mounted) { + update(() => attachmentVersion++); + } + }), + ), + ), + BusyMarkActionRow( + title: context.l10n.nextcloudSaveLocalCopy, + enabled: !busy, + onTap: busy + ? null + : () => unawaited(run(() => _saveCopy(note))), + ), + BusyMarkActionRow( + title: context.l10n.nextcloudDeleteNote, + destructive: true, + enabled: !busy && !note.readonly && !note.error, + onTap: busy || note.readonly || note.error + ? null + : () => unawaited( + run(() async { + if (await _deleteNote(note) && + context.mounted) { + Navigator.pop(context); + } + }), + ), + ), + ], ), - CheckboxListTile( - title: Text(context.l10n.nextcloudFavorites), - value: favorite, - onChanged: (v) => setDialogState(() => favorite = v ?? false), + if (note.readonly) Text(context.l10n.notesReadOnlyHelp), + _ManagedAttachments( + key: ValueKey(attachmentVersion), + note: repository.noteById(note.localId) ?? note, ), - if (!note.readonly && !note.error) - BusyMarkActionRow( - title: context.l10n.nextcloudAddAttachment, - onTap: () => Navigator.pop(context, 'attach'), + if (busy) const LinearProgressIndicator(), + if (error != null) + Semantics( + liveRegion: true, + child: BusyMarkStatusBox( + message: error!, + kind: BusyMarkStatusKind.error, + ), ), ], ), - _ManagedAttachments(note: note), - if (note.errorMessage != null) Text(note.errorMessage!), - ], - ), + ); + }, ), ); - if (!mounted || action == null) return; - final controller = ref.read(workspaceControllerProvider.notifier); - if (action == 'copy') { - if (await controller.openNextcloudNote(note.localId) && mounted) { - await saveActiveToNewLocation(context, ref); - } - } else if (action == 'attach') { - await addNextcloudFileAttachment(context, ref, note.localId); - } else if (action == 'save') { - await controller.updateNextcloudNoteMetadata( - note.localId, - snapshot: snapshot, - title: note.readonly || title == snapshot.note.title ? null : title, - category: note.readonly || category == snapshot.note.category - ? null - : category, - favorite: favorite == snapshot.note.favorite ? null : favorite, - ); - } else if (action == 'delete') { - final confirmed = await showBusyMarkModalDialog( - context, - builder: (context) => BusyMarkDialogShell( - title: context.l10n.nextcloudDeleteNote, - actions: [ - BusyMarkDialogButton( - label: context.l10n.cancel, - onPressed: () => Navigator.pop(context, false), - ), - BusyMarkDialogButton( - label: context.l10n.delete, - destructive: true, - onPressed: () => Navigator.pop(context, true), - ), - ], - children: [Text(note.title)], - ), - ); - if (confirmed == true) await controller.deleteNextcloudNote(note.localId); - } } } +/// A durable note condition always takes precedence over account activity. +String nextcloudNoteStatusLabel( + BuildContext context, + NextcloudNote note, { + bool unsaved = false, +}) { + if (unsaved) return context.l10n.closeUnsavedChangesTitle; + if (_metadataNeedsReview(note)) return context.l10n.gitConflicts; + if (!{ + NoteSyncState.synced, + NoteSyncState.pending, + NoteSyncState.syncing, + NoteSyncState.offline, + }.contains(note.syncState)) { + return nextcloudSyncLabel(context, note.syncState); + } + if (note.syncState == NoteSyncState.offline) { + return '${context.l10n.notesTextAvailable}${note.hasPendingChanges ? ' · ${context.l10n.nextcloudSavedLocally}' : ''} · ${context.l10n.nextcloudOffline}'; + } + if (note.hasPendingChanges || + note.syncState == NoteSyncState.pending || + note.syncState == NoteSyncState.syncing) { + return context.l10n.nextcloudSavedLocally; + } + if (notesAttachmentReferences( + note.content, + ).any((r) => r.reference.startsWith('busymark-attachment:'))) { + return context.l10n.nextcloudPendingUpload; + } + return nextcloudSyncLabel(context, note.syncState); +} + class NextcloudNoteStatus extends ConsumerWidget { const NextcloudNoteStatus({ super.key, @@ -846,6 +1180,8 @@ class NextcloudNoteStatus extends ConsumerWidget { final repository = ref.watch(nextcloudNotesRepositoryProvider).value; final note = repository?.noteById(localId); if (note == null) return const SizedBox.shrink(); + final deadline = repository!.writeDeadline(note.accountId); + final canRetry = deadline == null || !DateTime.now().isBefore(deadline); return Padding( padding: const EdgeInsets.symmetric( horizontal: BusyMarkSpacing.md, @@ -858,20 +1194,17 @@ class NextcloudNoteStatus extends ConsumerWidget { children: [ Expanded( child: Text( - unsaved - ? context.l10n.closeUnsavedChangesTitle - : _metadataNeedsReview(note) - ? context.l10n.gitConflicts - : repository!.isSynchronizing(note.accountId) - ? context.l10n.nextcloudSyncing - : notesAttachmentReferences(note.content).any( - (r) => r.reference.startsWith('busymark-attachment:'), - ) - ? context.l10n.nextcloudPendingUpload - : nextcloudSyncLabel(context, note.syncState), + nextcloudNoteStatusLabel(context, note, unsaved: unsaved), ), ), if (note.readonly) Text(context.l10n.nextcloudReadOnly), + if (note.syncState == NoteSyncState.reconnectRequired) + BusyMarkPushButton.standard( + onPressed: () => context.go( + '/settings?returnTo=workspace&page=nextcloudNotes', + ), + child: Text(context.l10n.nextcloudReconnect), + ), if (!note.readonly && !note.error) IconButton( tooltip: context.l10n.nextcloudAddAttachment, @@ -896,35 +1229,47 @@ class NextcloudNoteStatus extends ConsumerWidget { if (note.syncState == NoteSyncState.locked || note.syncState == NoteSyncState.offline) BusyMarkPushButton.standard( - onPressed: () => unawaited( - ref - .read(workspaceControllerProvider.notifier) - .refreshNextcloudNotes(), - ), + onPressed: !canRetry + ? null + : () => unawaited( + ref + .read(workspaceControllerProvider.notifier) + .refreshNextcloudNotes(), + ), child: Text(context.l10n.visualizationRetry), ), if (note.syncState == NoteSyncState.rejected || note.syncState == NoteSyncState.storageFull) BusyMarkPushButton.standard( - onPressed: () => unawaited( - ref - .read(workspaceControllerProvider.notifier) - .retryRejectedNextcloudNote(localId), - ), + onPressed: !canRetry + ? null + : () => unawaited( + ref + .read(workspaceControllerProvider.notifier) + .retryRejectedNextcloudNote(localId), + ), child: Text(context.l10n.visualizationRetry), ), ], ), - if (repository?.accountById(note.accountId)?.lastServerCheck + if (repository.isSynchronizing(note.accountId) && + { + NoteSyncState.syncing, + NoteSyncState.pending, + }.contains(note.syncState)) + const LinearProgressIndicator(), + if (repository.accountById(note.accountId)?.lastServerCheck case final checked?) Text( context.l10n.nextcloudServerCheck( '${MaterialLocalizations.of(context).formatShortDate(checked.toLocal())} ${MaterialLocalizations.of(context).formatTimeOfDay(TimeOfDay.fromDateTime(checked.toLocal()))}', ), ), - if (note.errorMessage != null) + if (note.errorMessage != null && + !_metadataNeedsReview(note) && + note.syncState != NoteSyncState.synced) Text( - note.errorMessage!, + nextcloudSyncLabel(context, note.syncState), style: Theme.of(context).textTheme.bodySmall, ), ], @@ -997,7 +1342,7 @@ Future addNextcloudFileAttachment( onPressed: () => Navigator.pop(context), ), ], - children: [Text(error.toString())], + children: [Text(localizeNotesFailure(context, error))], ), ); } @@ -1021,14 +1366,16 @@ Future showNextcloudConflict( final current = repository.noteById(note.localId); if (current == null || !context.mounted) return; note = current; - final localMetadata = note.metadataConflict != null; - final remote = note.remote; + var localMetadata = note.metadataConflict != null; + var remote = note.remote; final creationCandidates = note.serverId == null && !localMetadata ? repository.uncertainCreationCandidates(note.localId) : const []; var creationCandidateServerId = remote?.id; NoteState? selectedCreationRemote() { - if (remote != null && remote.id == creationCandidateServerId) return remote; + if (remote != null && remote!.id == creationCandidateServerId) { + return remote; + } return creationCandidates .where((candidate) => candidate.serverId == creationCandidateServerId) .map((candidate) => candidate.base) @@ -1036,20 +1383,22 @@ Future showNextcloudConflict( .firstOrNull; } - final canOverwriteRemote = localMetadata + var canOverwriteRemote = localMetadata ? !note.readonly && !note.error : note.serverId != null && remote != null && !remote.readonly && !remote.error; - final deletedRemotely = + // Losing remote write permission must not hide a merge the user could edit. + var retainMergeEditor = canOverwriteRemote; + var deletedRemotely = note.syncState == NoteSyncState.deletedRemotely && remote == null; - final merge = localMetadata + var merge = localMetadata ? NotesConflictMerge.metadata(note) : remote == null ? null : NotesConflictMerge(note, remote); - final canMergeRemote = + var canMergeRemote = canOverwriteRemote || (!localMetadata && note.serverId != null && @@ -1069,10 +1418,168 @@ Future showNextcloudConflict( if (merge?.favorite.conflicted == true) NotesMergeAttribute.favorite: merge!.favorite, }; - final resolution = await showBusyMarkModalDialog( + var busy = false; + var needsReview = false; + String? failure; + void refreshReview() { + note = repository.noteById(note.localId) ?? note; + localMetadata = note.metadataConflict != null; + remote = note.remote; + deletedRemotely = + note.syncState == NoteSyncState.deletedRemotely && remote == null; + merge = localMetadata + ? NotesConflictMerge.metadata(note) + : remote == null + ? null + : NotesConflictMerge(note, remote!); + canOverwriteRemote = localMetadata + ? !note.readonly && !note.error + : note.serverId != null && + remote != null && + !remote!.readonly && + !remote!.error; + retainMergeEditor = retainMergeEditor || canOverwriteRemote; + canMergeRemote = + canOverwriteRemote || + (!localMetadata && + note.serverId != null && + remote != null && + remote!.readonly && + !remote!.error && + merge!.content.value == remote!.content && + merge!.title.value == remote!.title && + merge!.category.value == remote!.category); + conflicts.clear(); + if (merge?.title.conflicted == true) { + conflicts[NotesMergeAttribute.title] = merge!.title; + } + if (merge?.category.conflicted == true) { + conflicts[NotesMergeAttribute.category] = merge!.category; + } + if (merge?.favorite.conflicted == true) { + conflicts[NotesMergeAttribute.favorite] = merge!.favorite; + } + choices.clear(); + needsReview = true; + } + + await showBusyMarkModalDialog( context, + barrierDismissible: false, builder: (context) => StatefulBuilder( builder: (context, setDialogState) { + Future apply(NoteConflictResolution? resolution) async { + if (busy || needsReview || resolution == null) return; + setDialogState(() { + busy = true; + failure = null; + }); + try { + if (deletedRemotely && + resolution == NoteConflictResolution.takeRemote) { + if (!context.mounted) return; + final confirmed = await showBusyMarkModalDialog( + context, + builder: (context) => BusyMarkDialogShell( + title: context.l10n.discard, + actions: [ + BusyMarkDialogButton( + label: context.l10n.cancel, + onPressed: () => Navigator.pop(context, false), + ), + BusyMarkDialogButton( + label: context.l10n.discard, + destructive: true, + onPressed: () => Navigator.pop(context, true), + ), + ], + children: [Text(note.title)], + ), + ); + if (confirmed != true) return; + } + if (note.serverId == null && + !localMetadata && + resolution == NoteConflictResolution.saveAsNew) { + if (!context.mounted) return; + final confirmed = await showBusyMarkModalDialog( + context, + builder: (context) => BusyMarkDialogShell( + title: context.l10n.nextcloudCreateSeparate, + actions: [ + BusyMarkDialogButton( + label: context.l10n.cancel, + onPressed: () => Navigator.pop(context, false), + ), + BusyMarkDialogButton( + label: context.l10n.nextcloudCreateSeparate, + onPressed: () => Navigator.pop(context, true), + ), + ], + children: [Text(context.l10n.nextcloudCreateSeparateWarning)], + ), + ); + if (confirmed != true) return; + } + if (!context.mounted) return; + final selected = selectedCreationRemote(); + final review = + resolution == NoteConflictResolution.useServerNote && + selected != null + ? NotesCreationReview( + localId: note.localId, + accountId: note.accountId, + attemptId: note.creationAttempt!.id, + revision: note.revision, + candidate: selected, + ) + : null; + { + final success = await ref + .read(workspaceControllerProvider.notifier) + .resolveNextcloudConflict( + note.localId, + resolution, + expectedRevision: note.revision, + metadataChoices: choices, + mergedContent: resolution == NoteConflictResolution.merge + ? merged + : null, + creationCandidateServerId: creationCandidateServerId, + creationReview: review, + reviewedRemote: remote, + ); + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + setDialogState( + () => failure = context.l10n.notesOperationFailed, + ); + } + } + } on NotesConflictReviewChanged { + if (context.mounted) { + setDialogState(() { + refreshReview(); + failure = context.l10n.notesRemoteReviewChanged; + }); + } + } on Object catch (error) { + if (context.mounted) { + setDialogState( + () => failure = localizeNotesFailure(context, error), + ); + } + } finally { + if (context.mounted) { + setDialogState(() { + busy = false; + }); + } + } + } + final displayedRemote = localMetadata ? null : note.serverId == null @@ -1096,233 +1603,265 @@ Future showNextcloudConflict( source: note.content, ), ); - return BusyMarkDialogShell( - title: context.l10n.gitConflicts, - maxWidth: 960, - actions: [ - BusyMarkDialogButton( - label: context.l10n.cancel, - onPressed: () => Navigator.pop(context), - ), - BusyMarkDialogButton( - label: note.serverId == null && !localMetadata - ? context.l10n.nextcloudCreateSeparate - : context.l10n.nextcloudNewNote, - onPressed: () => - Navigator.pop(context, NoteConflictResolution.saveAsNew), - ), - if (localMetadata) - BusyMarkDialogButton( - label: context.l10n.nextcloudUsePreviousEdit, - onPressed: canOverwriteRemote - ? () => Navigator.pop( - context, - NoteConflictResolution.takeRemote, - ) - : null, - ), - if (displayedRemote != null && - !displayedRemote.error && - (note.serverId != null || note.creationAttempt != null)) - BusyMarkDialogButton( - label: note.serverId == null - ? context.l10n.nextcloudUseServerNote - : context.l10n.nextcloudTakeRemote, - onPressed: displayedRemote.readonly && note.serverId == null - ? null - : () => Navigator.pop( - context, - note.serverId == null - ? NoteConflictResolution.useServerNote - : NoteConflictResolution.takeRemote, + return PopScope( + canPop: !busy, + child: BusyMarkDialogShell( + closable: !busy, + title: context.l10n.gitConflicts, + maxWidth: 960, + actions: + [ + BusyMarkDialogButton( + label: context.l10n.cancel, + onPressed: () => Navigator.pop(context), ), - ), - if (deletedRemotely) - BusyMarkDialogButton( - label: context.l10n.discard, - destructive: true, - onPressed: () => - Navigator.pop(context, NoteConflictResolution.takeRemote), - ), - - if (canOverwriteRemote) - BusyMarkDialogButton( - label: context.l10n.keepMine, - onPressed: () => - Navigator.pop(context, NoteConflictResolution.keepLocal), - ), - if (canMergeRemote) - BusyMarkDialogButton( - label: context.l10n.nextcloudMerge, - onPressed: choices.length != conflicts.length - ? null - : () => - Navigator.pop(context, NoteConflictResolution.merge), - ), - ], - children: [ - if (note.errorMessage != null) Text(note.errorMessage!), - if (creationCandidates.length > 1) - DropdownButton( - isExpanded: true, - value: creationCandidateServerId, - hint: Text(context.l10n.nextcloudSelectCandidate), - items: [ - for (final candidate in creationCandidates) - DropdownMenuItem( - value: candidate.serverId, - child: Text( - '${candidate.title}${candidate.category.isEmpty ? '' : ' — ${candidate.category}'} (#${candidate.serverId})', + BusyMarkDialogButton( + label: note.serverId == null && !localMetadata + ? context.l10n.nextcloudCreateSeparate + : context.l10n.notesNewFromRetainedText, + onPressed: () => + unawaited(apply(NoteConflictResolution.saveAsNew)), ), - ), - ], - onChanged: (value) => setDialogState(() { - creationCandidateServerId = value; - }), - ), - if (note.serverId == null && displayedRemote != null) ...[ - Text( - '#${displayedRemote.id} · ${displayedRemote.title} · ${displayedRemote.category}', - ), - Text( - DateTime.fromMillisecondsSinceEpoch( - displayedRemote.modified * 1000, - ).toLocal().toString(), - ), - Text( - note.creationAttempt?.matches(displayedRemote) == true - ? context.l10n.nextcloudExactCandidate - : context.l10n.nextcloudPossibleCandidate, - ), - Text(context.l10n.nextcloudUseServerNoteExplanation), - ], - if (note.serverId != null && !deletedRemotely) - _UncertainAttachments(note: note), - SizedBox( - height: 300, - child: SourceComparisonView(comparison: comparison), - ), - if (canMergeRemote && conflicts.isNotEmpty) - Text(context.l10n.nextcloudChooseConflictingAttributes), - if (canMergeRemote) - for (final entry in conflicts.entries) - ListTile( - title: Text(switch (entry.key) { - NotesMergeAttribute.title => context.l10n.pdfTitlePageTitle, - NotesMergeAttribute.category => - context.l10n.syntaxReferenceCategory, - NotesMergeAttribute.favorite => - context.l10n.nextcloudFavorites, - }), - subtitle: DropdownButton( - isExpanded: true, - value: choices[entry.key], - hint: Text( - context.l10n.nextcloudChooseConflictingAttributes, - ), - items: [ - DropdownMenuItem( - value: NotesMergeChoice.local, - child: Text( - '${context.l10n.keepMine}: ${entry.value.local}', + if (localMetadata) + BusyMarkDialogButton( + label: context.l10n.nextcloudUsePreviousEdit, + onPressed: canOverwriteRemote + ? () => unawaited( + apply(NoteConflictResolution.takeRemote), + ) + : null, ), - ), - DropdownMenuItem( - value: NotesMergeChoice.remote, - child: Text( - '${localMetadata ? context.l10n.nextcloudPreviousLocalEdit : context.l10n.nextcloudTakeRemote}: ${entry.value.remote}', + if (displayedRemote != null && + !displayedRemote.error && + (note.serverId != null || + note.creationAttempt != null)) + BusyMarkDialogButton( + label: note.serverId == null + ? context.l10n.nextcloudUseServerNote + : context.l10n.nextcloudTakeRemote, + onPressed: + displayedRemote.readonly && note.serverId == null + ? null + : () => unawaited( + apply( + note.serverId == null + ? NoteConflictResolution.useServerNote + : NoteConflictResolution.takeRemote, + ), + ), + ), + if (deletedRemotely) + BusyMarkDialogButton( + label: context.l10n.discard, + destructive: true, + onPressed: () => unawaited( + apply(NoteConflictResolution.takeRemote), + ), + ), + + if (canOverwriteRemote) + BusyMarkDialogButton( + label: context.l10n.keepMine, + onPressed: () => unawaited( + apply(NoteConflictResolution.keepLocal), + ), + ), + if (canMergeRemote) + BusyMarkDialogButton( + label: context.l10n.nextcloudMerge, + onPressed: + choices.length != conflicts.length || + (!canOverwriteRemote && + merged != remote?.content) + ? null + : () => unawaited( + apply(NoteConflictResolution.merge), + ), ), + ] + .map( + (button) => BusyMarkDialogButton( + label: button.label, + icon: button.icon, + destructive: button.destructive, + suggested: button.suggested, + onPressed: + busy || + (needsReview && + button.label != context.l10n.cancel) + ? null + : button.onPressed, ), + ) + .toList(), + children: [ + if (busy) const LinearProgressIndicator(), + if (failure != null) + BusyMarkStatusBox( + message: failure!, + kind: BusyMarkStatusKind.error, + ), + if (needsReview) + BusyMarkActionRow( + title: context.l10n.notesReviewRemote, + enabled: !busy, + onTap: () => setDialogState(() { + needsReview = false; + failure = null; + }), + ), + if (note.errorMessage != null) + Text(nextcloudSyncLabel(context, note.syncState)), + if (creationCandidates.length > 1) + BusyMarkComboRow( + title: context.l10n.nextcloudSelectCandidate, + values: [ + null, + for (final candidate in creationCandidates) + candidate.serverId, + ], + selected: creationCandidateServerId, + enabled: !busy, + labelFor: (id) { + if (id == null) { + return context.l10n.nextcloudSelectCandidate; + } + final candidate = creationCandidates.firstWhere( + (c) => c.serverId == id, + ); + return '${candidate.title}${candidate.category.isEmpty ? '' : ' · ${candidate.category}'} (#$id)'; + }, + onSelected: (value) => + setDialogState(() => creationCandidateServerId = value), + ), + if (note.serverId == null && displayedRemote != null) ...[ + Text( + '#${displayedRemote.id} · ${displayedRemote.title} · ${displayedRemote.category}', + ), + Text( + notesLocalDate( + context, + DateTime.fromMillisecondsSinceEpoch( + displayedRemote.modified * 1000, + ), + ), + ), + Text( + note.creationAttempt?.matches(displayedRemote) == true + ? context.l10n.nextcloudExactCandidate + : context.l10n.nextcloudPossibleCandidate, + ), + Text(context.l10n.nextcloudUseServerNoteExplanation), + ], + if (note.serverId != null && !deletedRemotely) + _UncertainAttachments( + note: note, + enabled: !busy, + run: (operation) async { + if (busy) return; + setDialogState(() { + busy = true; + failure = null; + }); + final before = note; + try { + await operation(); + await ref + .read(workspaceControllerProvider.notifier) + .refreshNextcloudNotes(); + if (!context.mounted) return; + final after = repository.noteById(note.localId); + setDialogState(() { + if (after != null && + (after.revision != before.revision || + jsonEncode(after.remote?.toJson()) != + jsonEncode(before.remote?.toJson()))) { + refreshReview(); + failure = context.l10n.notesRemoteReviewChanged; + } + }); + } on Object catch (error) { + if (context.mounted) { + setDialogState( + () => failure = localizeNotesFailure(context, error), + ); + } + } finally { + if (context.mounted) setDialogState(() => busy = false); + } + }, + ), + SizedBox( + height: MediaQuery.sizeOf(context).height * .3, + child: SourceComparisonView(comparison: comparison), + ), + if (canMergeRemote && conflicts.isNotEmpty) + Text(context.l10n.nextcloudChooseConflictingAttributes), + if (canMergeRemote) + for (final entry in conflicts.entries) + BusyMarkComboRow( + title: switch (entry.key) { + NotesMergeAttribute.title => + context.l10n.pdfTitlePageTitle, + NotesMergeAttribute.category => + context.l10n.syntaxReferenceCategory, + NotesMergeAttribute.favorite => + context.l10n.nextcloudFavorites, + }, + values: const [ + null, + NotesMergeChoice.local, + NotesMergeChoice.remote, ], - onChanged: (value) => setDialogState(() { - if (value != null) choices[entry.key] = value; + selected: choices[entry.key], + enabled: !busy, + labelFor: (choice) { + if (choice == null) { + return context + .l10n + .nextcloudChooseConflictingAttributes; + } + final value = choice == NotesMergeChoice.local + ? entry.value.local + : entry.value.remote; + final label = value is bool + ? (value + ? context.l10n.wsEnabled + : context.l10n.wsDisabled) + : '$value'; + return '${choice == NotesMergeChoice.local + ? context.l10n.keepMine + : localMetadata + ? context.l10n.nextcloudPreviousLocalEdit + : context.l10n.nextcloudTakeRemote}: $label'; + }, + onSelected: (value) => setDialogState(() { + if (value == null) { + choices.remove(entry.key); + } else { + choices[entry.key] = value; + } }), ), + if (!canOverwriteRemote && retainMergeEditor) + Text(context.l10n.nextcloudReadOnly), + if (canOverwriteRemote || retainMergeEditor) + BusyMarkGroupedTextEntry( + key: const ValueKey('nextcloud-conflict-merge'), + label: context.l10n.nextcloudMerge, + enabled: !busy, + readOnly: !canOverwriteRemote, + initialValue: merged, + minLines: 5, + maxLines: 12, + onChanged: (value) => merged = value, ), - if (canOverwriteRemote) - BusyMarkGroupedTextEntry( - label: context.l10n.nextcloudMerge, - initialValue: merged, - minLines: 5, - maxLines: 12, - onChanged: (value) => merged = value, - ), - ], + ], + ), ); }, ), ); - if (deletedRemotely && resolution == NoteConflictResolution.takeRemote) { - if (!context.mounted) return; - final confirmed = await showBusyMarkModalDialog( - context, - builder: (context) => BusyMarkDialogShell( - title: context.l10n.discard, - actions: [ - BusyMarkDialogButton( - label: context.l10n.cancel, - onPressed: () => Navigator.pop(context, false), - ), - BusyMarkDialogButton( - label: context.l10n.discard, - destructive: true, - onPressed: () => Navigator.pop(context, true), - ), - ], - children: [Text(note.title)], - ), - ); - if (confirmed != true) return; - } - if (note.serverId == null && - !localMetadata && - resolution == NoteConflictResolution.saveAsNew) { - if (!context.mounted) return; - final confirmed = await showBusyMarkModalDialog( - context, - builder: (context) => BusyMarkDialogShell( - title: context.l10n.nextcloudCreateSeparate, - actions: [ - BusyMarkDialogButton( - label: context.l10n.cancel, - onPressed: () => Navigator.pop(context, false), - ), - BusyMarkDialogButton( - label: context.l10n.nextcloudCreateSeparate, - onPressed: () => Navigator.pop(context, true), - ), - ], - children: [Text(context.l10n.nextcloudCreateSeparateWarning)], - ), - ); - if (confirmed != true) return; - } - final selected = selectedCreationRemote(); - final review = - resolution == NoteConflictResolution.useServerNote && selected != null - ? NotesCreationReview( - localId: note.localId, - accountId: note.accountId, - attemptId: note.creationAttempt!.id, - revision: note.revision, - candidate: selected, - ) - : null; - if (resolution != null) { - await ref - .read(workspaceControllerProvider.notifier) - .resolveNextcloudConflict( - note.localId, - resolution, - expectedRevision: note.revision, - metadataChoices: choices, - mergedContent: resolution == NoteConflictResolution.merge - ? merged - : null, - creationCandidateServerId: creationCandidateServerId, - creationReview: review, - ); - } } Future _retryAttachments( @@ -1348,41 +1887,26 @@ Future _retryAttachments( ), ); if (confirmed != true || !context.mounted) return; - try { - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - await repository.retryUncertainAttachments(note.localId); - await ref - .read(workspaceControllerProvider.notifier) - .refreshNextcloudNotes(); - if (context.mounted) Navigator.pop(context); - } on Object catch (error) { - if (!context.mounted) return; - await showBusyMarkModalDialog( - context, - builder: (context) => BusyMarkDialogShell( - title: context.l10n.warning, - actions: [ - BusyMarkDialogButton( - label: context.l10n.close, - onPressed: () => Navigator.pop(context), - ), - ], - children: [Text(error.toString())], - ), - ); - } + final repository = await ref.read(nextcloudNotesRepositoryProvider.future); + await repository.retryUncertainAttachments(note.localId); } class _UncertainAttachments extends ConsumerStatefulWidget { - const _UncertainAttachments({required this.note}); + const _UncertainAttachments({ + required this.note, + required this.enabled, + required this.run, + }); final NextcloudNote note; + final bool enabled; + final Future Function(Future Function()) run; @override ConsumerState<_UncertainAttachments> createState() => _UncertainAttachmentsState(); } class _ManagedAttachments extends ConsumerStatefulWidget { - const _ManagedAttachments({required this.note}); + const _ManagedAttachments({super.key, required this.note}); final NextcloudNote note; @override ConsumerState<_ManagedAttachments> createState() => @@ -1404,9 +1928,19 @@ class _ManagedAttachmentsState extends ConsumerState<_ManagedAttachments> { final attachments = snapshot.data ?.where((a) => a.state != 'deleted') .toList(); - if (attachments == null || attachments.isEmpty) { - return const SizedBox.shrink(); + if (snapshot.hasError) { + return BusyMarkActionRow( + title: context.l10n.notesInspectionFailed, + trailing: const Icon(BusyMarkGlyphs.refresh), + onTap: () => setState(() => _attachments = _load()), + ); } + if (attachments == null) return const LinearProgressIndicator(); + if (attachments.isEmpty) return const SizedBox.shrink(); + final account = ref + .watch(nextcloudNotesRepositoryProvider) + .value + ?.accountById(widget.note.accountId); return Column( children: [ BusyMarkGroupedList( @@ -1415,11 +1949,18 @@ class _ManagedAttachmentsState extends ConsumerState<_ManagedAttachments> { for (final attachment in attachments) BusyMarkActionRow( title: attachment.filename, - trailing: IconButton( + subtitle: + attachment.remotePath != null && + account?.supportsAttachmentDeletion != true + ? context.l10n.notesRemoteDeleteUnavailable + : null, + trailing: BusyMarkCompactIconButton( tooltip: context.l10n.delete, - icon: const Icon(BusyMarkGlyphs.delete), + icon: BusyMarkGlyphs.delete, onPressed: - _busy || + (attachment.remotePath != null && + account?.supportsAttachmentDeletion != true) || + _busy || widget.note.readonly || widget.note.error || attachment.state == 'uploading' || @@ -1479,7 +2020,9 @@ class _ManagedAttachmentsState extends ConsumerState<_ManagedAttachments> { } if (mounted) setState(() => _attachments = _load()); } on Object catch (error) { - if (mounted) setState(() => _error = error.toString()); + if (mounted) { + setState(() => _error = localizeNotesFailure(context, error)); + } } finally { if (mounted) setState(() => _busy = false); } @@ -1487,7 +2030,7 @@ class _ManagedAttachmentsState extends ConsumerState<_ManagedAttachments> { } class _UncertainAttachmentsState extends ConsumerState<_UncertainAttachments> { - late final _attachments = ref + late var _attachments = ref .read(nextcloudNotesRepositoryProvider.future) .then((repository) => repository.attachments(widget.note.localId)); String _serverReference = ''; @@ -1501,12 +2044,27 @@ class _UncertainAttachmentsState extends ConsumerState<_UncertainAttachments> { ?.where((a) => a.state == 'uncertain' || a.state == 'uploading') .toList() ?? []; + if (snapshot.hasError) { + return BusyMarkActionRow( + title: context.l10n.notesInspectionFailed, + trailing: const Icon(BusyMarkGlyphs.refresh), + onTap: () => setState(() { + _attachments = ref + .read(nextcloudNotesRepositoryProvider.future) + .then((r) => r.attachments(widget.note.localId)); + }), + ); + } + if (snapshot.connectionState != ConnectionState.done) { + return const LinearProgressIndicator(); + } if (pending.isEmpty) return const SizedBox.shrink(); return BusyMarkGroupedList( filled: true, children: [ BusyMarkGroupedTextEntry( label: context.l10n.nextcloudAttachmentReference, + enabled: widget.enabled, onChanged: (value) => _serverReference = value, errorText: _error, ), @@ -1515,35 +2073,44 @@ class _UncertainAttachmentsState extends ConsumerState<_UncertainAttachments> { title: attachment.filename, trailing: BusyMarkPushButton.standard( child: Text(context.l10n.nextcloudTakeRemote), - onPressed: () => unawaited(_adopt(attachment)), + onPressed: widget.enabled + ? () => unawaited(_perform(() => _adopt(attachment))) + : null, ), ), BusyMarkActionRow( title: context.l10n.visualizationRetry, subtitle: context.l10n.nextcloudAttachmentRetryWarning, - onTap: () => - unawaited(_retryAttachments(context, ref, widget.note)), + enabled: widget.enabled, + onTap: widget.enabled + ? () => unawaited( + _perform( + () => _retryAttachments(context, ref, widget.note), + ), + ) + : null, ), ], ); }, ); - Future _adopt(NotesAttachment attachment) async { - try { - final repository = await ref.read( - nextcloudNotesRepositoryProvider.future, - ); - await repository.adoptUncertainAttachment( - widget.note.localId, - attachment.reference, - _serverReference, - ); - await ref - .read(workspaceControllerProvider.notifier) - .refreshNextcloudNotes(); - if (mounted) Navigator.pop(context); - } on Object catch (error) { - if (mounted) setState(() => _error = error.toString()); + Future _perform(Future Function() operation) async { + await widget.run(operation); + if (mounted) { + setState(() { + _attachments = ref + .read(nextcloudNotesRepositoryProvider.future) + .then((r) => r.attachments(widget.note.localId)); + }); } } + + Future _adopt(NotesAttachment attachment) async { + final repository = await ref.read(nextcloudNotesRepositoryProvider.future); + await repository.adoptUncertainAttachment( + widget.note.localId, + attachment.reference, + _serverReference, + ); + } } diff --git a/lib/src/nextcloud_notes/presentation/notes_workspace_ui.dart b/lib/src/nextcloud_notes/presentation/notes_workspace_ui.dart index 83053aea..4455c77c 100644 --- a/lib/src/nextcloud_notes/presentation/notes_workspace_ui.dart +++ b/lib/src/nextcloud_notes/presentation/notes_workspace_ui.dart @@ -1,15 +1,15 @@ import 'dart:async'; +import 'dart:io'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../app/app_settings.dart'; import '../../app/busymark_design.dart'; import '../../app/busymark_dialogs.dart'; -import '../../app/busymark_glyphs.dart'; import '../../app/localization.dart'; +import '../../app/path_label.dart'; +import 'notes_messages.dart'; import '../../local_history/local_history_controller.dart'; import '../../local_history/local_history_models.dart'; import '../../workspace/workspace_controller.dart'; @@ -17,10 +17,8 @@ import '../application/nextcloud_connection.dart'; import '../application/notes_navigation.dart'; import '../application/notes_repository.dart'; import '../application/notes_offline_controller.dart'; -import '../application/notes_search_controller.dart'; import '../application/notes_transfer_service.dart'; import '../domain/notes_models.dart'; -import '../domain/notes_search.dart'; final notesOfflineProvider = FutureProvider.autoDispose .family((ref, accountId) async { @@ -49,9 +47,11 @@ class NotesMatchNavigation { this.start, this.end, this.digest, - this.request, - ); + this.request, { + this.accountId, + }); final String localId, digest; + final String? accountId; final int start, end, request; } @@ -63,129 +63,216 @@ final notesMatchNavigationProvider = class NotesMatchNavigationController extends Notifier { @override NotesMatchNavigation? build() => null; - void navigate(String id, int start, int end, String digest) { + void navigate( + String id, + int start, + int end, + String digest, { + String? accountId, + }) { state = NotesMatchNavigation( id, start, end, digest, (state?.request ?? 0) + 1, + accountId: accountId, ); } } -Future showNotesSearch( +Future showNotesBatch( BuildContext context, - WidgetRef ref, { - String query = '', - String? account, - bool recovery = false, + WidgetRef ref, + List ids, { + String? initialAction, + List? reviewedTargets, }) async { - final accountId = - account ?? - ref.read(workspaceControllerProvider).workspace?.nextcloudAccountId; - if (accountId == null) return; - final focus = FocusManager.instance.primaryFocus; - final choice = - await showBusyMarkModalDialog< - ({NotesSearchHit hit, String query, bool wholeWord, bool source}) - >( - context, - builder: (_) => NotesSearchDialog( - accountId: accountId, - initialQuery: query, - recovery: recovery, - ), - ); - if (!context.mounted) return; - if (choice == null) { - if (focus?.context?.mounted == true) focus!.requestFocus(); - return; - } - if (ref.read(workspaceControllerProvider).workspace?.nextcloudAccountId != - accountId) { - return; - } - if (recovery) { - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - final note = repository.noteById(choice.hit.localId); - if (context.mounted && - note != null && - note.accountId == accountId && - isNotesRecovery(note)) { - await showNotesRecovery(context, ref, note); - } - return; - } - final controller = ref.read(workspaceControllerProvider.notifier); - if (!await controller.openNextcloudNote(choice.hit.localId) || - !context.mounted) { - return; - } - final buffer = ref.read(workspaceControllerProvider).activeBuffer; - if (buffer?.remoteNote?.localId != choice.hit.localId) return; final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - final note = repository.noteById(choice.hit.localId); - if (note == null) return; - var hit = choice.hit; - // Even a revision collision cannot apply offsets to a different document. - if (buffer!.revision != hit.revision || - notesSearchDigest(buffer.text) != hit.digest) { - final refreshed = await relocateNotesSearchHit( - NotesSearchQuery(choice.query, wholeWord: choice.wholeWord), - NotesSearchOverlay( - note.localId, - buffer.revision, - note.title, - note.category, - buffer.text, - ), - hit.start ?? 0, - ); - if (refreshed == null || !context.mounted) return; - final current = ref.read(workspaceControllerProvider).activeBuffer; - if (current?.remoteNote?.localId != note.localId || - current!.revision != refreshed.revision || - notesSearchDigest(current.text) != refreshed.digest) { - return; - } - hit = refreshed; - } - if (hit.start == null || hit.end == null) return; - if (choice.source) { - await ref - .read(appSettingsControllerProvider.notifier) - .setDocumentViewMode(DocumentViewModePreference.source); - } - ref - .read(notesMatchNavigationProvider.notifier) - .navigate(hit.localId, hit.start!, hit.end!, hit.digest); + final reviewed = + reviewedTargets ?? + [ + for (final id in ids) + if (repository.noteById(id) != null) repository.metadataSnapshot(id), + ]; + if (!context.mounted || reviewed.isEmpty) return; + var action = initialAction ?? 'favorite'; + var category = reviewed.length == 1 ? reviewed.single.note.category : ''; + var busy = false; + List? outcomes; + String? error; + await showBusyMarkModalDialog( + context, + barrierDismissible: false, + builder: (context) => StatefulBuilder( + builder: (context, update) { + String actionLabel(String value) => switch (value) { + 'move' => context.l10n.notesMove, + 'unfavorite' => context.l10n.notesRemoveFavorite, + _ => context.l10n.nextcloudFavorites, + }; + String statusLabel(NotesBatchStatus value) => switch (value) { + NotesBatchStatus.changed => context.l10n.nextcloudSavedLocally, + NotesBatchStatus.skipped => context.l10n.notesSkipped, + NotesBatchStatus.conflicted => context.l10n.gitConflicts, + NotesBatchStatus.failed => context.l10n.notesFailed, + }; + return PopScope( + canPop: !busy, + child: BusyMarkDialogShell( + title: '${actionLabel(action)} (${reviewed.length})', + closable: !busy, + actions: [ + BusyMarkDialogButton( + label: outcomes == null + ? context.l10n.cancel + : context.l10n.close, + onPressed: busy ? null : () => Navigator.pop(context), + ), + if (outcomes == null) + BusyMarkDialogButton( + label: context.l10n.save, + onPressed: busy + ? null + : () async { + update(() { + busy = true; + error = null; + }); + try { + final result = await ref + .read(workspaceControllerProvider.notifier) + .batchNextcloudMetadata( + reviewed, + category: action == 'move' ? category : null, + favorite: action == 'move' + ? null + : action == 'favorite', + ); + if (context.mounted) { + update(() => outcomes = result); + } + } on Object catch (failure) { + if (context.mounted) { + update( + () => error = localizeNotesFailure( + context, + failure, + ), + ); + } + } finally { + if (context.mounted) update(() => busy = false); + } + }, + ), + ], + children: [ + if (initialAction == null && outcomes == null) + BusyMarkComboRow( + title: context.l10n.notesActions, + values: const ['favorite', 'unfavorite', 'move'], + selected: action, + labelFor: actionLabel, + enabled: !busy, + onSelected: (value) => update(() => action = value), + ), + if (action == 'move' && outcomes == null) + BusyMarkGroupedTextEntry( + label: context.l10n.syntaxReferenceCategory, + initialValue: category, + enabled: !busy, + onChanged: (value) => category = value, + ), + if (busy) const LinearProgressIndicator(), + if (error != null) + BusyMarkStatusBox( + message: error!, + kind: BusyMarkStatusKind.error, + ), + if (outcomes != null) + Semantics( + liveRegion: true, + child: Text( + [ + for (final status in NotesBatchStatus.values) + '${statusLabel(status)}: ${outcomes!.where((o) => o.status == status).length}', + ].join(' · '), + ), + ), + SizedBox( + height: MediaQuery.sizeOf(context).height * .3, + child: ListView( + children: [ + for (final snapshot in reviewed) + BusyMarkActionRow( + title: snapshot.note.title, + subtitle: outcomes == null + ? snapshot.note.category + : null, + subtitleWidget: outcomes == null + ? null + : Text(() { + final outcome = outcomes!.firstWhere( + (o) => o.localId == snapshot.note.localId, + ); + final reason = localizeNotesBatchReason( + context, + outcome, + ); + return [ + statusLabel(outcome.status), + if (reason.isNotEmpty) reason, + ].join(' · '); + }()), + ), + ], + ), + ), + if (outcomes?.any((o) => o.detail != null) == true) + ExpansionTile( + title: Text(context.l10n.notesTechnicalDetails), + children: [ + for (final outcome in outcomes!.where( + (o) => o.detail != null, + )) + SelectableText('${outcome.title}: ${outcome.detail}'), + ], + ), + ], + ), + ); + }, + ), + ); } -class NotesSearchDialog extends ConsumerStatefulWidget { - const NotesSearchDialog({ - super.key, - required this.accountId, - this.initialQuery = '', - this.recovery = false, - }); - final String accountId, initialQuery; - final bool recovery; +Future showNotesOffline( + BuildContext context, + WidgetRef ref, + NextcloudNote note, +) async { + await showBusyMarkModalDialog( + context, + builder: (_) => NotesOfflineDialog(note: note), + ); +} + +class NotesOfflineDialog extends ConsumerStatefulWidget { + const NotesOfflineDialog({super.key, required this.note}); + final NextcloudNote note; @override - ConsumerState createState() => _NotesSearchDialogState(); + ConsumerState createState() => _NotesOfflineDialogState(); } -class _NotesSearchDialogState extends ConsumerState { - NotesSearchController? _controller; - StreamSubscription? _results; - StreamSubscription? _repositoryChanges; - Timer? _debounce; - late final _text = TextEditingController(text: widget.initialQuery); - late final _focus = FocusNode(onKeyEvent: _key); - final _scroll = ScrollController(); - NotesSearchState _state = const NotesSearchState(loading: true); - bool _wholeWord = false; - int _selected = 0, _limit = 80; +class _NotesOfflineDialogState extends ConsumerState { + NotesOfflineController? _controller; + NotesOfflineStatus? _status; + StreamSubscription? _subscription; + int _generation = 0; + bool _loading = true, _busy = false; + bool _pausing = false; + String? _error; @override void initState() { super.initState(); @@ -193,442 +280,201 @@ class _NotesSearchDialogState extends ConsumerState { } Future _initialize() async { - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - if (!mounted) return; - _controller = NotesSearchController( - repository.store, - widget.accountId, - recovery: widget.recovery, - ); - _results = _controller!.changes.listen((state) { + try { + final controller = await ref.read( + notesOfflineProvider(widget.note.accountId).future, + ); + if (!mounted) return; + _controller = controller; + _subscription = controller.changes.listen((_) => unawaited(_inspect())); + await _inspect(); + } on Object { if (mounted) { setState(() { - _state = state; - _selected = 0; + _loading = false; + _error = context.l10n.notesInspectionFailed; }); } - }); - _repositoryChanges = repository.changes.listen((_) => _schedule()); - _search(); + } } - void _schedule() { - _controller?.cancel(); - _debounce?.cancel(); - _debounce = Timer(const Duration(milliseconds: 150), _search); + Future _reload() async { + ref.invalidate(notesOfflineProvider(widget.note.accountId)); + await _initialize(); } - void _search() { - final repository = ref.read(nextcloudNotesRepositoryProvider).value; - if (repository == null || _controller == null) return; - final overlays = [ - for (final buffer - in ref.read(workspaceControllerProvider).documentBuffers) - if (!widget.recovery && - buffer.isDirty && - buffer.remoteNote?.accountId == widget.accountId && - repository.noteById(buffer.remoteNote!.localId) != null) - NotesSearchOverlay( - buffer.remoteNote!.localId, - buffer.revision, - repository.noteById(buffer.remoteNote!.localId)!.title, - repository.noteById(buffer.remoteNote!.localId)!.category, - buffer.text, - ), - ]; - unawaited( - _controller!.search( - _text.text, - wholeWord: _wholeWord, - limit: _limit, - overlays: overlays, - ), - ); + Future _inspect() async { + final request = ++_generation; + try { + final status = await _controller!.inspect(widget.note); + if (mounted && request == _generation) { + setState(() { + _status = status; + _loading = false; + _error = null; + }); + } + } on Object { + if (mounted && request == _generation) { + setState(() { + _loading = false; + _error = context.l10n.notesInspectionFailed; + }); + } + } } - KeyEventResult _key(FocusNode node, KeyEvent event) { - if (event is! KeyDownEvent) return KeyEventResult.ignored; - if (event.logicalKey == LogicalKeyboardKey.escape) { - Navigator.pop(context); - return KeyEventResult.handled; - } - if (event.logicalKey == LogicalKeyboardKey.enter) { - _open(); - return KeyEventResult.handled; + Future _run( + Future Function(NotesOfflineController) action, { + bool interrupt = false, + }) async { + if ((interrupt ? _pausing : _busy || _pausing) || _controller == null) { + return; } - if (event.logicalKey == LogicalKeyboardKey.arrowUp || - event.logicalKey == LogicalKeyboardKey.arrowDown) { - if (_state.hits.isNotEmpty) { - setState( - () => _selected = - (_selected + - (event.logicalKey == LogicalKeyboardKey.arrowDown - ? 1 - : -1)) - .clamp(0, _state.hits.length - 1), - ); - if (_scroll.hasClients) { - _scroll.animateTo( - (_selected * 90.0).clamp(0, _scroll.position.maxScrollExtent), - duration: const Duration(milliseconds: 100), - curve: Curves.easeOut, - ); - } + setState(() { + if (interrupt) { + _pausing = true; + } else { + _busy = true; + } + _error = null; + }); + try { + await action(_controller!); + await _inspect(); + } on Object { + if (mounted) setState(() => _error = context.l10n.notesOperationFailed); + } finally { + if (mounted) { + setState(() { + if (interrupt) { + _pausing = false; + } else { + _busy = false; + } + }); } - return KeyEventResult.handled; } - return KeyEventResult.ignored; - } - - void _open({bool source = false, int? index}) { - if (_state.loading || _state.hits.isEmpty) return; - Navigator.pop(context, ( - hit: _state.hits[index ?? _selected], - query: _text.text, - wholeWord: _wholeWord, - source: source, - )); } @override void dispose() { - _debounce?.cancel(); - unawaited(_results?.cancel()); - unawaited(_repositoryChanges?.cancel()); - unawaited(_controller?.dispose()); - _text.dispose(); - _focus.dispose(); - _scroll.dispose(); + _generation++; + unawaited(_subscription?.cancel()); super.dispose(); } @override Widget build(BuildContext context) { - ref.listen( - workspaceControllerProvider.select((s) => s.documentBuffers), - (a, b) => _schedule(), - ); + // Keep the controller's auto-dispose owner alive for the whole dialog. + ref.watch(notesOfflineProvider(widget.note.accountId)); + final controller = _controller; + final status = _status; + final failed = + controller?.failures.keys.any( + (key) => key.startsWith('${widget.note.localId}:'), + ) ?? + false; + final retained = + controller?.individuallyRetained(widget.note.localId) ?? false; + final broader = + controller?.requirements.any( + (r) => + r['kind'] == 'category' && + categoryIncludes(r['target'] as String, widget.note.category), + ) ?? + false; return BusyMarkDialogShell( - title: widget.recovery - ? '${context.l10n.notesRecovery} · ${context.l10n.search}' - : context.l10n.search, - maxWidth: 760, - children: [ - TextField( - autofocus: true, - focusNode: _focus, - controller: _text, - decoration: InputDecoration(labelText: context.l10n.search), - onChanged: (_) { - _limit = 80; - _schedule(); - }, - ), - if (widget.recovery) Text(context.l10n.notesRecoveryHelp), - Text( - context.l10n.notesSearchHelp, - style: Theme.of(context).textTheme.bodySmall, - ), - CheckboxListTile( - dense: true, - title: Text(context.l10n.sourceSearchWholeWord), - value: _wholeWord, - onChanged: (v) => setState(() { - _wholeWord = v ?? false; - _schedule(); - }), - ), - if (_state.loading) const LinearProgressIndicator(), - if (_state.indexing) Text(context.l10n.notesIndexing), - if (_state.error != null) - Text('${context.l10n.warning}: ${_state.error}'), - SizedBox( - height: 360, - child: _state.hits.isEmpty - ? Center( - child: Text( - _state.loading - ? context.l10n.notesIndexing - : context.l10n.noResults, + title: context.l10n.notesOffline, + actions: [ + if (status?.paused == true) + BusyMarkDialogButton( + label: context.l10n.notesOfflineResume, + onPressed: _busy + ? null + : () => unawaited( + _run((c) => c.retry(widget.note.localId, false)), ), - ) - : ListView.builder( - key: const ValueKey('notes-search-results'), - controller: _scroll, - itemCount: _state.hits.length, - itemBuilder: (context, index) { - final hit = _state.hits[index]; - final from = - hit.snippetMatchStart ?? - (hit.start == null - ? 0 - : (hit.start! - hit.snippetStart).clamp( - 0, - hit.snippet.length, - )); - final to = - hit.snippetMatchEnd ?? - (hit.end == null - ? 0 - : (hit.end! - hit.snippetStart).clamp( - from, - hit.snippet.length, - )); - return ListTile( - key: ValueKey(( - 'notes-search-hit', - hit.localId, - hit.start, - hit.end, - )), - selected: index == _selected, - title: Text( - hit.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (hit.category.isNotEmpty) - Text( - hit.category, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: hit.snippet - .substring(0, from) - .replaceAll(RegExp(r'[\r\n\t]'), ' '), - ), - TextSpan( - text: hit.snippet - .substring(from, to) - .replaceAll(RegExp(r'[\r\n\t]'), ' '), - style: TextStyle( - backgroundColor: Theme.of( - context, - ).colorScheme.primaryContainer, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: hit.snippet - .substring(to) - .replaceAll(RegExp(r'[\r\n\t]'), ' '), - ), - ], - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - trailing: widget.recovery || hit.start == null - ? null - : IconButton( - tooltip: context.l10n.notesSourceLocation, - icon: const Icon(BusyMarkGlyphs.code), - onPressed: () => - _open(source: true, index: index), - ), - onTap: () => _open(index: index), - ); - }, - ), - ), - if (_state.truncated) - TextButton( - onPressed: () { - _limit += 80; - _search(); - }, - child: Text(context.l10n.workspaceSearchShowMore), - ), - ], - ); - } -} - -Future showNotesBatch( - BuildContext context, - WidgetRef ref, - List ids, { - String? initialAction, - List? reviewedTargets, -}) async { - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - final reviewed = - reviewedTargets ?? - [ - for (final id in ids) - if (repository.noteById(id) != null) repository.metadataSnapshot(id), - ]; - if (!context.mounted || reviewed.isEmpty) return; - var action = initialAction ?? 'favorite'; - var category = ''; - final confirmed = await showBusyMarkModalDialog( - context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => BusyMarkDialogShell( - title: '${context.l10n.notesSelected} (${reviewed.length})', - actions: [ + ) + else if (status?.running == true) BusyMarkDialogButton( - label: context.l10n.cancel, - onPressed: () => Navigator.pop(context, false), + label: context.l10n.notesOfflinePause, + onPressed: _pausing + ? null + : () => unawaited( + _run( + (c) => c.cancelNote(widget.note.localId), + interrupt: true, + ), + ), ), + if (failed || + _error != null || + (status != null && + status.missing.isNotEmpty && + !status.running && + !status.paused)) BusyMarkDialogButton( - label: context.l10n.save, - onPressed: () => Navigator.pop(context, true), - ), - ], - children: [ - SizedBox( - height: 140, - child: ListView( - children: [ - for (final snapshot in reviewed) - ListTile( - dense: true, - title: Text(snapshot.note.title), - subtitle: Text(snapshot.note.category), + label: context.l10n.visualizationRetry, + onPressed: _busy + ? null + : () => unawaited( + controller == null + ? _reload() + : _run((c) => c.retry(widget.note.localId, false)), ), - ], - ), ), - DropdownButton( - isExpanded: true, - value: action, - items: [ - DropdownMenuItem( - value: 'favorite', - child: Text(context.l10n.nextcloudFavorites), - ), - DropdownMenuItem( - value: 'unfavorite', - child: Text(context.l10n.notesRemoveFavorite), - ), - DropdownMenuItem( - value: 'move', - child: Text(context.l10n.notesMove), + BusyMarkDialogButton( + label: context.l10n.close, + onPressed: () => Navigator.pop(context), + ), + ], + children: [ + Text(widget.note.title), + BusyMarkSwitchRow( + title: context.l10n.notesOfflineKeep, + subtitle: context.l10n.notesOfflineRetentionHelp, + value: retained, + enabled: !_busy && controller != null, + onChanged: (value) => unawaited( + _run( + (c) => c.setRequirement( + 'note', + widget.note.localId, + required: value, + waitForDownloads: false, ), - ], - onChanged: (v) => setDialogState(() => action = v!), + ), ), - if (action == 'move') - TextField( - decoration: InputDecoration( - labelText: context.l10n.syntaxReferenceCategory, - ), - onChanged: (v) => category = v, + ), + if (broader) Text(context.l10n.notesOfflineBroader), + if (_loading || _busy || _pausing) const LinearProgressIndicator(), + if (_error != null) + BusyMarkStatusBox(message: _error!, kind: BusyMarkStatusKind.error), + if (status != null) ...[ + Text(context.l10n.notesTextAvailable), + Text( + '${status.available && status.external.isEmpty + ? context.l10n.notesAvailableOffline + : status.available + ? context.l10n.notesTextAvailable + : context.l10n.notesIncomplete} · ${context.l10n.notesAttachments}: ${status.availableCount}/${status.requiredCount}', + ), + if (status.paused) Text(context.l10n.notesDownloadsPaused), + if (status.running) const LinearProgressIndicator(), + if (failed) + BusyMarkStatusBox( + message: context.l10n.notesInspectionFailed, + kind: BusyMarkStatusKind.warning, ), + for (final reference in status.missing) + Text('${context.l10n.notesIncomplete}: $reference'), + if (status.external.isNotEmpty) + Text(context.l10n.notesOfflineExternal), ], - ), - ), - ); - if (confirmed != true || !context.mounted) return; - final outcomes = await ref - .read(workspaceControllerProvider.notifier) - .batchNextcloudMetadata( - reviewed, - category: action == 'move' ? category : null, - favorite: action == 'move' ? null : action == 'favorite', - ); - if (!context.mounted) return; - await showBusyMarkModalDialog( - context, - builder: (context) => BusyMarkDialogShell( - title: context.l10n.notesSelected, - children: [ - for (final outcome in outcomes) - ListTile( - title: Text(outcome.title), - subtitle: Text( - [ - switch (outcome.status) { - NotesBatchStatus.changed => - context.l10n.nextcloudSavedLocally, - NotesBatchStatus.skipped => context.l10n.notesSkipped, - NotesBatchStatus.conflicted => context.l10n.gitConflicts, - NotesBatchStatus.failed => context.l10n.notesFailed, - }, - if (outcome.detail != null) outcome.detail!, - ].join(' · '), - ), - ), ], - ), - ); -} - -Future showNotesOffline( - BuildContext context, - WidgetRef ref, - NextcloudNote note, -) async { - final controller = await ref.read( - notesOfflineProvider(note.accountId).future, - ); - if (!context.mounted) return; - await showBusyMarkModalDialog( - context, - builder: (context) => StreamBuilder( - stream: controller.changes, - builder: (context, _) => FutureBuilder( - future: controller.inspect(note), - builder: (context, snapshot) { - final status = snapshot.data; - return BusyMarkDialogShell( - title: context.l10n.notesOffline, - actions: [ - BusyMarkDialogButton( - label: context.l10n.visualizationRetry, - onPressed: () => unawaited(controller.retry(note.localId)), - ), - BusyMarkDialogButton( - label: context.l10n.cancel, - onPressed: () => unawaited(controller.cancelNote(note.localId)), - ), - BusyMarkDialogButton( - label: controller.individuallyRetained(note.localId) - ? context.l10n.notesOfflineRemove - : context.l10n.notesOffline, - onPressed: () => unawaited( - controller.setRequirement( - 'note', - note.localId, - required: !controller.individuallyRetained(note.localId), - ), - ), - ), - ], - children: [ - Text(note.title), - if (status == null) - const LinearProgressIndicator() - else ...[ - Text(context.l10n.notesTextAvailable), - Text( - '${status.available ? context.l10n.notesAvailableOffline : context.l10n.notesIncomplete} · ${status.availableCount}/${status.requiredCount}', - ), - if (status.running) const LinearProgressIndicator(), - for (final reference in status.missing) Text(reference), - for (final reference in status.external) - Text('${context.l10n.notesIncomplete}: $reference'), - for (final failure in controller.failures.entries.where( - (e) => e.key.startsWith('${note.localId}:'), - )) - Text(failure.value), - ], - ], - ); - }, - ), - ), - ); + ); + } } Future showNotesRecovery( @@ -636,10 +482,9 @@ Future showNotesRecovery( WidgetRef ref, NextcloudNote note, ) async { - await ref.read(localHistoryControllerProvider.notifier).refresh(); - if (!context.mounted) return; await showBusyMarkModalDialog( context, + barrierDismissible: false, builder: (_) => NotesRecoveryDialog(note: note), ); } @@ -654,138 +499,237 @@ class NotesRecoveryDialog extends ConsumerStatefulWidget { class _NotesRecoveryDialogState extends ConsumerState { LocalHistoryDocument? _document; + List _summaries = const []; LocalHistoryRevision? _revision; late String _source = widget.note.content; + String _selected = ''; NotesOfflineStatus? _availability; int _generation = 0; + bool _loading = true, _busy = false; + String? _error; + String? _createdNoteId; + @override void initState() { super.initState(); - unawaited(_inspect()); + unawaited(_load()); } - Future _inspect() async { + Future _load() async { final generation = ++_generation; - final repository = await ref.read(nextcloudNotesRepositoryProvider.future); - final availability = await _inspectRecoveryAvailability( - repository, - widget.note, - source: _source, - ); - if (mounted && generation == _generation) { - setState(() => _availability = availability); + try { + final history = await ref + .read(localHistoryControllerProvider.notifier) + .readRecoveryHistory(); + if (!mounted || generation != _generation) return; + _document = history.documents + .where( + (d) => + d.remoteNote?.accountId == widget.note.accountId && + d.remoteNote?.localId == widget.note.localId, + ) + .firstOrNull; + _summaries = _document == null ? [] : history.revisionsFor(_document!.id); + await _select('', generation: generation); + } on Object { + if (mounted && generation == _generation) { + setState(() { + _loading = false; + _error = context.l10n.notesOperationFailed; + }); + } + } + } + + Future _select(String id, {int? generation}) async { + if (_busy || _createdNoteId != null) return; + final request = generation ?? ++_generation; + setState(() { + _selected = id; + _loading = true; + _error = null; + _availability = null; + }); + try { + final revision = id.isEmpty + ? null + : await ref + .read(localHistoryControllerProvider.notifier) + .readRecoveryRevision(_document!.id, id); + if (!mounted || request != _generation) return; + if (id.isNotEmpty && revision == null) { + throw StateError('Missing revision'); + } + final source = revision?.source ?? widget.note.content; + final repository = await ref.read( + nextcloudNotesRepositoryProvider.future, + ); + if (!mounted || request != _generation) return; + final availability = await _inspectRecoveryAvailability( + repository, + widget.note, + source: source, + ); + if (!mounted || request != _generation) return; + setState(() { + _revision = revision; + _source = source; + _availability = availability; + _loading = false; + }); + } on Object { + if (mounted && request == _generation) { + setState(() { + _loading = false; + _error = context.l10n.notesInspectionFailed; + }); + } + } + } + + Future _recover() async { + if (_busy || + _loading || + _availability == null || + _availability!.missing.isNotEmpty) { + return; + } + // Freeze the reviewed source before any await and reject repeated activation. + final revision = _revision; + final document = _document; + final source = _source; + setState(() { + _busy = true; + _error = null; + }); + try { + final controller = ref.read(workspaceControllerProvider.notifier); + if (_createdNoteId != null) { + final opened = await controller.openNextcloudNote(_createdNoteId!); + if (!mounted) return; + if (opened) { + Navigator.pop(context); + } else { + setState(() => _error = context.l10n.notesRecoveryCreatedNotOpened); + } + return; + } + final result = revision == null || document == null + ? await controller.recoverDeletedNextcloudNote( + widget.note.localId, + content: source, + ) + : await controller.recoverNextcloudHistoryRevision( + document: document, + revision: revision, + ); + if (!mounted) return; + _createdNoteId = result.noteId; + if (result.opened) { + Navigator.pop(context); + } else { + setState( + () => _error = result.created + ? context.l10n.notesRecoveryCreatedNotOpened + : localizeNotesFailure(context, result.failure), + ); + } + } on Object catch (failure) { + if (mounted) { + setState(() => _error = localizeNotesFailure(context, failure)); + } + } finally { + if (mounted) setState(() => _busy = false); } } @override - Widget build(BuildContext context) { - final history = ref.watch(localHistoryControllerProvider); - _document ??= history.snapshot.documents - .where( - (d) => - d.remoteNote?.accountId == widget.note.accountId && - d.remoteNote?.localId == widget.note.localId, - ) - .firstOrNull; - final summaries = _document == null - ? [] - : history.snapshot.revisionsFor(_document!.id); - return BusyMarkDialogShell( + void dispose() { + _generation++; + super.dispose(); + } + + @override + Widget build(BuildContext context) => PopScope( + canPop: !_busy, + child: BusyMarkDialogShell( title: context.l10n.notesRecovery, maxWidth: 760, + closable: !_busy, actions: [ BusyMarkDialogButton( - label: - '${context.l10n.notesRecovery}: ${context.l10n.nextcloudNewNote}', - onPressed: _availability == null || _availability!.missing.isNotEmpty + label: context.l10n.close, + onPressed: _busy ? null : () => Navigator.pop(context), + ), + if (_error != null && _createdNoteId == null) + BusyMarkDialogButton( + label: context.l10n.visualizationRetry, + onPressed: _busy + ? null + : () => unawaited( + _document == null ? _load() : _select(_selected), + ), + ), + BusyMarkDialogButton( + label: _createdNoteId != null + ? context.l10n.open + : '${context.l10n.notesRecovery}: ${context.l10n.nextcloudNewNote}', + onPressed: + _busy || + _loading || + _availability == null || + _availability!.missing.isNotEmpty ? null - : () async { - final controller = ref.read( - workspaceControllerProvider.notifier, - ); - final success = _revision == null || _document == null - ? await controller.recoverDeletedNextcloudNote( - widget.note.localId, - content: _source, - ) - : await controller.recoverNextcloudHistoryRevision( - document: _document!, - revision: _revision!, - ); - if (context.mounted && success) Navigator.pop(context); - }, + : () => unawaited(_recover()), ), ], children: [ Text(widget.note.title), Text(context.l10n.notesRecoveryHelp), - if (summaries.isEmpty) Text(context.l10n.notesNoHistory), - if (!ref - .read(localHistoryControllerProvider.notifier) - .policy - .recordingEnabled) - Text(context.l10n.localHistoryWarningRecordingDisabled), - DropdownButton( - isExpanded: true, - value: _revision?.summary.id ?? '', - items: [ - DropdownMenuItem( - value: '', - child: Text(context.l10n.notesTextAvailable), - ), - for (final summary in summaries) - DropdownMenuItem( - value: summary.id, - child: Text(summary.capturedAt.toLocal().toString()), - ), - ], - onChanged: (id) async { - if (id == '') { - setState(() { - _revision = null; - _source = widget.note.content; - _availability = null; - }); - await _inspect(); - return; - } - if (_document == null) return; - final controller = ref.read( - localHistoryControllerProvider.notifier, - ); - controller.selectDocument(_document!.id); - await controller.selectRevision(id!); - final revision = ref - .read(localHistoryControllerProvider) - .selectedRevision; - if (mounted && revision != null) { - setState(() { - _revision = revision; - _source = revision.source; - _availability = null; - }); - await _inspect(); - } - }, + if (!_loading && _summaries.isEmpty) Text(context.l10n.notesNoHistory), + BusyMarkComboRow( + title: context.l10n.localHistory, + selected: _selected, + enabled: !_busy && _createdNoteId == null, + values: ['', for (final summary in _summaries) summary.id], + labelFor: (id) => id.isEmpty + ? context.l10n.notesCurrentText + : notesLocalDate( + context, + _summaries.firstWhere((s) => s.id == id).capturedAt, + ), + onSelected: (id) => unawaited(_select(id)), ), - if (_availability == null) - const LinearProgressIndicator() - else ...[ + if (_loading || _busy) const LinearProgressIndicator(), + if (_busy) + Semantics( + liveRegion: true, + child: Text(context.l10n.notesRecovering), + ), + if (_error != null) + Semantics( + liveRegion: true, + child: BusyMarkStatusBox( + message: _error!, + kind: BusyMarkStatusKind.error, + ), + ), + if (_availability case final status?) ...[ Text( - '${context.l10n.notesAvailableOffline}: ${_availability!.availableCount}/${_availability!.requiredCount}', + '${context.l10n.notesTextAvailable} · ${context.l10n.notesAttachments}: ${status.availableCount}/${status.requiredCount}', ), - for (final item in _availability!.missing) - Text('${context.l10n.notesIncomplete}: $item'), - for (final item in _availability!.external) + for (final item in status.missing) Text('${context.l10n.notesIncomplete}: $item'), + if (status.external.isNotEmpty) + Text(context.l10n.notesOfflineExternal), ], SizedBox( - height: 260, + height: MediaQuery.sizeOf(context).height * .3, child: SingleChildScrollView(child: SelectableText(_source)), ), ], - ); - } + ), + ); } Future showNotesExport( @@ -793,7 +737,6 @@ Future showNotesExport( WidgetRef ref, List ids, ) async { - final completedLabel = context.l10n.notesCompleted; final incompleteLabel = context.l10n.notesIncomplete; final parent = await getDirectoryPath( confirmButtonText: context.l10n.notesExport, @@ -828,6 +771,7 @@ Future showNotesExport( barrierDismissible: false, builder: (_) => NotesTransferProgressDialog( title: context.l10n.notesExport, + exporting: true, operation: (cancel, progress) async { final result = await NotesTransferService(repository).exportSnapshot( notes: captured, @@ -835,12 +779,14 @@ Future showNotesExport( cancellation: cancel, onProgress: progress, ); - return [ - result.complete ? completedLabel : incompleteLabel, - '${result.count}', - result.path, - ...result.omissions, - ]; + return NotesTransferResult( + phase: result.complete + ? NotesTransferPhase.completed + : NotesTransferPhase.partial, + created: result.count, + destination: result.path, + omissions: result.omissions, + ); }, ), ); @@ -856,9 +802,6 @@ Future showNotesImport( .workspace ?.nextcloudAccountId; if (accountId == null) return; - final savedLocally = context.l10n.nextcloudSavedLocally; - final alreadyImported = context.l10n.notesImportAlreadyImported; - final synced = context.l10n.nextcloudSynced; final source = folder ? await getDirectoryPath(confirmButtonText: context.l10n.notesImport) : (await openFile( @@ -872,11 +815,20 @@ Future showNotesImport( if (source == null || !context.mounted) return; final repository = await ref.read(nextcloudNotesRepositoryProvider.future); final service = NotesTransferService(repository); + if (!context.mounted) return; try { - var review = await service.review(source, accountId); + var review = await showBusyMarkModalDialog( + context, + barrierDismissible: false, + builder: (_) => NotesImportScanDialog( + scan: (cancel) => + service.review(source, accountId, cancellation: cancel), + ), + ); + if (review == null || !context.mounted) return; final pending = (await repository.store.importOperations( accountId, - )).where((o) => o['root'] == review.root).toList(); + )).where((o) => o['root'] == review!.root).toList(); if (!context.mounted) return; if (pending.isNotEmpty) { final choice = await showBusyMarkModalDialog( @@ -891,7 +843,7 @@ Future showNotesImport( } final confirmed = await showBusyMarkModalDialog( context, - builder: (_) => NotesImportReviewDialog(review: review), + builder: (_) => NotesImportReviewDialog(review: review!), ); if (confirmed != true || !context.mounted) return; await showBusyMarkModalDialog( @@ -901,7 +853,7 @@ Future showNotesImport( title: context.l10n.notesImport, operation: (cancel, progress) async { final results = await service.importReviewed( - review, + review!, accountId, cancellation: cancel, onProgress: progress, @@ -909,10 +861,27 @@ Future showNotesImport( ref .read(workspaceControllerProvider.notifier) .synchronizeImportedNotes(accountId); - return [ - for (final result in results) - '${result.item}: ${result.error ?? [if (result.alreadyImported) alreadyImported, repository.noteById(result.noteId ?? '')?.hasPendingChanges == false ? synced : savedLocally].join(' · ')}', - ]; + final total = review!.items.where((item) => item.selected).length; + final failures = results.where((item) => item.error != null).length; + final skipped = results.where((item) => item.alreadyImported).length; + final created = results + .where((item) => item.noteId != null && !item.alreadyImported) + .length; + final notStarted = total - results.length; + return NotesTransferResult( + phase: notStarted == 0 && failures == 0 + ? NotesTransferPhase.completed + : created > 0 || skipped > 0 + ? NotesTransferPhase.partial + : cancel.cancelled + ? NotesTransferPhase.cancelled + : NotesTransferPhase.failed, + created: created, + skipped: skipped, + failed: failures, + notStarted: notStarted, + items: results, + ); }, ), ); @@ -922,7 +891,7 @@ Future showNotesImport( context, builder: (context) => BusyMarkDialogShell( title: context.l10n.warning, - children: [Text(error.toString())], + children: [Text(localizeNotesFailure(context, error))], ), ); } @@ -944,18 +913,34 @@ class NotesImportOperationDialog extends StatelessWidget { ), ], children: [ - ListTile( - title: Text(context.l10n.notesImportNew), - onTap: () => Navigator.pop(context, ''), - ), - for (final operation in operations) - ListTile( - title: Text(context.l10n.notesImportResume), - subtitle: Text( - '${operation['root']}\n${DateTime.fromMillisecondsSinceEpoch((operation['created_at'] as int) * 1000).toLocal()}', + BusyMarkGroupedList( + filled: true, + children: [ + BusyMarkActionRow( + title: context.l10n.notesImportNew, + onTap: () => Navigator.pop(context, ''), ), - onTap: () => Navigator.pop(context, operation['id'] as String), - ), + for (final operation in operations) + BusyMarkActionRow( + title: context.l10n.notesImportResume, + subtitleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BusyMarkPathLabel(path: operation['root'] as String), + Text( + notesLocalDate( + context, + DateTime.fromMillisecondsSinceEpoch( + (operation['created_at'] as int) * 1000, + ), + ), + ), + ], + ), + onTap: () => Navigator.pop(context, operation['id'] as String), + ), + ], + ), ], ); } @@ -986,9 +971,20 @@ class _NotesImportReviewDialogState extends State { ), ], children: [ - for (final issue in widget.review.issues) Text(issue), + if (widget.review.issues.isNotEmpty) ...[ + BusyMarkStatusBox( + message: context.l10n.notesIncomplete, + kind: BusyMarkStatusKind.warning, + ), + ExpansionTile( + title: Text(context.l10n.notesTechnicalDetails), + children: [ + for (final issue in widget.review.issues) SelectableText(issue), + ], + ), + ], SizedBox( - height: 400, + height: (MediaQuery.sizeOf(context).height * .5).clamp(0.0, 400.0), child: ListView.builder( itemCount: widget.review.items.length, itemBuilder: (context, index) { @@ -996,33 +992,55 @@ class _NotesImportReviewDialogState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - CheckboxListTile( - value: item.selected, - onChanged: item.alreadyImported - ? null - : (v) => setState(() => item.selected = v ?? false), - title: Text(item.title), - subtitle: Text( - item.alreadyImported - ? context.l10n.notesImportAlreadyImported - : item.collision - ? context.l10n.notesImportDistinct - : item.path, - ), - ), - TextFormField( - initialValue: item.category, - readOnly: item.alreadyImported, - decoration: InputDecoration( - labelText: context.l10n.syntaxReferenceCategory, - ), - onChanged: (v) => item.category = v, + BusyMarkGroupedList( + filled: true, + children: [ + MergeSemantics( + child: BusyMarkActionRow( + title: item.title, + subtitle: item.alreadyImported + ? context.l10n.notesImportAlreadyImported + : item.collision + ? context.l10n.notesImportDistinct + : item.path, + enabled: !item.alreadyImported, + leading: ExcludeFocus( + child: BusyMarkCheckbox( + value: item.selected, + onChanged: item.alreadyImported + ? null + : (value) => setState( + () => item.selected = value ?? false, + ), + ), + ), + onTap: item.alreadyImported + ? null + : () => setState( + () => item.selected = !item.selected, + ), + ), + ), + BusyMarkGroupedTextEntry( + label: context.l10n.syntaxReferenceCategory, + initialValue: item.category, + readOnly: item.alreadyImported, + onChanged: (value) => item.category = value, + ), + ], ), Text('${context.l10n.notesAttachments}: ${item.media.length}'), for (final reference in item.media.keys) Text(reference, style: Theme.of(context).textTheme.bodySmall), - for (final issue in item.issues) - Text('${context.l10n.notesIncomplete}: $issue'), + if (item.issues.isNotEmpty) ...[ + Text(context.l10n.notesIncomplete), + ExpansionTile( + title: Text(context.l10n.notesTechnicalDetails), + children: [ + for (final issue in item.issues) SelectableText(issue), + ], + ), + ], const Divider(), ], ); @@ -1033,14 +1051,57 @@ class _NotesImportReviewDialogState extends State { ); } +enum NotesTransferPhase { + scanning, + running, + cancelling, + cancelled, + completed, + partial, + failed, +} + +class NotesTransferResult { + const NotesTransferResult({ + required this.phase, + this.created = 0, + this.skipped = 0, + this.failed = 0, + this.notStarted = 0, + this.destination, + this.omissions = const [], + this.items = const [], + this.failure, + }); + final NotesTransferPhase phase; + final int created, skipped, failed, notStarted; + final String? destination; + final List omissions; + final List items; + final Object? failure; +} + +String _transferLabel(BuildContext context, NotesTransferPhase phase) => + switch (phase) { + NotesTransferPhase.scanning => context.l10n.notesScanning, + NotesTransferPhase.running => context.l10n.notesTransferring, + NotesTransferPhase.cancelling => context.l10n.notesCancelling, + NotesTransferPhase.cancelled => context.l10n.notesCancelled, + NotesTransferPhase.completed => context.l10n.notesCompleted, + NotesTransferPhase.partial => context.l10n.notesPartial, + NotesTransferPhase.failed => context.l10n.notesTransferFailed, + }; + class NotesTransferProgressDialog extends StatefulWidget { const NotesTransferProgressDialog({ super.key, required this.title, required this.operation, + this.exporting = false, }); + final bool exporting; final String title; - final Future> Function( + final Future Function( NotesTransferCancellation, void Function(int, int), ) @@ -1054,7 +1115,8 @@ class _NotesTransferProgressDialogState extends State { final _cancel = NotesTransferCancellation(); int _completed = 0, _total = 0; - List? _result; + NotesTransferPhase _phase = NotesTransferPhase.running; + NotesTransferResult? _result; @override void initState() { super.initState(); @@ -1062,7 +1124,7 @@ class _NotesTransferProgressDialogState } Future _run() async { - List result; + NotesTransferResult result; try { result = await widget.operation(_cancel, (a, b) { if (mounted) { @@ -1072,10 +1134,188 @@ class _NotesTransferProgressDialogState }); } }); + } on NotesTransferCancelled { + result = NotesTransferResult( + phase: NotesTransferPhase.cancelled, + notStarted: _total - _completed, + ); } on Object catch (error) { - result = [error.toString()]; + result = NotesTransferResult( + phase: !widget.exporting && _completed > 0 + ? NotesTransferPhase.partial + : NotesTransferPhase.failed, + failed: 1, + notStarted: (_total - _completed).clamp(0, _total), + failure: error, + ); + } + if (mounted) { + setState(() { + _result = result; + _phase = result.phase; + }); + } + } + + @override + void dispose() { + _cancel.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final result = _result; + return PopScope( + canPop: result != null, + child: BusyMarkDialogShell( + title: widget.title, + closable: result != null, + actions: [ + BusyMarkDialogButton( + label: result == null ? context.l10n.cancel : context.l10n.close, + onPressed: _phase == NotesTransferPhase.cancelling + ? null + : () { + if (result == null) { + setState(() { + _cancel.cancel(); + _phase = NotesTransferPhase.cancelling; + }); + } else { + Navigator.pop(context); + } + }, + ), + ], + children: [ + Semantics( + liveRegion: true, + child: Text( + widget.exporting && + result?.destination != null && + _phase == NotesTransferPhase.partial + ? context.l10n.notesExportPublishedWithOmissions + : _transferLabel(context, _phase), + ), + ), + if (result == null) ...[ + if (_total > 0) Text('$_completed/$_total'), + LinearProgressIndicator( + value: _total > 0 ? _completed / _total : null, + ), + ] else ...[ + if (result.destination != null) SelectableText(result.destination!), + if (widget.exporting && result.destination == null) + Text(context.l10n.notesExportNotPublished), + if (!widget.exporting) + Text( + '${context.l10n.notesCreated}: ${result.created} · ${context.l10n.notesSkipped}: ${result.skipped} · ${context.l10n.notesFailed}: ${result.failed} · ${context.l10n.notesNotStarted}: ${result.notStarted}', + ), + if (!widget.exporting && + (result.phase == NotesTransferPhase.partial || + result.phase == NotesTransferPhase.cancelled)) + Text(context.l10n.notesImportPartialHelp), + if (result.failure != null) ...[ + Text( + localizeNotesTransferFailure( + context, + widget.exporting && + result.failure is FileSystemException && + result.failure is! NotesTransferException + ? NotesTransferFailureReason.destinationUnavailable + : notesTransferFailureReason(result.failure), + exporting: widget.exporting, + ), + ), + ExpansionTile( + title: Text(context.l10n.notesTechnicalDetails), + children: [SelectableText('${result.failure}')], + ), + ], + SizedBox( + height: MediaQuery.sizeOf(context).height * .3, + child: ListView( + children: [ + for (final item in result.items) + BusyMarkActionRow( + title: item.item, + subtitleWidget: item.error != null + ? Text( + localizeNotesTransferFailure( + context, + item.reason ?? + notesTransferFailureReason(item.cause), + ), + ) + : null, + subtitle: item.error != null + ? null + : item.alreadyImported + ? context.l10n.notesImportAlreadyImported + : context.l10n.nextcloudSavedLocally, + ), + for (final item in result.items.where((i) => i.error != null)) + ExpansionTile( + title: Text( + '${item.item} · ${context.l10n.notesTechnicalDetails}', + ), + children: [SelectableText(item.error!)], + ), + for (final omission in result.omissions) + Text('${context.l10n.notesIncomplete}: $omission'), + ], + ), + ), + ], + ], + ), + ); + } +} + +/// The review scan shares the service cancellation boundary. A cancelled scan +/// can never return a review for a later route or selection. +class NotesImportScanDialog extends StatefulWidget { + const NotesImportScanDialog({super.key, required this.scan}); + final Future Function(NotesTransferCancellation) scan; + @override + State createState() => _NotesImportScanDialogState(); +} + +class _NotesImportScanDialogState extends State { + final _cancel = NotesTransferCancellation(); + NotesTransferPhase _phase = NotesTransferPhase.scanning; + bool _settled = false; + Object? _failure; + @override + void initState() { + super.initState(); + unawaited(_run()); + } + + Future _run() async { + try { + final review = await widget.scan(_cancel); + if (!mounted) return; + setState(() { + _settled = true; + _phase = _cancel.cancelled + ? NotesTransferPhase.cancelled + : NotesTransferPhase.completed; + }); + if (!_cancel.cancelled) Navigator.pop(context, review); + } on Object catch (error) { + if (mounted) { + setState(() { + _settled = true; + _failure = error is NotesTransferCancelled ? null : error; + _phase = error is NotesTransferCancelled + ? NotesTransferPhase.cancelled + : NotesTransferPhase.failed; + }); + } } - if (mounted) setState(() => _result = result); } @override @@ -1086,34 +1326,45 @@ class _NotesTransferProgressDialogState @override Widget build(BuildContext context) => PopScope( - canPop: _result != null, + canPop: _settled, child: BusyMarkDialogShell( - title: widget.title, - closable: _result != null, + title: context.l10n.notesImport, + closable: _settled, actions: [ BusyMarkDialogButton( - label: _result == null ? context.l10n.cancel : context.l10n.close, - onPressed: () { - if (_result == null) { - setState(_cancel.cancel); - } else { - Navigator.pop(context); - } - }, + label: _settled ? context.l10n.close : context.l10n.cancel, + onPressed: _phase == NotesTransferPhase.cancelling + ? null + : () { + if (_settled) { + Navigator.pop(context); + } else { + setState(() { + _cancel.cancel(); + _phase = NotesTransferPhase.cancelling; + }); + } + }, ), ], children: [ - Text('$_completed/$_total'), - if (_result == null) - const LinearProgressIndicator() - else - SizedBox( - height: 300, - child: ListView( - children: [for (final line in _result!) Text(line)], + Semantics( + liveRegion: true, + child: Text(_transferLabel(context, _phase)), + ), + if (!_settled) const LinearProgressIndicator(), + if (_failure != null) ...[ + Text( + localizeNotesTransferFailure( + context, + notesTransferFailureReason(_failure), ), ), - if (_cancel.cancelled) Text(context.l10n.notesIncomplete), + ExpansionTile( + title: Text(context.l10n.notesTechnicalDetails), + children: [SelectableText('$_failure')], + ), + ], ], ), ); diff --git a/lib/src/platform/header_bar_configuration.dart b/lib/src/platform/header_bar_configuration.dart index 0ad1759a..a347b09d 100644 --- a/lib/src/platform/header_bar_configuration.dart +++ b/lib/src/platform/header_bar_configuration.dart @@ -41,9 +41,6 @@ class HeaderBarLabels { required this.settings, required this.settingsShortcut, required this.settingsGtkAccelerator, - required this.keyboardShortcuts, - required this.keyboardShortcutsShortcut, - required this.keyboardShortcutsGtkAccelerator, required this.syntaxReference, required this.syntaxReferenceShortcut, required this.syntaxReferenceGtkAccelerator, @@ -82,9 +79,6 @@ class HeaderBarLabels { final String settings; final String settingsShortcut; final String settingsGtkAccelerator; - final String keyboardShortcuts; - final String keyboardShortcutsShortcut; - final String keyboardShortcutsGtkAccelerator; final String syntaxReference; final String syntaxReferenceShortcut; final String syntaxReferenceGtkAccelerator; @@ -123,9 +117,6 @@ class HeaderBarLabels { 'settings': settings, 'settingsShortcut': settingsShortcut, 'settingsGtkAccelerator': settingsGtkAccelerator, - 'keyboardShortcuts': keyboardShortcuts, - 'keyboardShortcutsShortcut': keyboardShortcutsShortcut, - 'keyboardShortcutsGtkAccelerator': keyboardShortcutsGtkAccelerator, 'syntaxReference': syntaxReference, 'syntaxReferenceShortcut': syntaxReferenceShortcut, 'syntaxReferenceGtkAccelerator': syntaxReferenceGtkAccelerator, diff --git a/lib/src/platform/linux_header_bar_service.dart b/lib/src/platform/linux_header_bar_service.dart index 98a4d12d..d9459a42 100644 --- a/lib/src/platform/linux_header_bar_service.dart +++ b/lib/src/platform/linux_header_bar_service.dart @@ -20,7 +20,6 @@ enum HeaderBarAction { fullScreen, menu, settings, - keyboardShortcuts, syntaxReference, reportIssue, aboutBusyMark, @@ -397,7 +396,6 @@ class LinuxHeaderBarService extends ChangeNotifier { 'fullScreen' => HeaderBarAction.fullScreen, 'menu' => HeaderBarAction.menu, 'settings' => HeaderBarAction.settings, - 'keyboardShortcuts' => HeaderBarAction.keyboardShortcuts, 'syntaxReference' => HeaderBarAction.syntaxReference, 'reportIssue' => HeaderBarAction.reportIssue, 'aboutBusyMark' => HeaderBarAction.aboutBusyMark, diff --git a/lib/src/platform/native_menu_service.dart b/lib/src/platform/native_menu_service.dart index fae6e370..5b999d36 100644 --- a/lib/src/platform/native_menu_service.dart +++ b/lib/src/platform/native_menu_service.dart @@ -25,6 +25,7 @@ final class NativeMenuEntry { this.iconName, this.iconColorArgb, this.shortcut, + this.gtkAccelerator, this.enabled = true, this.checkable = false, this.selected = false, @@ -39,6 +40,7 @@ final class NativeMenuEntry { }) : iconName = null, iconColorArgb = null, shortcut = null, + gtkAccelerator = null, checkable = false, selected = false, mutuallyExclusive = false, @@ -49,6 +51,7 @@ final class NativeMenuEntry { iconName = null, iconColorArgb = null, shortcut = null, + gtkAccelerator = null, enabled = false, checkable = false, selected = false, @@ -60,6 +63,7 @@ final class NativeMenuEntry { final String? iconName; final int? iconColorArgb; final String? shortcut; + final String? gtkAccelerator; final bool enabled; final bool checkable; final bool selected; @@ -73,6 +77,8 @@ final class NativeMenuEntry { if (iconName != null && iconName!.isNotEmpty) 'icon': iconName!, if (iconColorArgb != null) 'iconColor': iconColorArgb!, if (shortcut != null && shortcut!.isNotEmpty) 'shortcut': shortcut!, + if (gtkAccelerator != null && gtkAccelerator!.isNotEmpty) + 'gtkAccelerator': gtkAccelerator!, 'enabled': enabled, 'checkable': checkable, 'selected': selected, diff --git a/lib/src/platform/user_directories.dart b/lib/src/platform/user_directories.dart new file mode 100644 index 00000000..1ec41a53 --- /dev/null +++ b/lib/src/platform/user_directories.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +/// The desktop's actual home, including the original home in a Snap sandbox. +String? get userHomeDirectory { + for (final key in ['SNAP_REAL_HOME', 'HOME', 'USERPROFILE']) { + final value = Platform.environment[key]; + if (value != null && value.isNotEmpty && p.isAbsolute(value)) { + return p.normalize(value); + } + } + return null; +} diff --git a/lib/src/search/search_action_availability.dart b/lib/src/search/search_action_availability.dart new file mode 100644 index 00000000..41d5928a --- /dev/null +++ b/lib/src/search/search_action_availability.dart @@ -0,0 +1,42 @@ +import '../app/command_registry.dart'; +import '../workspace/workspace_model.dart'; +import 'search_session.dart'; + +bool searchActionEnabled( + BusyMarkCommandRegistry registry, + BusyMarkCommand command, + SearchSessionController controller, + WorkspaceState workspace, +) { + final target = controller.commandTarget; + if (registry.requiresDocumentTarget(command) && + (!controller.workspaceVisible || + target?.bufferId == null || + target?.isCurrent(workspace) != true)) { + return false; + } + final activeNotesAction = + controller.workspaceVisible && + (workspace.workspace?.isRemote == true || + controller.actionAccountId != null) && + { + BusyMarkCommandIds.newNote, + BusyMarkCommandIds.browseNotes, + }.contains(command.id); + if ((registry.requiresWorkspaceTarget(command) || activeNotesAction) && + (!controller.workspaceVisible || + target?.workspaceId == null || + target?.workspaceId != workspace.workspace?.id)) { + return false; + } + if (registry.isContextual(command)) { + return controller.workspaceVisible && + target?.isCurrent(workspace) == true && + registry.canExecuteInContext(command.id, target!.context); + } + if (!controller.workspaceVisible && + command.scope != BusyMarkCommandScope.application) { + return false; + } + return command.canExecute; +} diff --git a/lib/src/search/search_activation.dart b/lib/src/search/search_activation.dart new file mode 100644 index 00000000..4b2ff3a8 --- /dev/null +++ b/lib/src/search/search_activation.dart @@ -0,0 +1,357 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart' as p; + +import '../app/app_settings.dart'; +import '../app/busymark_toast.dart'; +import '../app/command_registry.dart'; +import '../app/localization.dart'; +import '../local_history/local_history_controller.dart'; +import '../local_history/local_history_models.dart'; +import '../nextcloud_notes/application/nextcloud_connection.dart'; +import '../nextcloud_notes/application/notes_navigation.dart'; +import '../nextcloud_notes/domain/notes_search.dart'; +import '../nextcloud_notes/presentation/notes_workspace_ui.dart'; +import '../workspace/document_navigation.dart'; +import '../workspace/outline_navigation.dart'; +import '../workspace/recent_navigation.dart'; +import '../workspace/workspace_controller.dart'; +import 'search_session.dart'; +import 'search_action_availability.dart'; + +bool actionEnabled(WidgetRef ref, ActionSearchResult result) { + final registry = BusyMarkCommandRegistryScope.read(ref.context); + return registry != null && + searchActionEnabled( + registry, + result.command, + ref.read(searchSessionProvider.notifier), + ref.read(workspaceControllerProvider), + ); +} + +Future activateSelectedSearchResult( + BuildContext context, + WidgetRef ref, +) async { + final controller = ref.read(searchSessionProvider.notifier); + if (!await controller.submit() || !context.mounted) return; + final state = ref.read(searchSessionProvider); + if (state.results.isEmpty) return; + await activateSearchResult(context, ref, state.results[state.selected]); +} + +final _searchActivationProvider = Provider((ref) => _SearchActivation()); + +class _SearchActivation { + bool busy = false; +} + +Future activateSearchResult( + BuildContext context, + WidgetRef ref, + SidebarSearchResult result, { + bool source = false, +}) async { + final guard = ref.read(_searchActivationProvider); + if (guard.busy) return; + guard.busy = true; + final session = ref.read(searchSessionProvider.notifier); + final snapshot = ref.read(searchSessionProvider); + final workspace = ref.read(workspaceControllerProvider).workspace; + final controller = ref.read(workspaceControllerProvider.notifier); + void unavailable() { + if (context.mounted) { + BusyMarkToastOverlay.show( + context, + message: context.l10n.commandUnavailableInContext, + ); + } + } + + try { + if (!snapshot.active || + !snapshot.results.any((r) => identical(r, result))) { + unavailable(); + return; + } + session.select(snapshot.results.indexWhere((r) => identical(r, result))); + bool currentRequest() => + ref.context.mounted && + session.workspaceVisible && + ref.read(workspaceControllerProvider).workspace?.id == workspace?.id && + ref.read(searchSessionProvider).active && + ref.read(searchSessionProvider).requestRevision == + snapshot.requestRevision && + ref.read(searchSessionProvider).focusRequest == snapshot.focusRequest && + ref.read(searchSessionProvider).query == snapshot.query && + ref.read(searchSessionProvider).category == snapshot.category && + ref.read(searchSessionProvider).recovery == snapshot.recovery && + ref.read(searchSessionProvider).historyDocumentId == + snapshot.historyDocumentId; + switch (result) { + case OutlineSearchResult(): + final live = ref.read(workspaceControllerProvider); + final buffer = live.activeBuffer; + if (!currentRequest() || + buffer?.id != result.bufferId || + buffer?.revision != result.revision || + notesSearchDigest(buffer!.text) != result.digest || + !navigateOutlineHeading( + ref, + workspaceId: result.workspaceId, + bufferId: result.bufferId, + source: buffer.text, + heading: result.heading, + )) { + unavailable(); + } + + case ActionSearchResult(:final command): + if (!actionEnabled(ref, result)) { + unavailable(); + return; + } + final registry = BusyMarkCommandRegistryScope.read(context)!; + final success = await registry.executeInContext( + command.id, + session.commandTarget?.context, + ); + if (success && + ref.context.mounted && + ref.read(searchSessionProvider).focusRequest == + snapshot.focusRequest && + ref.read(searchSessionProvider).category == snapshot.category) { + session.close(); + } + case RecentSearchResult(:final resource): + await openRecentResource(context, ref, resource); + case DocumentSearchResult( + :final workspaceId, + :final accountId, + :final document, + ): + if (!session.workspaceVisible || workspace?.id != workspaceId) { + unavailable(); + return; + } + if (accountId != null) { + final repo = await ref.read(nextcloudNotesRepositoryProvider.future); + final note = repo.noteById(document.localId!); + if (!context.mounted || + !currentRequest() || + ref.read(workspaceControllerProvider).workspace?.id != + workspaceId || + note?.accountId != accountId || + isNotesRecovery(note!) || + repo.isCreationCandidateBinding(note.localId)) { + unavailable(); + return; + } + if (await controller.openNextcloudNote( + document.localId!, + acceptNavigation: currentRequest, + )) { + if (!currentRequest()) return; + revealOpenedDocument(ref); + session.close(restoreFocus: false); + } + } else { + if (!workspace!.files.any( + (f) => f.absolutePath == document.filePath, + )) { + unavailable(); + return; + } + if (await controller.openActiveFile( + document.filePath!, + acceptNavigation: currentRequest, + )) { + if (!currentRequest()) return; + revealOpenedDocument(ref); + session.close(restoreFocus: false); + } + } + case TopicSearchResult(:final workspaceId, :final path): + if (!session.workspaceVisible || + workspace?.id != workspaceId || + ![ + for (final module + in workspace!.writersideProject?.modules ?? + [ + if (workspace.writersideModule != null) + workspace.writersideModule!, + ]) + ...module.topics, + ].any((t) => p.equals(t.filePath, path))) { + unavailable(); + return; + } + if (await controller.openActiveFile( + path, + acceptNavigation: currentRequest, + )) { + if (!currentRequest()) return; + revealOpenedDocument(ref); + session.close(restoreFocus: false); + } + case HistorySearchResult(:final match): + if (!currentRequest()) return; + final history = ref.read(localHistoryControllerProvider.notifier); + final retained = ref.read(localHistoryControllerProvider).snapshot; + final document = retained.documents + .where((d) => d.id == match.document.id) + .firstOrNull; + if (document == null) { + unavailable(); + return; + } + if (match is LocalHistoryRevisionMatch && + !retained.revisions.any( + (r) => + r.id == match.revision.id && + r.documentId == document.id && + r.checksum == match.revision.checksum, + )) { + unavailable(); + return; + } + history.inspectRetainedDocument(document.id); + if (match is LocalHistoryRevisionMatch) { + await history.selectRevision( + match.revision.id, + acceptSelection: currentRequest, + ); + } else { + // Metadata matches show the unfiltered retained timeline. They never + // open a live path, bind a buffer or create a replacement document. + session.close(restoreFocus: false); + ref.read(localHistoryOpenRequestProvider.notifier).request(); + } + case TextSearchResult(:final workspaceId, :final match): + if (!session.workspaceVisible || workspace?.id != workspaceId) { + unavailable(); + return; + } + // An open-buffer ID is only a hint: clean tabs may close without + // invalidating a workspace result. Resolve its durable file identity + // again, including a tab reopened since this result was produced. + final live = ref.read(workspaceControllerProvider); + final destination = match.filePath != null + ? live.bufferForPath(match.filePath!) + : live.documentBuffers + .where((buffer) => buffer.id == match.bufferId) + .firstOrNull; + bool validSource(String text) => + match.startOffset >= 0 && + match.endOffset > match.startOffset && + match.endOffset <= text.length && + notesSearchDigest(text) == match.digest; + if (destination != null) { + if (!validSource(destination.text)) { + unavailable(); + return; + } + if (live.activeBufferId != destination.id && + !await controller.activateDocumentBuffer( + destination.id, + acceptNavigation: currentRequest, + )) { + return; + } + } else if (match.filePath != null) { + if (!await controller.openActiveFile( + match.filePath!, + acceptNavigation: currentRequest, + )) { + if (currentRequest()) unavailable(); + return; + } + } else { + unavailable(); + return; + } + if (!context.mounted || + !currentRequest() || + ref.read(workspaceControllerProvider).workspace?.id != + workspaceId) { + return; + } + final buffer = ref.read(workspaceControllerProvider).activeBuffer; + if (buffer == null || + (match.filePath != null && buffer.filePath != match.filePath) || + !validSource(buffer.text)) { + unavailable(); + return; + } + revealOpenedDocument(ref); + ref + .read(notesMatchNavigationProvider.notifier) + .navigate( + buffer.remoteNote?.localId ?? buffer.filePath ?? buffer.id, + match.startOffset, + match.endOffset, + match.digest, + accountId: buffer.remoteNote?.accountId, + ); + case NoteSearchResult(:final accountId, :final hit, :final recovery): + final repo = await ref.read(nextcloudNotesRepositoryProvider.future); + final note = repo.noteById(hit.localId); + if (!context.mounted || + !currentRequest() || + ref + .read(workspaceControllerProvider) + .workspace + ?.nextcloudAccountId != + accountId || + note?.accountId != accountId || + isNotesRecovery(note!) != recovery) { + unavailable(); + return; + } + if (recovery) { + session.close(restoreFocus: false); + await showNotesRecovery(context, ref, note); + return; + } + if (!await controller.openNextcloudNote( + hit.localId, + acceptNavigation: currentRequest, + ) || + !context.mounted || + !currentRequest()) { + return; + } + final buffer = ref.read(workspaceControllerProvider).activeBuffer; + if (buffer?.remoteNote?.accountId != accountId || + buffer?.remoteNote?.localId != hit.localId || + notesSearchDigest(buffer!.text) != hit.digest) { + unavailable(); + return; + } + if (source) { + controller.updateActiveEditorMode(DocumentViewModePreference.source); + await ref + .read(appSettingsControllerProvider.notifier) + .setDocumentViewMode(DocumentViewModePreference.source); + if (!context.mounted || !currentRequest()) return; + } + revealOpenedDocument(ref); + if (hit.start != null && hit.end != null) { + ref + .read(notesMatchNavigationProvider.notifier) + .navigate( + hit.localId, + hit.start!, + hit.end!, + hit.digest, + accountId: accountId, + ); + } + } + } finally { + guard.busy = false; + } +} diff --git a/lib/src/search/search_replace_service.dart b/lib/src/search/search_replace_service.dart index 1c1b0cf7..dd5a2184 100644 --- a/lib/src/search/search_replace_service.dart +++ b/lib/src/search/search_replace_service.dart @@ -33,18 +33,14 @@ class TextReplacementPreview { required this.options, required this.replacement, required this.matches, - this.invalidRegex = false, this.truncated = false, - this.hasZeroLengthMatches = false, }); final String source; final SourceSearchOptions options; final String replacement; final List matches; - final bool invalidRegex; final bool truncated; - final bool hasZeroLengthMatches; String apply({Set? selectedMatchIds}) { if (matches.isEmpty) { @@ -79,8 +75,6 @@ enum WorkspaceReplacementSourceKind { disk, dirtyBuffer } enum WorkspaceReplacementIssueKind { remoteWorkspaceUnsupported, cancelled, - invalidRegex, - zeroLengthMatches, oversized, unreadable, invalidUtf8, @@ -147,8 +141,7 @@ class WorkspaceReplacementPreview { bool get isComplete => !issues.any( (issue) => issue.kind == WorkspaceReplacementIssueKind.truncated || - issue.kind == WorkspaceReplacementIssueKind.cancelled || - issue.kind == WorkspaceReplacementIssueKind.invalidRegex, + issue.kind == WorkspaceReplacementIssueKind.cancelled, ); } @@ -186,44 +179,16 @@ class SearchReplacementService { maximumMatches: matchLimit + 1, stopAfterMaximumMatches: true, ); - if (search.invalidRegex) { - return TextReplacementPreview( - source: source, - options: options, - replacement: replacement, - matches: const [], - invalidRegex: true, - ); - } - RegExp? expression; - if (options.regex) { - expression = RegExp( - options.query, - caseSensitive: options.caseSensitive, - multiLine: true, - unicode: true, - ); - } final matches = []; for (final (index, match) in search.matches.take(matchLimit).indexed) { final original = source.substring(match.fullStart, match.fullEnd); - var renderedReplacement = replacement; - if (expression != null) { - final regexMatch = expression.matchAsPrefix(source, match.fullStart); - if (regexMatch is RegExpMatch && regexMatch.end == match.fullEnd) { - renderedReplacement = _expandRegexReplacement( - replacement, - regexMatch, - ); - } - } matches.add( TextReplacementMatch( id: '$idPrefix:$index:${match.fullStart}:${match.fullEnd}', start: match.fullStart, end: match.fullEnd, original: original, - replacement: renderedReplacement, + replacement: replacement, ), ); } @@ -233,7 +198,6 @@ class SearchReplacementService { replacement: replacement, matches: List.unmodifiable(matches), truncated: search.totalMatchCount > matchLimit, - hasZeroLengthMatches: search.hasZeroLengthMatches, ); } @@ -254,48 +218,18 @@ class SearchReplacementService { matches: const [], ); } - var renderedReplacement = replacement; - if (options.regex) { - try { - final expression = RegExp( - options.query, - caseSensitive: options.caseSensitive, - multiLine: true, - unicode: true, - ); - final match = expression.matchAsPrefix(source, start); - if (match is! RegExpMatch || match.end != end) { - return TextReplacementPreview( - source: source, - options: options, - replacement: replacement, - matches: const [], - ); - } - renderedReplacement = _expandRegexReplacement(replacement, match); - } on FormatException { - return TextReplacementPreview( - source: source, - options: options, - replacement: replacement, - matches: const [], - invalidRegex: true, - ); - } - } else { - final match = RegExp( - RegExp.escape(options.query), - caseSensitive: options.caseSensitive, - unicode: true, - ).matchAsPrefix(source, start); - if (match == null || match.end != end) { - return TextReplacementPreview( - source: source, - options: options, - replacement: replacement, - matches: const [], - ); - } + final match = RegExp( + RegExp.escape(options.query), + caseSensitive: false, + unicode: true, + ).matchAsPrefix(source, start); + if (match == null || match.end != end) { + return TextReplacementPreview( + source: source, + options: options, + replacement: replacement, + matches: const [], + ); } return TextReplacementPreview( source: source, @@ -307,7 +241,7 @@ class SearchReplacementService { start: start, end: end, original: source.substring(start, end), - replacement: renderedReplacement, + replacement: replacement, ), ], ); @@ -431,23 +365,6 @@ class SearchReplacementService { ); break; } - if (preview.invalidRegex) { - issues.add( - WorkspaceReplacementIssue( - kind: WorkspaceReplacementIssueKind.invalidRegex, - filePath: path, - ), - ); - break; - } - if (preview.hasZeroLengthMatches) { - issues.add( - WorkspaceReplacementIssue( - kind: WorkspaceReplacementIssueKind.zeroLengthMatches, - filePath: path, - ), - ); - } if (preview.matches.isEmpty && !preview.truncated) { continue; } @@ -700,62 +617,6 @@ class SearchReplacementService { issues: List.unmodifiable(issues), ); } - - String _expandRegexReplacement(String replacement, RegExpMatch match) { - final output = StringBuffer(); - for (var index = 0; index < replacement.length; index++) { - final character = replacement[index]; - if (character != r'$' || index + 1 >= replacement.length) { - output.write(character); - continue; - } - final next = replacement[index + 1]; - if (next == r'$') { - output.write(r'$'); - index++; - continue; - } - if (next == '&') { - output.write(match.group(0) ?? ''); - index++; - continue; - } - if (next == '{') { - final close = replacement.indexOf('}', index + 2); - if (close > index + 2) { - final name = replacement.substring(index + 2, close); - try { - output.write(match.namedGroup(name) ?? ''); - index = close; - continue; - } on ArgumentError { - // Preserve an unknown capture reference literally. - } - } - } - if (_isDigit(next)) { - var end = index + 1; - while (end < replacement.length && - end < index + 3 && - _isDigit(replacement[end])) { - end++; - } - final group = int.parse(replacement.substring(index + 1, end)); - if (group <= match.groupCount) { - output.write(match.group(group) ?? ''); - index = end - 1; - continue; - } - } - output.write(r'$'); - } - return output.toString(); - } - - bool _isDigit(String value) { - final unit = value.codeUnitAt(0); - return unit >= 48 && unit <= 57; - } } class WorkspaceReplacementCancellation { @@ -813,9 +674,6 @@ class SearchReplacementWorker { final request = { 'source': source, 'query': options.query, - 'caseSensitive': options.caseSensitive, - 'wholeWord': options.wholeWord, - 'regex': options.regex, 'replacement': replacement, 'maximumMatches': maximumMatches, 'targetStart': targetStart, @@ -869,12 +727,7 @@ class SearchReplacementWorker { void _replacementWorkerMain(List payload) { final sendPort = payload[0] as SendPort; final request = payload[1] as Map; - final options = SourceSearchOptions( - query: request['query']! as String, - caseSensitive: request['caseSensitive']! as bool, - wholeWord: request['wholeWord']! as bool, - regex: request['regex']! as bool, - ); + final options = SourceSearchOptions(query: request['query']! as String); final service = SearchReplacementService( maximumMatches: request['maximumMatches']! as int, ); @@ -896,9 +749,7 @@ void _replacementWorkerMain(List payload) { replacement: replacement, ); sendPort.send({ - 'invalidRegex': preview.invalidRegex, 'truncated': preview.truncated, - 'hasZeroLengthMatches': preview.hasZeroLengthMatches, 'matches': [ for (final match in preview.matches) [ @@ -933,9 +784,7 @@ TextReplacementPreview _decodeReplacementPreview( replacement: item[4]! as String, ), ]), - invalidRegex: payload['invalidRegex']! as bool, truncated: payload['truncated']! as bool, - hasZeroLengthMatches: payload['hasZeroLengthMatches']! as bool, ); } diff --git a/lib/src/search/search_result_presentation.dart b/lib/src/search/search_result_presentation.dart new file mode 100644 index 00000000..0defdd72 --- /dev/null +++ b/lib/src/search/search_result_presentation.dart @@ -0,0 +1,147 @@ +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart' show DateFormat; +import 'package:path/path.dart' as p; + +import '../app/localization.dart'; +import '../app/app_settings.dart'; +import '../local_history/local_history_models.dart'; +import '../workspace/recent_presentation.dart'; +import 'search_result_preview.dart'; +import 'search_session.dart'; + +/// Display projections never change the metadata used for activation or matching. +class SearchResultPresentation { + const SearchResultPresentation( + this.title, + this.subtitle, { + this.shortcut, + this.details, + }); + final String title, subtitle; + final String? shortcut, details; +} + +SearchResultPresentation presentSearchResult( + BuildContext context, + SidebarSearchResult result, { + String? Function(String accountId)? accountLabel, + Iterable recentResources = const [], + String? workspaceRoot, +}) { + String account(String id) => + accountLabel?.call(id) ?? context.l10n.searchAccountUnavailable; + String location(String path, String title) { + if (p.basename(path) == title || + p.basenameWithoutExtension(path) == title) { + final parent = p.dirname(path); + return parent == '.' ? '' : parent; + } + return path; + } + + return switch (result) { + TextSearchResult(:final match) => SearchResultPresentation( + searchResultTitle(context, match.lineText ?? ''), + context.l10n.searchResultLine(match.relativePath, match.line), + ), + NoteSearchResult(:final hit, :final accountId) => SearchResultPresentation( + hit.snippet.isEmpty ? hit.title : hit.snippet, + [ + hit.title, + if (hit.category.isNotEmpty) hit.category, + account(accountId), + ].join(' · '), + ), + DocumentSearchResult(:final document, :final accountId) => + SearchResultPresentation( + accountId == null ? p.basename(document.path) : document.title, + accountId == null + ? location(document.path, p.basename(document.path)) + : [ + if (document.path.isNotEmpty) document.path, + account(accountId), + ].join(' · '), + details: accountId == null ? document.filePath ?? document.path : null, + ), + TopicSearchResult( + :final title, + :final module, + :final topicId, + :final relativePath, + ) => + SearchResultPresentation( + title, + [ + module, + if (topicId != p.basenameWithoutExtension(relativePath)) topicId, + location(relativePath, title), + ].where((s) => s.isNotEmpty).toSet().join(' · '), + details: relativePath, + ), + RecentSearchResult(:final resource) => SearchResultPresentation( + recentResourceTitle(context, resource), + recentResourceLocation( + context, + resource, + recentResources: recentResources, + workspaceRoot: workspaceRoot, + accountLabel: resource.accountId == null + ? null + : accountLabel?.call(resource.accountId!), + ), + details: resource.accountId == null + ? resource.path + : recentResourceLocation( + context, + resource, + accountLabel: accountLabel?.call(resource.accountId!), + ), + ), + OutlineSearchResult( + :final heading, + :final ancestors, + :final documentName, + ) => + SearchResultPresentation( + heading.text, + [ + context.l10n.headingLevelAbbreviation(heading.level), + if (ancestors.isNotEmpty) ancestors.join(' › ') else documentName, + ].join(' · '), + ), + HistorySearchResult(:final match) => SearchResultPresentation( + match.document.displayName, + [ + if (match is LocalHistoryRevisionMatch) + DateFormat.yMd( + Localizations.localeOf(context).toLanguageTag(), + ).add_Hms().format(match.revision.capturedAt.toLocal()), + if (match.document.remoteNote != null) + account(match.document.remoteNote!.accountId) + else + location( + match is LocalHistoryRevisionMatch + ? match.revision.historicalPath ?? '' + : match.document.currentPath ?? + match.document.historicalPaths.lastOrNull ?? + '', + match.document.displayName, + ), + ].where((s) => s.isNotEmpty).join(' · '), + ), + ActionSearchResult( + :final title, + :final category, + :final command, + :final enabled, + :final disabledReason, + ) => + SearchResultPresentation( + title, + enabled + ? category + : '$category · ${disabledReason ?? context.l10n.commandUnavailableInContext}', + shortcut: command.shortcut?.label, + ), + }; +} diff --git a/lib/src/search/search_result_preview.dart b/lib/src/search/search_result_preview.dart new file mode 100644 index 00000000..6ec609f1 --- /dev/null +++ b/lib/src/search/search_result_preview.dart @@ -0,0 +1,55 @@ +import 'package:flutter/widgets.dart'; +import '../app/localization.dart'; +import '../editor/source/source_search.dart'; +import 'workspace_text_search.dart'; + +String stripMarkdownForSearchResult(String line, {BuildContext? context}) { + // Preview navigation also calls this helper directly with source lines. + var value = boundedSearchSnippet(line).trim(); + final fence = RegExp( + r'^(```+|~~~+)\s*([A-Za-z0-9_+\-#.]*)', + ).firstMatch(value); + if (fence != null) { + final language = fence.group(2)?.trim() ?? ''; + return language.isEmpty + ? context?.l10n.codeBlock ?? 'code block' + : language; + } + + value = value + .replaceFirst(RegExp(r'^\s{0,3}#{1,6}\s+'), '') + .replaceFirst(RegExp(r'^\s{0,3}>\s?'), '') + .replaceFirst(RegExp(r'^\s{0,3}[-+*]\s+\[[ xX]\]\s+'), '') + .replaceFirst(RegExp(r'^\s{0,3}[-+*]\s+'), '') + .replaceFirst(RegExp(r'^\s{0,3}\d+[.)]\s+'), '') + .trim(); + final replacements = { + RegExp(r'!\[([^\]]*)\]\([^)]+\)'): (match) => match.group(1) ?? '', + RegExp(r'\[([^\]]+)\]\([^)]+\)'): (match) => match.group(1) ?? '', + RegExp(r'\[([^\]]+)\]\[[^\]]*\]'): (match) => match.group(1) ?? '', + RegExp(r'`([^`]*)`'): (match) => match.group(1) ?? '', + RegExp(r'\*\*([^*]+)\*\*'): (match) => match.group(1) ?? '', + RegExp(r'__([^_]+)__'): (match) => match.group(1) ?? '', + RegExp(r'\*([^*]+)\*'): (match) => match.group(1) ?? '', + RegExp(r'_([^_]+)_'): (match) => match.group(1) ?? '', + RegExp(r'~~([^~]+)~~'): (match) => match.group(1) ?? '', + RegExp(r'<[^>]+>'): (_) => '', + }; + for (final entry in replacements.entries) { + value = value.replaceAllMapped(entry.key, entry.value); + } + value = value.replaceAll(RegExp(r'\s+'), ' ').trim(); + return value.isEmpty + ? context?.l10n.untitledResult ?? 'untitled result' + : value; +} + +String searchResultTitle(BuildContext context, String line) { + final trimmed = stripMarkdownForSearchResult(line, context: context); + if (trimmed.length <= 120) { + return trimmed; + } + var end = 117; + if (!sourceSearchRangeHasSafeBoundaries(trimmed, end, end)) end--; + return '${trimmed.substring(0, end)}...'; +} diff --git a/lib/src/search/search_session.dart b/lib/src/search/search_session.dart new file mode 100644 index 00000000..4da2ffe5 --- /dev/null +++ b/lib/src/search/search_session.dart @@ -0,0 +1,1525 @@ +import 'package:flutter/foundation.dart'; +import '../app/command_presentation.dart'; +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart' as p; + +import '../app/app_router.dart'; +import '../app/app_settings.dart'; +import '../app/busymark_design.dart'; +import '../app/command_registry.dart'; +import '../app/quick_open.dart'; +import '../editor/source/source_document.dart'; +import '../editor/source/source_search.dart'; +import '../local_history/local_history_controller.dart'; +import '../local_history/local_history_models.dart'; +import '../markdown/document_outline.dart'; +import '../workspace/outline_navigation.dart'; +import '../nextcloud_notes/application/nextcloud_connection.dart'; +import '../nextcloud_notes/application/notes_navigation.dart'; +import '../nextcloud_notes/application/notes_search_controller.dart'; +import '../nextcloud_notes/domain/notes_search.dart'; +import '../workspace/document_buffer.dart'; +import '../workspace/workspace_controller.dart'; +import '../workspace/workspace_model.dart'; +import 'workspace_search_scope.dart'; +import 'workspace_text_search.dart'; + +enum SearchCategory { + file, + workspace, + outline, + documents, + topics, + recent, + localHistory, + actions, +} + +const searchCommandDestinations = { + BusyMarkCommandIds.search: SearchCategory.file, + BusyMarkCommandIds.searchWorkspace: SearchCategory.workspace, + BusyMarkCommandIds.searchHeadings: SearchCategory.outline, + BusyMarkCommandIds.quickOpen: SearchCategory.documents, + BusyMarkCommandIds.searchTopics: SearchCategory.topics, + BusyMarkCommandIds.searchRecent: SearchCategory.recent, + BusyMarkCommandIds.findLocalHistory: SearchCategory.localHistory, + BusyMarkCommandIds.commandPalette: SearchCategory.actions, +}; + +String searchCommandForCategory(SearchCategory category) => + searchCommandDestinations.entries + .singleWhere((e) => e.value == category) + .key; + +/// A result is qualified by its owning workspace/account, never a synthetic path. +sealed class SidebarSearchResult { + const SidebarSearchResult(); + String get identity; +} + +final class TextSearchResult extends SidebarSearchResult { + const TextSearchResult(this.workspaceId, this.match); + final String workspaceId; + final WorkspaceTextMatch match; + @override + String get identity => + '$workspaceId:${match.bufferId ?? match.filePath}:${match.startOffset}:${match.endOffset}'; +} + +final class NoteSearchResult extends SidebarSearchResult { + const NoteSearchResult(this.accountId, this.hit, this.recovery); + final String accountId; + final NotesSearchHit hit; + final bool recovery; + @override + String get identity => '$accountId:${hit.localId}:${hit.start}:${hit.end}'; +} + +final class OutlineSearchResult extends SidebarSearchResult { + const OutlineSearchResult( + this.workspaceId, + this.bufferId, + this.revision, + this.digest, + this.heading, + this.ancestors, + this.documentName, + ); + final String workspaceId, bufferId, digest, documentName; + final int revision; + final DocumentOutlineHeading heading; + final List ancestors; + @override + String get identity => + '$workspaceId:$bufferId:${heading.id}:${heading.editorBlockId}:${heading.sourceStartOffset}:${heading.sourceStartLine}'; +} + +final class DocumentSearchResult extends SidebarSearchResult { + const DocumentSearchResult(this.workspaceId, this.document, {this.accountId}); + final String workspaceId; + final String? accountId; + final QuickOpenDocument document; + @override + String get identity => '$workspaceId:${document.identity}'; +} + +final class TopicSearchResult extends SidebarSearchResult { + const TopicSearchResult( + this.workspaceId, + this.path, + this.title, + this.topicId, + this.module, + this.relativePath, + ); + final String workspaceId, path, title, topicId, module, relativePath; + @override + String get identity => '$workspaceId:$path'; +} + +final class RecentSearchResult extends SidebarSearchResult { + const RecentSearchResult(this.resource); + final RecentResource resource; + @override + String get identity => resource.identity; +} + +final class HistorySearchResult extends SidebarSearchResult { + const HistorySearchResult(this.match); + final LocalHistorySearchMatch match; + @override + String get identity => match.identity; +} + +/// Metadata is localized at the presentation boundary; commands remain in the registry. +final class ActionSearchResult extends SidebarSearchResult { + const ActionSearchResult( + this.command, + this.title, + this.category, + this.description, { + this.enabled = true, + this.disabledReason, + }); + final BusyMarkCommand command; + final String title, category; + final String? description, disabledReason; + final bool enabled; + @override + String get identity => command.id; +} + +/// Identities carried with every generation, independently of retained buffers. +class SearchSessionContext { + const SearchSessionContext( + this.route, + this.workspaceId, + this.bufferId, + this.revision, + this.accountId, + ); + final String route; + final String? workspaceId, bufferId, accountId; + final int? revision; +} + +class SearchSessionState { + const SearchSessionState({ + this.active = false, + this.context, + this.category = SearchCategory.file, + this.recovery = false, + this.historyDocumentId, + this.query = '', + this.results = const [], + this.selected = 0, + this.searching = false, + this.indexing = false, + this.truncated = false, + this.skippedFiles = const [], + this.error, + this.focusRequest = 0, + this.generation = 0, + this.requestRevision = 0, + this.documentMatchCount = 0, + this.documentFirstMatchIndex = 0, + this.documentCurrentMatchIndex, + this.sourceNavigationRequest = 0, + this.appliedSourceNavigation = 0, + }); + final bool active; + final SearchSessionContext? context; + final SearchCategory category; + final bool recovery; + final String? historyDocumentId; + final String query; + final List results; + final int selected, focusRequest, generation; + final int requestRevision; + final int documentMatchCount, documentFirstMatchIndex; + final int? documentCurrentMatchIndex; + final int sourceNavigationRequest, appliedSourceNavigation; + final bool searching, indexing, truncated; + final List skippedFiles; + final String? error; + + /// Text queries preserve literal whitespace; name queries use their existing + /// normalization. This is the single gate for every request entry point. + bool get queryIsEmpty => switch (category) { + SearchCategory.file => query.isEmpty, + SearchCategory.workspace => + context?.accountId == null ? query.isEmpty : query.trim().isEmpty, + _ => query.trim().isEmpty, + }; + bool get showingInitialList => + queryIsEmpty && category == SearchCategory.actions; + bool get waitingForInput => queryIsEmpty && !showingInitialList; + bool get fileActive => active && category == SearchCategory.file; + bool get workspaceActive => active && category == SearchCategory.workspace; + bool get textActive => fileActive || workspaceActive; + SourceSearchSnapshot sourceSnapshot(DocumentBuffer? buffer) { + final ranges = <(int, int)>[]; + if (textActive && + !waitingForInput && + results.isNotEmpty && + buffer != null && + (workspaceActive || + context?.bufferId == buffer.id && + context?.revision == buffer.revision)) { + final remote = buffer.remoteNote; + final digest = notesSearchDigest(buffer.text); + for (final result in results) { + if (result is TextSearchResult && + result.match.digest == digest && + result.match.startOffset >= 0 && + result.match.endOffset <= buffer.text.length && + (result.match.bufferId == buffer.id || + buffer.filePath != null && + result.match.filePath == buffer.filePath)) { + ranges.add((result.match.startOffset, result.match.endOffset)); + } else if (result is NoteSearchResult && + remote != null && + result.accountId == remote.accountId && + result.hit.localId == remote.localId && + result.hit.digest == digest && + result.hit.start != null && + result.hit.end != null) { + ranges.add((result.hit.start!, result.hit.end!)); + } + } + } + return SourceSearchSnapshot( + source: buffer?.text ?? '', + truncated: category != SearchCategory.file && truncated, + searching: searching, + navigationRequest: sourceNavigationRequest, + appliedNavigation: appliedSourceNavigation, + sourceRevision: buffer?.revision, + sessionRevision: requestRevision, + result: SourceSearchResult( + options: options, + totalMatchCount: category == SearchCategory.file + ? documentMatchCount + : null, + firstMatchIndex: category == SearchCategory.file + ? documentFirstMatchIndex + : 0, + currentMatchIndex: category == SearchCategory.file + ? documentCurrentMatchIndex + : null, + matches: [ + for (final (start, end) in ranges) + SourceSearchMatch( + fullStart: start, + fullEnd: end, + visibleStart: start, + visibleEnd: end, + hidden: false, + ), + ], + ), + ); + } + + SourceSearchOptions get options => + SourceSearchOptions(query: textActive ? query : ''); + SearchSessionState copyWith({ + bool? active, + SearchSessionContext? context, + SearchCategory? category, + bool? recovery, + String? historyDocumentId, + bool clearHistoryScope = false, + String? query, + List? results, + int? selected, + bool? searching, + bool? indexing, + bool? truncated, + List? skippedFiles, + String? error, + int? focusRequest, + int? generation, + int? requestRevision, + int? documentMatchCount, + int? documentFirstMatchIndex, + int? documentCurrentMatchIndex, + bool clearDocumentSelection = false, + int? sourceNavigationRequest, + int? appliedSourceNavigation, + }) => SearchSessionState( + active: active ?? this.active, + context: context ?? this.context, + category: category ?? this.category, + recovery: recovery ?? this.recovery, + historyDocumentId: clearHistoryScope + ? null + : historyDocumentId ?? this.historyDocumentId, + query: query ?? this.query, + results: results ?? this.results, + selected: selected ?? this.selected, + searching: searching ?? this.searching, + indexing: indexing ?? this.indexing, + truncated: truncated ?? this.truncated, + skippedFiles: skippedFiles ?? this.skippedFiles, + error: error, + focusRequest: focusRequest ?? this.focusRequest, + generation: generation ?? this.generation, + requestRevision: requestRevision ?? this.requestRevision, + documentMatchCount: documentMatchCount ?? this.documentMatchCount, + documentFirstMatchIndex: + documentFirstMatchIndex ?? this.documentFirstMatchIndex, + documentCurrentMatchIndex: clearDocumentSelection + ? null + : documentCurrentMatchIndex ?? this.documentCurrentMatchIndex, + sourceNavigationRequest: + sourceNavigationRequest ?? this.sourceNavigationRequest, + appliedSourceNavigation: + appliedSourceNavigation ?? this.appliedSourceNavigation, + ); +} + +final searchSessionProvider = + NotifierProvider( + SearchSessionController.new, + ); + +/// One window owns one bounded, ephemeral search session. No queries are persisted. +class SearchSessionController extends Notifier { + SearchSessionController({SourceSearchWorker? worker}) + : _worker = worker ?? SourceSearchWorker(); + int _originContextRevision = 0; + final SourceSearchWorker _worker; + Timer? _debounce; + NotesSearchController? _notes; + ProviderSubscription? _historyChanges; + StreamSubscription? _repositoryChanges; + StreamSubscription? _noteChanges; + final _queries = {}; + SourceSearchOptions _recoveryOptions = const SourceSearchOptions(); + final _selectedIdentities = {}; + final _limits = {}; + String? _historyScope; + List _commands = const []; + int _request = 0, _limit = maxWorkspaceSearchResults; + int _sourceWindowStart = 0, _sourceNavigationSerial = 0; + int? _sourceCurrentIndex; + _DocumentSearchNavigation? _sourceNavigation; + bool _disposed = false, _hasOpened = false; + FocusNode? _returnFocus; + WeakReference? _originFocus; + String? _originWorkspace, _originBuffer; + Future _running = Future.value(); + SearchCommandTarget? commandTarget; + String? actionAccountId; + + bool get workspaceVisible => + ref.read(appRouterProvider).routeInformationProvider.value.uri.path == + '/workspace'; + List get categories { + final route = ref + .read(appRouterProvider) + .routeInformationProvider + .value + .uri + .path; + if (route != '/' && route != '/workspace') return const []; + final workspace = workspaceVisible + ? ref.read(workspaceControllerProvider).workspace + : null; + final buffer = workspace == null + ? null + : ref.read(workspaceControllerProvider).activeBuffer; + return [ + if (buffer != null) SearchCategory.file, + if (workspace != null) SearchCategory.workspace, + if (buffer != null) SearchCategory.outline, + if (workspace != null) SearchCategory.documents, + if (workspace?.writersideProject != null || + workspace?.writersideModule != null) + SearchCategory.topics, + SearchCategory.recent, + if (workspace != null) SearchCategory.localHistory, + SearchCategory.actions, + ]; + } + + @override + SearchSessionState build() { + final router = ref.read(appRouterProvider); + var route = router.routeInformationProvider.value.uri; + void routeChanged() { + final next = router.routeInformationProvider.value.uri; + if (next == route) return; + route = next; + _originContextRevision++; + close(restoreFocus: false); + commandTarget = null; + actionAccountId = null; + _originFocus = null; + _originWorkspace = null; + _originBuffer = null; + } + + router.routeInformationProvider.addListener(routeChanged); + void focusChanged() { + if (state.active || !workspaceVisible) return; + final focus = FocusManager.instance.primaryFocus; + final contextRevision = _originContextRevision; + // Focus notifications can refer to an element being deactivated by a + // rename, tab close or route change. Inspect ancestry after finalization. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!ref.mounted || + state.active || + !workspaceVisible || + contextRevision != _originContextRevision || + focus?.context?.mounted != true) { + return; + } + if (_isOrigin(focus)) { + _originFocus = WeakReference(focus!); + final workspace = ref.read(workspaceControllerProvider); + _originWorkspace = workspace.workspace?.id; + _originBuffer = + Actions.maybeFind(focus.context!) != + null + ? workspace.activeBufferId + : null; + } + }); + WidgetsBinding.instance.scheduleFrame(); + } + + FocusManager.instance.addListener(focusChanged); + ref.listen(workspaceControllerProvider, (before, after) { + if (before?.activeBuffer?.editorState.mode != + after.activeBuffer?.editorState.mode) { + cancelSourceNavigation(state.sourceNavigationRequest); + } + if (before?.workspace?.id != after.workspace?.id || + before?.activeBufferId != after.activeBufferId) { + _originContextRevision++; + } + if (before?.workspace?.id != after.workspace?.id) { + commandTarget = null; + _originFocus = null; + _returnFocus = null; + if (state.recovery && workspaceVisible && after.workspace != null) { + open( + category: after.activeBuffer == null + ? SearchCategory.workspace + : SearchCategory.file, + ); + return; + } + } + if (state.active && _searchInputsChanged(state.category, before, after)) { + if (categories.isEmpty) { + close(restoreFocus: false); + return; + } + if (!categories.contains(state.category)) { + open(category: categories.first); + } else { + _schedule(); + } + } + }); + ref.listen(appSettingsControllerProvider, (before, after) { + if (state.active && + state.category == SearchCategory.recent && + !identical(before?.recentWorkspaces, after.recentWorkspaces)) { + _schedule(); + } + }); + ref.onDispose(() { + _historyChanges?.close(); + _disposed = true; + _request++; + _debounce?.cancel(); + _worker.dispose(); + unawaited(_notes?.dispose()); + unawaited(_repositoryChanges?.cancel()); + unawaited(_noteChanges?.cancel()); + router.routeInformationProvider.removeListener(routeChanged); + FocusManager.instance.removeListener(focusChanged); + commandTarget = null; + _returnFocus = null; + actionAccountId = null; + }); + return const SearchSessionState(); + } + + List _commandCategories = const []; + + void configureCommands( + List commands, { + List categoryOrder = const [], + }) { + final categoriesChanged = !listEquals(_commandCategories, categoryOrder); + _commandCategories = categoryOrder; + final changed = + categoriesChanged || + commands.length != _commands.length || + commands.indexed.any((entry) { + final (i, value) = entry; + final old = _commands[i]; + return value.identity != old.identity || + value.enabled != old.enabled || + value.title != old.title || + value.category != old.category || + value.description != old.description || + value.disabledReason != old.disabledReason || + value.command.shortcut?.label != old.command.shortcut?.label; + }); + _commands = commands; + if (changed && state.active && state.category == SearchCategory.actions) { + // Metadata arrives from the localized registry boundary during rendering. + // Publish after the frame, without executing a handler or restarting work. + final request = _request; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_current(request) && state.category == SearchCategory.actions) { + _publishActions(); + } + }); + WidgetsBinding.instance.scheduleFrame(); + } + } + + void _publishActions() { + if (state.results.isNotEmpty && + state.selected >= 0 && + state.selected < state.results.length) { + _selectedIdentities[state.category] = + state.results[state.selected].identity; + } + final query = state.query.trim(); + int? rank(ActionSearchResult c) => commandMatchRank( + name: c.title, + category: c.category, + description: c.description, + id: c.identity, + query: query, + ); + final categories = _commandCategories.isEmpty + ? _commands.map((c) => c.category).toSet().toList() + : _commandCategories; + final results = _commands.where((c) => rank(c) != null).toList() + ..sort((a, b) { + if (query.isEmpty) { + final group = categories + .indexOf(a.category) + .compareTo(categories.indexOf(b.category)); + if (group != 0) return group; + } else { + final relevance = rank(a)!.compareTo(rank(b)!); + if (relevance != 0) return relevance; + } + if (a.enabled != b.enabled) return a.enabled ? -1 : 1; + final name = a.title.compareTo(b.title); + if (name != 0) return name; + final category = a.category.compareTo(b.category); + return category != 0 ? category : a.identity.compareTo(b.identity); + }); + state = state.copyWith( + results: List.unmodifiable(results), + selected: _selectionFor(results), + searching: false, + ); + } + + bool _isOrigin(FocusNode? focus) { + final context = focus?.context; + return context != null && + (Actions.maybeFind(context) != null || + context.findAncestorWidgetOfExactType() != + null || + context.findAncestorStateOfType() != null); + } + + void captureOrigin() { + if (state.active) return; + var focus = FocusManager.instance.primaryFocus; + final workspace = ref.read(workspaceControllerProvider); + if (workspaceVisible && + !_isOrigin(focus) && + _originWorkspace == workspace.workspace?.id && + (_originBuffer == null || _originBuffer == workspace.activeBufferId) && + _originFocus?.target?.context?.mounted == true) { + focus = _originFocus?.target; + } + _returnFocus = focus; + actionAccountId = workspaceVisible + ? workspace.workspace?.nextcloudAccountId + : null; + commandTarget = workspaceVisible + ? SearchCommandTarget.capture(workspace, focus) + : null; + } + + void open({SearchCategory? category, bool? recovery}) { + final available = categories; + if (available.isEmpty) return; + final requested = + category ?? + (_hasOpened && available.contains(state.category) + ? state.category + : available.contains(SearchCategory.file) + ? SearchCategory.file + : SearchCategory.recent); + if (!available.contains(requested)) return; + + captureOrigin(); + _resetSourceNavigation(); + _remember(); + final accountId = ref + .read(workspaceControllerProvider) + .workspace + ?.nextcloudAccountId; + final recoveryDestination = + accountId != null && + ref.read(notesNavigationProvider(accountId)).destination == + NotesDestination.recovery; + final isRecovery = + requested == SearchCategory.workspace && + (recovery ?? recoveryDestination); + final options = isRecovery + ? _recoveryOptions + : _queries[requested] ?? const SourceSearchOptions(); + _limit = _limits[requested] ?? maxWorkspaceSearchResults; + _hasOpened = true; + state = state.copyWith( + active: true, + category: requested, + recovery: isRecovery, + historyDocumentId: requested == SearchCategory.localHistory + ? _historyScope + : null, + clearHistoryScope: + requested != SearchCategory.localHistory || _historyScope == null, + query: options.query, + results: const [], + selected: 0, + focusRequest: state.focusRequest + 1, + requestRevision: state.requestRevision + 1, + ); + _schedule(); + } + + void _observeHistory() { + _historyChanges ??= ref.listen(localHistoryControllerProvider, ( + before, + after, + ) { + if (state.active && + state.category == SearchCategory.localHistory && + before?.searchRevision != after.searchRevision) { + _schedule(); + } + }); + } + + void openCategory(SearchCategory category) { + if (category == SearchCategory.localHistory) { + openHistory(); + } else { + open(category: category); + } + } + + void openHistory({String? documentId}) { + _historyScope = documentId; + open(category: SearchCategory.localHistory); + } + + void clearHistoryScope() { + _historyScope = null; + set(state.copyWith(clearHistoryScope: true)); + } + + void toggle() => state.active ? close() : open(); + void close({bool restoreFocus = true}) { + if (!state.active) return; + _historyChanges?.close(); + _historyChanges = null; + _remember(); + _resetSourceNavigation(); + _cancel(); + state = state.copyWith( + active: false, + results: const [], + searching: false, + indexing: false, + generation: _request, + requestRevision: state.requestRevision + 1, + documentMatchCount: 0, + documentFirstMatchIndex: 0, + clearDocumentSelection: true, + sourceNavigationRequest: 0, + appliedSourceNavigation: 0, + ); + final target = _returnFocus; + _returnFocus = null; + commandTarget = null; + actionAccountId = null; + unawaited(_notes?.dispose()); + _notes = null; + unawaited(_repositoryChanges?.cancel()); + _repositoryChanges = null; + if (restoreFocus && !(rootNavigatorKey.currentState?.canPop() ?? false)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (target?.context?.mounted == true && target!.canRequestFocus) { + target.requestFocus(); + } + }); + } + } + + void _remember() { + final options = SourceSearchOptions(query: state.query); + if (state.recovery) { + _recoveryOptions = options; + } else { + _queries[state.category] = options; + } + _limits[state.category] = _limit; + if (state.selected >= 0 && state.selected < state.results.length) { + _selectedIdentities[state.category] = + state.results[state.selected].identity; + } + } + + void set(SearchSessionState next) { + if (next.query != state.query && next.category != SearchCategory.actions) { + _selectedIdentities.remove(next.category); + } + _resetSourceNavigation(); + state = next.copyWith( + results: const [], + selected: 0, + requestRevision: state.requestRevision + 1, + ); + _limit = maxWorkspaceSearchResults; + _remember(); + _schedule(); + } + + void query(String value) => set(state.copyWith(query: value)); + void select(int index) { + if (state.results.isEmpty) return; + state = state.copyWith(selected: index.clamp(0, state.results.length - 1)); + _selectedIdentities[state.category] = + state.results[state.selected].identity; + } + + void moveSelection(int delta) => select(state.selected + delta); + + void showMore() { + if (!state.active || state.waitingForInput) return; + _limit += maxWorkspaceSearchResults; + if (state.textActive && state.category == SearchCategory.file) { + _sourceWindowStart = + ((_sourceCurrentIndex ?? _sourceWindowStart) ~/ _limit) * _limit; + } + _schedule(); + } + + void _resetSourceNavigation() { + _sourceNavigation = null; + _sourceCurrentIndex = null; + _sourceWindowStart = 0; + } + + /// Selects by full-document ordinal or post-edit source offset. Displayed + /// rows remain a bounded window; scanning/counting stays in the same worker. + int? navigateSource(SourceSearchNavigationRequest request) { + final workspace = ref.read(workspaceControllerProvider); + final buffer = workspace.activeBuffer; + if (!state.textActive || + state.category != SearchCategory.file || + !workspaceVisible || + buffer == null || + (buffer.id != request.documentId && + buffer.filePath != request.documentId) || + buffer.text != request.source || + state.options != request.options || + request.matchIndex != null && request.matchIndex! < 0 || + request.minimumFullOffset != null && + (request.minimumFullOffset! < 0 || + request.minimumFullOffset! > buffer.text.length)) { + return null; + } + final id = ++_sourceNavigationSerial; + final index = request.matchIndex; + final navigation = _DocumentSearchNavigation( + id, + request, + workspace.workspace!.id, + buffer.id, + buffer.revision, + buffer.editorState.mode, + state.requestRevision, + ); + if (!state.searching && + index != null && + index >= state.documentFirstMatchIndex && + index < state.documentFirstMatchIndex + state.results.length) { + _sourceNavigation = request.reveal ? navigation : null; + _sourceCurrentIndex = index; + state = state.copyWith( + documentCurrentMatchIndex: index, + selected: index - state.documentFirstMatchIndex, + sourceNavigationRequest: request.reveal ? id : 0, + appliedSourceNavigation: request.reveal ? id : 0, + ); + return id; + } + _sourceNavigation = navigation; + if (index != null) _sourceWindowStart = (index ~/ _limit) * _limit; + state = state.copyWith(sourceNavigationRequest: id); + _schedule(); + return id; + } + + void cancelSourceNavigation(int id) { + if (id == 0 || state.sourceNavigationRequest != id) return; + final searchingForTarget = _sourceNavigation != null; + _sourceNavigation = null; + _sourceCurrentIndex = null; + state = state.copyWith( + sourceNavigationRequest: 0, + appliedSourceNavigation: 0, + clearDocumentSelection: true, + ); + if (searchingForTarget) _schedule(); + } + + bool _sourceNavigationIsCurrent(_DocumentSearchNavigation navigation) { + final workspace = ref.read(workspaceControllerProvider); + final buffer = workspace.activeBuffer; + return state.textActive && + state.category == SearchCategory.file && + workspaceVisible && + workspace.workspace?.id == navigation.workspaceId && + buffer?.id == navigation.bufferId && + buffer?.revision == navigation.revision && + buffer?.editorState.mode == navigation.mode && + buffer?.text == navigation.request.source && + state.options == navigation.request.options && + state.requestRevision == navigation.requestRevision; + } + + void refresh(WorkspaceState _) => _schedule(); + void _cancel() { + _request++; + _debounce?.cancel(); + _worker.cancel(); + _notes?.cancel(); + unawaited(_noteChanges?.cancel()); + _noteChanges = null; + } + + void _schedule() { + _cancel(); + final request = _request; + final visible = workspaceVisible; + final workspace = ref.read(workspaceControllerProvider); + final navigation = _sourceNavigation; + final validNavigation = + navigation != null && _sourceNavigationIsCurrent(navigation); + if (!validNavigation && + (navigation != null || + state.context?.bufferId != workspace.activeBufferId || + state.context?.revision != workspace.activeBuffer?.revision || + !state.textActive || + state.category != SearchCategory.file)) { + _resetSourceNavigation(); + } + state = state.copyWith( + context: SearchSessionContext( + ref.read(appRouterProvider).routeInformationProvider.value.uri.path, + visible ? workspace.workspace?.id : null, + visible ? workspace.activeBufferId : null, + visible ? workspace.activeBuffer?.revision : null, + visible ? workspace.workspace?.nextcloudAccountId : null, + ), + results: const [], + selected: 0, + searching: state.active, + indexing: false, + truncated: false, + skippedFiles: const [], + generation: request, + documentMatchCount: 0, + documentFirstMatchIndex: _sourceWindowStart, + clearDocumentSelection: true, + sourceNavigationRequest: validNavigation ? navigation.id : 0, + appliedSourceNavigation: 0, + ); + if (state.active && + state.category == SearchCategory.localHistory && + !state.waitingForInput) { + _observeHistory(); + } else { + _historyChanges?.close(); + _historyChanges = null; + } + if (!state.active || state.waitingForInput) { + _selectedIdentities.remove(state.category); + state = state.copyWith(searching: false); + return; + } + _debounce = Timer(const Duration(milliseconds: 120), () { + _debounce = null; + _running = _run(request); + unawaited(_running); + }); + } + + Future submit() async { + if (state.waitingForInput) return state.active && !state.searching; + final generation = _request; + if (_debounce?.isActive == true) { + _debounce!.cancel(); + _debounce = null; + _running = _run(generation); + } + await _running; + return _current(generation) && !state.searching; + } + + bool _current(int request) => + !_disposed && request == _request && state.active; + Future _run(int request) async { + if (!_current(request) || state.waitingForInput) return; + final snapshot = state; + final workspaceState = ref.read(workspaceControllerProvider); + final workspace = workspaceState.workspace; + void publish(List results, {bool truncated = false}) { + if (_current(request)) { + state = state.copyWith( + results: List.unmodifiable(results), + selected: _selectionFor(results), + searching: false, + truncated: truncated, + ); + } + } + + try { + switch (snapshot.category) { + case SearchCategory.file: + case SearchCategory.workspace: + if (workspace == null || !workspaceVisible) return; + if (snapshot.category != SearchCategory.file && workspace.isRemote) { + await _searchNotes(request, workspaceState, snapshot); + return; + } + final options = snapshot.options; + WorkspaceTextOutcome outcome; + void publishText( + WorkspaceTextOutcome outcome, { + required bool searching, + }) { + if (!_current(request)) return; + final results = [ + for (final match in outcome.matches) + TextSearchResult(workspace.id, match), + ]; + state = state.copyWith( + results: List.unmodifiable(results), + selected: _selectionFor(results), + searching: searching, + truncated: outcome.truncated, + skippedFiles: outcome.skippedFiles, + ); + } + if (snapshot.category == SearchCategory.file) { + await _searchDocument(request, workspaceState, snapshot); + return; + } else { + outcome = await loadWorkspaceTextMatches( + workspaceState, + options, + loadText: ref.read(workspaceServiceProvider).loadText, + isCancelled: () => !_current(request), + worker: _worker, + maximumResults: _limit, + onProgress: (outcome) => publishText(outcome, searching: true), + ); + } + if (!_current(request)) return; + publishText(outcome, searching: false); + case SearchCategory.outline: + final buffer = workspaceState.activeBuffer; + if (workspace == null || buffer == null || !workspaceVisible) return; + final digest = notesSearchDigest(buffer.text); + final matches = [ + for (final match in matchDocumentOutline( + activeDocumentOutline(workspaceState), + snapshot.query, + maximumResults: _limit + 1, + )) + OutlineSearchResult( + workspace.id, + buffer.id, + buffer.revision, + digest, + match.heading, + List.unmodifiable(match.ancestors.map((h) => h.text)), + buffer.displayName, + ), + ]; + publish( + matches.take(_limit).toList(), + truncated: matches.length > _limit, + ); + case SearchCategory.documents: + if (workspace == null || !workspaceVisible) return; + final documents = []; + if (workspace.isRemote) { + final repository = await ref.read( + nextcloudNotesRepositoryProvider.future, + ); + if (!_current(request)) return; + documents.addAll( + repository.notes + .where( + (n) => + n.accountId == workspace.nextcloudAccountId && + !isNotesRecovery(n) && + !repository.isCreationCandidateBinding(n.localId), + ) + .map( + (n) => QuickOpenDocument( + title: n.title, + path: n.category, + localId: n.localId, + ), + ), + ); + _observeRepository(repository.changes); + } else { + documents.addAll( + workspace.files + .where(isSearchableWorkspaceDocument) + .map( + (f) => QuickOpenDocument( + title: p.basenameWithoutExtension(f.relativePath), + path: f.relativePath, + filePath: f.absolutePath, + ), + ), + ); + } + final query = snapshot.query; + final limit = _limit; + final ranked = await rankQuickOpenAsync( + documents, + query, + limit: limit + 1, + ); + publish([ + for (final document in ranked.take(limit)) + DocumentSearchResult( + workspace.id, + document, + accountId: workspace.nextcloudAccountId, + ), + ], truncated: ranked.length > limit); + case SearchCategory.topics: + if (workspace == null || !workspaceVisible) return; + final query = normalizeNotesSearch(snapshot.query.trim()); + final seen = {}; + final topics = []; + final modules = + workspace.writersideProject?.modules ?? + [ + if (workspace.writersideModule != null) + workspace.writersideModule!, + ]; + for (final module in modules) { + for (final topic in module.topics) { + final path = p.normalize(topic.filePath); + if (!seen.add(path)) continue; + final title = topic.title ?? topic.id; + final relative = p.relative(path, from: workspace.rootPath); + if (!normalizeNotesSearch( + '$title ${topic.id} ${topic.fileName} $relative', + ).contains(query)) { + continue; + } + topics.add( + TopicSearchResult( + workspace.id, + path, + title, + topic.id, + module.config.moduleName ?? p.basename(module.rootPath), + relative, + ), + ); + } + } + topics.sort((a, b) { + final title = a.title.compareTo(b.title); + return title == 0 ? a.path.compareTo(b.path) : title; + }); + publish( + topics.take(_limit).toList(), + truncated: topics.length > _limit, + ); + case SearchCategory.recent: + final query = normalizeNotesSearch(snapshot.query.trim()); + final history = ref + .read(appSettingsControllerProvider) + .recentWorkspaces; + publish([ + for (final item in history) + if (normalizeNotesSearch( + '${item.title ?? ''} ${item.path} ${item.ownerRoot ?? ''} ${item.accountId ?? ''} ${item.accountLabel ?? ''}', + ).contains(query)) + RecentSearchResult(item), + ]); + case SearchCategory.localHistory: + final history = ref.read(localHistoryControllerProvider.notifier); + void publishHistory( + LocalHistorySearchPage page, { + bool complete = false, + }) { + if (!_current(request)) return; + final results = [ + for (final match in page.matches) HistorySearchResult(match), + ]; + state = state.copyWith( + results: results, + selected: _selectionFor(results), + searching: !complete, + truncated: page.truncated, + ); + } + final page = await history.findRetained( + query: snapshot.query, + documentId: snapshot.historyDocumentId, + maximumResults: _limit, + isCancelled: () => !_current(request), + onProgress: publishHistory, + ); + if (page != null) publishHistory(page, complete: true); + case SearchCategory.actions: + if (_current(request)) _publishActions(); + } + } on Object catch (error) { + if (_current(request)) { + state = state.copyWith( + results: const [], + searching: false, + error: error.toString(), + ); + } + } + } + + int _selectionFor(List results) { + final id = _selectedIdentities[state.category]; + final found = results.indexWhere((r) => r.identity == id); + return found < 0 ? 0 : found; + } + + void _observeRepository(Stream changes) { + _repositoryChanges ??= changes.listen((_) { + if (state.active && + (state.category == SearchCategory.documents || + state.textActive && state.category != SearchCategory.file)) { + _schedule(); + } + }); + } + + Future _searchDocument( + int request, + WorkspaceState workspace, + SearchSessionState snapshot, + ) async { + final buffer = workspace.activeBuffer; + if (buffer == null) { + if (_current(request)) state = state.copyWith(searching: false); + return; + } + final document = SourceDocument(fullText: buffer.text); + final navigation = _sourceNavigation; + var matches = await _worker.search( + document, + snapshot.options, + firstMatchIndex: _sourceWindowStart, + minimumFullOffset: navigation?.request.minimumFullOffset, + maximumMatches: _limit, + ); + if (!_current(request)) return; + if (matches == null) { + state = state.copyWith( + searching: false, + skippedFiles: [buffer.displayName], + ); + return; + } + // An offset past the actual final occurrence wraps to the document's first + // window, never to the first row in the previously displayed page. + if (matches.matches.isEmpty && matches.totalMatchCount > 0) { + matches = await _worker.search( + document, + snapshot.options, + maximumMatches: _limit, + ); + if (!_current(request) || matches == null) return; + } + if (navigation != null && !_sourceNavigationIsCurrent(navigation)) return; + var selected = navigation?.request.minimumFullOffset != null + ? matches.matches.isEmpty + ? null + : matches.firstMatchIndex + : navigation?.request.matchIndex ?? _sourceCurrentIndex; + if (selected != null && + (selected < matches.firstMatchIndex || + selected >= matches.firstMatchIndex + matches.matches.length)) { + selected = null; + } + _sourceCurrentIndex = selected; + _sourceWindowStart = matches.firstMatchIndex; + // Retain the request until the editor acknowledges applying it. A derived + // workspace refresh may otherwise erase a ready-but-not-yet-revealed match. + _sourceNavigation = selected != null && navigation?.request.reveal == true + ? navigation + : null; + final digest = notesSearchDigest(buffer.text); + state = state.copyWith( + documentMatchCount: matches.totalMatchCount, + documentFirstMatchIndex: matches.firstMatchIndex, + documentCurrentMatchIndex: selected, + clearDocumentSelection: selected == null, + selected: selected == null ? 0 : selected - matches.firstMatchIndex, + sourceNavigationRequest: navigation?.id ?? 0, + appliedSourceNavigation: navigation?.request.reveal == true + ? navigation!.id + : 0, + searching: false, + truncated: matches.totalMatchCount > matches.matches.length, + results: [ + for (final match in matches.matches) + TextSearchResult( + workspace.workspace!.id, + WorkspaceTextMatch( + filePath: buffer.filePath, + bufferId: buffer.id, + digest: digest, + relativePath: buffer.displayName, + fileKind: resolveWorkspaceDocumentContext( + workspace.workspace!, + buffer, + ).kind, + line: document.lineIndex.lineNumberAtOffset(match.fullStart), + startOffset: match.fullStart, + endOffset: match.fullEnd, + query: snapshot.query, + lineText: boundedSearchSnippet( + document.lineIndex + .lineAt( + document.lineIndex.lineNumberAtOffset(match.fullStart), + ) + .text, + matchStart: + match.fullStart - + document.lineIndex.offsetForLine( + document.lineIndex.lineNumberAtOffset(match.fullStart), + ), + ), + ), + ), + ], + ); + } + + Future _searchNotes( + int request, + WorkspaceState workspace, + SearchSessionState snapshot, + ) async { + final account = workspace.workspace!.nextcloudAccountId!; + final repository = await ref.read(nextcloudNotesRepositoryProvider.future); + if (!_current(request)) return; + _observeRepository(repository.changes); + await _notes?.dispose(); + if (!_current(request)) return; + final controller = NotesSearchController( + repository.store, + account, + recovery: snapshot.recovery, + ); + _notes = controller; + _noteChanges = controller.changes.listen((value) { + if (!_current(request)) return; + state = state.copyWith( + results: [ + for (final hit in value.hits) + NoteSearchResult(account, hit, snapshot.recovery), + ], + searching: value.loading, + indexing: value.indexing, + truncated: value.truncated, + error: value.error?.toString(), + ); + }); + await controller.search( + snapshot.query, + limit: _limit, + overlays: [ + for (final buffer in workspace.documentBuffers) + if (buffer.isDirty && + buffer.remoteNote?.accountId == account && + repository.noteById(buffer.remoteNote!.localId) != null) + NotesSearchOverlay( + buffer.remoteNote!.localId, + buffer.revision, + repository.noteById(buffer.remoteNote!.localId)!.title, + repository.noteById(buffer.remoteNote!.localId)!.category, + buffer.text, + ), + ], + ); + } +} + +class _DocumentSearchNavigation { + const _DocumentSearchNavigation( + this.id, + this.request, + this.workspaceId, + this.bufferId, + this.revision, + this.mode, + this.requestRevision, + ); + final int id, revision, requestRevision; + final SourceSearchNavigationRequest request; + final String workspaceId, bufferId; + final DocumentViewModePreference mode; +} + +/// Context plus document identity, revision, view and selection are all required. +/// Losing the target never falls back to the current query field or another editor. +class SearchCommandTarget { + SearchCommandTarget( + this.workspaceId, + this.bufferId, + this.revision, + this.editorState, + this.focus, + this.selectionSnapshot, + ); + final String? workspaceId, bufferId; + final int? revision; + final DocumentEditorState? editorState; + final FocusNode? focus; + final BusyMarkContextCommandSnapshot? selectionSnapshot; + BuildContext? get context => + focus?.context?.mounted == true ? focus!.context : null; + static SearchCommandTarget capture(WorkspaceState state, FocusNode? focus) => + SearchCommandTarget( + state.workspace?.id, + state.activeBufferId, + state.activeBuffer?.revision, + state.activeBuffer?.editorState, + focus, + focus?.context == null + ? null + : (Actions.maybeFind(focus!.context!) + as BusyMarkContextCommandAction?) + ?.captureTarget + ?.call(), + ); + bool isCurrent(WorkspaceState state) => + context != null && + workspaceId == state.workspace?.id && + bufferId == state.activeBufferId && + revision == state.activeBuffer?.revision && + editorState?.mode == state.activeBuffer?.editorState.mode && + (selectionSnapshot?.isCurrent ?? + (editorState?.selection == + state.activeBuffer?.editorState.selection && + editorState?.wysiwygState.anchorBlockId == + state.activeBuffer?.editorState.wysiwygState.anchorBlockId && + editorState?.wysiwygState.anchorOffset == + state.activeBuffer?.editorState.wysiwygState.anchorOffset && + editorState?.wysiwygState.extentBlockId == + state.activeBuffer?.editorState.wysiwygState.extentBlockId && + editorState?.wysiwygState.extentOffset == + state.activeBuffer?.editorState.wysiwygState.extentOffset)); +} + +// Search dependencies differ from editor/navigation dependencies. In particular, +// a tab selection, inspector update, or derived outline refresh must not rescan +// every workspace file or retained historical revision. +bool _searchInputsChanged( + SearchCategory category, + WorkspaceState? before, + WorkspaceState after, +) { + if (before == null || before.workspace?.id != after.workspace?.id) { + return true; + } + switch (category) { + case SearchCategory.outline: + if (!identical(before.liveOutline, after.liveOutline) || + !identical(before.preview, after.preview)) { + return true; + } + continue document; + document: + case SearchCategory.file: + final a = before.activeBuffer, b = after.activeBuffer; + return a?.id != b?.id || + a?.revision != b?.revision || + a?.text != b?.text || + a?.filePath != b?.filePath || + a?.displayName != b?.displayName; + case SearchCategory.workspace: + if (after.workspace?.isRemote == true) { + final account = after.workspace?.nextcloudAccountId; + Map overlays(WorkspaceState value) => { + for (final buffer in value.documentBuffers) + if (buffer.dirty && buffer.remoteNote?.accountId == account) + buffer.id: (buffer.revision, buffer.text), + }; + return !mapEquals(overlays(before), overlays(after)); + } + Map files( + WorkspaceState value, + ) => { + for (final file in value.workspace?.files ?? const []) + if (isSearchableWorkspaceDocument(file)) + file.absolutePath: ( + file.relativePath, + file.kind, + file.size, + file.lastModified, + ), + }; + final a = files(before), b = files(after); + if (!mapEquals(a, b)) return true; + Map overlays(WorkspaceState value) => { + for (final buffer in value.documentBuffers) + if (buffer.dirty && b.containsKey(buffer.filePath)) + buffer.filePath!: (buffer.id, buffer.revision, buffer.text), + }; + return !mapEquals(overlays(before), overlays(after)); + case SearchCategory.documents: + final a = before.workspace?.files ?? const []; + final b = after.workspace?.files ?? const []; + return !mapEquals( + {for (final f in a) f.absolutePath: (f.relativePath, f.kind)}, + {for (final f in b) f.absolutePath: (f.relativePath, f.kind)}, + ); + case SearchCategory.topics: + return !identical( + before.workspace?.writersideProject, + after.workspace?.writersideProject, + ) || + !identical( + before.workspace?.writersideModule, + after.workspace?.writersideModule, + ); + case SearchCategory.recent: + case SearchCategory.localHistory: + case SearchCategory.actions: + return false; + } +} + +bool workspaceSearchInputsChanged( + WorkspaceState? before, + WorkspaceState after, +) { + if (before == null || + before.workspace?.id != after.workspace?.id || + before.activeBufferId != after.activeBufferId || + before.activeText != after.activeText || + !identical(before.workspace, after.workspace)) { + return true; + } + if (before.documentBuffers.length != after.documentBuffers.length) { + return true; + } + for (var i = 0; i < after.documentBuffers.length; i++) { + final a = before.documentBuffers[i], b = after.documentBuffers[i]; + if (a.id != b.id || + a.filePath != b.filePath || + a.revision != b.revision || + a.text != b.text) { + return true; + } + } + return false; +} diff --git a/lib/src/search/search_sidebar.dart b/lib/src/search/search_sidebar.dart new file mode 100644 index 00000000..8b1970ed --- /dev/null +++ b/lib/src/search/search_sidebar.dart @@ -0,0 +1,726 @@ +import '../app/command_presentation.dart'; +import '../app/keyboard_shortcut_badge.dart'; +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../app/app_settings.dart'; +import '../app/busymark_design.dart'; +import '../app/busymark_glyphs.dart'; +import '../app/busymark_search_field.dart'; +import '../app/command_registry.dart'; +import '../app/localization.dart'; +import '../local_history/local_history_controller.dart'; +import '../local_history/local_history_models.dart'; +import '../nextcloud_notes/application/nextcloud_connection.dart'; +import '../workspace/recent_navigation.dart'; +import '../workspace/workspace_controller.dart'; +import '../workspace/workspace_glyphs.dart'; +import 'search_activation.dart'; +import 'search_result_presentation.dart'; +import 'search_action_availability.dart'; +import 'search_session.dart'; + +String searchCategoryLabel(BuildContext context, SearchCategory category) => + switch (category) { + SearchCategory.file => context.l10n.searchDocumentTextCategory, + SearchCategory.workspace => context.l10n.searchWorkspaceTextCategory, + SearchCategory.outline => context.l10n.searchHeadingsCategory, + SearchCategory.documents => context.l10n.searchDocumentsCategory, + SearchCategory.topics => context.l10n.searchTopicsCategory, + SearchCategory.recent => context.l10n.recent, + SearchCategory.localHistory => context.l10n.localHistory, + SearchCategory.actions => context.l10n.actions, + }; + +bool _resultBodyHasFocus() { + var result = false; + FocusManager.instance.primaryFocus?.context?.visitAncestorElements((element) { + final widget = element.widget; + if (widget is IconButton || + widget is BusyMarkCompactIconButton || + widget is BusyMarkMenuButton) { + return false; + } + if (widget is SearchResultList) { + result = true; + return false; + } + return true; + }); + return result; +} + +/// The only query entry, mounted in the existing header on both routes. +class SearchHeaderField extends ConsumerStatefulWidget { + const SearchHeaderField({super.key}); + @override + ConsumerState createState() => _SearchHeaderFieldState(); +} + +class _SearchHeaderFieldState extends ConsumerState { + final _text = TextEditingController(); + @override + void dispose() { + _text.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(searchSessionProvider); + if (_text.text != state.query) { + _text.value = TextEditingValue( + text: state.query, + selection: TextSelection.collapsed(offset: state.query.length), + ); + } + final controller = ref.read(searchSessionProvider.notifier); + return Focus( + onKeyEvent: (_, event) { + if (event is! KeyDownEvent || _text.value.composing.isValid) { + return KeyEventResult.ignored; + } + final key = event.logicalKey; + if (key == LogicalKeyboardKey.arrowDown || + key == LogicalKeyboardKey.arrowUp) { + controller.moveSelection( + key == LogicalKeyboardKey.arrowDown ? 1 : -1, + ); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: BusyMarkSearchField( + key: const ValueKey('shared-search-query'), + controller: _text, + hintText: switch (state.category) { + SearchCategory.file => context.l10n.searchDocumentHint, + SearchCategory.workspace => context.l10n.searchWorkspaceHint, + SearchCategory.outline => context.l10n.searchOutlineHint, + SearchCategory.documents => context.l10n.searchDocumentsHint, + SearchCategory.topics => context.l10n.searchTopicsHint, + SearchCategory.recent => context.l10n.searchRecentHint, + SearchCategory.localHistory => context.l10n.searchHistoryHint, + SearchCategory.actions => context.l10n.searchActionsHint, + }, + autofocus: state.active, + focusRequest: state.focusRequest, + clearOnEscape: false, + onChanged: controller.query, + onMoveSelection: controller.moveSelection, + onClear: () => controller.query(''), + onEscape: controller.close, + onSubmitted: (_) => + unawaited(activateSelectedSearchResult(context, ref)), + ), + ); + } +} + +/// One lazy result surface, shared by all categories and both application routes. +class SearchSidebar extends ConsumerWidget { + const SearchSidebar({super.key, this.onReviewReplacement}); + final Future Function()? onReviewReplacement; + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(searchSessionProvider); + if (!state.active) return const SizedBox.shrink(); + final controller = ref.read(searchSessionProvider.notifier); + final commands = BusyMarkCommandRegistryScope.maybeOf(context); + if (commands != null) { + controller.configureCommands([ + for (final c in commands.visibleCommands()) + ActionSearchResult( + c, + c.label(context), + c.category(context), + c.description?.call(context), + enabled: searchActionEnabled( + commands, + c, + controller, + ref.read(workspaceControllerProvider), + ), + disabledReason: + BusyMarkCommandCatalog.notesWorkspaceCommandIds.contains( + c.id, + ) && + ref.read(workspaceControllerProvider).workspace?.isRemote != + true + ? context.l10n.searchRequiresNotesWorkspace + : c.disabledReason?.call(context), + ), + ], categoryOrder: commandCategoryOrder(context)); + } + final workspace = ref.watch(workspaceControllerProvider); + final remote = workspace.workspace?.isRemote == true; + final historyDocuments = + state.category == SearchCategory.localHistory && + state.historyDocumentId != null + ? ref.watch(localHistoryControllerProvider).snapshot.documents + : const []; + final container = ProviderScope.containerOf(context, listen: false); + final accountId = workspace.workspace?.nextcloudAccountId; + final activeAccount = + remote && container.exists(nextcloudConnectionProvider) + ? ref.watch(nextcloudConnectionProvider).account + : null; + final cachedAccount = + remote && container.exists(nextcloudNotesRepositoryProvider) + ? ref + .watch(nextcloudNotesRepositoryProvider) + .value + ?.accountById(accountId ?? '') + : null; + final account = activeAccount?.id == accountId + ? activeAccount + : cachedAccount; + final historyDocument = state.historyDocumentId == null + ? null + : historyDocuments + .where((d) => d.id == state.historyDocumentId) + .firstOrNull; + final contextLabel = + state.fileActive || state.category == SearchCategory.outline + ? workspace.activeBuffer?.displayName + : state.workspaceActive || + state.category == SearchCategory.documents || + state.category == SearchCategory.topics + ? (state.recovery + ? context.l10n.notesRecovery + : remote + ? (account?.id == workspace.workspace?.nextcloudAccountId + ? '${account!.loginName} · ${account.server.host}' + : context.l10n.searchAccountUnavailable) + : workspace.workspace?.filesystemRootPath) + : state.category == SearchCategory.localHistory + ? historyDocument?.displayName ?? + (state.historyDocumentId != null + ? context.l10n.searchResourceUnavailable + : null) + : null; + return Focus( + onKeyEvent: (_, event) { + if (!state.active || event is! KeyDownEvent) { + return KeyEventResult.ignored; + } + if (event.logicalKey == LogicalKeyboardKey.escape) { + controller.close(); + return KeyEventResult.handled; + } + if (!_resultBodyHasFocus()) return KeyEventResult.ignored; + if (event.logicalKey == LogicalKeyboardKey.arrowDown || + event.logicalKey == LogicalKeyboardKey.arrowUp) { + controller.moveSelection( + event.logicalKey == LogicalKeyboardKey.arrowDown ? 1 : -1, + ); + return KeyEventResult.handled; + } + if (event.logicalKey == LogicalKeyboardKey.enter) { + unawaited(activateSelectedSearchResult(context, ref)); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: BusyMarkSpacing.sm, + vertical: BusyMarkSpacing.xs, + ), + child: Row( + children: [ + const Icon(BusyMarkGlyphs.search, size: BusyMarkSizes.iconSm), + const SizedBox(width: BusyMarkSpacing.xs), + Text(context.l10n.searchSidebarLabel), + const SizedBox(width: BusyMarkSpacing.xs), + Expanded( + child: Semantics( + label: context.l10n.searchCategory, + child: BusyMarkPopupSelector( + key: const ValueKey('search-category'), + value: state.category, + label: searchCategoryLabel(context, state.category), + tooltip: context.l10n.searchCategory, + fullWidth: true, + maxLabelLines: 2, + options: [ + for (final category in controller.categories) + BusyMarkPopupSelectorOption( + value: category, + label: searchCategoryLabel(context, category), + shortcut: + (commands ?? + BusyMarkCommandCatalog + .metadata)[searchCommandForCategory( + category, + )] + ?.shortcut + ?.label, + gtkAccelerator: + (commands ?? + BusyMarkCommandCatalog + .metadata)[searchCommandForCategory( + category, + )] + ?.shortcut + ?.gtkAccelerator, + ), + ], + onSelected: (category) => + controller.openCategory(category), + ), + ), + ), + ], + ), + ), + if (contextLabel != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: BusyMarkSpacing.md, + ), + child: Row( + children: [ + Expanded( + child: Tooltip( + message: contextLabel, + child: Text( + contextLabel, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + if (state.category == SearchCategory.localHistory && + state.historyDocumentId != null) + BusyMarkCompactIconButton( + tooltip: context.l10n.clearHistorySearchScope, + icon: BusyMarkGlyphs.clear, + onPressed: controller.clearHistoryScope, + ), + ], + ), + ), + if (remote && state.workspaceActive) + _SearchNotice(context.l10n.notesSearchHelp), + if (!remote && + state.workspaceActive && + !state.recovery && + onReviewReplacement != null && + state.query.isNotEmpty && + !state.searching) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: BusyMarkSpacing.sm, + ), + child: BusyMarkPushButton.standard( + onPressed: () => unawaited(onReviewReplacement!()), + child: Text(context.l10n.workspaceReplace), + ), + ), + if (state.searching) + const LinearProgressIndicator( + key: ValueKey('workspace-search-progress'), + ), + if (state.indexing) _SearchNotice(context.l10n.notesIndexing), + if (state.error != null) _SearchNotice(state.error!), + if (state.skippedFiles.isNotEmpty) + Tooltip( + message: state.skippedFiles.join('\n'), + child: _SearchNotice( + '${context.l10n.workspaceSearchSkippedFiles} · ${state.skippedFiles.length}\n' + '${state.skippedFiles.join('\n')}', + ), + ), + if (state.truncated) + _SearchNotice(context.l10n.workspaceSearchIncomplete), + if (state.truncated) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: BusyMarkSpacing.sm, + ), + child: Tooltip( + message: context.l10n.workspaceSearchIncomplete, + child: TextButton( + onPressed: controller.showMore, + child: Text(context.l10n.workspaceSearchShowMore), + ), + ), + ), + Expanded( + child: (state.results.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(BusyMarkSpacing.sm), + child: Text( + state.searching || + state.error != null || + state.skippedFiles.isNotEmpty + ? '' + : state.waitingForInput + ? context.l10n.searchEnterText + : state.showingInitialList + ? context.l10n.commandUnavailableInContext + : context.l10n.noResults, + ), + ), + ) + : SearchResultList( + groupActions: + state.category == SearchCategory.actions && + state.query.trim().isEmpty, + results: state.results, + selected: state.selected, + onActivate: (result) => + activateSearchResult(context, ref, result), + )), + ), + ], + ), + ); + } +} + +class _SearchNotice extends StatelessWidget { + const _SearchNotice(this.text); + final String text; + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: BusyMarkSpacing.md, + vertical: BusyMarkSpacing.xs, + ), + child: Text( + text, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ); +} + +class SearchResultList extends ConsumerStatefulWidget { + const SearchResultList({ + super.key, + required this.results, + this.selected = -1, + required this.onActivate, + this.groupActions = false, + }); + final bool groupActions; + final List results; + final int selected; + final Future Function(SidebarSearchResult) onActivate; + @override + ConsumerState createState() => _SearchResultListState(); +} + +class _SearchResultListState extends ConsumerState { + final _scroll = ScrollController(); + final _keys = {}; + final _focusNodes = {}; + @override + void initState() { + super.initState(); + final selected = widget.selected; + final results = widget.results; + if (selected >= 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(_reveal(selected, results, restoreFocus: false)); + }); + } + } + + @override + void didUpdateWidget(covariant SearchResultList oldWidget) { + super.didUpdateWidget(oldWidget); + if (!identical(oldWidget.results, widget.results)) { + _keys.clear(); + final retained = {for (final result in widget.results) result.identity}; + final obsolete = [ + for (final id in _focusNodes.keys) + if (!retained.contains(id)) id, + ]; + final released = [for (final id in obsolete) _focusNodes.remove(id)!]; + // Old lazy rows detach this frame. Keep focus by resource identity when + // availability reorders Actions; release handles for discarded pages. + WidgetsBinding.instance.addPostFrameCallback((_) { + for (final node in released) { + node.dispose(); + } + }); + } + if (widget.selected >= 0 && + (oldWidget.selected != widget.selected || + !identical(oldWidget.results, widget.results))) { + final index = widget.selected; + final results = widget.results; + final restoreFocus = _resultBodyHasFocus(); + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(_reveal(index, results, restoreFocus: restoreFocus)); + }); + } + } + + Future _reveal( + int index, + List results, { + required bool restoreFocus, + }) async { + // Rows wrap and follow text scaling. Advance by actual viewports until the + // lazy row mounts, then reveal its measured geometry rather than guessing + // its offset from an assumed fixed row height. + while (mounted && + _scroll.hasClients && + widget.selected == index && + identical(widget.results, results)) { + final target = _keys[index]?.currentContext; + if (target != null && target.mounted) { + await Scrollable.ensureVisible( + target, + alignment: .5, + duration: const Duration(milliseconds: 80), + ); + if (restoreFocus && + mounted && + widget.selected == index && + identical(widget.results, results)) { + _focusNodes[results[index].identity]?.requestFocus(); + } + return; + } + final mountedRows = _keys.entries.where( + (entry) => entry.value.currentContext != null, + ); + final before = mountedRows.isNotEmpty && index < mountedRows.first.key; + final position = _scroll.position; + final offset = + (position.pixels + position.viewportDimension * (before ? -.8 : .8)) + .clamp(0.0, position.maxScrollExtent); + if (offset == position.pixels) return; + _scroll.jumpTo(offset); + await WidgetsBinding.instance.endOfFrame; + } + } + + @override + void dispose() { + for (final node in _focusNodes.values) { + node.dispose(); + } + _scroll.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final rows = widget.results; + final recentResources = ref.watch( + appSettingsControllerProvider.select((s) => s.recentWorkspaces), + ); + final workspaceRoot = ref.watch( + workspaceControllerProvider.select( + (s) => s.workspace?.filesystemRootPath, + ), + ); + final colors = BusyMarkSurfaceColors.of(context); + final container = ProviderScope.containerOf(context, listen: false); + final activeAccount = container.exists(nextcloudConnectionProvider) + ? ref.watch(nextcloudConnectionProvider).account + : null; + final cachedAccounts = container.exists(nextcloudNotesRepositoryProvider) + ? ref.watch(nextcloudNotesRepositoryProvider).value?.accounts + : null; + final accountLabels = { + for (final account in cachedAccounts ?? []) + account.id: '${account.loginName} · ${account.server.host}', + if (activeAccount != null) + activeAccount.id: + '${activeAccount.loginName} · ${activeAccount.server.host}', + }; + return Material( + color: BusyMarkLinuxPalette.transparent, + child: ListView.builder( + key: const ValueKey('sidebar-search-results'), + controller: _scroll, + padding: BusyMarkInsets.sidebarList, + itemCount: rows.length, + itemBuilder: (context, index) { + final result = rows[index]; + final presentation = presentSearchResult( + context, + result, + recentResources: recentResources, + workspaceRoot: workspaceRoot, + accountLabel: (id) => accountLabels[id], + ); + final title = presentation.title; + final subtitle = widget.groupActions && result is ActionSearchResult + ? (result.enabled ? '' : result.disabledReason ?? '') + : presentation.subtitle; + final shortcut = presentation.shortcut; + final enabled = result is! ActionSearchResult || result.enabled; + final selected = + widget.selected >= 0 && + widget.selected < widget.results.length && + widget.results[widget.selected].identity == result.identity; + final key = result is NoteSearchResult + ? ValueKey(( + 'notes-search-hit', + result.hit.localId, + result.hit.start, + result.hit.end, + )) + : ValueKey(result.identity); + final row = BusyMarkSidebarRecordRow( + key: key, + geometryKey: _keys.putIfAbsent(index, GlobalKey.new), + icon: result is RecentSearchResult + ? WorkspaceGlyphs.forRecent(result.resource) + : result is ActionSearchResult + ? BusyMarkGlyphs.keyboard + : result is OutlineSearchResult + ? BusyMarkGlyphs.tag + : result is HistorySearchResult + ? BusyMarkGlyphs.history + : BusyMarkGlyphs.document, + selected: selected, + compact: true, + focusNode: _focusNodes.putIfAbsent(result.identity, FocusNode.new), + onFocusChange: (focused) { + if (focused && + widget.selected >= 0 && + ref.read(searchSessionProvider).active) { + ref.read(searchSessionProvider.notifier).select(index); + } + }, + semanticsLabel: + '$title\n$subtitle${presentation.details == null ? '' : '\n${presentation.details}'}${shortcut == null ? '' : '\n$shortcut'}', + title: result is RecentSearchResult ? title : null, + subtitle: result is RecentSearchResult ? subtitle : null, + pathSubtitle: + result is RecentSearchResult && + result.resource.accountId == null, + tooltip: result is RecentSearchResult ? presentation.details : null, + content: result is RecentSearchResult + ? null + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: enabled + ? colors.foreground + : colors.mutedForeground, + ), + ), + if (subtitle.isNotEmpty) + Text( + subtitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colors.mutedForeground), + ), + ], + ), + trailing: + result is NoteSearchResult && + !result.recovery && + result.hit.start != null + ? BusyMarkCompactIconButton( + tooltip: context.l10n.notesSourceLocation, + icon: BusyMarkGlyphs.code, + onPressed: () => unawaited( + activateSearchResult(context, ref, result, source: true), + ), + ) + : shortcut == null + ? null + : ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 120), + child: BusyMarkKeyboardShortcutBadge(shortcut), + ), + onTap: enabled ? () => unawaited(widget.onActivate(result)) : null, + menuTooltip: result is RecentSearchResult + ? context.l10n.actions + : null, + menuItemsBuilder: result is RecentSearchResult + ? (context) => _recentMenuItems(context, result.resource) + : null, + onMenuSelected: result is RecentSearchResult + ? (action) => unawaited( + runRecentNavigationAction( + context, + ref, + result.resource, + action, + ), + ) + : null, + ); + if (!widget.groupActions || + result is! ActionSearchResult || + index > 0 && + rows[index - 1] is ActionSearchResult && + (rows[index - 1] as ActionSearchResult).category == + result.category) { + return row; + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + BusyMarkSpacing.sm, + BusyMarkSpacing.sm, + BusyMarkSpacing.sm, + BusyMarkSpacing.xs, + ), + child: Semantics( + header: true, + child: Text( + result.category, + style: Theme.of(context).textTheme.labelMedium, + ), + ), + ), + row, + ], + ); + }, + ), + ); + } +} + +List> _recentMenuItems( + BuildContext context, + RecentResource resource, +) => [ + if (resource.accountId == null) ...[ + BusyMarkPopupMenuItem( + value: RecentNavigationAction.openInFiles, + label: context.l10n.openInFiles, + icon: BusyMarkGlyphs.folderOpen, + ), + BusyMarkPopupMenuItem( + value: RecentNavigationAction.copyPath, + label: context.l10n.copyPath, + icon: BusyMarkGlyphs.copy, + ), + ], + BusyMarkPopupMenuItem( + value: RecentNavigationAction.removeFromRecent, + label: context.l10n.removeFromRecent, + icon: BusyMarkGlyphs.clear, + ), +]; diff --git a/lib/src/search/welcome_notes_action.dart b/lib/src/search/welcome_notes_action.dart new file mode 100644 index 00000000..413b941b --- /dev/null +++ b/lib/src/search/welcome_notes_action.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../app/app_router.dart'; +import '../app/busymark_toast.dart'; +import '../app/localization.dart'; +import '../nextcloud_notes/application/nextcloud_connection.dart'; +import '../workspace/workspace_controller.dart'; +import '../workspace/workspace_safety.dart'; + +enum WelcomeNotesAction { create, browse } + +final welcomeNotesActionProvider = + NotifierProvider( + WelcomeNotesActionController.new, + ); + +class WelcomeNotesActionController extends Notifier { + bool busy = false; + @override + WelcomeNotesAction? build() { + final router = ref.read(appRouterProvider); + void routeChanged() { + final uri = router.routeInformationProvider.value.uri; + if (uri.path != '/' && + !(uri.path == settingsRoutePath && + uri.queryParameters['page'] == 'nextcloudNotes')) { + clear(); + } + } + + router.routeInformationProvider.addListener(routeChanged); + ref.onDispose( + () => router.routeInformationProvider.removeListener(routeChanged), + ); + return null; + } + + void request(WelcomeNotesAction action) => state = action; + void clear() { + if (ref.mounted && state != null) state = null; + } +} + +Future performWelcomeNotesAction( + BuildContext context, + WidgetRef ref, + WelcomeNotesAction action, +) async { + final pending = ref.read(welcomeNotesActionProvider.notifier); + if (pending.busy) return; + pending.busy = true; + try { + final origin = ref + .read(appRouterProvider) + .routeInformationProvider + .value + .uri; + if (origin.path != '/' && origin.path != '/workspace') return; + await ref.read(nextcloudConnectionProvider.notifier).reload(); + if (!context.mounted || + ref.read(appRouterProvider).routeInformationProvider.value.uri != + origin) { + pending.clear(); + return; + } + final activeAccount = origin.path == '/workspace' + ? ref.read(workspaceControllerProvider).workspace?.nextcloudAccountId + : null; + final repository = ref.read(nextcloudNotesRepositoryProvider).value; + final account = activeAccount == null + ? ref.read(nextcloudConnectionProvider).account + : repository?.accountById(activeAccount); + if (activeAccount != null && account == null) { + pending.clear(); + BusyMarkToastOverlay.show( + context, + message: context.l10n.searchResourceUnavailable, + ); + return; + } + if (account == null) { + // Returning without a successful connection cancels the pending operation. + if (ref.read(welcomeNotesActionProvider) != null) { + pending.clear(); + return; + } + pending.request(action); + context.go( + '${settingsLocation(SettingsReturnTarget.welcome)}&page=nextcloudNotes', + ); + return; + } + pending + .clear(); // Claim once before any asynchronous work or pointer reactivation. + bool currentRequest() => + context.mounted && + ref.read(appRouterProvider).routeInformationProvider.value.uri == + origin && + (activeAccount == null + ? ref.read(nextcloudConnectionProvider).account?.id == account.id + : ref + .read(workspaceControllerProvider) + .workspace + ?.nextcloudAccountId == + account.id && + repository?.accountById(account.id) != null); + if (!await confirmSafeToContinue(context, ref) || !context.mounted) return; + if (ref.read(appRouterProvider).routeInformationProvider.value.uri != + origin) { + return; + } + final controller = ref.read(workspaceControllerProvider.notifier); + if (ref.read(workspaceControllerProvider).workspace?.nextcloudAccountId != + account.id && + !await controller.openNextcloudWorkspace( + account.id, + acceptNavigation: currentRequest, + )) { + return; + } + if (!context.mounted || + ref.read(appRouterProvider).routeInformationProvider.value.uri != + origin || + !currentRequest() || + ref.read(workspaceControllerProvider).workspace?.nextcloudAccountId != + account.id) { + return; + } + if (action == WelcomeNotesAction.create && + !await controller.createNextcloudNote( + acceptNavigation: currentRequest, + )) { + return; + } + if (context.mounted && currentRequest()) context.go('/workspace'); + } finally { + pending.busy = false; + } +} diff --git a/lib/src/search/workspace_text_search.dart b/lib/src/search/workspace_text_search.dart new file mode 100644 index 00000000..434d5301 --- /dev/null +++ b/lib/src/search/workspace_text_search.dart @@ -0,0 +1,173 @@ +import 'dart:math' as math; +import '../workspace/workspace_model.dart'; +import '../editor/source/source_search.dart'; +import '../editor/source/source_document.dart'; +import '../nextcloud_notes/domain/notes_search.dart'; +import 'workspace_search_scope.dart'; + +class WorkspaceTextMatch { + const WorkspaceTextMatch({ + required this.filePath, + required this.relativePath, + required this.fileKind, + required this.line, + required this.startOffset, + required this.endOffset, + required this.query, + this.lineText, + required this.digest, + this.bufferId, + }); + + final String? filePath; + final String relativePath; + final DocumentKind fileKind; + final int line; + final int startOffset; + final int endOffset; + final String query; + final String? lineText; + final String digest; + final String? bufferId; +} + +const int maxWorkspaceSearchResults = 80; +const int maxWorkspaceSearchFileBytes = 1024 * 1024; + +class WorkspaceTextOutcome { + const WorkspaceTextOutcome({ + this.matches = const [], + this.truncated = false, + this.skippedFiles = const [], + }); + + final List matches; + final bool truncated; + final List skippedFiles; +} + +Future loadWorkspaceTextMatches( + WorkspaceState state, + SourceSearchOptions options, { + required Future Function(String path) loadText, + required bool Function() isCancelled, + required SourceSearchWorker worker, + required int maximumResults, + void Function(WorkspaceTextOutcome outcome)? onProgress, +}) async { + final workspace = state.workspace; + if (workspace == null || options.query.isEmpty || isCancelled()) { + return const WorkspaceTextOutcome(); + } + final results = []; + final skippedFiles = []; + final publicationClock = Stopwatch()..start(); + var publishedMatches = 0; + var lastPublication = 0; + final sortedFiles = + workspace.files.where(isSearchableWorkspaceDocument).toList() + ..sort((a, b) => a.relativePath.compareTo(b.relativePath)); + Future read(DocumentFile file) async { + final buffer = state.bufferForPath(file.absolutePath); + if (buffer != null) return buffer.text; + if (file.absolutePath == state.activeBuffer?.filePath) { + return state.activeText; + } + if (file.size > maxWorkspaceSearchFileBytes || isCancelled()) return null; + try { + return await loadText(file.absolutePath); + } on Object { + return null; + } + } + + // Bound read-ahead to eight eligible files. Matching remains ordered and + // cancellable; a slow file does not serialize every subsequent disk read. + final pending = >{}; + var nextRead = 0; + for (var index = 0; index < sortedFiles.length; index++) { + if (isCancelled()) return const WorkspaceTextOutcome(); + while (nextRead < sortedFiles.length && nextRead < index + 8) { + pending[nextRead] = read(sortedFiles[nextRead]); + nextRead++; + } + final file = sortedFiles[index]; + final buffer = state.bufferForPath(file.absolutePath); + final text = await pending.remove(index)!; + if (isCancelled()) return const WorkspaceTextOutcome(); + if (text == null) skippedFiles.add(file.relativePath); + if (text != null) { + final document = SourceDocument(fullText: text); + final search = await worker.search( + document, + options, + maximumMatches: maximumResults + 1 - results.length, + stopAfterMaximumMatches: true, + reuseWorker: true, + ); + if (isCancelled()) return const WorkspaceTextOutcome(); + if (search == null) { + skippedFiles.add(file.relativePath); + } else { + final digest = notesSearchDigest(text); + for (final match in search.matches) { + final line = document.lineIndex.lineNumberAtOffset(match.fullStart); + final sourceLine = document.lineIndex.lineAt(line); + results.add( + WorkspaceTextMatch( + digest: digest, + bufferId: buffer?.id, + filePath: file.absolutePath, + relativePath: file.relativePath, + fileKind: file.kind, + line: line, + startOffset: match.fullStart, + endOffset: match.fullEnd, + query: options.query, + lineText: boundedSearchSnippet( + sourceLine.text, + matchStart: match.fullStart - sourceLine.startOffset, + ), + ), + ); + } + } + } + // Only report truncation once an additional result was actually found. + if (results.length > maximumResults) { + return WorkspaceTextOutcome( + matches: List.unmodifiable(results.take(maximumResults)), + truncated: true, + skippedFiles: List.unmodifiable(skippedFiles), + ); + } + if (onProgress != null && + results.length > publishedMatches && + (publishedMatches == 0 || + publicationClock.elapsedMilliseconds - lastPublication >= 50)) { + publishedMatches = results.length; + lastPublication = publicationClock.elapsedMilliseconds; + onProgress( + WorkspaceTextOutcome( + matches: List.unmodifiable(results), + skippedFiles: List.unmodifiable(skippedFiles), + ), + ); + } + } + return WorkspaceTextOutcome( + matches: List.unmodifiable(results), + skippedFiles: List.unmodifiable(skippedFiles), + ); +} + +String boundedSearchSnippet(String line, {int matchStart = 0}) { + const maximumLength = 512; + if (line.length <= 120) return line; + var start = math.max(0, matchStart.clamp(0, line.length) - 40); + if (!sourceSearchRangeHasSafeBoundaries(line, start, start)) start--; + var end = math.min(line.length, start + maximumLength - 2); + if (!sourceSearchRangeHasSafeBoundaries(line, end, end)) end--; + return '${start > 0 ? '…' : ''}${line.substring(start, end)}' + '${end < line.length ? '…' : ''}'; +} diff --git a/lib/src/spellcheck/spelling_release_smoke.dart b/lib/src/spellcheck/spelling_release_smoke.dart index e1c4b7a2..3aefece2 100644 --- a/lib/src/spellcheck/spelling_release_smoke.dart +++ b/lib/src/spellcheck/spelling_release_smoke.dart @@ -282,7 +282,6 @@ Future runSpellingReleaseSmoke(String reportPath) async { wordWrap: true, searchActive: false, searchOptions: const SourceSearchOptions(), - onSearchOptionsChanged: (_) {}, onChanged: (_, _) {}, onOpenSearch: () {}, onCloseSearch: () {}, @@ -397,7 +396,6 @@ helo wordWrap: true, searchActive: false, searchOptions: const SourceSearchOptions(), - onSearchOptionsChanged: (_) {}, onChanged: (_, _) {}, onTransactionalChanged: (text, sourceFilePath, previousSelection, selection, undoGroup) => diff --git a/lib/src/visualization/visualization_cache.dart b/lib/src/visualization/visualization_cache.dart index 181ad223..1b0af78a 100644 --- a/lib/src/visualization/visualization_cache.dart +++ b/lib/src/visualization/visualization_cache.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'dart:typed_data'; import 'package:path/path.dart' as p; @@ -25,6 +26,7 @@ class VisualizationCache { final LinkedHashMap _memory = LinkedHashMap(); + Future _trimTail = Future.value(); Future get(String key) async { final memoryResult = _memory.remove(key); @@ -80,7 +82,11 @@ class VisualizationCache { final temporary = File( '${file.path}.tmp-$pid-${DateTime.now().microsecondsSinceEpoch}', ); - await temporary.writeAsBytes(bytes, flush: true); + // Derived images are disposable. Await the complete write and atomic + // rename, but avoid a durable flush that can hold rendering slots for + // seconds behind unrelated filesystem writes. Incomplete cache entries + // are rejected on read and recomputed. + await temporary.writeAsBytes(bytes); try { await temporary.rename(file.path); } on FileSystemException { @@ -115,32 +121,25 @@ class VisualizationCache { } } - Future _trimDiskBestEffort() async { - try { - final files = <({File file, FileStat stat})>[]; - await for (final entity in diskRoot.list(followLinks: false)) { - if (entity is! File || p.extension(entity.path) != '.json') { - continue; - } - final stat = await entity.stat(); - files.add((file: entity, stat: stat)); - } - files.sort( - (left, right) => left.stat.modified.compareTo(right.stat.modified), - ); - var bytes = files.fold(0, (sum, entry) => sum + entry.stat.size); - var count = files.length; - for (final entry in files) { - if (count <= maximumDiskEntries && bytes <= maximumDiskBytes) { - break; - } - await entry.file.delete(); - count--; - bytes -= entry.stat.size; + Future _trimDiskBestEffort() { + final previous = _trimTail; + final root = diskRoot.absolute.path; + final entryLimit = maximumDiskEntries; + final byteLimit = maximumDiskBytes; + + final trim = () async { + await previous; + try { + // One maintenance worker at a time. Callers still await trimming and + // occupy their bounded rendering slots; no background writes accrue. + // Avoid a main-isolate round trip for every cached file's metadata. + await _trimInWorker(root, entryLimit, byteLimit); + } on Object { + // Best-effort LRU maintenance only. Keep the queue usable after failure. } - } on Object { - // Best-effort LRU maintenance only. - } + }(); + _trimTail = trim; + return trim; } static Directory _defaultDiskRoot(Map? environment) { @@ -156,6 +155,33 @@ class VisualizationCache { } } +// This closure's scope contains only explicit primitive inputs, rather than a +// cache, rendered images, queued Futures or other main-isolate state. +Future _trimInWorker(String root, int maximumEntries, int maximumBytes) => + Isolate.run( + () => _trimDirectory(root, maximumEntries, maximumBytes), + debugName: 'Visualization cache maintenance', + ); + +void _trimDirectory(String root, int maximumEntries, int maximumBytes) { + final files = <({File file, FileStat stat})>[]; + for (final entity in Directory(root).listSync(followLinks: false)) { + if (entity is! File || p.extension(entity.path) != '.json') continue; + files.add((file: entity, stat: entity.statSync())); + } + files.sort( + (left, right) => left.stat.modified.compareTo(right.stat.modified), + ); + var bytes = files.fold(0, (sum, entry) => sum + entry.stat.size); + var count = files.length; + for (final entry in files) { + if (count <= maximumEntries && bytes <= maximumBytes) break; + entry.file.deleteSync(); + count--; + bytes -= entry.stat.size; + } +} + Map _encodeResult( String key, VisualizationRenderResult result, diff --git a/lib/src/visualization/visualization_coordinator.dart b/lib/src/visualization/visualization_coordinator.dart index f09e198b..eb7ffba6 100644 --- a/lib/src/visualization/visualization_coordinator.dart +++ b/lib/src/visualization/visualization_coordinator.dart @@ -216,12 +216,16 @@ class VisualizationCoordinator { _throwIfSuperseded(queued.request, queued.token); if (result.isSuccessful) { _rememberLastSuccessful(queued.request.blockKey, result); - await cache.put(queued.request.cacheKey, result); } - _throwIfSuperseded(queued.request, queued.token); if (!queued.completer.isCompleted) { queued.completer.complete(result); } + if (result.isSuccessful) { + // Publishing a rendered result does not require best-effort disk I/O. + // put warms the memory cache before its first await. Keep awaiting the + // write here so pending cache work still occupies a bounded queue slot. + await cache.put(queued.request.cacheKey, result); + } } on VisualizationCancelledException { if (!queued.completer.isCompleted) { queued.completer.completeError( diff --git a/lib/src/workspace/document_buffer.dart b/lib/src/workspace/document_buffer.dart index c4b977e0..c98b7fbf 100644 --- a/lib/src/workspace/document_buffer.dart +++ b/lib/src/workspace/document_buffer.dart @@ -3,7 +3,6 @@ import 'dart:math' as math; import 'package:flutter/services.dart'; import '../app/app_settings.dart'; -import '../editor/source/source_search.dart'; import '../editor/wysiwyg/wysiwyg_session_state.dart'; import '../spellcheck/spelling_language.dart'; import '../nextcloud_notes/domain/notes_models.dart'; @@ -115,7 +114,6 @@ class DocumentEditorState { this.selection = const TextSelection.collapsed(offset: 0), this.scrollOffset = 0, this.foldedRegionKeys = const {}, - this.searchOptions = const SourceSearchOptions(), this.searchReplacement = '', this.undoState = const DocumentUndoState(), this.wysiwygState = const WysiwygEditorSessionState(), @@ -126,7 +124,6 @@ class DocumentEditorState { final TextSelection selection; final double scrollOffset; final Set foldedRegionKeys; - final SourceSearchOptions searchOptions; final String searchReplacement; final DocumentUndoState undoState; final WysiwygEditorSessionState wysiwygState; @@ -137,7 +134,6 @@ class DocumentEditorState { TextSelection? selection, double? scrollOffset, Set? foldedRegionKeys, - SourceSearchOptions? searchOptions, String? searchReplacement, DocumentUndoState? undoState, WysiwygEditorSessionState? wysiwygState, @@ -150,7 +146,6 @@ class DocumentEditorState { foldedRegionKeys: Set.unmodifiable( foldedRegionKeys ?? this.foldedRegionKeys, ), - searchOptions: searchOptions ?? this.searchOptions, searchReplacement: searchReplacement ?? this.searchReplacement, undoState: undoState ?? this.undoState, wysiwygState: wysiwygState ?? this.wysiwygState, @@ -164,11 +159,6 @@ class DocumentEditorState { 'selectionExtent': selection.extentOffset, 'scrollOffset': scrollOffset, 'foldedRegionKeys': foldedRegionKeys.toList(), - 'searchQuery': searchOptions.query, - 'searchCaseSensitive': searchOptions.caseSensitive, - 'searchWholeWord': searchOptions.wholeWord, - 'searchRegex': searchOptions.regex, - 'searchReplacement': searchReplacement, 'wysiwygState': wysiwygState.toJson(), 'spellingLanguage': spellingLanguage.toJson(), }; @@ -189,12 +179,6 @@ class DocumentEditorState { ?.map((value) => value.toString()) .toSet() ?? const {}, - searchOptions: SourceSearchOptions( - query: json['searchQuery']?.toString() ?? '', - caseSensitive: json['searchCaseSensitive'] as bool? ?? false, - wholeWord: json['searchWholeWord'] as bool? ?? false, - regex: json['searchRegex'] as bool? ?? false, - ), searchReplacement: json['searchReplacement']?.toString() ?? '', wysiwygState: WysiwygEditorSessionState.fromJson( (json['wysiwygState'] as Map?)?.cast() ?? const {}, diff --git a/lib/src/workspace/document_navigation.dart b/lib/src/workspace/document_navigation.dart new file mode 100644 index 00000000..a05ac7d9 --- /dev/null +++ b/lib/src/workspace/document_navigation.dart @@ -0,0 +1,17 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../git/application/git_controller.dart'; +import '../local_history/local_history_controller.dart'; +import 'workspace_controller.dart'; + +/// Reveal a successfully opened document using the same view transition as +/// selecting its editor tab. Resource validation and opening happen first. +void revealOpenedDocument(WidgetRef ref) { + final history = ref.read(localHistoryControllerProvider.notifier); + history.clearComparison(); + ref.read(gitControllerProvider.notifier).deactivateDiffFile(); + final buffer = ref.read(workspaceControllerProvider).activeBuffer; + if (buffer != null) unawaited(history.selectDocumentForBuffer(buffer)); +} diff --git a/lib/src/workspace/outline_navigation.dart b/lib/src/workspace/outline_navigation.dart new file mode 100644 index 00000000..c689b5ec --- /dev/null +++ b/lib/src/workspace/outline_navigation.dart @@ -0,0 +1,196 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart' as p; + +import '../editor/wysiwyg/writerside_editing_adapter.dart'; +import '../markdown/document_outline.dart'; +import '../markdown/preview_model.dart'; +import 'workspace_controller.dart'; +import 'workspace_model.dart'; + +final outlineNavigationTargetProvider = + NotifierProvider< + OutlineNavigationTargetController, + OutlineNavigationTarget? + >(OutlineNavigationTargetController.new); +final outlineViewportTargetProvider = + NotifierProvider( + OutlineViewportTargetController.new, + ); + +class OutlineNavigationTargetController + extends Notifier { + @override + OutlineNavigationTarget? build() => null; + + void set(OutlineNavigationTarget? target) { + state = target; + } +} + +class OutlineViewportTargetController extends Notifier { + @override + OutlineViewportTarget? build() => null; + + void set(OutlineViewportTarget target) { + if (state?.sameLocationAs(target) ?? false) { + return; + } + state = target; + } +} + +class OutlineNavigationTarget { + const OutlineNavigationTarget({ + required this.workspaceId, + required this.bufferId, + required this.filePath, + required this.headingId, + required this.line, + this.editorBlockId, + }); + + final String workspaceId; + final String bufferId; + final String? filePath; + final String headingId; + final int? line; + final String? editorBlockId; +} + +class OutlineViewportTarget { + const OutlineViewportTarget({ + required this.workspaceId, + required this.bufferId, + required this.filePath, + required this.headingId, + required this.sourceStartOffset, + required this.editorBlockId, + }); + + final String workspaceId; + final String bufferId; + final String? filePath; + final String? headingId; + final int? sourceStartOffset; + final String? editorBlockId; + + bool sameLocationAs(OutlineViewportTarget other) { + return workspaceId == other.workspaceId && + bufferId == other.bufferId && + filePath == other.filePath && + headingId == other.headingId && + sourceStartOffset == other.sourceStartOffset && + editorBlockId == other.editorBlockId; + } +} + +void setOutlineViewportTarget( + WidgetRef ref, { + required Workspace workspace, + required String? bufferId, + required DocumentOutlineHeading? heading, +}) { + ref + .read(outlineViewportTargetProvider.notifier) + .set( + OutlineViewportTarget( + workspaceId: workspace.id, + bufferId: bufferId ?? '', + filePath: workspace.activeFilePath, + headingId: heading?.id, + sourceStartOffset: heading?.sourceStartOffset, + editorBlockId: heading?.editorBlockId, + ), + ); +} + +List activeDocumentOutline(WorkspaceState state) { + final liveOutline = state.liveOutline; + final workspace = state.workspace; + if (liveOutline != null && + workspace != null && + liveOutline.matches(workspace, state.activeBuffer, state.activeText)) { + return liveOutline.headings; + } + final preview = state.preview; + if (preview != null) { + return preview.outline; + } + final buffer = state.activeBuffer; + if (workspace != null && + buffer != null && + resolveWorkspaceDocumentContext(workspace, buffer).kind == + DocumentKind.writersideXmlTopic) { + return const WritersideEditingAdapter() + .parseXml( + filePath: buffer.filePath ?? workspace.activeFilePath ?? '', + source: buffer.text, + ) + ?.outline ?? + const []; + } + final markdown = workspace?.markdown; + if (workspace == null || markdown == null) return const []; + if (buffer == null) { + if (state.documentBuffers.isNotEmpty) return const []; + return [ + for (final heading in markdown.headings) + DocumentOutlineHeading.fromMarkdown(heading), + ]; + } + final context = resolveWorkspaceDocumentContext(workspace, buffer); + if (!p.equals(markdown.filePath, context.parserPath) || + markdown.source != buffer.text || + markdown.mode != context.markdownMode) { + return const []; + } + return [ + for (final heading in markdown.headings) + DocumentOutlineHeading.fromMarkdown(heading), + ]; +} + +/// Navigate a heading only against the source/model that produced it. +bool navigateOutlineHeading( + WidgetRef ref, { + required String workspaceId, + required String? bufferId, + required String source, + required DocumentOutlineHeading heading, +}) { + final current = ref.read(workspaceControllerProvider); + final workspace = current.workspace; + if (workspace == null || + workspace.id != workspaceId || + current.activeBufferId != bufferId || + current.activeText != source || + !activeDocumentOutline(current).any( + (h) => + h.id == heading.id && + h.editorBlockId == heading.editorBlockId && + h.sourceStartOffset == heading.sourceStartOffset && + h.sourceStartLine == heading.sourceStartLine && + h.text == heading.text, + )) { + return false; + } + setOutlineViewportTarget( + ref, + workspace: workspace, + bufferId: bufferId, + heading: heading, + ); + ref + .read(outlineNavigationTargetProvider.notifier) + .set( + OutlineNavigationTarget( + workspaceId: workspaceId, + bufferId: bufferId ?? '', + filePath: workspace.activeFilePath, + headingId: heading.id, + line: heading.sourceStartLine, + editorBlockId: heading.editorBlockId, + ), + ); + return true; +} diff --git a/lib/src/workspace/presentation/keyboard_shortcuts_settings.dart b/lib/src/workspace/presentation/keyboard_shortcuts_settings.dart new file mode 100644 index 00000000..3cdadd68 --- /dev/null +++ b/lib/src/workspace/presentation/keyboard_shortcuts_settings.dart @@ -0,0 +1,437 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/app_settings.dart'; +import '../../app/busymark_design.dart'; +import '../../app/busymark_dialogs.dart'; +import '../../app/command_presentation.dart'; +import '../../app/command_registry.dart'; +import '../../app/keymap_controller.dart'; +import '../../app/keyboard_shortcut_badge.dart'; +import '../../app/busymark_glyphs.dart'; +import '../../app/localization.dart'; +import '../../app/shortcut_binding.dart'; +import '../../app/shortcut_capture.dart'; + +class KeyboardShortcutsSettings extends ConsumerStatefulWidget { + const KeyboardShortcutsSettings({super.key}); + @override + ConsumerState createState() => + _KeyboardShortcutsSettingsState(); +} + +class _KeyboardShortcutsSettingsState + extends ConsumerState { + bool _restoring = false; + + @override + Widget build(BuildContext context) { + final settings = ref.watch(appSettingsControllerProvider); + final registry = effectiveCommandRegistry( + BusyMarkCommandCatalog.metadata, + settings.shortcutOverrides, + ); + final l10n = context.l10n; + final categories = commandCategoryOrder(context); + final commands = registry.commands.toList() + ..sort((a, b) { + final group = categories + .indexOf(a.category(context)) + .compareTo(categories.indexOf(b.category(context))); + if (group != 0) return group; + final name = a.label(context).compareTo(b.label(context)); + return name != 0 ? name : a.id.compareTo(b.id); + }); + final groups = >{ + for (final category in categories) + if (commands.any((c) => c.category(context) == category)) + category: commands + .where((c) => c.category(context) == category) + .toList(), + }; + final deferred = deferredDefaultConflicts( + BusyMarkCommandCatalog.metadata, + validatedShortcutOverrides( + BusyMarkCommandCatalog.metadata, + settings.shortcutOverrides, + ), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (ref.watch(keymapControllerProvider) == KeymapUpdateStatus.failed) + BusyMarkBanner( + title: l10n.shortcutSaveFailed, + actionLabel: l10n.shortcutRetry, + onAction: () => + unawaited(ref.read(keymapControllerProvider.notifier).retry()), + ), + Expanded( + child: ListView.builder( + key: const ValueKey('settings-shortcut-list'), + itemCount: groups.length, + itemBuilder: (context, i) { + final group = groups.entries.elementAt(i); + return BusyMarkGroupedList( + title: group.key, + filled: true, + children: [ + for (final command in group.value) + _commandRow( + context, + registry, + command, + deferred[command.id], + ), + ], + ); + }, + ), + ), + const SizedBox(height: BusyMarkSpacing.sm), + Align( + alignment: AlignmentDirectional.centerEnd, + child: BusyMarkPushButton.standard( + key: const ValueKey('reset-all-shortcuts'), + onPressed: _restoring ? null : _restoreAll, + child: Text(l10n.shortcutRestoreAll), + ), + ), + ], + ); + } + + Widget _commandRow( + BuildContext context, + BusyMarkCommandRegistry registry, + BusyMarkCommand command, + List? conflicts, + ) { + final configurable = shortcutConfigurable(command); + final description = [ + commandContextLabel(context, command), + if (!configurable) context.l10n.shortcutFixed, + if (conflicts != null) context.l10n.shortcutDefaultConflict, + for (final a + in registry + .activators(command.id) + .skip(1) + .whereType()) + '\u2066${BusyMarkKeyBinding.fromActivator(a).label}\u2069', + ].join(' · '); + return BusyMarkActionRow( + key: ValueKey('shortcut-edit-${command.id}'), + title: command.label(context), + subtitle: description, + tooltip: '${command.label(context)}\n$description', + enabled: configurable, + trailing: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 160), + child: BusyMarkKeyboardShortcutBadge( + command.shortcut?.label ?? context.l10n.shortcutUnassigned, + ), + ), + onTap: configurable + ? () => showBusyMarkModalDialog( + context, + builder: (_) => _ShortcutEditor(command: command), + ) + : null, + ); + } + + Future _restoreAll() async { + if (_restoring) return; + setState(() => _restoring = true); + try { + final confirmed = await showBusyMarkModalDialog( + context, + builder: (c) => BusyMarkDialogShell( + title: context.l10n.shortcutRestoreAll, + actions: [ + BusyMarkPushButton.standard( + onPressed: () => Navigator.pop(c, false), + child: Text(context.l10n.cancel), + ), + BusyMarkPushButton.suggested( + onPressed: () => Navigator.pop(c, true), + child: Text(context.l10n.shortcutRestoreAll), + ), + ], + children: [Text(context.l10n.shortcutRestoreAllQuestion)], + ), + ); + if (confirmed == true && mounted) { + await ref.read(keymapControllerProvider.notifier).resetAll(); + } + } finally { + if (mounted) setState(() => _restoring = false); + } + } +} + +class _ShortcutEditor extends ConsumerStatefulWidget { + const _ShortcutEditor({required this.command}); + final BusyMarkCommand command; + @override + ConsumerState<_ShortcutEditor> createState() => _ShortcutEditorState(); +} + +class _ShortcutEditorState extends ConsumerState<_ShortcutEditor> { + late Map _expected; + BusyMarkKeyBinding? _candidate; + bool _recording = false, _saving = false; + bool _reset = false; + List _conflicts = const []; + String? _message; + final _assignFocus = FocusNode(); + final _captureFocus = FocusNode(); + + @override + void initState() { + super.initState(); + _expected = ref.read(appSettingsControllerProvider).shortcutOverrides; + _conflicts = + deferredDefaultConflicts( + BusyMarkCommandCatalog.metadata, + validatedShortcutOverrides( + BusyMarkCommandCatalog.metadata, + _expected, + ), + )[widget.command.id] ?? + const []; + if (_conflicts.isNotEmpty) { + _candidate = defaultBinding( + BusyMarkCommandCatalog.metadata[widget.command.id]!, + ); + _reset = true; + } + + _captureFocus.addListener(() { + if (!_captureFocus.hasFocus && _recording) { + _cancelCapture(); + } + }); + } + + @override + void dispose() { + BusyMarkShortcutCapture.end(this); + _assignFocus.dispose(); + _captureFocus.dispose(); + super.dispose(); + } + + void _cancelCapture() { + BusyMarkShortcutCapture.end(this); + if (mounted) { + setState(() { + _recording = false; + }); + } + } + + void _record() { + _captureFocus.requestFocus(); + setState(() { + _recording = true; + _conflicts = const []; + _message = null; + }); + BusyMarkShortcutCapture.begin( + this, + onCandidate: (binding) { + BusyMarkShortcutCapture.end(this); + if (!mounted) return; + setState(() { + _candidate = binding; + _recording = false; + _reset = false; + _expected = ref.read(appSettingsControllerProvider).shortcutOverrides; + _conflicts = effectiveCommandRegistry( + BusyMarkCommandCatalog.metadata, + _expected, + ).conflicts(widget.command.id, binding); + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _assignFocus.requestFocus(); + }); + }, + onCancel: _cancelCapture, + ); + } + + Future _save({ + bool reset = false, + bool remove = false, + bool reassign = false, + }) async { + if (_saving) return; + _cancelCapture(); + setState(() { + _saving = true; + _reset = reset; + }); + final result = await ref + .read(keymapControllerProvider.notifier) + .update( + widget.command.id, + remove ? null : _candidate, + expected: _expected, + reset: reset, + reassign: reassign ? _conflicts.map((c) => c.id).toSet() : const {}, + ); + if (!mounted) return; + if (result.status == KeymapUpdateStatus.saved) { + Navigator.pop(context); + return; + } + setState(() { + _saving = false; + _conflicts = result.conflicts; + _expected = ref.read(appSettingsControllerProvider).shortcutOverrides; + _message = switch (result.status) { + KeymapUpdateStatus.failed => context.l10n.shortcutSaveFailed, + KeymapUpdateStatus.changed => context.l10n.shortcutChanged, + KeymapUpdateStatus.invalid => context.l10n.shortcutInvalid, + _ => null, + }; + }); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final current = effectiveCommandRegistry( + BusyMarkCommandCatalog.metadata, + ref.watch(appSettingsControllerProvider).shortcutOverrides, + )[widget.command.id]!; + return BusyMarkDialogShell( + title: widget.command.label(context), + actions: [ + BusyMarkPushButton.standard( + onPressed: _saving + ? null + : () { + _cancelCapture(); + Navigator.pop(context); + }, + child: Text(l10n.cancel), + ), + BusyMarkPushButton.suggested( + key: const ValueKey('assign-shortcut'), + focusNode: _assignFocus, + onPressed: + _saving || _recording || _candidate == null && _conflicts.isEmpty + ? null + : () => _save(reset: _reset, reassign: _conflicts.isNotEmpty), + child: Text( + _conflicts.isEmpty ? l10n.shortcutAssign : l10n.shortcutReassign, + ), + ), + ], + children: [ + if (_saving) + Semantics( + label: l10n.save, + liveRegion: true, + child: const LinearProgressIndicator( + key: ValueKey('shortcut-saving'), + ), + ), + BusyMarkGroupedList( + filled: true, + children: [ + BusyMarkActionRow( + title: l10n.shortcutCurrentBinding, + subtitle: commandContextLabel(context, widget.command), + trailing: BusyMarkKeyboardShortcutBadge( + current.shortcut?.label ?? l10n.shortcutUnassigned, + ), + ), + BusyMarkActionRow( + key: const ValueKey('record-shortcut'), + title: _recording ? l10n.shortcutPressKeys : l10n.shortcutRecord, + enabled: !_saving, + leading: const Icon(BusyMarkGlyphs.keyboard), + onTap: _recording ? _cancelCapture : _record, + // Focus remains on the explicit recording control, never an editor. + trailing: Focus( + focusNode: _captureFocus, + skipTraversal: true, + includeSemantics: false, + child: const SizedBox.shrink(), + ), + ), + if (_candidate != null) + BusyMarkActionRow( + title: l10n.shortcutProposedBinding, + trailing: BusyMarkKeyboardShortcutBadge(_candidate!.label), + ), + ], + ), + if (_candidate?.hasKnownSystemConflict == true) + Text(l10n.shortcutSystemWarning), + if (_conflicts.isNotEmpty) + BusyMarkGroupedList( + title: l10n.shortcutConflicts, + filled: true, + children: [ + for (final c in _conflicts) + BusyMarkActionRow( + title: c.label(context), + subtitle: commandContextLabel(context, c), + trailing: BusyMarkKeyboardShortcutBadge( + effectiveCommandRegistry( + BusyMarkCommandCatalog.metadata, + ref + .read(appSettingsControllerProvider) + .shortcutOverrides, + )[c.id]?.shortcut?.label ?? + l10n.shortcutUnassigned, + ), + ), + ], + ), + if (_message != null) + Semantics( + liveRegion: true, + child: Text(_message!, key: const ValueKey('shortcut-feedback')), + ), + BusyMarkGroupedList( + filled: true, + children: [ + BusyMarkActionRow( + key: const ValueKey('remove-shortcut'), + title: l10n.shortcutRemove, + enabled: !_saving, + onTap: () => _save(remove: true), + ), + BusyMarkActionRow( + key: const ValueKey('restore-shortcut-default'), + title: l10n.shortcutRestoreDefault, + enabled: !_saving, + onTap: () => _save(reset: true), + ), + ], + ), + ], + ); + } +} + +String commandContextLabel(BuildContext context, BusyMarkCommand command) => + switch (command.scope) { + BusyMarkCommandScope.application => + context.l10n.shortcutContextApplication, + BusyMarkCommandScope.documentView => context.l10n.shortcutContextDocument, + BusyMarkCommandScope.editor || + BusyMarkCommandScope.textEditing => context.l10n.shortcutContextEditor, + BusyMarkCommandScope.wysiwyg => context.l10n.editor, + BusyMarkCommandScope.sourceSymbol => + context.l10n.shortcutContextWritersideSource, + BusyMarkCommandScope.sidebar => context.l10n.shortcutContextWorkspace, + BusyMarkCommandScope.tree => context.l10n.shortcutContextTree, + }; diff --git a/lib/src/workspace/presentation/settings_screen.dart b/lib/src/workspace/presentation/settings_screen.dart index 4e107f1c..72568ee4 100644 --- a/lib/src/workspace/presentation/settings_screen.dart +++ b/lib/src/workspace/presentation/settings_screen.dart @@ -1,3 +1,4 @@ +import 'keyboard_shortcuts_settings.dart'; import '../../nextcloud_notes/presentation/nextcloud_settings.dart'; import 'dart:async'; import 'dart:convert'; @@ -19,11 +20,12 @@ import '../../app/app_locale.dart'; import '../../app/busymark_dialogs.dart'; import '../../app/busymark_design.dart'; import '../../app/busymark_motion_widgets.dart'; +import '../../app/busymark_search_field.dart'; import '../../app/linux/linux_page_frame.dart'; import '../../app/linux/linux_header_style.dart'; import '../../app/busymark_glyphs.dart'; import '../../app/busymark_main_menu.dart'; -import '../../app/busymark_shortcuts.dart'; +import '../../app/command_registry.dart'; import '../../app/busymark_toast.dart'; import '../../app/localization.dart'; import '../../app/window_control_service.dart'; @@ -64,7 +66,7 @@ class _SettingsScreenState extends ConsumerState { void didUpdateWidget(covariant SettingsScreen oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.initialPage != widget.initialPage) { - _page = widget.initialPage; + _selectPage(widget.initialPage); } } @@ -104,6 +106,7 @@ class _SettingsScreenState extends ConsumerState { }); }); final pageBody = switch (_page) { + SettingsPage.keyboardShortcuts => const KeyboardShortcutsSettings(), SettingsPage.appearance => BusyMarkGroupedList( title: l10n.appearance, filled: true, @@ -491,6 +494,7 @@ class _SettingsScreenState extends ConsumerState { Expanded( child: BusyMarkClamp( maxWidth: BusyMarkSizes.settingsWidth, + scrollable: _page != SettingsPage.keyboardShortcuts, margin: EdgeInsets.zero, padding: BusyMarkInsets.settingsPage, child: BusyMarkKeyedCrossfade( @@ -528,7 +532,9 @@ class _SettingsScreenState extends ConsumerState { synchronizer: headerBar.configurationSynchronizer, configuration: headerConfiguration, enabled: headerBar.isAvailable, - child: Scaffold(backgroundColor: colors.view, body: body), + child: BusyMarkLocalTextEditingShortcuts( + child: Scaffold(backgroundColor: colors.view, body: body), + ), ), ); }, @@ -597,6 +603,11 @@ class _SettingsScreenState extends ConsumerState { ); void _goBack() { + unawaited(_leaveSettings()); + } + + Future _leaveSettings() async { + if (!await confirmNotesSettingsDeparture(context) || !mounted) return; if (_page == SettingsPage.spellingDictionaries) { _selectPage(SettingsPage.editor); return; @@ -605,7 +616,13 @@ class _SettingsScreenState extends ConsumerState { } void _selectPage(SettingsPage page) { - if (_page != page) setState(() => _page = page); + unawaited(_selectPageSafely(page)); + } + + Future _selectPageSafely(SettingsPage page) async { + if (_page == page) return; + if (!await confirmNotesSettingsDeparture(context) || !mounted) return; + setState(() => _page = page); final router = GoRouter.maybeOf(context); final uri = router?.state.uri; if (router == null || uri == null || uri.path != settingsRoutePath) { @@ -638,8 +655,6 @@ class _SettingsScreenState extends ConsumerState { _goBack(); case HeaderBarAction.aboutBusyMark: showBusyMarkAboutDialog(context); - case HeaderBarAction.keyboardShortcuts: - showBusyMarkKeyboardShortcutsDialog(context); case HeaderBarAction.syntaxReference: showBusyMarkSyntaxReferenceDialog(context); case HeaderBarAction.reportIssue: @@ -682,11 +697,6 @@ class _SettingsScreenState extends ConsumerState { unawaited(ref.read(windowControlServiceProvider).toggleFullScreen()); case BusyMarkMainMenuAction.settings: _selectPage(SettingsPage.appearance); - case BusyMarkMainMenuAction.keyboardShortcuts: - showBusyMarkKeyboardShortcutsDialog(context); - case BusyMarkMainMenuAction.quickOpen: - case BusyMarkMainMenuAction.commandPalette: - return; case BusyMarkMainMenuAction.syntaxReference: showBusyMarkSyntaxReferenceDialog(context); case BusyMarkMainMenuAction.reportIssue: @@ -1311,6 +1321,7 @@ enum SettingsPage { appearance, editor, spellingDictionaries, + keyboardShortcuts, validation, history, ai, @@ -1321,6 +1332,7 @@ enum SettingsPage { const _primarySettingsPages = [ SettingsPage.appearance, SettingsPage.editor, + SettingsPage.keyboardShortcuts, SettingsPage.validation, SettingsPage.history, SettingsPage.ai, @@ -1337,6 +1349,7 @@ SettingsPage _settingsNavigationSelection(SettingsPage page) { SettingsPage settingsPageFromRouteValue(String? value) { return switch (value) { + 'keyboardShortcuts' => SettingsPage.keyboardShortcuts, 'editor' => SettingsPage.editor, 'spellingDictionaries' => SettingsPage.spellingDictionaries, 'validation' => SettingsPage.validation, @@ -1357,6 +1370,7 @@ String settingsPageRouteValue(SettingsPage page) => page.name; String _settingsPageLabel(BuildContext context, SettingsPage page) { final l10n = context.l10n; return switch (page) { + SettingsPage.keyboardShortcuts => l10n.keyboardShortcuts, SettingsPage.appearance => l10n.appearance, SettingsPage.editor => l10n.editor, SettingsPage.spellingDictionaries => l10n.spellingDictionaries, @@ -1370,6 +1384,7 @@ String _settingsPageLabel(BuildContext context, SettingsPage page) { IconData _settingsPageIcon(SettingsPage page) { return switch (page) { + SettingsPage.keyboardShortcuts => BusyMarkGlyphs.keyboard, SettingsPage.appearance => BusyMarkGlyphs.appearance, SettingsPage.editor => BusyMarkGlyphs.editorView, SettingsPage.spellingDictionaries => BusyMarkGlyphs.symbols, @@ -1488,7 +1503,12 @@ class _SettingsFallbackHeader extends StatelessWidget { tooltip: context.l10n.back, icon: BusyMarkGlyphs.headerBackFor(Directionality.of(context)), nativeIcon: BusyMarkLinuxHeaderIcon.back, - shortcut: BusyMarkAppShortcutLabels.back, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)[BusyMarkCommandIds + .back] + ?.shortcut + ?.label, onPressed: onBack, ), ], diff --git a/lib/src/workspace/presentation/welcome_screen.dart b/lib/src/workspace/presentation/welcome_screen.dart index 75ef3f7d..b26b3595 100644 --- a/lib/src/workspace/presentation/welcome_screen.dart +++ b/lib/src/workspace/presentation/welcome_screen.dart @@ -1,3 +1,8 @@ +import '../../app/command_registry.dart'; +import '../recent_navigation.dart'; +import '../../search/search_session.dart'; +import '../../search/search_sidebar.dart'; +import '../../search/welcome_notes_action.dart'; import '../../nextcloud_notes/application/nextcloud_connection.dart'; import 'dart:async'; import 'dart:io'; @@ -6,10 +11,8 @@ import 'package:file_selector/file_selector.dart'; import 'package:busymark/src/app/startup_path.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:path/path.dart' as p; -import 'package:url_launcher/url_launcher.dart'; import 'package:yaru/yaru.dart'; import '../../app/app_settings.dart'; @@ -20,8 +23,6 @@ import '../../app/linux/linux_page_frame.dart'; import '../../app/linux/linux_header_style.dart'; import '../../app/busymark_glyphs.dart'; import '../../app/busymark_main_menu.dart'; -import '../../app/busymark_shortcuts.dart'; -import '../../app/busymark_toast.dart'; import '../../app/localization.dart'; import '../../app/window_control_service.dart'; import '../../core/debug_log.dart'; @@ -31,7 +32,6 @@ import '../../platform/gtk_header_icon_service.dart'; import '../../platform/linux_header_bar_service.dart'; import '../../writerside/writerside_project_creator.dart'; import '../workspace_controller.dart'; -import '../workspace_glyphs.dart'; import '../workspace_message.dart'; import '../workspace_safety.dart'; @@ -43,6 +43,8 @@ class WelcomeScreen extends ConsumerStatefulWidget { } class _WelcomeScreenState extends ConsumerState { + bool _navigateWhenContentReady = false; + XTypeGroup _markdownTypes(BuildContext context) => XTypeGroup( label: context.l10n.fileTypeMarkdown, extensions: ['md', 'markdown'], @@ -51,10 +53,44 @@ class _WelcomeScreenState extends ConsumerState { @override Widget build(BuildContext context) { final state = ref.watch(workspaceControllerProvider); + ref.listen(workspaceControllerProvider, (previous, next) { + if (_navigateWhenContentReady && + previous?.isLoading == true && + !next.isLoading && + next.workspace != null && + mounted) { + _navigateWhenContentReady = false; + context.go('/workspace'); + } + }); final settings = ref.watch(appSettingsControllerProvider); final headerBar = ref.watch(linuxHeaderBarServiceProvider); final colors = BusyMarkSurfaceColors.of(context); - final sidebarVisible = settings.sidebarVisible; + final search = ref.watch(searchSessionProvider); + final sidebarVisible = settings.sidebarVisible || search.active; + final connection = + ProviderScope.containerOf( + context, + listen: false, + ).exists(nextcloudConnectionProvider) + ? ref.watch(nextcloudConnectionProvider) + : const NextcloudConnectionState(); + final pending = ref.watch(welcomeNotesActionProvider); + if (pending != null && connection.account != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && + ref.read(welcomeNotesActionProvider) == pending && + ref + .read(appRouterProvider) + .routeInformationProvider + .value + .uri + .path == + '/') { + unawaited(performWelcomeNotesAction(context, ref, pending)); + } + }); + } ref.listen(headerBarActionsProvider, (previous, next) { next.whenData((event) { _handleHeaderBarAction(context, event.action); @@ -73,19 +109,11 @@ class _WelcomeScreenState extends ConsumerState { if (!mounted) { return; } - final settingsController = ref.read( - appSettingsControllerProvider.notifier, - ); - await settingsController.waitUntilLoaded(); - if (!mounted) { - return; - } - final reopenCleanSession = ref - .read(appSettingsControllerProvider) - .reopenPreviousWorkspaceOnStartup; + // Register startup restoration before awaiting settings so an early + // close also waits for recovery publication and sees its dirty state. final restored = await ref .read(workspaceControllerProvider.notifier) - .restoreStartupSession(reopenCleanSession: reopenCleanSession); + .restoreStartupSession(); if (restored && mounted) { this.context.go('/workspace'); } @@ -95,10 +123,28 @@ class _WelcomeScreenState extends ConsumerState { final welcomeMainColor = colors.view; final welcomeSidebar = SizedBox( width: BusyMarkSizes.sidebarWidth, - child: _WelcomeSidebar( - recentWorkspaces: settings.recentWorkspaces, - onOpenRecent: _openPath, - onRecentAction: _performRecentAction, + child: BusyMarkSidebarSurface( + showEndBorder: false, + child: IndexedStack( + index: search.active ? 1 : 0, + children: [ + ExcludeFocus( + excluding: search.active, + child: SearchResultList( + results: [ + for (final r in settings.recentWorkspaces) + RecentSearchResult(r), + ], + onActivate: (r) => openRecentResource( + context, + ref, + (r as RecentSearchResult).resource, + ), + ), + ), + const SearchSidebar(), + ], + ), ), ); final welcomeContent = ColoredBox( @@ -110,6 +156,11 @@ class _WelcomeScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (connection.removedWithWarning) + BusyMarkStatusBox( + message: context.l10n.notesRemovedWarning, + kind: BusyMarkStatusKind.warning, + ), BusyMarkGroupedList( title: context.l10n.create, filled: true, @@ -123,6 +174,21 @@ class _WelcomeScreenState extends ConsumerState { ), onTap: _createMarkdownFile, ), + BusyMarkActionRow( + key: const ValueKey('welcome-new-nextcloud-note'), + title: context.l10n.nextcloudNewNote, + subtitle: connection.account == null + ? context.l10n.nextcloudNotes + : 'Nextcloud · ${connection.account!.server}', + leading: const Icon(BusyMarkGlyphs.newDocument), + onTap: () => unawaited( + performWelcomeNotesAction( + context, + ref, + WelcomeNotesAction.create, + ), + ), + ), BusyMarkActionRow( title: context.l10n.createWritersideProject, subtitle: context.l10n.createWritersideProjectDescription, @@ -158,30 +224,21 @@ class _WelcomeScreenState extends ConsumerState { ), BusyMarkActionRow( key: const ValueKey('welcome-nextcloud-notes'), - title: context.l10n.nextcloudNotes, + title: context.l10n.browseNotes, + subtitle: connection.account == null + ? context.l10n.nextcloudNotes + : 'Nextcloud · ${connection.account!.server}', leading: const Icon(BusyMarkGlyphs.documentHistory), trailing: Icon( BusyMarkGlyphs.forwardFor(Directionality.of(context)), ), - onTap: () async { - final account = ref - .read(nextcloudConnectionProvider) - .account; - if (account == null) { - context.go( - '${settingsLocation(SettingsReturnTarget.welcome)}&page=nextcloudNotes', - ); - } else { - if (!await confirmSafeToContinue(context, ref) || - !context.mounted) { - return; - } - final opened = await ref - .read(workspaceControllerProvider.notifier) - .openNextcloudWorkspace(account.id); - if (opened && context.mounted) context.go('/workspace'); - } - }, + onTap: () => unawaited( + performWelcomeNotesAction( + context, + ref, + WelcomeNotesAction.browse, + ), + ), ), ], ), @@ -205,11 +262,11 @@ class _WelcomeScreenState extends ConsumerState { .copyWith( title: context.l10n.appTitle, viewMode: AppViewMode.editor, - searchQuery: '', + searchQuery: search.query, canRefresh: false, documentControlsVisible: false, - searchActive: false, - searchVisible: false, + searchActive: search.active, + searchVisible: true, sidebarVisible: sidebarVisible, sidebarToggleVisible: true, backVisible: false, @@ -226,8 +283,13 @@ class _WelcomeScreenState extends ConsumerState { sidebarHeader: const BusyMarkLinuxBrandHeader(), sidebarBody: welcomeSidebar, sidebarAvailable: + search.active || constraints.maxWidth >= - BusyMarkSizes.sidebarWidth + BusyMarkSizes.previewMinWidth, + BusyMarkSizes.sidebarWidth + BusyMarkSizes.previewMinWidth, + sidebarOverlay: + search.active && + constraints.maxWidth < + BusyMarkSizes.sidebarWidth + BusyMarkSizes.previewMinWidth, sidebarExpanded: sidebarVisible, sidebarTransitionGeneration: ref.watch( sidebarTransitionGenerationProvider, @@ -242,14 +304,31 @@ class _WelcomeScreenState extends ConsumerState { icon: BusyMarkGlyphs.sidebar, nativeIcon: BusyMarkLinuxHeaderIcon.sidebar, selected: sidebarVisible, - shortcut: BusyMarkSidebarShortcutLabels.toggleSidebar, + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog + .metadata)[BusyMarkCommandIds.toggleSidebar] + ?.shortcut + ?.label, onPressed: _toggleSidebar, ), ], ), - title: BusyMarkLinuxHeaderTitle(context.l10n.appTitle), + centerAllocation: search.active + ? BusyMarkLinuxHeaderCenterAllocation.fillBetweenControls + : BusyMarkLinuxHeaderCenterAllocation.centeredBetweenControls, + title: search.active + ? const SearchHeaderField() + : BusyMarkLinuxHeaderTitle(context.l10n.appTitle), trailing: BusyMarkLinuxHeaderControlGroup( children: [ + BusyMarkHeaderIconButton( + tooltip: context.l10n.search, + icon: BusyMarkGlyphs.search, + selected: search.active, + onPressed: () => + ref.read(searchSessionProvider.notifier).toggle(), + ), BusyMarkMainMenuButton( onSelected: (action) => _handleMainMenuAction(context, headerBar, action), @@ -270,8 +349,6 @@ class _WelcomeScreenState extends ConsumerState { context.go(settingsLocation(SettingsReturnTarget.welcome)); case HeaderBarAction.aboutBusyMark: showBusyMarkAboutDialog(context); - case HeaderBarAction.keyboardShortcuts: - showBusyMarkKeyboardShortcutsDialog(context); case HeaderBarAction.syntaxReference: showBusyMarkSyntaxReferenceDialog(context); case HeaderBarAction.reportIssue: @@ -281,8 +358,9 @@ class _WelcomeScreenState extends ConsumerState { ); case HeaderBarAction.sidebarToggle: _toggleSidebar(); - case HeaderBarAction.back: case HeaderBarAction.search: + ref.read(searchSessionProvider.notifier).toggle(); + case HeaderBarAction.back: case HeaderBarAction.refresh: case HeaderBarAction.save: case HeaderBarAction.export: @@ -323,11 +401,6 @@ class _WelcomeScreenState extends ConsumerState { unawaited(ref.read(windowControlServiceProvider).toggleFullScreen()); case BusyMarkMainMenuAction.settings: context.go(settingsLocation(SettingsReturnTarget.welcome)); - case BusyMarkMainMenuAction.keyboardShortcuts: - showBusyMarkKeyboardShortcutsDialog(context); - case BusyMarkMainMenuAction.quickOpen: - case BusyMarkMainMenuAction.commandPalette: - return; case BusyMarkMainMenuAction.syntaxReference: showBusyMarkSyntaxReferenceDialog(context); case BusyMarkMainMenuAction.reportIssue: @@ -473,7 +546,15 @@ class _WelcomeScreenState extends ConsumerState { if (!safe) { return; } - await ref.read(workspaceControllerProvider.notifier).openPath(path); + _navigateWhenContentReady = true; + try { + // The operation still awaits bookkeeping and reports its errors. The + // listener may publish the prepared, monitored editor while that write + // is pending, without making it a fire-and-forget persistence operation. + await ref.read(workspaceControllerProvider.notifier).openPath(path); + } finally { + _navigateWhenContentReady = false; + } if (!mounted) { return; } @@ -482,152 +563,6 @@ class _WelcomeScreenState extends ConsumerState { context.go('/workspace'); } } - - Future _performRecentAction( - RecentWorkspace recent, - _RecentWorkspaceAction action, - ) async { - switch (action) { - case _RecentWorkspaceAction.openInFiles: - final rawPath = recent.path.trim(); - if (rawPath.isEmpty) { - return; - } - final normalized = p.normalize(rawPath); - final target = - FileSystemEntity.typeSync(normalized) == - FileSystemEntityType.directory - ? normalized - : p.dirname(normalized); - final launched = await launchUrl( - Uri.file(target), - mode: LaunchMode.externalApplication, - ); - if (!launched && mounted) { - BusyMarkToastOverlay.show( - context, - message: context.l10n.couldNotOpenTarget(target), - priority: BusyMarkToastPriority.high, - ); - } - case _RecentWorkspaceAction.copyPath: - await Clipboard.setData(ClipboardData(text: recent.path)); - case _RecentWorkspaceAction.removeFromRecent: - await ref - .read(appSettingsControllerProvider.notifier) - .removeRecentWorkspace(recent.path); - } - } -} - -class _WelcomeSidebar extends StatelessWidget { - const _WelcomeSidebar({ - required this.recentWorkspaces, - required this.onOpenRecent, - required this.onRecentAction, - }); - - final List recentWorkspaces; - final Future Function(String path) onOpenRecent; - final Future Function( - RecentWorkspace recent, - _RecentWorkspaceAction action, - ) - onRecentAction; - - @override - Widget build(BuildContext context) { - return BusyMarkSidebarSurface( - showEndBorder: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: ListView( - padding: BusyMarkInsets.sidebarList, - children: [ - if (recentWorkspaces.isNotEmpty) - _WelcomeSidebarSection( - title: context.l10n.recent, - children: [ - for (final recent in recentWorkspaces) - BusyMarkSidebarRecordRow<_RecentWorkspaceAction>( - icon: WorkspaceGlyphs.forRecent(recent), - title: busyMarkLtrIsolateFor( - context, - _displayPath(recent.path), - ), - subtitle: busyMarkLtrIsolateFor(context, recent.path), - onTap: () => unawaited(onOpenRecent(recent.path)), - menuTooltip: context.l10n.actions, - menuItemsBuilder: _recentWorkspaceMenuItems, - onMenuSelected: (action) => - unawaited(onRecentAction(recent, action)), - ), - ], - ), - ], - ), - ), - ], - ), - ); - } -} - -class _WelcomeSidebarSection extends StatelessWidget { - const _WelcomeSidebarSection({required this.title, required this.children}); - - final String title; - final List children; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: BusyMarkSpacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: BusyMarkSpacing.xs), - child: Text(title, style: busyMarkSectionHeaderStyle(context)), - ), - const SizedBox(height: BusyMarkSpacing.sm), - ...children, - ], - ), - ); - } -} - -enum _RecentWorkspaceAction { openInFiles, copyPath, removeFromRecent } - -List> _recentWorkspaceMenuItems( - BuildContext context, -) { - return [ - BusyMarkPopupMenuItem( - value: _RecentWorkspaceAction.openInFiles, - label: context.l10n.openInFiles, - icon: BusyMarkGlyphs.folderOpen, - ), - BusyMarkPopupMenuItem( - value: _RecentWorkspaceAction.copyPath, - label: context.l10n.copyPath, - icon: BusyMarkGlyphs.copy, - ), - const PopupMenuDivider(height: BusyMarkSpacing.sm), - BusyMarkPopupMenuItem( - value: _RecentWorkspaceAction.removeFromRecent, - label: context.l10n.removeFromRecent, - icon: BusyMarkGlyphs.clear, - ), - ]; -} - -String _displayPath(String path) { - final name = p.basename(path); - return name.isEmpty ? path : name; } class BusyMarkCreateWritersideProjectDialog extends StatefulWidget { @@ -973,6 +908,7 @@ BusyMarkStatusKind busyMarkWorkspaceMessageStatusKind( BusyMarkStatusKind.information, WorkspaceMessageCode.recoveryRestored => BusyMarkStatusKind.information, WorkspaceMessageCode.saveBlockedFileChangedOnDisk || + WorkspaceMessageCode.recentWorkspaceUpdateFailed || WorkspaceMessageCode.recoveryDamaged => BusyMarkStatusKind.warning, WorkspaceMessageCode.openFailed || WorkspaceMessageCode.createWritersideProjectFailed || diff --git a/lib/src/workspace/presentation/workspace_screen.dart b/lib/src/workspace/presentation/workspace_screen.dart index 576d286d..587ab8e5 100644 --- a/lib/src/workspace/presentation/workspace_screen.dart +++ b/lib/src/workspace/presentation/workspace_screen.dart @@ -1,3 +1,6 @@ +import '../../search/search_result_preview.dart'; +import '../../search/search_session.dart'; +import '../../search/search_sidebar.dart'; import '../../editor/wysiwyg/wysiwyg_commands.dart'; import '../../writerside/writerside_property_validation.dart'; import '../../writerside/writerside_effective_properties.dart'; @@ -51,7 +54,6 @@ import '../../app/linux/linux_page_frame.dart'; import '../../app/linux/linux_header_style.dart'; import '../../app/busymark_glyphs.dart'; import '../../app/busymark_main_menu.dart'; -import '../../app/busymark_search_field.dart'; import '../../app/busymark_toast.dart'; import '../../app/command_registry.dart'; import '../../app/localization.dart'; @@ -62,6 +64,7 @@ import '../../core/diagnostic.dart'; import '../../core/diagnostic_localizations.dart'; import '../../core/path_utils.dart' show isTextDocumentationPath, slugForHeading; + import '../../core/source_span.dart'; import '../../comparison/source_comparison.dart'; import '../../core/uri_utils.dart'; @@ -80,7 +83,6 @@ import '../../editor/source/source_controller.dart'; import '../../editor/source/source_autocomplete.dart'; import '../../editor/source/source_document.dart'; import '../../editor/source/source_editor.dart'; -import '../../editor/source/source_search.dart'; import '../../editor/wysiwyg/wysiwyg_editor.dart'; import '../../editor/wysiwyg/writerside_editing_adapter.dart'; import '../../editor/wysiwyg/wysiwyg_session_state.dart'; @@ -94,6 +96,7 @@ import '../../git/presentation/git_file_status_colors.dart'; import '../../git/presentation/git_sidebar_tab.dart'; import '../../markdown/busymark_document.dart'; import '../../markdown/document_outline.dart'; +import '../outline_navigation.dart'; import '../../markdown/markdown_model.dart'; import '../../markdown/markdown_parser.dart'; import '../../markdown/markdown_section_editor.dart'; @@ -163,24 +166,11 @@ class _ProblemsPresentation extends Notifier { } } -final _outlineNavigationTargetProvider = - NotifierProvider< - _OutlineNavigationTargetController, - _OutlineNavigationTarget? - >(_OutlineNavigationTargetController.new); -final _outlineViewportTargetProvider = - NotifierProvider<_OutlineViewportTargetController, _OutlineViewportTarget?>( - _OutlineViewportTargetController.new, - ); final _sourceNavigationTargetProvider = NotifierProvider< _SourceNavigationTargetController, _SourceNavigationTarget? >(_SourceNavigationTargetController.new); -final _workspaceSearchProvider = - NotifierProvider<_WorkspaceSearchController, _WorkspaceSearchState>( - _WorkspaceSearchController.new, - ); final _searchNavigationTargetProvider = NotifierProvider< _SearchNavigationTargetController, @@ -191,29 +181,6 @@ final _sidebarShortcutRequestProvider = _SidebarShortcutRequestController.new, ); -class _OutlineNavigationTargetController - extends Notifier<_OutlineNavigationTarget?> { - @override - _OutlineNavigationTarget? build() => null; - - void set(_OutlineNavigationTarget? target) { - state = target; - } -} - -class _OutlineViewportTargetController - extends Notifier<_OutlineViewportTarget?> { - @override - _OutlineViewportTarget? build() => null; - - void set(_OutlineViewportTarget target) { - if (state?.sameLocationAs(target) ?? false) { - return; - } - state = target; - } -} - class _SourceNavigationTargetController extends Notifier<_SourceNavigationTarget?> { @override @@ -224,189 +191,6 @@ class _SourceNavigationTargetController } } -class _WorkspaceSearchController extends Notifier<_WorkspaceSearchState> { - static const _debounceDelay = Duration(milliseconds: 120); - - Timer? _debounce; - final _worker = SourceSearchWorker(); - int _resultLimit = _maxWorkspaceSearchResults; - late Future Function(String path) _loadText; - var _request = 0; - var _disposed = false; - - @override - _WorkspaceSearchState build() { - _loadText = ref.read(workspaceServiceProvider).loadText; - ref.listen(workspaceControllerProvider, (previous, next) { - final activeBufferChanged = - previous?.activeBufferId != next.activeBufferId; - var shouldRefresh = _workspaceSearchInputsChanged(previous, next); - if (activeBufferChanged && !state.active) { - final options = next.activeBuffer?.editorState.searchOptions; - if (options != null) { - state = state - .withOptions(options) - .copyWith(matches: const [], searching: false); - } - } else if (!activeBufferChanged) { - final previousOptions = - previous?.activeBuffer?.editorState.searchOptions; - final nextOptions = next.activeBuffer?.editorState.searchOptions; - if (nextOptions != null && - previousOptions != nextOptions && - !_sameSourceSearchOptions(state.options, nextOptions)) { - state = state - .withOptions(nextOptions) - .copyWith(matches: const [], searching: false); - shouldRefresh = true; - } - } - if (shouldRefresh) { - refresh(next); - } - }); - ref.onDispose(() { - _disposed = true; - _request += 1; - _debounce?.cancel(); - _worker.dispose(); - }); - return const _WorkspaceSearchState(); - } - - void set(_WorkspaceSearchState searchState) { - final inputChanged = - state.query != searchState.query || - state.caseSensitive != searchState.caseSensitive || - state.wholeWord != searchState.wholeWord || - state.regex != searchState.regex || - state.active != searchState.active; - if (inputChanged) _resultLimit = _maxWorkspaceSearchResults; - state = searchState.copyWith( - matches: inputChanged ? const [] : state.matches, - searching: false, - ); - final buffer = ref.read(workspaceControllerProvider).activeBuffer; - if (buffer != null && - !_sameSourceSearchOptions( - buffer.editorState.searchOptions, - state.options, - )) { - ref - .read(workspaceControllerProvider.notifier) - .updateActiveEditorState( - buffer.editorState.copyWith(searchOptions: state.options), - ); - } - _schedule(ref.read(workspaceControllerProvider)); - } - - void refresh(WorkspaceState workspaceState) { - if (!state.active || state.options.query.isEmpty) { - return; - } - _schedule(workspaceState); - } - - Future submit() async { - _debounce?.cancel(); - _worker.cancel(); - final request = ++_request; - final workspaceState = ref.read(workspaceControllerProvider); - final options = state.options; - if (!state.active || - options.query.isEmpty || - workspaceState.workspace == null) { - return false; - } - state = state.copyWith(matches: const [], searching: true); - await _runSearch( - request: request, - workspaceState: workspaceState, - options: options, - ); - return !_disposed && request == _request; - } - - void _schedule(WorkspaceState workspaceState) { - _debounce?.cancel(); - _worker.cancel(); - final request = ++_request; - final workspace = workspaceState.workspace; - final options = state.options; - if (!state.active || options.query.isEmpty || workspace == null) { - final clearMatches = options.query.isEmpty || workspace == null; - if (state.searching || (clearMatches && state.matches.isNotEmpty)) { - state = state.copyWith( - matches: clearMatches ? const [] : state.matches, - searching: false, - ); - } - return; - } - state = state.copyWith( - matches: const [], - searching: true, - truncated: false, - skippedFiles: const [], - invalidRegex: false, - hasZeroLengthMatches: false, - ); - _debounce = Timer(_debounceDelay, () { - _debounce = null; - unawaited( - _runSearch( - request: request, - workspaceState: workspaceState, - options: options, - ), - ); - }); - } - - Future _runSearch({ - required int request, - required WorkspaceState workspaceState, - required SourceSearchOptions options, - }) async { - try { - final result = await _loadWorkspaceSearchMatches( - workspaceState, - options, - loadText: _loadText, - isCancelled: () => _disposed || request != _request, - worker: _worker, - maximumResults: _resultLimit, - ); - if (_disposed || request != _request) { - return; - } - state = state.copyWith( - matches: result.matches, - searching: false, - truncated: result.truncated, - skippedFiles: result.skippedFiles, - invalidRegex: result.invalidRegex, - hasZeroLengthMatches: result.hasZeroLengthMatches, - ); - } on Object { - if (_disposed || request != _request) { - return; - } - state = state.copyWith( - matches: const [], - searching: false, - skippedFiles: [workspaceState.workspace?.filesystemRootPath ?? ''], - ); - } - } - - void showMore() { - _resultLimit += _maxWorkspaceSearchResults; - _schedule(ref.read(workspaceControllerProvider)); - } -} - class _SearchNavigationTargetController extends Notifier<_SearchNavigationTarget?> { @override @@ -430,51 +214,6 @@ class _SidebarShortcutRequestController extends Notifier<_SidebarTab?> { } } -class _OutlineNavigationTarget { - const _OutlineNavigationTarget({ - required this.workspaceId, - required this.bufferId, - required this.filePath, - required this.headingId, - required this.line, - this.editorBlockId, - }); - - final String workspaceId; - final String bufferId; - final String? filePath; - final String headingId; - final int? line; - final String? editorBlockId; -} - -class _OutlineViewportTarget { - const _OutlineViewportTarget({ - required this.workspaceId, - required this.bufferId, - required this.filePath, - required this.headingId, - required this.sourceStartOffset, - required this.editorBlockId, - }); - - final String workspaceId; - final String bufferId; - final String? filePath; - final String? headingId; - final int? sourceStartOffset; - final String? editorBlockId; - - bool sameLocationAs(_OutlineViewportTarget other) { - return workspaceId == other.workspaceId && - bufferId == other.bufferId && - filePath == other.filePath && - headingId == other.headingId && - sourceStartOffset == other.sourceStartOffset && - editorBlockId == other.editorBlockId; - } -} - class _SourceNavigationTarget { const _SourceNavigationTarget({ required this.filePath, @@ -493,149 +232,6 @@ class _SourceNavigationTarget { final bool xmlChildren; } -class _WorkspaceSearchState { - const _WorkspaceSearchState({ - this.active = false, - this.query = '', - this.caseSensitive = false, - this.wholeWord = false, - this.regex = false, - this.matches = const [], - this.searching = false, - this.truncated = false, - this.skippedFiles = const [], - this.invalidRegex = false, - this.hasZeroLengthMatches = false, - }); - - final bool active; - final String query; - final bool caseSensitive; - final bool wholeWord; - final bool regex; - final List<_WorkspaceSearchMatch> matches; - final bool searching; - final bool truncated; - final List skippedFiles; - final bool invalidRegex; - final bool hasZeroLengthMatches; - - SourceSearchOptions get options => SourceSearchOptions( - query: query, - caseSensitive: caseSensitive, - wholeWord: wholeWord, - regex: regex, - ); - - _WorkspaceSearchState copyWith({ - bool? active, - String? query, - bool? caseSensitive, - bool? wholeWord, - bool? regex, - List<_WorkspaceSearchMatch>? matches, - bool? searching, - bool? truncated, - List? skippedFiles, - bool? invalidRegex, - bool? hasZeroLengthMatches, - }) { - return _WorkspaceSearchState( - active: active ?? this.active, - query: query ?? this.query, - caseSensitive: caseSensitive ?? this.caseSensitive, - wholeWord: wholeWord ?? this.wholeWord, - regex: regex ?? this.regex, - matches: matches ?? this.matches, - searching: searching ?? this.searching, - truncated: truncated ?? this.truncated, - skippedFiles: skippedFiles ?? this.skippedFiles, - invalidRegex: invalidRegex ?? this.invalidRegex, - hasZeroLengthMatches: hasZeroLengthMatches ?? this.hasZeroLengthMatches, - ); - } - - _WorkspaceSearchState withOptions(SourceSearchOptions options) { - return copyWith( - query: options.query, - caseSensitive: options.caseSensitive, - wholeWord: options.wholeWord, - regex: options.regex, - ); - } -} - -bool _sameSourceSearchOptions( - SourceSearchOptions first, - SourceSearchOptions second, -) { - return first.query == second.query && - first.caseSensitive == second.caseSensitive && - first.wholeWord == second.wholeWord && - first.regex == second.regex; -} - -bool _workspaceSearchInputsChanged( - WorkspaceState? previous, - WorkspaceState next, -) { - if (previous == null || - previous.activeBufferId != next.activeBufferId || - previous.activeText != next.activeText) { - return true; - } - if (previous.documentBuffers.length != next.documentBuffers.length) { - return true; - } - for (var index = 0; index < next.documentBuffers.length; index++) { - final before = previous.documentBuffers[index]; - final after = next.documentBuffers[index]; - if (before.id != after.id || - before.filePath != after.filePath || - before.revision != after.revision || - before.text != after.text) { - return true; - } - } - final previousWorkspace = previous.workspace; - final nextWorkspace = next.workspace; - if (identical(previousWorkspace, nextWorkspace)) { - return false; - } - if (previousWorkspace == null || nextWorkspace == null) { - return previousWorkspace != nextWorkspace; - } - final previousActivePath = previous.activeBuffer?.filePath; - final nextActivePath = next.activeBuffer?.filePath; - return previousActivePath != nextActivePath || - !_sameWorkspaceSearchFiles(previousWorkspace.files, nextWorkspace.files); -} - -bool _sameWorkspaceSearchFiles( - List first, - List second, -) { - final firstSearchable = first.where(_isOpenableTextDocument).toList() - ..sort((left, right) => left.absolutePath.compareTo(right.absolutePath)); - final secondSearchable = second.where(_isOpenableTextDocument).toList() - ..sort((left, right) => left.absolutePath.compareTo(right.absolutePath)); - if (firstSearchable.length != secondSearchable.length) { - return false; - } - for (var index = 0; index < firstSearchable.length; index++) { - final left = firstSearchable[index]; - final right = secondSearchable[index]; - if (left.absolutePath != right.absolutePath || - left.relativePath != right.relativePath || - left.kind != right.kind || - left.size != right.size || - left.lastModified != right.lastModified) { - return false; - } - } - return true; -} - class _SearchNavigationTarget { const _SearchNavigationTarget({ required this.filePath, @@ -658,46 +254,12 @@ class _SearchNavigationTarget { final int request; } -class _ToggleSearchIntent extends Intent { - const _ToggleSearchIntent(); -} - -class _OpenSearchIntent extends Intent { - const _OpenSearchIntent(); -} - class _SelectSidebarTabIntent extends Intent { const _SelectSidebarTabIntent(this.tab); final _SidebarTab tab; } -final _headerSearchFocusRequestProvider = - NotifierProvider<_HeaderSearchFocusRequest, int>( - _HeaderSearchFocusRequest.new, - ); - -class _HeaderSearchFocusRequest extends Notifier { - @override - int build() => 0; - void request() => state++; -} - -final _searchReturnFocusProvider = Provider<_SearchReturnFocus>( - (ref) => _SearchReturnFocus(), -); - -class _SearchReturnFocus { - FocusNode? node; - void restore() { - final target = node; - node = null; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (target?.context?.mounted == true) target!.requestFocus(); - }); - } -} - Future _publishWritersideTitleDialog( BuildContext context, WidgetRef ref, @@ -900,19 +462,19 @@ class WorkspaceScreen extends ConsumerWidget { if (workspace == null) { return const WelcomeScreen(); } - final searchState = ref.watch(_workspaceSearchProvider); + final searchState = ref.watch(searchSessionProvider); final documentViewMode = state.activeBuffer?.editorState.mode ?? settings.documentViewMode; final gitState = ref.watch(gitControllerProvider); final localHistoryState = ref.watch(localHistoryControllerProvider); - final searchResults = _workspaceSearchResults(context, searchState.matches); final colors = BusyMarkSurfaceColors.of(context); final headerBar = ref.watch(linuxHeaderBarServiceProvider); final settingsController = ref.read(appSettingsControllerProvider.notifier); final sidebarVisible = - settings.sidebarVisible && _hasWorkspaceSidebar(workspace); - final documentOutline = _activeDocumentOutline(state); + searchState.active || + (settings.sidebarVisible && _hasWorkspaceSidebar(workspace)); + final documentOutline = activeDocumentOutline(state); final canExportPdf = canExportWorkspacePdf(state); final canExportHtml = canExportWorkspaceHtml(state); final sidebar = SizedBox( @@ -922,8 +484,6 @@ class WorkspaceScreen extends ConsumerWidget { workspace: workspace, outline: documentOutline, searchState: searchState, - searchResults: searchResults, - onOpenSearchResult: (result) => _openSearchResult(context, ref, result), ), ); final workspaceContent = Column( @@ -981,44 +541,16 @@ class WorkspaceScreen extends ConsumerWidget { _handleHeaderBarAction(context, ref, event.action); }); }); - ref.listen(headerBarSearchEventsProvider, (previous, next) { - next.whenData((event) { - switch (event) { - case HeaderBarSearchQueryChanged(:final query): - final current = ref.read(_workspaceSearchProvider); - if (current.query == query && current.active) { - return; - } - _clearGitDetailSelection(ref); - ref - .read(_workspaceSearchProvider.notifier) - .set(current.copyWith(active: true, query: query)); - unawaited(settingsController.setSidebarVisible(true)); - case HeaderBarSearchSubmitted(:final query): - final current = ref.read(_workspaceSearchProvider); - ref - .read(_workspaceSearchProvider.notifier) - .set(current.copyWith(active: true, query: query)); - unawaited(settingsController.setSidebarVisible(true)); - unawaited(_submitSearch(context, ref)); - case HeaderBarSearchCleared(): - _clearSearchQuery(ref); - case HeaderBarSearchEscapePressed(): - _closeSearch(ref); - case HeaderBarSearchFocusChanged(): - break; - } - }); - }); - ref.listen(workspaceSearchOpenRequestProvider, (previous, next) { - if (next != previous) { - _openSearch(ref); - } - }); - ref.listen(workspaceSearchCloseRequestProvider, (previous, next) { - if (next != previous) { - _closeSearch(ref); - } + ref.listen(workspaceNavigationCommandProvider, (_, request) { + if (request == null) return; + final tab = switch (request.$1) { + BusyMarkCommandIds.sidebarFiles => _SidebarTab.files, + BusyMarkCommandIds.sidebarToc => _SidebarTab.toc, + BusyMarkCommandIds.sidebarOutline => _SidebarTab.outline, + BusyMarkCommandIds.sidebarGit => _SidebarTab.git, + _ => null, + }; + if (tab != null) _selectSidebarShortcut(ref, tab); }); ref.listen(clipboardHistoryOpenRequestProvider, (previous, next) { if (next == previous) return; @@ -1033,7 +565,10 @@ class WorkspaceScreen extends ConsumerWidget { if (next == previous) return; _selectSidebarShortcut(ref, _SidebarTab.localHistory); final buffer = ref.read(workspaceControllerProvider).activeBuffer; - if (buffer != null) { + if (buffer != null && + !ref + .read(localHistoryControllerProvider) + .inspectingRetainedDocument) { unawaited( ref .read(localHistoryControllerProvider.notifier) @@ -1041,11 +576,6 @@ class WorkspaceScreen extends ConsumerWidget { ); } }); - ref.listen(localHistoryFindRequestProvider, (previous, next) { - if (next == previous) return; - _selectSidebarShortcut(ref, _SidebarTab.localHistory); - ref.read(localHistoryControllerProvider.notifier).beginDocumentSearch(); - }); ref.listen(workspaceControllerProvider, (previous, next) { final nextWorkspace = next.workspace; if (nextWorkspace == null) { @@ -1087,53 +617,20 @@ class WorkspaceScreen extends ConsumerWidget { enabled: headerBar.isAvailable, child: Shortcuts( shortcuts: { - commandRegistry[BusyMarkCommandIds.search]!.shortcut!.activator: - const _OpenSearchIntent(), - commandRegistry[BusyMarkCommandIds.sidebarFiles]!.shortcut!.activator: - const _SelectSidebarTabIntent(_SidebarTab.files), - const SingleActivator(LogicalKeyboardKey.numpad1, control: true): - const _SelectSidebarTabIntent(_SidebarTab.files), - commandRegistry[BusyMarkCommandIds.sidebarToc]!.shortcut!.activator: - const _SelectSidebarTabIntent(_SidebarTab.toc), - const SingleActivator(LogicalKeyboardKey.numpad2, control: true): - const _SelectSidebarTabIntent(_SidebarTab.toc), - commandRegistry[BusyMarkCommandIds.sidebarOutline]! - .shortcut! - .activator: const _SelectSidebarTabIntent( - _SidebarTab.outline, - ), - const SingleActivator(LogicalKeyboardKey.numpad3, control: true): - const _SelectSidebarTabIntent(_SidebarTab.outline), - commandRegistry[BusyMarkCommandIds.sidebarGit]!.shortcut!.activator: - const _SelectSidebarTabIntent(_SidebarTab.git), - const SingleActivator(LogicalKeyboardKey.numpad4, control: true): - const _SelectSidebarTabIntent(_SidebarTab.git), - commandRegistry[BusyMarkCommandIds.localHistory]!.shortcut!.activator: - const _SelectSidebarTabIntent(_SidebarTab.localHistory), - const SingleActivator(LogicalKeyboardKey.numpad5, control: true): - const _SelectSidebarTabIntent(_SidebarTab.localHistory), - commandRegistry[BusyMarkCommandIds.clipboardHistory]! - .shortcut! - .activator: const _SelectSidebarTabIntent( - _SidebarTab.clipboard, - ), - const SingleActivator(LogicalKeyboardKey.numpad6, control: true): - const _SelectSidebarTabIntent(_SidebarTab.clipboard), + for (final entry in const { + BusyMarkCommandIds.sidebarFiles: _SidebarTab.files, + BusyMarkCommandIds.sidebarToc: _SidebarTab.toc, + BusyMarkCommandIds.sidebarGit: _SidebarTab.git, + BusyMarkCommandIds.localHistory: _SidebarTab.localHistory, + BusyMarkCommandIds.clipboardHistory: _SidebarTab.clipboard, + }.entries) + ...commandRegistry.bindingIntents( + entry.key, + _SelectSidebarTabIntent(entry.value), + ), }, child: Actions( actions: { - _OpenSearchIntent: CallbackAction<_OpenSearchIntent>( - onInvoke: (intent) { - _openSearch(ref); - return null; - }, - ), - _ToggleSearchIntent: CallbackAction<_ToggleSearchIntent>( - onInvoke: (intent) { - _toggleSearch(ref); - return null; - }, - ), _SelectSidebarTabIntent: CallbackAction<_SelectSidebarTabIntent>( onInvoke: (intent) { _selectSidebarShortcut(ref, intent.tab); @@ -1154,7 +651,8 @@ class WorkspaceScreen extends ConsumerWidget { key: const ValueKey('workspace-page-frame'), sidebarHeader: const BusyMarkLinuxBrandHeader(), sidebarBody: sidebar, - sidebarAvailable: sidebarAvailable, + sidebarAvailable: sidebarAvailable || searchState.active, + sidebarOverlay: searchState.active && !sidebarAvailable, sidebarExpanded: sidebarVisible, sidebarTransitionGeneration: ref.watch( sidebarTransitionGenerationProvider, @@ -1215,18 +713,7 @@ class WorkspaceScreen extends ConsumerWidget { duration: BusyMarkMotion.search, children: [ BusyMarkLinuxHeaderTitle(title), - _HeaderSearchField( - active: searchState.active, - focusRequest: ref.watch( - _headerSearchFocusRequestProvider, - ), - query: searchState.query, - onChanged: (query) => _setSearchQuery(ref, query), - onClear: () => _clearSearchQuery(ref), - onSubmitted: () => - unawaited(_submitSearch(context, ref)), - onEscape: () => _closeSearch(ref), - ), + const SearchHeaderField(), ], ), trailing: BusyMarkLinuxHeaderControlGroup( @@ -1276,9 +763,6 @@ class WorkspaceScreen extends ConsumerWidget { tooltip: context.l10n.search, icon: BusyMarkGlyphs.search, selected: searchState.active, - shortcut: commandRegistry[BusyMarkCommandIds.search] - ?.shortcut - ?.label, onPressed: () => _toggleSearch(ref), ), BusyMarkMainMenuButton( @@ -1330,54 +814,8 @@ class WorkspaceScreen extends ConsumerWidget { ); } - void _toggleSearch(WidgetRef ref) { - final search = ref.read(_workspaceSearchProvider); - if (search.active) { - _closeSearch(ref); - } else { - _openSearch(ref); - } - } - - void _openSearch(WidgetRef ref) { - if (ref.read(workspaceControllerProvider).workspace?.isRemote == true) { - final target = ref.context; - unawaited(showNotesSearch(target, ref)); - return; - } - final search = ref.read(_workspaceSearchProvider); - ref.read(_headerSearchFocusRequestProvider.notifier).request(); - if (!search.active) { - ref.read(_searchReturnFocusProvider).node = - FocusManager.instance.primaryFocus; - } - if (search.active) { - unawaited( - ref - .read(appSettingsControllerProvider.notifier) - .setSidebarVisible(true), - ); - return; - } - _clearGitDetailSelection(ref); - ref - .read(_workspaceSearchProvider.notifier) - .set(search.copyWith(active: true)); - unawaited( - ref.read(appSettingsControllerProvider.notifier).setSidebarVisible(true), - ); - } - - void _closeSearch(WidgetRef ref) { - final search = ref.read(_workspaceSearchProvider); - if (!search.active) { - return; - } - ref - .read(_workspaceSearchProvider.notifier) - .set(search.copyWith(active: false)); - ref.read(_searchReturnFocusProvider).restore(); - } + void _toggleSearch(WidgetRef ref) => + ref.read(searchSessionProvider.notifier).toggle(); void _selectSidebarShortcut(WidgetRef ref, _SidebarTab tab) { final state = ref.read(workspaceControllerProvider); @@ -1388,40 +826,13 @@ class WorkspaceScreen extends ConsumerWidget { if (workspace == null || !_sidebarTabsFor(workspace.kind).contains(tab)) { return; } - _closeSearch(ref); + ref.read(searchSessionProvider.notifier).close(restoreFocus: false); unawaited( ref.read(appSettingsControllerProvider.notifier).setSidebarVisible(true), ); ref.read(_sidebarShortcutRequestProvider.notifier).select(tab); } - void _setSearchQuery(WidgetRef ref, String query) { - final current = ref.read(_workspaceSearchProvider); - ref - .read(_workspaceSearchProvider.notifier) - .set(current.copyWith(active: true, query: query)); - } - - void _clearSearchQuery(WidgetRef ref) { - final current = ref.read(_workspaceSearchProvider); - ref - .read(_workspaceSearchProvider.notifier) - .set(current.copyWith(query: '')); - } - - Future _submitSearch(BuildContext context, WidgetRef ref) async { - final current = await ref.read(_workspaceSearchProvider.notifier).submit(); - if (!current || !context.mounted) { - return; - } - final matches = ref.read(_workspaceSearchProvider).matches; - if (matches.isEmpty) { - return; - } - final result = _workspaceSearchResults(context, matches).first; - await _openSearchResult(context, ref, result); - } - void _handleHeaderBarAction( BuildContext context, WidgetRef ref, @@ -1446,8 +857,6 @@ class WorkspaceScreen extends ConsumerWidget { execute(BusyMarkCommandIds.fullScreen); case HeaderBarAction.settings: execute(BusyMarkCommandIds.settings); - case HeaderBarAction.keyboardShortcuts: - execute(BusyMarkCommandIds.keyboardShortcuts); case HeaderBarAction.syntaxReference: execute(BusyMarkCommandIds.syntaxReference); case HeaderBarAction.reportIssue: @@ -1497,11 +906,6 @@ class WorkspaceScreen extends ConsumerWidget { unawaited(ref.read(windowControlServiceProvider).toggleFullScreen()); case BusyMarkMainMenuAction.settings: context.go(settingsLocation(SettingsReturnTarget.workspace)); - case BusyMarkMainMenuAction.keyboardShortcuts: - showBusyMarkKeyboardShortcutsDialog(context); - case BusyMarkMainMenuAction.quickOpen: - case BusyMarkMainMenuAction.commandPalette: - return; case BusyMarkMainMenuAction.syntaxReference: showBusyMarkSyntaxReferenceDialog(context); case BusyMarkMainMenuAction.reportIssue: @@ -1570,46 +974,7 @@ class WorkspaceScreen extends ConsumerWidget { DocumentViewModePreference.preview => BusyMarkCommandIds.viewReading, DocumentViewModePreference.split => BusyMarkCommandIds.viewSplit, }; - return commands[id]!.shortcut!.label; - } - - Future _openSearchResult( - BuildContext context, - WidgetRef ref, - _WorkspaceSearchResult result, - ) async { - final state = ref.read(workspaceControllerProvider); - final workspace = state.workspace; - final searchOptions = ref.read(_workspaceSearchProvider).options; - if (workspace == null) { - return; - } - final activePath = state.activeBuffer?.filePath; - if (activePath != result.filePath) { - final opened = await ref - .read(workspaceControllerProvider.notifier) - .openActiveFile(result.filePath); - if (!opened) return; - } - if (!context.mounted || - ref.read(workspaceControllerProvider).workspace?.id != workspace.id || - ref.read(_workspaceSearchProvider).options != searchOptions) { - return; - } - _clearGitDetailSelection(ref); - final previous = ref.read(_searchNavigationTargetProvider); - ref - .read(_searchNavigationTargetProvider.notifier) - .set( - _SearchNavigationTarget( - filePath: result.filePath, - line: result.line, - startOffset: result.startOffset, - endOffset: result.endOffset, - query: result.query, - request: (previous?.request ?? 0) + 1, - ), - ); + return commands[id]?.shortcut?.label ?? ''; } Future _showProblemsDialog(BuildContext context, WidgetRef ref) async { @@ -2268,97 +1633,25 @@ class _GitFileList extends StatelessWidget { } } -class _HeaderSearchField extends StatefulWidget { - const _HeaderSearchField({ - required this.query, - required this.onChanged, - required this.onClear, - required this.onSubmitted, - required this.onEscape, - this.active = true, - this.focusRequest = 0, - }); - - final bool active; - final int focusRequest; - final String query; - final ValueChanged onChanged; - final VoidCallback onClear; - final VoidCallback onSubmitted; - final VoidCallback onEscape; - - @override - State<_HeaderSearchField> createState() => _HeaderSearchFieldState(); -} - -class _HeaderSearchFieldState extends State<_HeaderSearchField> { - late final TextEditingController _controller; - var _focusRequest = 0; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.query); - } - - @override - void didUpdateWidget(covariant _HeaderSearchField oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.active && - (!oldWidget.active || widget.focusRequest != oldWidget.focusRequest)) { - _focusRequest++; - } - if (widget.query != _controller.text) { - _controller.value = TextEditingValue( - text: widget.query, - selection: TextSelection.collapsed(offset: widget.query.length), - ); - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BusyMarkSearchField( - controller: _controller, - hintText: context.l10n.search, - autofocus: widget.active, - clearOnEscape: false, - focusRequest: _focusRequest, - onChanged: widget.onChanged, - onSubmitted: (_) => widget.onSubmitted(), - onClear: widget.onClear, - onEscape: widget.onEscape, - ); - } -} - class _Sidebar extends ConsumerStatefulWidget { const _Sidebar({ super.key, required this.workspace, required this.outline, required this.searchState, - required this.searchResults, - required this.onOpenSearchResult, }); final Workspace workspace; final List outline; - final _WorkspaceSearchState searchState; - final List<_WorkspaceSearchResult> searchResults; - final Future Function(_WorkspaceSearchResult result) onOpenSearchResult; + final SearchSessionState searchState; @override ConsumerState<_Sidebar> createState() => _SidebarState(); } class _SidebarState extends ConsumerState<_Sidebar> { + final _outlineKey = GlobalKey<_OutlineTabState>(); + final _notesKey = GlobalKey(); late final WorkspaceController _topicActionController; late int _tab; late String _workspaceId; @@ -2395,9 +1688,6 @@ class _SidebarState extends ConsumerState<_Sidebar> { @override void didUpdateWidget(covariant _Sidebar oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.searchState.active && !oldWidget.searchState.active) { - _clearGitDetailSelection(ref); - } if (widget.workspace.id != _workspaceId) { _workspaceId = widget.workspace.id; _activeFilePath = widget.workspace.activeFilePath; @@ -2415,12 +1705,14 @@ class _SidebarState extends ConsumerState<_Sidebar> { Widget build(BuildContext context) { final tabs = _sidebarTabsFor(widget.workspace.kind); ref.listen(workspaceControllerProvider, (previous, next) { - if (_workspaceSearchInputsChanged(previous, next)) { + if (workspaceSearchInputsChanged(previous, next)) { _replacementCancellation?.cancel(); } }); - ref.listen(_workspaceSearchProvider, (previous, next) { + ref.listen(searchSessionProvider, (previous, next) { if (previous?.options != next.options || + previous?.category != next.category || + previous?.recovery != next.recovery || previous?.active != next.active) { _replacementCancellation?.cancel(); } @@ -2448,110 +1740,146 @@ class _SidebarState extends ConsumerState<_Sidebar> { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _SidebarHeader( - workspace: widget.workspace, - tabs: tabs, - selectedTab: widget.searchState.active ? null : selectedTab, - repositoryInfo: repositoryInfo, - showTabMenu: !widget.searchState.active && tabs.length > 1, - onSelectTab: (tab) => _selectTab(tab, tabs), - loadGitMenuItems: (menuContext, repository) => - _loadWorkspaceGitMenuItems(menuContext, ref, repository), - onGitAction: (menuContext, action) => - _performWorkspaceGitAction(menuContext, ref, action), - onRefineActiveDocument: () => - unawaited(_refineActiveDocumentWithAi(context)), - ), + if (!widget.searchState.active) + _SidebarHeader( + workspace: widget.workspace, + tabs: tabs, + selectedTab: selectedTab, + repositoryInfo: repositoryInfo, + showTabMenu: tabs.length > 1, + onSelectTab: (tab) => _selectTab(tab, tabs), + loadGitMenuItems: (menuContext, repository) => + _loadWorkspaceGitMenuItems(menuContext, ref, repository), + onGitAction: (menuContext, action) => + _performWorkspaceGitAction(menuContext, ref, action), + onRefineActiveDocument: () => + unawaited(_refineActiveDocumentWithAi(context)), + ), Expanded( - child: BusyMarkKeyedCrossfade( - transitionKey: widget.searchState.active - ? 'search' - : _topicRenameReview != null - ? 'rename' - : _topicUsageReview != null - ? 'usages' - : selectedTab ?? 'empty', - child: widget.searchState.active - ? _SearchSidebar( - query: widget.searchState.query, - results: widget.searchResults, - searching: widget.searchState.searching, - state: widget.searchState, - onShowMore: () => ref - .read(_workspaceSearchProvider.notifier) - .showMore(), - onOpenResult: widget.onOpenSearchResult, - onReviewReplacement: _reviewWorkspaceReplacement, - ) - : _topicRenameReview != null - ? _WritersideTopicRenameSidebar( - plan: _topicRenameReview!, - onBack: () => setState(() => _topicRenameReview = null), - onDoRefactor: () => _applyReviewedTopicRename(context), - ) - : _topicUsageReview != null - ? _WritersideTopicUsagesSidebar( - review: _topicUsageReview!, - onBack: _closeTopicUsageReview, - onOpenUsage: (usage) => - _openWritersideTopicUsage(context, usage), - onDoRefactor: () => - _resumeWritersideTopicRemoval(context), - ) - : switch (selectedTab) { - _SidebarTab.notes => NextcloudNotesSidebar( - accountId: widget.workspace.nextcloudAccountId!, - ), - _SidebarTab.files => _FilesTab( - workspace: widget.workspace, - onShowFileHistory: _showFileHistory, - onRenameTopic: (path) => - _startWritersideTopicRename(context, path), - onRequestTopicRemoval: (target) => - _runWritersideTopicRemoval(context, target), - ), - _SidebarTab.toc => _TocTab( - workspace: widget.workspace, - onShowFileHistory: _showFileHistory, - onRenameTopic: (path) => - _startWritersideTopicRename(context, path), - onRequestTopicRemoval: (target) => - _runWritersideTopicRemoval(context, target), - ), - _SidebarTab.outline => _OutlineTab( + child: Stack( + fit: StackFit.expand, + children: [ + Offstage( + offstage: + widget.searchState.active || + selectedTab != _SidebarTab.outline, + child: ExcludeFocus( + excluding: + widget.searchState.active || + selectedTab != _SidebarTab.outline, + child: TickerMode( + enabled: + !widget.searchState.active && + selectedTab == _SidebarTab.outline, + child: _OutlineTab( + key: _outlineKey, workspace: widget.workspace, bufferId: ref .watch(workspaceControllerProvider) .activeBufferId, headings: widget.outline, ), - _SidebarTab.git => GitSidebarTab( - workspace: widget.workspace, - onOpenFile: (relativePath) => - _openGitDiffFile(context, ref, relativePath), - onConfirmDiscard: (files) => - _confirmDiscardGitFiles(context, ref, files), - onAfterWorkspaceFilesChanged: () => - _refreshWorkspaceAfterGitFileChanges(ref), - onConfirmSwitchBranch: (branchName) => - _confirmSwitchGitBranch(context, ref, branchName), - onConfirmPushSetUpstream: () => - _confirmGitPushSetUpstream(context, ref), - ), - _SidebarTab.clipboard => ClipboardHistoryPanel( - onEscape: () => ref - .read(clipboardInsertionRegistryProvider) - .target - ?.requestEditorFocus(), - ), - _SidebarTab.references => const ReferencesPanel(), - _SidebarTab.localHistory => LocalHistoryPanel( - focusSearchRequest: ref.watch( - localHistoryFindRequestProvider, - ), - ), - null => const SizedBox.shrink(), - }, + ), + ), + ), + Offstage( + offstage: + !widget.searchState.active && + selectedTab == _SidebarTab.outline, + child: ExcludeFocus( + excluding: + !widget.searchState.active && + selectedTab == _SidebarTab.outline, + child: BusyMarkKeyedCrossfade( + transitionKey: widget.searchState.active + ? 'search' + : _topicRenameReview != null + ? 'rename' + : _topicUsageReview != null + ? 'usages' + : selectedTab ?? 'empty', + child: widget.searchState.active + ? SearchSidebar( + onReviewReplacement: _reviewWorkspaceReplacement, + ) + : _topicRenameReview != null + ? _WritersideTopicRenameSidebar( + plan: _topicRenameReview!, + onBack: () => + setState(() => _topicRenameReview = null), + onDoRefactor: () => + _applyReviewedTopicRename(context), + ) + : _topicUsageReview != null + ? _WritersideTopicUsagesSidebar( + review: _topicUsageReview!, + onBack: _closeTopicUsageReview, + onOpenUsage: (usage) => + _openWritersideTopicUsage(context, usage), + onDoRefactor: () => + _resumeWritersideTopicRemoval(context), + ) + : switch (selectedTab) { + _SidebarTab.notes => NextcloudNotesSidebar( + key: _notesKey, + accountId: widget.workspace.nextcloudAccountId!, + ), + _SidebarTab.files => _FilesTab( + workspace: widget.workspace, + onShowFileHistory: _showFileHistory, + onRenameTopic: (path) => + _startWritersideTopicRename(context, path), + onRequestTopicRemoval: (target) => + _runWritersideTopicRemoval(context, target), + ), + _SidebarTab.toc => _TocTab( + workspace: widget.workspace, + onShowFileHistory: _showFileHistory, + onRenameTopic: (path) => + _startWritersideTopicRename(context, path), + onRequestTopicRemoval: (target) => + _runWritersideTopicRemoval(context, target), + ), + _SidebarTab.outline => const SizedBox.shrink(), + _SidebarTab.git => GitSidebarTab( + workspace: widget.workspace, + onOpenFile: (relativePath) => _openGitDiffFile( + context, + ref, + relativePath, + ), + onConfirmDiscard: (files) => + _confirmDiscardGitFiles( + context, + ref, + files, + ), + onAfterWorkspaceFilesChanged: () => + _refreshWorkspaceAfterGitFileChanges(ref), + onConfirmSwitchBranch: (branchName) => + _confirmSwitchGitBranch( + context, + ref, + branchName, + ), + onConfirmPushSetUpstream: () => + _confirmGitPushSetUpstream(context, ref), + ), + _SidebarTab.clipboard => ClipboardHistoryPanel( + onEscape: () => ref + .read(clipboardInsertionRegistryProvider) + .target + ?.requestEditorFocus(), + ), + _SidebarTab.references => const ReferencesPanel(), + _SidebarTab.localHistory => + const LocalHistoryPanel(), + null => const SizedBox.shrink(), + }, + ), + ), + ), + ], ), ), ], @@ -2564,6 +1892,7 @@ class _SidebarState extends ConsumerState<_Sidebar> { List<_SidebarTab> tabs, { bool showGitChanges = true, }) { + ref.read(searchSessionProvider.notifier).close(restoreFocus: false); final index = tabs.indexOf(tab); if (index < 0) { return; @@ -2571,6 +1900,17 @@ class _SidebarState extends ConsumerState<_Sidebar> { setState(() { _tab = index; }); + if (tab == _SidebarTab.outline || tab == _SidebarTab.notes) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && !ref.read(searchSessionProvider).active) { + if (tab == _SidebarTab.outline) { + _outlineKey.currentState?.requestNavigationFocus(); + } else { + _notesKey.currentState?.requestNavigationFocus(); + } + } + }); + } if (tab != _SidebarTab.localHistory) { ref.read(localHistoryControllerProvider.notifier).clearComparison(); } @@ -2979,6 +2319,18 @@ class _SidebarState extends ConsumerState<_Sidebar> { } Future _reviewWorkspaceReplacement() async { + final request = ref.read(searchSessionProvider); + final workspaceId = ref.read(workspaceControllerProvider).workspace?.id; + bool currentRequest() { + if (!mounted) return false; + final current = ref.read(searchSessionProvider); + return current.workspaceActive && + !current.recovery && + current.generation == request.generation && + ref.read(workspaceControllerProvider).workspace?.id == workspaceId; + } + + if (!currentRequest() || widget.workspace.isRemote) return; var replacement = ''; final headerBar = ref.read(linuxHeaderBarServiceProvider); final requested = await showBusyMarkModalEditorDialog( @@ -3007,7 +2359,7 @@ class _SidebarState extends ConsumerState<_Sidebar> { ], ), ); - if (requested == null || !mounted) { + if (requested == null || !mounted || !currentRequest()) { return; } final service = const SearchReplacementService(); @@ -3036,6 +2388,7 @@ class _SidebarState extends ConsumerState<_Sidebar> { _replacementCancellation = null; } if (!mounted || + !currentRequest() || preview == null || preview.issues.any( (issue) => issue.kind == WorkspaceReplacementIssueKind.cancelled, @@ -3054,7 +2407,7 @@ class _SidebarState extends ConsumerState<_Sidebar> { builder: (dialogContext) => _WorkspaceReplacementReviewDialog(preview: preview), ); - if (selected == null || selected.isEmpty || !mounted) { + if (selected == null || selected.isEmpty || !mounted || !currentRequest()) { return; } final normalizations = {}; @@ -3069,7 +2422,7 @@ class _SidebarState extends ConsumerState<_Sidebar> { context, file.relativePath, ); - if (normalization == null || !mounted) { + if (normalization == null || !mounted || !currentRequest()) { return; } normalizations[file.filePath] = normalization; @@ -3087,7 +2440,7 @@ class _SidebarState extends ConsumerState<_Sidebar> { await controller.validateActive(); await ref.read(gitControllerProvider.notifier).refresh(); ref - .read(_workspaceSearchProvider.notifier) + .read(searchSessionProvider.notifier) .refresh(ref.read(workspaceControllerProvider)); if (!mounted) { return; @@ -3166,7 +2519,9 @@ class _SidebarState extends ConsumerState<_Sidebar> { final requestedIndex = request == null ? -1 : tabs.indexOf(request); return requestedIndex >= 0 ? requestedIndex - : _preferredSidebarTabIndex(workspace); + : (tabs.contains(_SidebarTab.files) + ? tabs.indexOf(_SidebarTab.files) + : _preferredSidebarTabIndex(workspace)); } } @@ -4460,71 +3815,88 @@ class _WritersideTopicRenameSidebarState ), ), ), - Padding( - padding: const EdgeInsets.all(BusyMarkSpacing.sm), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.topicRenamePathChange( - widget.plan.oldTopicPath, - widget.plan.newTopicPath, - ), - key: const ValueKey('topic-rename-path-change'), - ), - if (widget.plan.updatesXmlTopicId) - Text( - context.l10n.topicRenameIdChange( - widget.plan.oldTopicId, - widget.plan.newTopicId, + Expanded( + child: CustomScrollView( + key: const ValueKey('topic-rename-preview-scroll'), + primary: false, + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(BusyMarkSpacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.topicRenamePathChange( + widget.plan.oldTopicPath, + widget.plan.newTopicPath, + ), + key: const ValueKey('topic-rename-path-change'), + ), + if (widget.plan.updatesXmlTopicId) + Text( + context.l10n.topicRenameIdChange( + widget.plan.oldTopicId, + widget.plan.newTopicId, + ), + key: const ValueKey('topic-rename-id-change'), + ), + for (final change in widget.plan.webFileNameChanges) + if (change.changed) + Text( + context.l10n.topicRenameUrlChange( + change.instanceId, + change.oldWebFileName, + change.newWebFileName, + ), + ), + const SizedBox(height: BusyMarkSpacing.sm), + Text( + context.l10n.topicRenameAffectedFiles, + style: Theme.of(context).textTheme.labelLarge, + ), + ], ), - key: const ValueKey('topic-rename-id-change'), ), - for (final change in widget.plan.webFileNameChanges) - if (change.changed) - Text( - context.l10n.topicRenameUrlChange( - change.instanceId, - change.oldWebFileName, - change.newWebFileName, + ), + SliverToBoxAdapter( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 180), + child: ListView.builder( + key: const ValueKey('topic-rename-affected-files'), + primary: false, + itemCount: files.length, + itemBuilder: (context, index) => ListTile( + dense: true, + selected: index == _selected, + leading: Icon( + index == 0 + ? BusyMarkGlyphs.edit + : BusyMarkGlyphs.document, + size: BusyMarkSizes.iconSm, + ), + title: Text( + index == 0 + ? '${p.basename(widget.plan.oldTopicPath)} → ${p.basename(widget.plan.newTopicPath)}' + : p.basename(files[index].path), + ), + subtitle: Text( + files[index].path, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () => setState(() => _selected = index), ), ), - const SizedBox(height: BusyMarkSpacing.sm), - Text( - context.l10n.topicRenameAffectedFiles, - style: Theme.of(context).textTheme.labelLarge, - ), - ], - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 180), - child: ListView.builder( - key: const ValueKey('topic-rename-affected-files'), - itemCount: files.length, - itemBuilder: (context, index) => ListTile( - dense: true, - selected: index == _selected, - leading: Icon( - index == 0 ? BusyMarkGlyphs.edit : BusyMarkGlyphs.document, - size: BusyMarkSizes.iconSm, - ), - title: Text( - index == 0 - ? '${p.basename(widget.plan.oldTopicPath)} → ${p.basename(widget.plan.newTopicPath)}' - : p.basename(files[index].path), + ), ), - subtitle: Text( - files[index].path, - maxLines: 1, - overflow: TextOverflow.ellipsis, + SliverToBoxAdapter( + child: Divider(height: 1, color: colors.subtleBorder), ), - onTap: () => setState(() => _selected = index), - ), + SourceComparisonView.sliver(comparison: comparison), + ], ), ), - Divider(height: 1, color: colors.subtleBorder), - Expanded(child: SourceComparisonView(comparison: comparison)), Padding( padding: const EdgeInsets.all(BusyMarkSpacing.sm), child: BusyMarkPushButton.standardIcon( @@ -4615,7 +3987,7 @@ class _FilesTabState extends ConsumerState<_FilesTab> { late String _workspaceId; late Set _expandedPaths; late final FocusNode _treeFocusNode; - final _selectedRowMenuKey = GlobalKey<_SidebarTreeRowState>(); + final _selectedRowMenuKey = GlobalKey(); String? _selectedPath; _FileTreeClipboardEntry? _cutEntry; @@ -4670,21 +4042,53 @@ class _FilesTabState extends ConsumerState<_FilesTab> { } return Shortcuts( shortcuts: { - (BusyMarkCommandRegistryScope.maybeOf(context) ?? - BusyMarkCommandCatalog.metadata)[BusyMarkCommandIds - .treeDeleteSelection]! - .shortcut! - .activator: - const _DeleteSelectedFileTreeEntryIntent(), + ...(BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .bindingIntents( + BusyMarkCommandIds.treeDeleteSelection, + const _DeleteSelectedFileTreeEntryIntent(), + ), const SingleActivator(LogicalKeyboardKey.contextMenu): const _ShowSelectedSidebarTreeMenuIntent(), const SingleActivator(LogicalKeyboardKey.f10, shift: true): const _ShowSelectedSidebarTreeMenuIntent(), - const SingleActivator(LogicalKeyboardKey.f6, shift: true): - const _RenameSelectedTopicFileIntent(), + ...(BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .bindingIntents( + BusyMarkCommandIds.treeRenameSelection, + const _RenameSelectedTopicFileIntent(), + ), }, child: Actions( actions: { + BusyMarkContextCommandIntent: BusyMarkContextCommandAction( + captureTarget: () { + final selected = _selectedPath, workspaceId = widget.workspace.id; + return BusyMarkContextCommandSnapshot( + () => + mounted && + widget.workspace.id == workspaceId && + _selectedPath == selected, + ); + }, + isCommandEnabled: (id) => + _selectedPath != null && + const { + BusyMarkCommandIds.treeDeleteSelection, + BusyMarkCommandIds.treeRenameSelection, + }.contains(id), + onCommand: (id) { + final target = _treeFocusNode.context; + if (target != null) { + Actions.maybeInvoke( + target, + id == BusyMarkCommandIds.treeRenameSelection + ? const _RenameSelectedTopicFileIntent() + : const _DeleteSelectedFileTreeEntryIntent(), + ); + } + }, + ), _DeleteSelectedFileTreeEntryIntent: CallbackAction<_DeleteSelectedFileTreeEntryIntent>( onInvoke: (_) { @@ -4752,7 +4156,7 @@ class _FilesTabState extends ConsumerState<_FilesTab> { ); } - return _SidebarTreeRow( + return BusyMarkSidebarTreeRow( key: selected ? _selectedRowMenuKey : null, title: busyMarkLtrIsolateFor(context, node.name), depth: entry.depth, @@ -5142,7 +4546,13 @@ Future<_FileTreeAction?> _showFileTreeMenu( ? context.l10n.renameTopicFile : context.l10n.rename, icon: BusyMarkGlyphs.edit, - shortcut: safeDeleteTopic ? 'Shift+F6' : null, + shortcut: safeDeleteTopic + ? (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)[BusyMarkCommandIds + .treeRenameSelection] + ?.shortcut + ?.label + : null, ), BusyMarkPopupMenuItem( value: _FileTreeAction.cut, @@ -5394,271 +4804,6 @@ Future _confirmDeleteFileTreeEntry( return confirmed ?? false; } -typedef _SidebarTreeMenuRequest = - void Function(BuildContext anchorContext, Offset globalPosition); - -class _SidebarRowSurface extends StatelessWidget { - const _SidebarRowSurface({ - super.key, - required this.child, - required this.enabled, - required this.clickable, - this.selected = false, - this.focusNode, - this.onKeyEvent, - this.onTap, - this.onDoubleTap, - this.onSecondaryTapUp, - this.onHoverChanged, - this.onFocusChange, - }); - - final Widget child; - final bool enabled; - final bool clickable; - final bool selected; - final FocusNode? focusNode; - final FocusOnKeyEventCallback? onKeyEvent; - final VoidCallback? onTap; - final VoidCallback? onDoubleTap; - final GestureTapUpCallback? onSecondaryTapUp; - final ValueChanged? onHoverChanged; - final ValueChanged? onFocusChange; - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: onHoverChanged == null ? null : (_) => onHoverChanged!(true), - onExit: onHoverChanged == null ? null : (_) => onHoverChanged!(false), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: BusyMarkStroke.hairline), - child: Material( - color: selected - ? busyMarkSelectedBackground(context) - : BusyMarkLinuxPalette.transparent, - borderRadius: BorderRadius.circular(BusyMarkRadius.md), - clipBehavior: Clip.antiAlias, - child: Focus( - onKeyEvent: onKeyEvent, - child: InkWell( - focusNode: focusNode, - hoverColor: clickable - ? busyMarkRowHoverColor(context) - : BusyMarkLinuxPalette.transparent, - onTap: enabled ? onTap : null, - onDoubleTap: enabled ? onDoubleTap : null, - onSecondaryTapUp: enabled ? onSecondaryTapUp : null, - onFocusChange: onFocusChange, - child: SizedBox( - height: BusyMarkSizes.sidebarTreeRowHeight, - child: child, - ), - ), - ), - ), - ), - ); - } -} - -class _SidebarTreeRow extends StatefulWidget { - const _SidebarTreeRow({ - super.key, - required this.title, - required this.depth, - this.icon, - this.leading, - this.trailing, - this.tooltip, - this.hasChildren = false, - this.expanded = false, - this.selected = false, - this.enabled = true, - this.muted = false, - this.compactHierarchyIndent = false, - this.vcsColor, - this.onToggle, - this.onTap, - this.onDoubleTap, - this.onMenuRequested, - }); - - final String title; - final int depth; - final IconData? icon; - final Widget? leading; - final Widget? trailing; - final String? tooltip; - final bool hasChildren; - final bool expanded; - final bool selected; - final bool enabled; - final bool muted; - final bool compactHierarchyIndent; - final BusyMarkVcsFileColor? vcsColor; - final VoidCallback? onToggle; - final VoidCallback? onTap; - final VoidCallback? onDoubleTap; - final _SidebarTreeMenuRequest? onMenuRequested; - - @override - State<_SidebarTreeRow> createState() => _SidebarTreeRowState(); -} - -class _SidebarTreeRowState extends State<_SidebarTreeRow> { - var _hovered = false; - var _focused = false; - - void showMenuFromKeyboard() { - final onMenuRequested = widget.onMenuRequested; - if (!widget.enabled || onMenuRequested == null) { - return; - } - onMenuRequested(context, _sidebarTreeMenuAnchor(context)); - } - - KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { - if (!widget.enabled || - widget.onMenuRequested == null || - !isBusyMarkContextMenuKeyEvent(event)) { - return KeyEventResult.ignored; - } - showMenuFromKeyboard(); - return KeyEventResult.handled; - } - - @override - Widget build(BuildContext context) { - final colors = BusyMarkSurfaceColors.of(context); - final direction = Directionality.of(context); - final clickable = - widget.enabled && - (widget.onTap != null || widget.onMenuRequested != null); - final vcsForeground = widget.vcsColor == null - ? null - : busyMarkVcsFileStatusColor(context, widget.vcsColor!); - final foreground = !widget.enabled || widget.muted - ? colors.disabledForeground - : vcsForeground ?? - (widget.selected ? colors.foreground : colors.mutedForeground); - final titleColor = !widget.enabled || widget.muted - ? colors.disabledForeground - : vcsForeground ?? colors.foreground; - final titleStyle = Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: titleColor); - final menuVisible = - widget.enabled && - widget.onMenuRequested != null && - (widget.selected || _hovered || _focused); - final depthBase = widget.compactHierarchyIndent - ? 0.0 - : BusyMarkSizes.sidebarTreeDepthBase; - final expanderWidth = widget.compactHierarchyIndent - ? BusyMarkSizes.sidebarTreeArrow - : BusyMarkSizes.sidebarTreeControl; - final expanderGap = widget.compactHierarchyIndent - ? 0.0 - : BusyMarkSpacing.xs; - final surface = _SidebarRowSurface( - enabled: widget.enabled, - clickable: clickable, - selected: widget.selected, - onKeyEvent: _handleKeyEvent, - onTap: widget.onTap, - onDoubleTap: widget.onDoubleTap, - onSecondaryTapUp: widget.onMenuRequested == null - ? null - : (details) => - widget.onMenuRequested!(context, details.globalPosition), - onHoverChanged: (hovered) => setState(() => _hovered = hovered), - onFocusChange: (focused) => setState(() => _focused = focused), - child: Row( - children: [ - SizedBox( - width: - depthBase + widget.depth * BusyMarkSizes.sidebarTreeDepthIndent, - ), - SizedBox.square( - dimension: expanderWidth, - child: widget.hasChildren - ? GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.enabled - ? widget.onToggle ?? widget.onTap - : null, - child: AnimatedRotation( - turns: widget.expanded - ? direction == TextDirection.rtl - ? -0.25 - : 0.25 - : 0, - duration: MediaQuery.disableAnimationsOf(context) - ? Duration.zero - : BusyMarkMotion.sidebarExpand, - child: Icon( - BusyMarkGlyphs.collapsedTreeArrowFor(direction), - size: BusyMarkSizes.sidebarTreeArrow, - color: foreground, - ), - ), - ) - : const SizedBox.shrink(), - ), - SizedBox(width: expanderGap), - if (widget.leading != null || widget.icon != null) ...[ - SizedBox.square( - dimension: BusyMarkSizes.sidebarTreeControl, - child: Center( - child: - widget.leading ?? - Icon( - widget.icon, - size: BusyMarkSizes.iconSm, - color: foreground, - ), - ), - ), - const SizedBox(width: BusyMarkSpacing.sm), - ], - Expanded( - child: Text( - widget.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: titleStyle, - ), - ), - if (widget.trailing != null) widget.trailing!, - if (widget.onMenuRequested != null) - SizedBox( - width: BusyMarkSizes.iconButton, - child: Center( - child: menuVisible - ? Builder( - builder: (buttonContext) => BusyMarkCompactIconButton( - tooltip: context.l10n.actions, - icon: BusyMarkGlyphs.menuVertical, - onPressed: () => widget.onMenuRequested!( - buttonContext, - _sidebarTreeMenuAnchor(buttonContext), - ), - ), - ) - : const SizedBox.shrink(), - ), - ) - else - const SizedBox(width: BusyMarkSpacing.xs), - ], - ), - ); - return widget.tooltip == null || widget.tooltip!.isEmpty - ? surface - : Tooltip(message: widget.tooltip!, child: surface); - } -} - Offset _sidebarTreeMenuAnchor(BuildContext context) { final renderObject = context.findRenderObject(); if (renderObject is! RenderBox || !renderObject.hasSize) { @@ -5984,7 +5129,7 @@ class _TocTabState extends ConsumerState<_TocTab> { String? _selectedInstanceTreePath; late Set _expandedNodeKeys; late final FocusNode _treeFocusNode; - final _rowMenuKeys = >{}; + final _rowMenuKeys = >{}; final _rowMenuPositions = {}; final _pointerModifiers = {}; String? _selectedNodePathKey; @@ -6186,7 +5331,7 @@ class _TocTabState extends ConsumerState<_TocTab> { final key = entries[i].pathKey; if (_rowMenuPositions[key] != i) { _rowMenuPositions[key] = i; - _rowMenuKeys[key] = GlobalKey<_SidebarTreeRowState>(); + _rowMenuKeys[key] = GlobalKey(); } } _TocTreeEntry? selectedEntry; @@ -6198,21 +5343,56 @@ class _TocTabState extends ConsumerState<_TocTab> { } return Shortcuts( shortcuts: { - (BusyMarkCommandRegistryScope.maybeOf(context) ?? - BusyMarkCommandCatalog.metadata)[BusyMarkCommandIds - .treeDeleteSelection]! - .shortcut! - .activator: - const _RemoveSelectedTocEntryIntent(), + ...(BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .bindingIntents( + BusyMarkCommandIds.treeDeleteSelection, + const _RemoveSelectedTocEntryIntent(), + ), const SingleActivator(LogicalKeyboardKey.contextMenu): const _ShowSelectedSidebarTreeMenuIntent(), const SingleActivator(LogicalKeyboardKey.f10, shift: true): const _ShowSelectedSidebarTreeMenuIntent(), - const SingleActivator(LogicalKeyboardKey.f6, shift: true): - const _RenameSelectedTopicFileIntent(), + ...(BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata) + .bindingIntents( + BusyMarkCommandIds.treeRenameSelection, + const _RenameSelectedTopicFileIntent(), + ), }, child: Actions( actions: { + BusyMarkContextCommandIntent: BusyMarkContextCommandAction( + captureTarget: () { + final selected = _selectedNodePathKey; + final selection = Set.of(_selectedNodePathKeys); + final workspaceId = widget.workspace.id; + return BusyMarkContextCommandSnapshot( + () => + mounted && + widget.workspace.id == workspaceId && + _selectedNodePathKey == selected && + setEquals(selection, _selectedNodePathKeys), + ); + }, + isCommandEnabled: (id) => + selectedEntry != null && + const { + BusyMarkCommandIds.treeDeleteSelection, + BusyMarkCommandIds.treeRenameSelection, + }.contains(id), + onCommand: (id) { + final target = _treeFocusNode.context; + if (target != null) { + Actions.maybeInvoke( + target, + id == BusyMarkCommandIds.treeRenameSelection + ? const _RenameSelectedTopicFileIntent() + : const _RemoveSelectedTocEntryIntent(), + ); + } + }, + ), _RemoveSelectedTocEntryIntent: CallbackAction<_RemoveSelectedTocEntryIntent>( onInvoke: (_) { @@ -6480,7 +5660,7 @@ class _TocTabState extends ConsumerState<_TocTab> { } } }, - child: _SidebarTreeRow( + child: BusyMarkSidebarTreeRow( key: _rowMenuKeys[key], title: label, enabled: true, @@ -8349,7 +7529,12 @@ Future<_TocTreeAction?> _showTocTreeMenu( value: _TocTreeAction.rename, label: context.l10n.renameTopicFile, icon: BusyMarkGlyphs.edit, - shortcut: 'Shift+F6', + shortcut: + (BusyMarkCommandRegistryScope.maybeOf(context) ?? + BusyMarkCommandCatalog.metadata)[BusyMarkCommandIds + .treeRenameSelection] + ?.shortcut + ?.label, enabled: hasTopicFile, ), BusyMarkPopupMenuItem( @@ -8949,7 +8134,7 @@ class _WritersideInstanceSelector extends StatelessWidget { message: context.l10n.instances, child: Semantics( expanded: trigger.isOpen, - child: _SidebarRowSurface( + child: BusyMarkSidebarRowSurface( key: const ValueKey('writerside-instance-selector-trigger'), enabled: selectorEnabled, clickable: selectorEnabled, @@ -9705,6 +8890,7 @@ Future _refineActiveSourceRangesWithAi( class _OutlineTab extends ConsumerStatefulWidget { const _OutlineTab({ + super.key, required this.workspace, required this.bufferId, required this.headings, @@ -9728,6 +8914,72 @@ class _OutlineTabState extends ConsumerState<_OutlineTab> { String? _revealedActiveNodeKey; Set _selectedNodeKeys = {}; String? _selectionAnchorKey; + List<_OutlineTreeEntry> _navigationEntries = const []; + int _keyboardIndex = -1; + String? _sourceAtBuild; + + final _navigationFocus = FocusNode(debugLabel: 'Outline navigation'); + void requestNavigationFocus() => _navigationFocus.requestFocus(); + + void _moveOutlineSelection(int delta) { + if (_navigationEntries.isEmpty) return; + setState( + () => _keyboardIndex = (_keyboardIndex + delta).clamp( + 0, + _navigationEntries.length - 1, + ), + ); + // Navigation belongs to the tree; retain it as rows are virtualized away. + _navigationFocus.requestFocus(); + if (_treeScrollController.hasClients) { + _treeScrollController.jumpTo( + (_keyboardIndex * _treeRowExtent).clamp( + 0, + _treeScrollController.position.maxScrollExtent, + ), + ); + } + } + + void _activateOutlineSelection() { + if (_navigationEntries.isEmpty) return; + final heading = + _navigationEntries[_keyboardIndex.clamp( + 0, + _navigationEntries.length - 1, + )] + .node + .heading; + _navigateHeading(heading); + } + + KeyEventResult _handleNavigationKeyEvent(FocusNode node, KeyEvent event) { + if (event is! KeyDownEvent) return KeyEventResult.ignored; + if (event.logicalKey == LogicalKeyboardKey.arrowDown || + event.logicalKey == LogicalKeyboardKey.arrowUp) { + _moveOutlineSelection( + event.logicalKey == LogicalKeyboardKey.arrowDown ? 1 : -1, + ); + return KeyEventResult.handled; + } + if (event.logicalKey == LogicalKeyboardKey.enter) { + _activateOutlineSelection(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + + void _navigateHeading(DocumentOutlineHeading heading) { + final source = _sourceAtBuild; + if (source == null) return; + navigateOutlineHeading( + ref, + workspaceId: widget.workspace.id, + bufferId: widget.bufferId, + source: source, + heading: heading, + ); + } @override void initState() { @@ -9759,6 +9011,7 @@ class _OutlineTabState extends ConsumerState<_OutlineTab> { @override void dispose() { + _navigationFocus.dispose(); _treeScrollController.dispose(); super.dispose(); } @@ -9969,8 +9222,13 @@ class _OutlineTabState extends ConsumerState<_OutlineTab> { ); } final tree = _buildOutlineTree(headings); - final entries = _visibleOutlineTreeEntries(tree, _expandedNodeKeys); - final viewportTarget = ref.watch(_outlineViewportTargetProvider); + final entries = _visibleOutlineTreeEntries( + tree, + _expandedNodeKeys, + ).toList(); + _navigationEntries = entries; + _sourceAtBuild = ref.read(workspaceControllerProvider).activeText; + final viewportTarget = ref.watch(outlineViewportTargetProvider); final activeNodeKey = _activeVisibleOutlineNodeKey( workspace: widget.workspace, bufferId: widget.bufferId, @@ -9983,76 +9241,83 @@ class _OutlineTabState extends ConsumerState<_OutlineTab> { for (var index = 0; index < headings.length; index += 1) _outlineNodeKey(headings[index]): index, }; - return ListView.builder( - key: const ValueKey('workspace-sidebar-outline-tree'), - controller: _treeScrollController, - padding: BusyMarkInsets.sidebarList, - itemExtent: _treeRowExtent, - itemCount: entries.length, - itemBuilder: (context, index) { - final entry = entries[index]; - final node = entry.node; - final heading = node.heading; - final key = _outlineNodeKey(heading); - final headingIndex = headingIndexes[key]!; - final expanded = _expandedNodeKeys.contains(key); - final hasChildren = node.children.isNotEmpty; - final selected = _selectedNodeKeys.isEmpty - ? key == activeNodeKey - : _selectedNodeKeys.contains(key); - void toggle() { - setState(() { - _revealedActiveNodeKey = null; - if (expanded) { - _expandedNodeKeys.remove(key); - } else { - _expandedNodeKeys.add(key); - } - }); - } - - return KeyedSubtree( - key: ValueKey('workspace-sidebar-outline-row-$headingIndex'), - child: _SidebarTreeRow( - title: heading.text, - depth: entry.depth, - icon: BusyMarkGlyphs.tag, - leading: _HeadingBadge(level: heading.level), - hasChildren: hasChildren, - expanded: expanded, - selected: selected, - onToggle: hasChildren ? toggle : null, - onTap: () { - final modifiers = _treeSelectionModifiers(); - _updateOutlineSelection(entries, index, modifiers); - if (modifiers.control || modifiers.shift) { - return; + return Focus( + focusNode: _navigationFocus, + onKeyEvent: _handleNavigationKeyEvent, + child: ListView.builder( + key: const ValueKey('workspace-sidebar-outline-tree'), + controller: _treeScrollController, + padding: BusyMarkInsets.sidebarList, + itemExtent: _treeRowExtent, + itemCount: entries.length, + itemBuilder: (context, index) { + final entry = entries[index]; + final node = entry.node; + final heading = node.heading; + final key = _outlineNodeKey(heading); + final headingIndex = headingIndexes[key]!; + final expanded = _expandedNodeKeys.contains(key); + final hasChildren = node.children.isNotEmpty; + final selected = _keyboardIndex >= 0 + ? index == _keyboardIndex + : _selectedNodeKeys.isEmpty + ? key == activeNodeKey + : _selectedNodeKeys.contains(key); + void toggle() { + setState(() { + _revealedActiveNodeKey = null; + if (expanded) { + _expandedNodeKeys.remove(key); + } else { + _expandedNodeKeys.add(key); } - _setOutlineViewportTarget( - ref, - workspace: widget.workspace, - bufferId: widget.bufferId, - heading: heading, - ); - ref - .read(_outlineNavigationTargetProvider.notifier) - .set( - _OutlineNavigationTarget( - workspaceId: widget.workspace.id, - bufferId: widget.bufferId ?? '', - filePath: widget.workspace.activeFilePath, - headingId: heading.id, - line: heading.sourceStartLine, - editorBlockId: heading.editorBlockId, - ), - ); - }, - onMenuRequested: (anchorContext, position) => unawaited( - _showSectionMenu(entries, headingIndexes, index, position), + }); + } + + return KeyedSubtree( + key: ValueKey('workspace-sidebar-outline-row-$headingIndex'), + child: BusyMarkSidebarTreeRow( + title: heading.text, + onNavigationKeyEvent: (node, event) { + if (event is KeyDownEvent && + { + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.enter, + }.contains(event.logicalKey)) { + _keyboardIndex = index; + return _handleNavigationKeyEvent(node, event); + } + return KeyEventResult.ignored; + }, + onFocusChange: (focused) { + if (focused && _keyboardIndex != index) { + setState(() => _keyboardIndex = index); + } + }, + depth: entry.depth, + icon: BusyMarkGlyphs.tag, + leading: _HeadingBadge(level: heading.level), + hasChildren: hasChildren, + expanded: expanded, + selected: selected, + onToggle: hasChildren ? toggle : null, + onTap: () { + final modifiers = _treeSelectionModifiers(); + _updateOutlineSelection(entries, index, modifiers); + if (modifiers.control || modifiers.shift) { + return; + } + _keyboardIndex = index; + _navigateHeading(heading); + }, + onMenuRequested: (anchorContext, position) => unawaited( + _showSectionMenu(entries, headingIndexes, index, position), + ), ), - ), - ); - }, + ); + }, + ), ); } @@ -10260,32 +9525,12 @@ Future _confirmDeleteOutlineSection( return confirmed ?? false; } -void _setOutlineViewportTarget( - WidgetRef ref, { - required Workspace workspace, - required String? bufferId, - required DocumentOutlineHeading? heading, -}) { - ref - .read(_outlineViewportTargetProvider.notifier) - .set( - _OutlineViewportTarget( - workspaceId: workspace.id, - bufferId: bufferId ?? '', - filePath: workspace.activeFilePath, - headingId: heading?.id, - sourceStartOffset: heading?.sourceStartOffset, - editorBlockId: heading?.editorBlockId, - ), - ); -} - String? _activeVisibleOutlineNodeKey({ required Workspace workspace, required String? bufferId, required List headings, required List<_OutlineTreeEntry> entries, - required _OutlineViewportTarget? target, + required OutlineViewportTarget? target, }) { if (target == null || target.workspaceId != workspace.id || @@ -10311,7 +9556,7 @@ String? _activeVisibleOutlineNodeKey({ int _outlineViewportHeadingIndex( List headings, - _OutlineViewportTarget target, + OutlineViewportTarget target, ) { final editorBlockId = target.editorBlockId; if (editorBlockId != null) { @@ -10337,52 +9582,6 @@ int _outlineViewportHeadingIndex( : headings.indexWhere((heading) => heading.id == headingId); } -List _activeDocumentOutline(WorkspaceState state) { - final liveOutline = state.liveOutline; - final workspace = state.workspace; - if (liveOutline != null && - workspace != null && - liveOutline.matches(workspace, state.activeBuffer, state.activeText)) { - return liveOutline.headings; - } - final preview = state.preview; - if (preview != null) { - return preview.outline; - } - final buffer = state.activeBuffer; - if (workspace != null && - buffer != null && - resolveWorkspaceDocumentContext(workspace, buffer).kind == - DocumentKind.writersideXmlTopic) { - return const WritersideEditingAdapter() - .parseXml( - filePath: buffer.filePath ?? workspace.activeFilePath ?? '', - source: buffer.text, - ) - ?.outline ?? - const []; - } - final markdown = workspace?.markdown; - if (workspace == null || markdown == null) return const []; - if (buffer == null) { - if (state.documentBuffers.isNotEmpty) return const []; - return [ - for (final heading in markdown.headings) - DocumentOutlineHeading.fromMarkdown(heading), - ]; - } - final context = resolveWorkspaceDocumentContext(workspace, buffer); - if (!p.equals(markdown.filePath, context.parserPath) || - markdown.source != buffer.text || - markdown.mode != context.markdownMode) { - return const []; - } - return [ - for (final heading in markdown.headings) - DocumentOutlineHeading.fromMarkdown(heading), - ]; -} - class _OutlineTreeNode { const _OutlineTreeNode({required this.heading, required this.children}); @@ -12117,8 +11316,17 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { ); } + final _previewSearchTimers = []; + void _cancelPreviewSearchTimers() { + for (final timer in _previewSearchTimers) { + timer.cancel(); + } + _previewSearchTimers.clear(); + } + @override void dispose() { + _cancelPreviewSearchTimers(); _inspectorWorkspaceController.propertyEdits.removeListener( _handlePropertyDraftChange, ); @@ -12239,7 +11447,7 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { if (!mounted || workspace == null) { return; } - _setOutlineViewportTarget( + setOutlineViewportTarget( ref, workspace: workspace, bufferId: widget.state.activeBufferId, @@ -12249,7 +11457,7 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { @override Widget build(BuildContext context) { - ref.listen(_outlineNavigationTargetProvider, (previous, next) { + ref.listen(outlineNavigationTargetProvider, (previous, next) { if (next == null) { return; } @@ -12263,19 +11471,22 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { _scrollToOutlineTarget(next); }); ref.listen(notesMatchNavigationProvider, (previous, next) { + final currentDocument = ref.read(workspaceControllerProvider); if (next == null || + next.accountId != null && + next.accountId != currentDocument.workspace?.nextcloudAccountId || next.request == previous?.request || - (widget.state.activeBuffer?.remoteNote?.localId ?? - widget.state.activeBuffer?.filePath) != + (currentDocument.activeBuffer?.remoteNote?.localId ?? + currentDocument.activeBuffer?.filePath ?? + currentDocument.activeBuffer?.id) != next.localId) { return; } - final source = widget.state.activeBuffer!.text; + final source = currentDocument.activeBuffer!.text; if (notesSearchDigest(source) != next.digest) return; final line = SourceDocument( fullText: source, ).lineIndex.lineNumberAtOffset(next.start); - final old = ref.read(_searchNavigationTargetProvider); ref .read(_searchNavigationTargetProvider.notifier) .set( @@ -12287,16 +11498,23 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { startOffset: next.start, endOffset: next.end, query: '', - request: (old?.request ?? 0) + 1, + request: next.request, ), ); }); + ref.listen(searchSessionProvider, (previous, next) { + if (previous?.textActive == true && + previous?.requestRevision != next.requestRevision) { + _cancelPreviewSearchTimers(); + _previewSearchScrollRequest = -1; + ref.read(_searchNavigationTargetProvider.notifier).set(null); + } + }); final searchTarget = ref.watch(_searchNavigationTargetProvider); if (searchTarget != null && (searchTarget.localId != null || - (ref.read(_workspaceSearchProvider).active && - ref.read(_workspaceSearchProvider).query == - searchTarget.query)) && + (ref.read(searchSessionProvider).textActive && + ref.read(searchSessionProvider).query == searchTarget.query)) && searchTarget.request != _lastSearchNavigationRequest) { _scrollToSearchTarget(searchTarget); } @@ -12390,7 +11608,7 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { direction: widget.editorToolbarDirection, ); final activeEditorPath = _activeEditorPath(); - final searchState = ref.watch(_workspaceSearchProvider); + final searchState = ref.watch(searchSessionProvider); final spellingBannerPresentation = activeBuffer != null && spellingSupported && @@ -12640,17 +11858,9 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { visualizationRevision: ref .read(workspaceControllerProvider.notifier) .editRevision, - onOpenSearch: () => ref - .read( - workspaceSearchOpenRequestProvider.notifier, - ) - .request(), onCloseSearch: () => ref - .read( - workspaceSearchCloseRequestProvider - .notifier, - ) - .request(), + .read(searchSessionProvider.notifier) + .close(), onUndo: () => ref .read(workspaceControllerProvider.notifier) .undoActiveBuffer(), @@ -12763,8 +11973,18 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { ), editorFontSize: widget.editorFontSize, wordWrap: widget.wordWrap, - searchActive: searchState.active, + searchActive: searchState.textActive, + searchControlsVisible: searchState.fileActive, searchOptions: searchState.options, + searchSnapshot: searchState.sourceSnapshot( + activeBuffer, + ), + onNavigateSearch: ref + .read(searchSessionProvider.notifier) + .navigateSource, + onCancelSearchNavigation: ref + .read(searchSessionProvider.notifier) + .cancelSourceNavigation, searchReplacement: activeBuffer?.editorState.searchReplacement ?? '', @@ -12793,14 +12013,6 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { ); } }, - onSearchOptionsChanged: (options) { - final current = ref.read( - _workspaceSearchProvider, - ); - ref - .read(_workspaceSearchProvider.notifier) - .set(current.withOptions(options)); - }, initialSelection: activeBuffer?.editorState.selection, initialScrollOffset: @@ -12847,17 +12059,9 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { ), ); }, - onOpenSearch: () => ref - .read( - workspaceSearchOpenRequestProvider.notifier, - ) - .request(), onCloseSearch: () => ref - .read( - workspaceSearchCloseRequestProvider - .notifier, - ) - .request(), + .read(searchSessionProvider.notifier) + .close(), onVisibleLineChanged: _handleSourceVisibleLineChanged, onChanged: (value, sourceFilePath) => @@ -15133,7 +14337,7 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { return resolveWorkspaceDocumentContext(workspace, buffer).kind; } - void _scrollToOutlineTarget(_OutlineNavigationTarget target) { + void _scrollToOutlineTarget(OutlineNavigationTarget target) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; @@ -15161,7 +14365,8 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { (target.localId != null ? target.localId != (widget.state.activeBuffer?.remoteNote?.localId ?? - widget.state.activeBuffer?.filePath) || + widget.state.activeBuffer?.filePath ?? + widget.state.activeBuffer?.id) || notesSearchDigest(widget.state.activeBuffer?.text ?? '') != target.digest : target.filePath != widget.state.activeBuffer?.filePath)) { @@ -15379,6 +14584,7 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { } void _schedulePreviewSearchScroll(_SearchNavigationTarget target) { + _cancelPreviewSearchTimers(); _previewSearchScrollRequest = target.request; WidgetsBinding.instance.addPostFrameCallback((_) { if (!_isCurrentPreviewSearchScroll(target) || @@ -15390,15 +14596,15 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { _scrollPreviewToSearchTarget(target); } }); - unawaited( - Future.delayed(BusyMarkMotion.previewSearchDelay, () { + _previewSearchTimers.add( + Timer(BusyMarkMotion.previewSearchDelay, () { if (_isCurrentPreviewSearchScroll(target)) { _scrollPreviewToSearchTarget(target); } }), ); - unawaited( - Future.delayed(BusyMarkMotion.scroll, () { + _previewSearchTimers.add( + Timer(BusyMarkMotion.scroll, () { if (_isCurrentPreviewSearchScroll(target)) { _scrollPreviewToSearchTarget(target); } @@ -15408,7 +14614,9 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { } bool _isCurrentPreviewSearchScroll(_SearchNavigationTarget target) { - return mounted && target.request == _previewSearchScrollRequest; + return mounted && + target.request == _previewSearchScrollRequest && + ref.read(searchSessionProvider).textActive; } bool _canUseWysiwyg(Workspace? workspace) { @@ -15449,10 +14657,14 @@ class _EditorPreviewSplitState extends ConsumerState<_EditorPreviewSplit> { return _cachedWysiwygDocument; } if (context.kind == DocumentKind.writersideXmlTopic) { - final document = const WritersideEditingAdapter().parseXml( - filePath: activePath, - source: buffer.text, - ); + final authored = context.writersideTopic?.document; + final adapter = const WritersideEditingAdapter(); + final document = + authored != null && + p.equals(authored.filePath, activePath) && + authored.source == buffer.text + ? adapter.fromAuthoredXml(authored) + : adapter.parseXml(filePath: activePath, source: buffer.text); _clearWysiwygCache(); if (document != null) _cacheWysiwygDocument(buffer.id, document); return document; @@ -16604,13 +15816,13 @@ class _PreviewInlineText extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final baseStyle = style ?? busyMarkDocumentBodyTextStyle(context); - final searchState = ref.watch(_workspaceSearchProvider); + final searchState = ref.watch(searchSessionProvider); final workspace = ref.watch(workspaceControllerProvider).workspace; final settings = ref.watch(appSettingsControllerProvider); final allowRemoteImages = settings.allowsRemoteImagesForWorkspace( _remoteImageWorkspacePath(workspace), ); - final highlightQuery = searchState.active ? searchState.query : ''; + final highlightQuery = searchState.textActive ? searchState.query : ''; final inlines = block.inlines.isEmpty ? [PreviewInline(kind: PreviewInlineKind.text, text: block.text)] : block.inlines; @@ -16713,7 +15925,7 @@ int? _previewSearchBlockIndexForSourceLine( if (rawLine.isEmpty) { return null; } - final strippedLine = _stripMarkdownForSearchResult(rawLine).toLowerCase(); + final strippedLine = stripMarkdownForSearchResult(rawLine).toLowerCase(); final rawNeedle = rawLine.toLowerCase(); if (strippedLine.isEmpty && rawNeedle.isEmpty) { return null; @@ -16850,7 +16062,7 @@ double _previewBlockTargetFraction( if (sourceLine.isNotEmpty && block.text.contains('\n')) { final sourceNeedles = { sourceLine, - _stripMarkdownForSearchResult(sourceLine), + stripMarkdownForSearchResult(sourceLine), }.where((value) => value.isNotEmpty).map((value) => value.toLowerCase()); final blockLines = block.text.split('\n'); for (final (index, blockLine) in blockLines.indexed) { @@ -17847,9 +17059,9 @@ void _navigatePreviewAnchor( return; } ref - .read(_outlineNavigationTargetProvider.notifier) + .read(outlineNavigationTargetProvider.notifier) .set( - _OutlineNavigationTarget( + OutlineNavigationTarget( workspaceId: workspace.id, bufferId: state.activeBufferId ?? '', filePath: workspace.activeFilePath, @@ -17976,143 +17188,6 @@ class _ProblemsList extends StatelessWidget { } } -class _SearchSidebar extends StatelessWidget { - const _SearchSidebar({ - required this.query, - required this.results, - required this.searching, - required this.onOpenResult, - required this.onReviewReplacement, - required this.state, - required this.onShowMore, - }); - - final String query; - final List<_WorkspaceSearchResult> results; - final bool searching; - final Future Function(_WorkspaceSearchResult result) onOpenResult; - final Future Function() onReviewReplacement; - final _WorkspaceSearchState state; - final VoidCallback onShowMore; - - @override - Widget build(BuildContext context) { - final normalizedQuery = query; - if (normalizedQuery.isEmpty) { - return _SidebarEmptyState( - icon: BusyMarkGlyphs.search, - title: context.l10n.search, - ); - } - if (searching && results.isEmpty) { - return const Center( - child: CircularProgressIndicator( - key: ValueKey('workspace-search-progress'), - ), - ); - } - final colors = BusyMarkSurfaceColors.of(context); - if (state.invalidRegex) { - return _SidebarEmptyState( - icon: BusyMarkGlyphs.searchUnavailable, - title: context.l10n.sourceSearchInvalidRegex, - ); - } - if (results.isEmpty && - state.skippedFiles.isEmpty && - !state.hasZeroLengthMatches) { - return _SidebarEmptyState( - icon: BusyMarkGlyphs.searchUnavailable, - title: context.l10n.noResults, - ); - } - final groups = _workspaceSearchFileGroups(results); - return Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - BusyMarkSpacing.sm, - BusyMarkSpacing.sm, - BusyMarkSpacing.sm, - 0, - ), - child: SizedBox( - width: double.infinity, - child: BusyMarkPushButton.standard( - onPressed: () => unawaited(onReviewReplacement()), - child: Text(context.l10n.workspaceReplace), - ), - ), - ), - Expanded( - child: ListView( - key: const ValueKey('workspace-search-results'), - padding: BusyMarkInsets.sidebarList, - children: [ - if (state.hasZeroLengthMatches) - Padding( - padding: BusyMarkInsets.searchResultRow, - child: Text(context.l10n.sourceSearchZeroLengthUnsupported), - ), - if (state.truncated) - Padding( - padding: BusyMarkInsets.searchResultRow, - child: Column( - children: [ - Text(context.l10n.workspaceSearchIncomplete), - TextButton( - onPressed: onShowMore, - child: Text(context.l10n.workspaceSearchShowMore), - ), - ], - ), - ), - if (state.skippedFiles.isNotEmpty) - Padding( - padding: BusyMarkInsets.searchResultRow, - child: Text( - '${context.l10n.workspaceSearchSkippedFiles}\n' - '${state.skippedFiles.join('\n')}', - ), - ), - for (final group in groups) ...[ - Padding( - padding: const EdgeInsets.fromLTRB( - BusyMarkSpacing.sm, - BusyMarkSpacing.sm, - BusyMarkSpacing.sm, - BusyMarkSpacing.xxs, - ), - child: Text( - busyMarkLtrIsolateFor(context, group.relativePath), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: colors.mutedForeground, - fontWeight: FontWeight.w700, - letterSpacing: 0, - ), - ), - ), - for (final result in group.results) ...[ - _SearchResultRow( - result: result, - onOpen: () => onOpenResult(result), - ), - Divider( - height: BusyMarkStroke.hairline, - color: colors.subtleBorder, - ), - ], - ], - ], - ), - ), - ], - ); - } -} - @visibleForTesting final class SpellingReviewCorrection { const SpellingReviewCorrection({ @@ -18923,10 +17998,6 @@ String _workspaceReplacementIssueLabel( WorkspaceReplacementIssueKind.remoteWorkspaceUnsupported => context.l10n.nextcloudWorkspaceReplaceUnavailable, WorkspaceReplacementIssueKind.cancelled => context.l10n.cancel, - WorkspaceReplacementIssueKind.invalidRegex => - context.l10n.sourceSearchInvalidRegex, - WorkspaceReplacementIssueKind.zeroLengthMatches => - context.l10n.sourceSearchZeroLengthUnsupported, WorkspaceReplacementIssueKind.oversized => context.l10n.workspaceReplaceIssueOversized, WorkspaceReplacementIssueKind.unreadable => @@ -18949,23 +18020,6 @@ String _workspaceReplacementIssueLabel( }; } -class _SearchResultRow extends StatelessWidget { - const _SearchResultRow({required this.result, required this.onOpen}); - - final _WorkspaceSearchResult result; - final Future Function() onOpen; - - @override - Widget build(BuildContext context) { - return _SidebarNavigationResultRow( - title: result.title, - subtitle: result.subtitle, - icon: result.icon, - onOpen: onOpen, - ); - } -} - class _SidebarNavigationResultRow extends StatelessWidget { const _SidebarNavigationResultRow({ required this.title, @@ -19029,325 +18083,9 @@ class _SidebarNavigationResultRow extends StatelessWidget { } } -class _WorkspaceSearchResult { - const _WorkspaceSearchResult({ - required this.filePath, - required this.relativePath, - required this.line, - required this.startOffset, - required this.endOffset, - required this.query, - required this.title, - required this.subtitle, - required this.icon, - }); - - final String filePath; - final String relativePath; - final int line; - final int startOffset; - final int endOffset; - final String query; - final String title; - final String subtitle; - final IconData icon; -} - -enum _WorkspaceSearchMatchKind { text, fileName } - -class _WorkspaceSearchMatch { - const _WorkspaceSearchMatch({ - required this.kind, - required this.filePath, - required this.relativePath, - required this.fileKind, - required this.line, - required this.startOffset, - required this.endOffset, - required this.query, - this.lineText, - }); - - final _WorkspaceSearchMatchKind kind; - final String filePath; - final String relativePath; - final DocumentKind fileKind; - final int line; - final int startOffset; - final int endOffset; - final String query; - final String? lineText; -} - -class _WorkspaceSearchFileGroup { - const _WorkspaceSearchFileGroup({ - required this.relativePath, - required this.results, - }); - - final String relativePath; - final List<_WorkspaceSearchResult> results; -} - -List<_WorkspaceSearchFileGroup> _workspaceSearchFileGroups( - List<_WorkspaceSearchResult> results, -) { - final groups = >{}; - for (final result in results) { - groups.putIfAbsent(result.relativePath, () => []).add(result); - } - return [ - for (final entry in groups.entries) - _WorkspaceSearchFileGroup( - relativePath: entry.key, - results: List.unmodifiable(entry.value), - ), - ]; -} - -List<_WorkspaceSearchResult> _workspaceSearchResults( - BuildContext context, - List<_WorkspaceSearchMatch> matches, -) { - return [ - for (final match in matches) - _WorkspaceSearchResult( - filePath: match.filePath, - relativePath: match.relativePath, - line: match.line, - startOffset: match.startOffset, - endOffset: match.endOffset, - query: match.query, - title: switch (match.kind) { - _WorkspaceSearchMatchKind.text => _searchResultTitle( - context, - match.lineText ?? '', - ), - _WorkspaceSearchMatchKind.fileName => busyMarkLtrIsolateFor( - context, - match.relativePath, - ), - }, - subtitle: switch (match.kind) { - _WorkspaceSearchMatchKind.text => context.l10n.searchResultLine( - match.relativePath, - match.line, - ), - _WorkspaceSearchMatchKind.fileName => _documentKindLabel( - context, - match.fileKind, - ), - }, - icon: switch (match.kind) { - _WorkspaceSearchMatchKind.text => BusyMarkGlyphs.paragraph, - _WorkspaceSearchMatchKind.fileName => _documentKindIcon( - match.fileKind, - ), - }, - ), - ]; -} - -const int _maxWorkspaceSearchResults = 80; -const int _maxWorkspaceSearchFileBytes = 1024 * 1024; - -class _WorkspaceSearchOutcome { - const _WorkspaceSearchOutcome({ - this.matches = const [], - this.truncated = false, - this.skippedFiles = const [], - this.invalidRegex = false, - this.hasZeroLengthMatches = false, - }); - - final List<_WorkspaceSearchMatch> matches; - final bool truncated; - final List skippedFiles; - final bool invalidRegex; - final bool hasZeroLengthMatches; -} - -Future<_WorkspaceSearchOutcome> _loadWorkspaceSearchMatches( - WorkspaceState state, - SourceSearchOptions options, { - required Future Function(String path) loadText, - required bool Function() isCancelled, - required SourceSearchWorker worker, - required int maximumResults, -}) async { - final workspace = state.workspace; - if (workspace == null || options.query.isEmpty || isCancelled()) { - return const _WorkspaceSearchOutcome(); - } - if (sourceSearchOptionsHaveInvalidRegex(options)) { - return const _WorkspaceSearchOutcome(invalidRegex: true); - } - final results = <_WorkspaceSearchMatch>[]; - final skippedFiles = []; - var hasZeroLengthMatches = false; - final sortedFiles = - workspace.files.where(isSearchableWorkspaceDocument).toList() - ..sort((a, b) => a.relativePath.compareTo(b.relativePath)); - for (final file in sortedFiles) { - if (isCancelled()) return const _WorkspaceSearchOutcome(); - final buffer = state.bufferForPath(file.absolutePath); - String? text; - if (buffer != null) { - text = buffer.text; - } else if (file.absolutePath == state.activeBuffer?.filePath) { - text = state.activeText; - } else if (file.size > _maxWorkspaceSearchFileBytes) { - skippedFiles.add(file.relativePath); - } else { - try { - text = await loadText(file.absolutePath); - } on Object { - skippedFiles.add(file.relativePath); - } - } - if (isCancelled()) return const _WorkspaceSearchOutcome(); - if (text != null) { - final document = SourceDocument(fullText: text); - final search = await worker.search( - document, - options, - maximumMatches: maximumResults + 1 - results.length, - stopAfterMaximumMatches: true, - ); - if (isCancelled()) return const _WorkspaceSearchOutcome(); - if (search == null) { - skippedFiles.add(file.relativePath); - } else { - hasZeroLengthMatches |= search.hasZeroLengthMatches; - for (final match in search.matches) { - final line = document.lineIndex.lineNumberAtOffset(match.fullStart); - final sourceLine = document.lineIndex.lineAt(line); - results.add( - _WorkspaceSearchMatch( - kind: _WorkspaceSearchMatchKind.text, - filePath: file.absolutePath, - relativePath: file.relativePath, - fileKind: file.kind, - line: line, - startOffset: match.fullStart, - endOffset: match.fullEnd, - query: options.query, - lineText: _boundedSearchSnippet( - sourceLine.text, - matchStart: match.fullStart - sourceLine.startOffset, - ), - ), - ); - } - } - } - if (results.length <= maximumResults && - _fileNameMatchesSearch(file.relativePath, options)) { - results.add( - _WorkspaceSearchMatch( - kind: _WorkspaceSearchMatchKind.fileName, - filePath: file.absolutePath, - relativePath: file.relativePath, - fileKind: file.kind, - line: 1, - startOffset: 0, - endOffset: 0, - query: options.query, - ), - ); - } - // Only report truncation once an additional result was actually found. - if (results.length > maximumResults) { - return _WorkspaceSearchOutcome( - matches: List.unmodifiable(results.take(maximumResults)), - truncated: true, - skippedFiles: List.unmodifiable(skippedFiles), - hasZeroLengthMatches: hasZeroLengthMatches, - ); - } - } - return _WorkspaceSearchOutcome( - matches: List.unmodifiable(results), - skippedFiles: List.unmodifiable(skippedFiles), - hasZeroLengthMatches: hasZeroLengthMatches, - ); -} - -bool _fileNameMatchesSearch(String relativePath, SourceSearchOptions options) { - if (options.regex || options.wholeWord) { - return false; - } - return RegExp( - RegExp.escape(options.query), - caseSensitive: options.caseSensitive, - unicode: true, - ).hasMatch(relativePath); -} - -String _searchResultTitle(BuildContext context, String line) { - final trimmed = _stripMarkdownForSearchResult(line, context: context); - if (trimmed.length <= 120) { - return trimmed; - } - var end = 117; - if (!sourceSearchRangeHasSafeBoundaries(trimmed, end, end)) end--; - return '${trimmed.substring(0, end)}...'; -} - // Limit the input to Markdown cleanup, whose regexes can backtrack on malformed // markup. Keep the match near the beginning so it survives the title's shorter // display limit. The two ellipses are included in the 512-code-unit budget. -String _boundedSearchSnippet(String line, {int matchStart = 0}) { - const maximumLength = 512; - if (line.length <= 120) return line; - var start = math.max(0, matchStart.clamp(0, line.length) - 40); - if (!sourceSearchRangeHasSafeBoundaries(line, start, start)) start--; - var end = math.min(line.length, start + maximumLength - 2); - if (!sourceSearchRangeHasSafeBoundaries(line, end, end)) end--; - return '${start > 0 ? '…' : ''}${line.substring(start, end)}' - '${end < line.length ? '…' : ''}'; -} - -String _stripMarkdownForSearchResult(String line, {BuildContext? context}) { - // Preview navigation also calls this helper directly with source lines. - var value = _boundedSearchSnippet(line).trim(); - final fence = RegExp( - r'^(```+|~~~+)\s*([A-Za-z0-9_+\-#.]*)', - ).firstMatch(value); - if (fence != null) { - final language = fence.group(2)?.trim() ?? ''; - return language.isEmpty - ? context?.l10n.codeBlock ?? 'code block' - : language; - } - - value = value - .replaceFirst(RegExp(r'^\s{0,3}#{1,6}\s+'), '') - .replaceFirst(RegExp(r'^\s{0,3}>\s?'), '') - .replaceFirst(RegExp(r'^\s{0,3}[-+*]\s+\[[ xX]\]\s+'), '') - .replaceFirst(RegExp(r'^\s{0,3}[-+*]\s+'), '') - .replaceFirst(RegExp(r'^\s{0,3}\d+[.)]\s+'), '') - .trim(); - final replacements = { - RegExp(r'!\[([^\]]*)\]\([^)]+\)'): (match) => match.group(1) ?? '', - RegExp(r'\[([^\]]+)\]\([^)]+\)'): (match) => match.group(1) ?? '', - RegExp(r'\[([^\]]+)\]\[[^\]]*\]'): (match) => match.group(1) ?? '', - RegExp(r'`([^`]*)`'): (match) => match.group(1) ?? '', - RegExp(r'\*\*([^*]+)\*\*'): (match) => match.group(1) ?? '', - RegExp(r'__([^_]+)__'): (match) => match.group(1) ?? '', - RegExp(r'\*([^*]+)\*'): (match) => match.group(1) ?? '', - RegExp(r'_([^_]+)_'): (match) => match.group(1) ?? '', - RegExp(r'~~([^~]+)~~'): (match) => match.group(1) ?? '', - RegExp(r'<[^>]+>'): (_) => '', - }; - for (final entry in replacements.entries) { - value = value.replaceAllMapped(entry.key, entry.value); - } - value = value.replaceAll(RegExp(r'\s+'), ' ').trim(); - return value.isEmpty - ? context?.l10n.untitledResult ?? 'untitled result' - : value; -} String _relativeDocumentPath(Workspace workspace, String path) { for (final file in workspace.files) { @@ -19382,24 +18120,6 @@ IconData _documentKindIcon(DocumentKind kind) { }; } -String _documentKindLabel(BuildContext context, DocumentKind kind) { - return switch (kind) { - DocumentKind.markdown => context.l10n.documentKindMarkdownFile, - DocumentKind.writersideMarkdownTopic => - context.l10n.documentKindWritersideMarkdownTopic, - DocumentKind.writersideXmlTopic => - context.l10n.documentKindWritersideXmlTopic, - DocumentKind.tree => context.l10n.documentKindWritersideTree, - DocumentKind.config => context.l10n.documentKindConfigurationFile, - DocumentKind.variables => context.l10n.documentKindVariablesFile, - DocumentKind.categories => context.l10n.documentKindCategoriesFile, - DocumentKind.gitIgnore => context.l10n.documentKindConfigurationFile, - DocumentKind.image => context.l10n.image, - DocumentKind.resource => context.l10n.documentKindResourceFile, - DocumentKind.unknown => context.l10n.file, - }; -} - class _DiagnosticRow extends ConsumerWidget { const _DiagnosticRow({required this.diagnostic, required this.onRevealed}); diff --git a/lib/src/workspace/presentation/writerside_toc_dialogs.dart b/lib/src/workspace/presentation/writerside_toc_dialogs.dart index 53934b06..4a99f6bd 100644 --- a/lib/src/workspace/presentation/writerside_toc_dialogs.dart +++ b/lib/src/workspace/presentation/writerside_toc_dialogs.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; import 'package:url_launcher/url_launcher.dart'; @@ -393,21 +392,14 @@ class _WritersideExistingTopicPickerState title: context.l10n.tocSelectExistingTopic, maxWidth: BusyMarkSizes.dialog, children: [ - Focus( - onKeyEvent: (_, event) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - final key = event.logicalKey; - if (key != LogicalKeyboardKey.arrowDown && - key != LogicalKeyboardKey.arrowUp) { - return KeyEventResult.ignored; - } + BusyMarkSearchField( + autofocus: true, + hintText: context.l10n.search, + onMoveSelection: (delta) { if (topics.isNotEmpty) { setState( - () => _selected = - (_selected + (key == LogicalKeyboardKey.arrowDown ? 1 : -1)) - .clamp(0, topics.length - 1), + () => + _selected = (_selected + delta).clamp(0, topics.length - 1), ); if (_scroll.hasClients) { _scroll.jumpTo( @@ -418,20 +410,15 @@ class _WritersideExistingTopicPickerState ); } } - return KeyEventResult.handled; }, - child: BusyMarkSearchField( - autofocus: true, - hintText: context.l10n.search, - onEscape: () => Navigator.pop(context), - onChanged: (value) => setState(() { - _query = value; - _selected = 0; - }), - onSubmitted: (_) { - if (topics.isNotEmpty) Navigator.pop(context, topics[_selected]); - }, - ), + onEscape: () => Navigator.pop(context), + onChanged: (value) => setState(() { + _query = value; + _selected = 0; + }), + onSubmitted: (_) { + if (topics.isNotEmpty) Navigator.pop(context, topics[_selected]); + }, ), SizedBox( height: 300, diff --git a/lib/src/workspace/recent_navigation.dart b/lib/src/workspace/recent_navigation.dart new file mode 100644 index 00000000..eeab54b6 --- /dev/null +++ b/lib/src/workspace/recent_navigation.dart @@ -0,0 +1,190 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:path/path.dart' as p; +import 'package:url_launcher/url_launcher.dart'; + +import '../app/app_router.dart'; +import '../app/app_settings.dart'; +import '../app/busymark_toast.dart'; +import '../app/localization.dart'; +import '../nextcloud_notes/application/nextcloud_connection.dart'; +import '../nextcloud_notes/application/notes_navigation.dart'; +import '../search/search_session.dart'; +import 'document_navigation.dart'; +import 'workspace_controller.dart'; +import 'workspace_safety.dart'; + +Future openRecentResource( + BuildContext context, + WidgetRef ref, + RecentResource resource, +) async { + final controller = ref.read(workspaceControllerProvider.notifier); + final origin = ref.read(appRouterProvider).routeInformationProvider.value.uri; + final search = ref.read(searchSessionProvider); + bool currentRequest() => + context.mounted && + ref.read(appRouterProvider).routeInformationProvider.value.uri == + origin && + ref.read(searchSessionProvider).requestRevision == + search.requestRevision && + (!search.active || + ref.read(searchSessionProvider).active && + ref.read(searchSessionProvider).focusRequest == + search.focusRequest && + ref.read(searchSessionProvider).category == search.category && + ref.read(searchSessionProvider).query == search.query); + final current = ref.read(workspaceControllerProvider).workspace; + bool opened = false; + if (resource.accountId != null) { + final repository = await ref.read(nextcloudNotesRepositoryProvider.future); + if (!context.mounted || !currentRequest()) return; + final account = repository.accounts + .where((a) => a.id == resource.accountId) + .firstOrNull; + final note = resource.localId == null + ? null + : repository.noteById(resource.localId!); + if (account == null || + resource.isDocument && + (note?.accountId != resource.accountId || + isNotesRecovery(note!) || + repository.isCreationCandidateBinding(note.localId))) { + BusyMarkToastOverlay.show( + context, + message: context.l10n.searchResourceUnavailable, + ); + return; + } + if (current?.nextcloudAccountId != resource.accountId) { + if (!await confirmSafeToContinue(context, ref) || + !context.mounted || + !currentRequest()) { + return; + } + if (!await controller.openNextcloudWorkspace( + resource.accountId!, + acceptNavigation: currentRequest, + )) { + return; + } + } + opened = + resource.localId == null || + await controller.openNextcloudNote( + resource.localId!, + acceptNavigation: currentRequest, + ); + } else { + if (!await FileSystemEntity.isFile(resource.path) && + !await Directory(resource.path).exists()) { + if (context.mounted) { + BusyMarkToastOverlay.show( + context, + message: context.l10n.searchResourceUnavailable, + ); + } + return; + } + if (!context.mounted || !currentRequest()) return; + final sameOwner = + current != null && + (resource.ownerRoot != null + ? p.equals(current.rootPath, resource.ownerRoot!) + : current.files.any( + (f) => p.equals(f.absolutePath, resource.path), + )); + if (resource.isDocument && sameOwner) { + opened = await controller.openActiveFile( + resource.path, + acceptNavigation: currentRequest, + ); + } else { + if (!await confirmSafeToContinue(context, ref) || + !context.mounted || + !currentRequest()) { + return; + } + await controller.openPath( + resource.ownerRoot ?? resource.path, + acceptNavigation: currentRequest, + ); + if (!context.mounted || !currentRequest()) return; + final workspace = ref.read(workspaceControllerProvider).workspace; + if (workspace == null) return; + if (resource.ownerRoot != null && + workspace.writersideProject == null && + workspace.writersideModule == null) { + BusyMarkToastOverlay.show( + context, + message: context.l10n.searchResourceUnavailable, + ); + return; + } + opened = + resource.ownerRoot == null || + await controller.openActiveFile( + resource.path, + acceptNavigation: currentRequest, + ); + } + } + if (opened && context.mounted) { + await controller.recordRecentResource( + RecentResource( + path: resource.path, + kind: resource.kind, + accountId: resource.accountId, + accountLabel: resource.accountLabel, + localId: resource.localId, + title: resource.title, + ownerRoot: resource.ownerRoot, + document: resource.isDocument, + lastOpenedAt: DateTime.now(), + ), + ); + if (!context.mounted || !currentRequest()) return; + revealOpenedDocument(ref); + ref.read(searchSessionProvider.notifier).close(restoreFocus: false); + context.go('/workspace'); + } +} + +enum RecentNavigationAction { openInFiles, copyPath, removeFromRecent } + +Future runRecentNavigationAction( + BuildContext context, + WidgetRef ref, + RecentResource resource, + RecentNavigationAction action, +) async { + switch (action) { + case RecentNavigationAction.removeFromRecent: + await ref + .read(appSettingsControllerProvider.notifier) + .removeRecentWorkspace(resource.identity); + case RecentNavigationAction.copyPath: + await Clipboard.setData(ClipboardData(text: resource.path)); + case RecentNavigationAction.openInFiles: + final type = await FileSystemEntity.type(resource.path); + if (!context.mounted) return; + if (type == FileSystemEntityType.notFound) { + BusyMarkToastOverlay.show( + context, + message: context.l10n.searchResourceUnavailable, + ); + return; + } + await launchUrl( + Uri.directory( + type == FileSystemEntityType.directory + ? resource.path + : p.dirname(resource.path), + ), + ); + } +} diff --git a/lib/src/workspace/recent_presentation.dart b/lib/src/workspace/recent_presentation.dart new file mode 100644 index 00000000..141773ca --- /dev/null +++ b/lib/src/workspace/recent_presentation.dart @@ -0,0 +1,71 @@ +import 'package:flutter/widgets.dart'; +import 'package:path/path.dart' as p; + +import '../app/app_settings.dart'; +import '../app/localization.dart'; +import '../platform/user_directories.dart'; + +String recentResourceTitle(BuildContext context, RecentResource resource) => + resource.title ?? + (resource.accountId == null + ? p.basename(resource.path) + : context.l10n.nextcloudNotes); + +String recentResourceLocation( + BuildContext context, + RecentResource resource, { + String? accountLabel, + String? homeDirectory, + String? workspaceRoot, + Iterable recentResources = const [], +}) { + if (resource.accountId != null) { + final label = accountLabel?.trim().isNotEmpty == true + ? accountLabel + : resource.accountLabel; + return label == null || label.trim().isEmpty + ? context.l10n.searchAccountUnavailable + : label; + } + final parent = p.dirname(p.normalize(resource.path)); + final home = homeDirectory ?? userHomeDirectory; + if (home == null || !(p.equals(home, parent) || p.isWithin(home, parent))) { + return parent; + } + String homeRelative(String path) => + p.equals(home, path) ? '~' : p.join('~', p.relative(path, from: home)); + if (!resource.isDocument || p.equals(p.dirname(parent), home)) { + return homeRelative(parent); + } + final roots = { + if (resource.ownerRoot != null) p.normalize(resource.ownerRoot!), + if (workspaceRoot != null) p.normalize(workspaceRoot), + for (final other in recentResources) ...[ + if (other.ownerRoot != null) p.normalize(other.ownerRoot!), + if (!other.isDocument && + other.accountId == null && + (other.kind == 'markdownFolder' || other.kind == 'writersideModule')) + p.normalize(other.path), + ], + }; + final owners = + roots + .where( + (root) => + (p.equals(home, root) || p.isWithin(home, root)) && + (p.equals(root, parent) || p.isWithin(root, parent)), + ) + .toList() + ..sort((a, b) => b.length.compareTo(a.length)); + if (owners.isEmpty || p.equals(owners.first, home)) { + return homeRelative(parent); + } + final owner = owners.first; + final ambiguous = roots.any( + (root) => p.basename(root) == p.basename(owner) && !p.equals(root, owner), + ); + final label = ambiguous ? homeRelative(owner) : p.basename(owner); + return p.equals(owner, parent) + ? label + : p.join(label, p.relative(parent, from: owner)); +} diff --git a/lib/src/workspace/recovery_persistence.dart b/lib/src/workspace/recovery_persistence.dart index ba1c2c11..b991315c 100644 --- a/lib/src/workspace/recovery_persistence.dart +++ b/lib/src/workspace/recovery_persistence.dart @@ -5,6 +5,8 @@ import 'dart:io'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import '../core/linux_private_permissions.dart'; + import 'document_buffer.dart'; import 'session_persistence.dart'; import 'text_format_metadata.dart'; @@ -538,6 +540,7 @@ class JsonDocumentRecoveryStore implements DocumentRecoveryStore { Future _serialized(Future Function() operation) { final completer = Completer(); + _queue = _queue.then((_) async { try { completer.complete(await operation()); @@ -577,10 +580,19 @@ int? _linuxProcessStartIdentity(int processId) { Future _withRecoveryLock(File recovery, Future Function() body) async { await recovery.parent.create(recursive: true); - final lock = await File('${recovery.path}.lock').open(mode: FileMode.append); + final lockFile = File('${recovery.path}.lock'); + final lock = await lockFile.open(mode: FileMode.append); final deadline = DateTime.now().add(const Duration(seconds: 30)); var acquired = false; try { + // Protect a newly created lock before releasing its first handle. Otherwise + // an owner-read-stripping creation mask prevents subsequent checkpoints. + if (Platform.isLinux) { + await LinuxPrivatePermissions.instance.restrict( + lockFile.path, + directory: false, + ); + } while (!acquired) { try { await lock.lock(FileLock.exclusive); @@ -593,6 +605,7 @@ Future _withRecoveryLock(File recovery, Future Function() body) async { ? code == 33 : false; if (!contention || !DateTime.now().isBefore(deadline)) rethrow; + await Future.delayed(const Duration(milliseconds: 25)); } } diff --git a/lib/src/workspace/session_persistence.dart b/lib/src/workspace/session_persistence.dart index 5c806192..35274fb4 100644 --- a/lib/src/workspace/session_persistence.dart +++ b/lib/src/workspace/session_persistence.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import '../core/linux_private_permissions.dart'; + import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -676,6 +678,7 @@ Future _withSessionLock( break; } on FileSystemException catch (error) { if (!_isSessionLockContention(error)) rethrow; + final remaining = budget - elapsed.elapsed; if (remaining <= Duration.zero) { throw FileSystemException( @@ -729,16 +732,16 @@ Future _writeAtomic(File target, Map json) async { await target.parent.create(recursive: true); await _setPrivatePermissions(target.parent, directory: true); final staging = await target.parent.createTemp('.busymark-state-'); - await _setPrivatePermissions(staging, directory: true); final staged = File(p.join(staging.path, p.basename(target.path))); try { + await _setPrivatePermissions(staging, directory: true); await staged.writeAsString( const JsonEncoder.withIndent(' ').convert(json), flush: true, ); await _setPrivatePermissions(staged, directory: false); await staged.rename(target.path); - await _setPrivatePermissions(target, directory: false); + // Rename publishes the already-private inode, preserving its permissions. } finally { try { if (await staged.exists()) { @@ -764,6 +767,13 @@ Future _setPrivatePermissions( if (Platform.isWindows) { return; } + if (Platform.isLinux) { + await LinuxPrivatePermissions.instance.restrict( + entity.path, + directory: directory, + ); + return; + } final result = await Process.run('chmod', [ directory ? '700' : '600', entity.path, diff --git a/lib/src/workspace/workspace_controller.dart b/lib/src/workspace/workspace_controller.dart index 06d78a48..b6e86dfb 100644 --- a/lib/src/workspace/workspace_controller.dart +++ b/lib/src/workspace/workspace_controller.dart @@ -3,6 +3,7 @@ import '../references/markdown_move_service.dart'; import '../nextcloud_notes/application/notes_navigation.dart'; import 'dart:async'; import 'dart:io'; + import 'dart:math' as math; import 'package:flutter/services.dart'; @@ -128,16 +129,6 @@ final workspaceControllerProvider = WorkspaceController.new, ); -final workspaceSearchOpenRequestProvider = - NotifierProvider( - WorkspaceSearchRequestController.new, - ); - -final workspaceSearchCloseRequestProvider = - NotifierProvider( - WorkspaceSearchRequestController.new, - ); - /// Identifies the exact editor revision that an asynchronous save-related /// operation was started for. /// @@ -284,6 +275,7 @@ class WorkspaceController extends Notifier { final _restoredRecoveryOwnerIdsByBuffer = {}; late Future _recoveryStart; + final _workspaceOpenings = >{}; Future _persistenceWrites = Future.value(); int get editRevision => state.activeBuffer?.revision ?? _editRevision; @@ -515,6 +507,15 @@ class WorkspaceController extends Notifier { if (!ref.mounted || state.workspace?.isRemote != true) return; final repository = _notesRepository; if (repository == null) return; + unawaited( + _settingsController.updateRecentNoteTitles({ + for (final recent in _settingsController.state.recentWorkspaces) + if (recent.accountId != null && recent.localId != null) + if (repository.noteById(recent.localId!) case final note? + when note.accountId == recent.accountId) + (note.accountId, note.localId): note.title, + }), + ); var contentChanged = false; final buffers = []; for (final buffer in state.documentBuffers) { @@ -617,7 +618,9 @@ class WorkspaceController extends Notifier { String accountId, { List? restoredTabs, String? activeBufferId, + bool Function()? acceptNavigation, }) async { + if (acceptNavigation?.call() == false) return false; if (!propertyEdits.flush()) return false; if (state.hasUnsavedChanges) return false; final prior = state; @@ -629,6 +632,7 @@ class WorkspaceController extends Notifier { } final settled = await _localHistory.flushAll(prior.documentBuffers); if (!ref.mounted || + acceptNavigation?.call() == false || priorRevision != _activeDocumentRevision || !identical(prior.workspace, state.workspace) || state.hasUnsavedChanges) { @@ -636,7 +640,7 @@ class WorkspaceController extends Notifier { } _cancelPendingDerivedRefresh(); _cancelAllAutoSaves(); - _invalidateActiveDocumentOperations(); + final operationRevision = _invalidateActiveDocumentOperations(); await _fileMonitor.stop(); for (final buffer in prior.documentBuffers) { await _localHistory.handleBufferClosed( @@ -645,6 +649,11 @@ class WorkspaceController extends Notifier { ); } final workspace = Workspace.nextcloudNotes(accountId); + if (!_isCurrentActiveDocumentOperation(operationRevision)) return false; + if (acceptNavigation?.call() == false) { + await _restoreCancelledNavigation(prior); + return false; + } final buffers = []; for (final tab in restoredTabs ?? const []) { final reference = tab.remoteNote; @@ -686,6 +695,20 @@ class WorkspaceController extends Notifier { ); } _schedulePersistence(); + if (restoredTabs == null) { + await _publishRecent( + () => _settingsController.recordOpenedWorkspace( + path: '', + kind: 'nextcloudNotes', + accountId: accountId, + title: repository.accounts + .where((a) => a.id == accountId) + .firstOrNull + ?.server + .toString(), + ), + ); + } unawaited( _notesCoordinator ?.request(NotesSyncTrigger.opening) @@ -705,9 +728,14 @@ class WorkspaceController extends Notifier { } } - Future openNextcloudNote(String localId) async { + Future openNextcloudNote( + String localId, { + bool Function()? acceptNavigation, + }) async { + if (acceptNavigation?.call() == false) return false; if (!propertyEdits.flush()) return false; final repository = await _ensureNotesRepository(); + if (!ref.mounted || acceptNavigation?.call() == false) return false; final note = repository.noteById(localId); final workspace = state.workspace; if (note == null || @@ -731,7 +759,9 @@ class WorkspaceController extends Notifier { buffer, documentBuffers: state.documentBuffers, openFilePaths: const [], + acceptNavigation: acceptNavigation, ); + if (result) await _recordOpenedDocument(); if (result && existing == null) { unawaited(_localHistory.observeOpened(buffer)); } @@ -770,22 +800,37 @@ class WorkspaceController extends Notifier { Future createNextcloudNote({ String title = 'New note', - String category = '', + String? category, String content = '', + bool Function()? acceptNavigation, }) async { + if (acceptNavigation?.call() == false) return false; if (!propertyEdits.flush()) return false; try { final accountId = state.workspace?.nextcloudAccountId; if (accountId == null) return false; final repository = await _ensureNotesRepository(); + if (acceptNavigation?.call() == false || + state.workspace?.nextcloudAccountId != accountId) { + return false; + } final note = await repository.create( accountId, title: title, - category: category, + category: + category ?? + (ref.read(notesNavigationProvider(accountId)).destination == + NotesDestination.category + ? ref.read(notesNavigationProvider(accountId)).category + : ''), content: content, ); - final opened = await openNextcloudNote(note.localId); + final opened = await openNextcloudNote( + note.localId, + acceptNavigation: acceptNavigation, + ); _scheduleRemoteSync(accountId); + _publishCreatedNextcloudNote(note, repository.notes); return opened; } on Object catch (error) { _reportNextcloudFailure(error); @@ -793,6 +838,24 @@ class WorkspaceController extends Notifier { } } + void _publishCreatedNextcloudNote( + NextcloudNote note, + Iterable notes, + ) { + final current = notes + .where( + (n) => n.localId == note.localId && n.accountId == note.accountId, + ) + .firstOrNull; + if (current == null) return; + ref + .read(notesNavigationProvider(current.accountId)) + .showCreated(current, notes); + ref + .read(notesCreatedProvider.notifier) + .created(note.accountId, note.localId); + } + void _reportNextcloudFailure(Object error) { if (ref.mounted) { state = state.copyWith( @@ -946,7 +1009,7 @@ class WorkspaceController extends Notifier { } } - Future updateNextcloudNoteMetadata( + Future updateNextcloudNoteMetadata( String localId, { NotesMetadataSnapshot? snapshot, String? title, @@ -961,10 +1024,10 @@ class WorkspaceController extends Notifier { .firstOrNull; if (buffer?.isDirty == true && !await _saveRemoteBufferSnapshot(buffer!, scheduleSync: false)) { - return; + return false; } final note = repository.noteById(localId); - if (note == null) return; + if (note == null) return false; final updated = await repository.patchMetadata( snapshot ?? repository.metadataSnapshot(localId), title: title, @@ -974,8 +1037,10 @@ class WorkspaceController extends Notifier { if (updated.revision != beforeRevision) { _scheduleRemoteSync(note.accountId); } + return updated.metadataConflict == null; } on Object catch (error) { _reportNextcloudFailure(error); + return false; } } @@ -999,6 +1064,7 @@ class WorkspaceController extends Notifier { snapshot.note.title, NotesBatchStatus.failed, 'Editor save failed.', + NotesBatchReason.editorNotSaved, ), ); } else { @@ -1098,7 +1164,9 @@ class WorkspaceController extends Notifier { } } - Future resolveNextcloudConflict( + /// A changed remote review is returned as [NotesConflictReviewChanged]; the + /// caller must present it before another decision. Other failures return false. + Future resolveNextcloudConflict( String localId, NoteConflictResolution resolution, { String? mergedContent, @@ -1106,6 +1174,7 @@ class WorkspaceController extends Notifier { int? expectedRevision, int? creationCandidateServerId, NotesCreationReview? creationReview, + NoteState? reviewedRemote, }) async { try { final repository = await _ensureNotesRepository(); @@ -1140,14 +1209,14 @@ class WorkspaceController extends Notifier { } if (buffer?.isDirty == true && !await _saveRemoteBufferSnapshot(buffer!, scheduleSync: false)) { - return; + return false; } if (buffer != null && !await _localHistory.captureBeforeLoss( LocalHistoryBufferSnapshot.fromBuffer(buffer), LocalHistoryCaptureReason.beforeRestore, )) { - return; + return false; } await repository.resolveConflict( localId, @@ -1157,50 +1226,59 @@ class WorkspaceController extends Notifier { expectedRevision: expectedRevision, creationCandidateServerId: creationCandidateServerId, creationReview: creationReview, + reviewedRemote: reviewedRemote, ); final accountId = repository.noteById(localId)?.accountId; if (accountId != null) _scheduleRemoteSync(accountId); + return true; + } on NotesConflictReviewChanged { + rethrow; } on Object catch (error) { _reportNextcloudFailure(error); + return false; } } - Future recoverDeletedNextcloudNote( + Future recoverDeletedNextcloudNote( String localId, { String? content, }) async { + String? recoveredId; try { final repository = await _ensureNotesRepository(); final original = repository.noteById(localId); if (original == null || state.workspace?.nextcloudAccountId != original.accountId) { - return false; + return const NotesRecoveryResult(); } final recovered = await repository.recoverAsNew( localId, content: content, ); - final opened = await openNextcloudNote(recovered.localId); + recoveredId = recovered.localId; _scheduleRemoteSync(original.accountId); - return opened; + _publishCreatedNextcloudNote(recovered, repository.notes); + final opened = await openNextcloudNote(recovered.localId); + return NotesRecoveryResult(noteId: recoveredId, opened: opened); } on Object catch (error) { _reportNextcloudFailure(error); - return false; + return NotesRecoveryResult(noteId: recoveredId, failure: error); } } - Future recoverNextcloudHistoryRevision({ + Future recoverNextcloudHistoryRevision({ required LocalHistoryDocument document, required LocalHistoryRevision revision, }) async { + String? recoveredId; try { final reference = document.remoteNote; if (reference == null || revision.summary.documentId != document.id) { - return false; + return const NotesRecoveryResult(); } if (state.workspace?.nextcloudAccountId != reference.accountId && !await openNextcloudWorkspace(reference.accountId)) { - return false; + return const NotesRecoveryResult(); } final repository = await _ensureNotesRepository(); final recovered = await repository.recoverAsNew( @@ -1208,18 +1286,21 @@ class WorkspaceController extends Notifier { title: document.displayName, content: revision.source, ); - final opened = await openNextcloudNote(recovered.localId); + recoveredId = recovered.localId; _scheduleRemoteSync(reference.accountId); - return opened; + _publishCreatedNextcloudNote(recovered, repository.notes); + final opened = await openNextcloudNote(recovered.localId); + return NotesRecoveryResult(noteId: recoveredId, opened: opened); } on Object catch (error) { _reportNextcloudFailure(error); - return false; + return NotesRecoveryResult(noteId: recoveredId, failure: error); } } /// Called after removal has committed, so session restoration cannot reopen /// an account whose durable store and keyring entry have been removed. Future closeRemovedNextcloudWorkspace(String accountId) async { + await _settingsController.removeRecentAccount(accountId); if (state.workspace?.nextcloudAccountId != accountId) return; _cancelPendingDerivedRefresh(); _cancelAllAutoSaves(); @@ -1233,7 +1314,30 @@ class WorkspaceController extends Notifier { await flushPersistence(); } - Future restorePreviousSession() async { + Future restorePreviousSession() => + _trackWorkspaceOpening(() => _restorePreviousSession()); + + Future _trackWorkspaceOpening(Future Function() operation) async { + final settled = Completer(); + _workspaceOpenings.add(settled.future); + try { + return await operation(); + } finally { + _workspaceOpenings.remove(settled.future); + settled.complete(); + } + } + + /// Wait before reading dirty state or persisting a shutdown snapshot. + /// Recovery ownership can already be adopted while the editor is unpublished. + /// Opening itself must not await this barrier (including its persistence). + Future waitForWorkspaceOpening() async { + while (_workspaceOpenings.isNotEmpty) { + await Future.wait(_workspaceOpenings.toList(growable: false)); + } + } + + Future _restorePreviousSession() async { if (state.workspace != null) { return state.documentBuffers.isNotEmpty; } @@ -1249,7 +1353,9 @@ class WorkspaceController extends Notifier { // an authoritative snapshot before consuming any durable journal: an // early failure must leave the persisted workspace and its obligations // untouched for the next startup. - if (!await _localHistory.refresh()) return false; + if (!await _localHistory.refresh()) { + return false; + } if (session?.pendingLocalHistoryReconciliations.any( (operation) => _localHistoryOperationOwnerIsLiveOtherProcess( operation.operationId, @@ -1517,7 +1623,8 @@ class WorkspaceController extends Notifier { } final buffers = []; final consumedRecoveries = <(String?, String)>{}; - for (final entry in sessionEntries) { + Future<({DocumentBuffer? buffer, DocumentRecoveryEntry? recovery})> + loadSessionEntry(DocumentSessionEntry entry) async { final ownedById = recoverEntries .where( (recovery) => @@ -1541,11 +1648,36 @@ class WorkspaceController extends Notifier { : ownedById.isEmpty && ownedByPath.length == 1 ? ownedByPath.single : null; + final buffer = await _restoreSessionBuffer(entry, recovered); - if (buffer != null) { - buffers.add(buffer); - if (recovered != null) { - consumedRecoveries.add((recovered.ownerId, recovered.id)); + return (buffer: buffer, recovery: recovered); + } + + // Bound file reads/decoding while preserving session order. A failed + // batch drains every admitted read before recovery can be released; + // no partial batch is published or left running after failure. + const readBatchSize = 4; + for ( + var offset = 0; + offset < sessionEntries.length; + offset += readBatchSize + ) { + if (!ref.mounted) return false; + final end = offset + readBatchSize < sessionEntries.length + ? offset + readBatchSize + : sessionEntries.length; + final loaded = await Future.wait([ + for (var index = offset; index < end; index++) + loadSessionEntry(sessionEntries[index]), + ], eagerError: false); + for (final item in loaded) { + final buffer = item.buffer; + if (buffer != null) { + buffers.add(buffer); + final recovered = item.recovery; + if (recovered != null) { + consumedRecoveries.add((recovered.ownerId, recovered.id)); + } } } } @@ -1599,6 +1731,7 @@ class WorkspaceController extends Notifier { if (!ref.mounted || !_preparedInputsCurrent(reparsed, buffers)) { return false; } + state = WorkspaceState( workspace: reparsed, preview: _safePreview(reparsed, active), @@ -1616,6 +1749,7 @@ class WorkspaceController extends Notifier { ) : null, ); + for (final entry in sessionEntries) { final recoveryOwnerId = entry.recoveryOwnerId; if (recoveryOwnerId != null && @@ -2167,15 +2301,19 @@ class WorkspaceController extends Notifier { /// Restores startup state when the previous run needs recovery, or when the /// user has explicitly enabled clean-session reopening. - Future restoreStartupSession({required bool reopenCleanSession}) async { - final recovery = await _recoveryStart; - final needsRecovery = - !recovery.cleanShutdown || recovery.entries.isNotEmpty; - if (!reopenCleanSession && !needsRecovery) { - return false; - } - return restorePreviousSession(); - } + Future restoreStartupSession({bool? reopenCleanSession}) => + _trackWorkspaceOpening(() async { + await _settingsController.waitUntilLoaded(); + final recovery = await _recoveryStart; + final needsRecovery = + !recovery.cleanShutdown || recovery.entries.isNotEmpty; + if (!(reopenCleanSession ?? + _settingsController.state.reopenPreviousWorkspaceOnStartup) && + !needsRecovery) { + return false; + } + return _restorePreviousSession(); + }); Future _persistRestoredLocalHistoryJournal( WorkspaceSessionSnapshot original, { @@ -2356,6 +2494,7 @@ class WorkspaceController extends Notifier { final pendingHistorySaveAs = _localHistory.pendingSaveAsOperations; final retainedHistoryCaptures = _localHistory.retainedDetachedCaptures; final prior = _persistenceWrites.then((_) {}, onError: (_, _) {}); + final write = prior.then( (_) => _persistSnapshot( snapshot, @@ -2563,8 +2702,10 @@ class WorkspaceController extends Notifier { } Future _settleWritesForShutdown() async { + await waitForWorkspaceOpening(); _cancelAllAutoSaves(); await _drainBufferWrites(); + await _settingsController.flushPersistence(); // A write that retained a newer dirty revision can schedule another // debounce while the queue is draining. Shutdown owns the final save or // recovery decision, so do not let that timer outlive it. @@ -3883,12 +4024,26 @@ class WorkspaceController extends Notifier { await _startMonitoring(state.workspace ?? workspace); } - Future openPath(String path) async { + Future openPath(String path, {bool Function()? acceptNavigation}) => + _trackWorkspaceOpening( + () => _openPath(path, acceptNavigation: acceptNavigation), + ); + + Future _openPath( + String path, { + bool Function()? acceptNavigation, + }) async { + if (acceptNavigation?.call() == false) return; if (!propertyEdits.flush()) return; - _invalidateFileMonitorWorkspace(); - _removalOperationRevision++; + final prior = state; + final recoveryOwners = Map.of( + _restoredRecoveryOwnerIdsByBuffer, + ); final closingBuffers = state.documentBuffers; final historySettled = await _localHistory.flushAll(closingBuffers); + if (!ref.mounted || acceptNavigation?.call() == false) return; + _invalidateFileMonitorWorkspace(); + _removalOperationRevision++; _cancelPendingDerivedRefresh(); _cancelAllAutoSaves(); final operationRevision = _invalidateActiveDocumentOperations(); @@ -3908,7 +4063,7 @@ class WorkspaceController extends Notifier { ? null : await _service.loadTextWithSnapshot(active); final text = load?.text ?? ''; - final loadedWorkspace = load == null + var loadedWorkspace = load == null ? workspace : workspace.copyWith(activeFileSnapshot: load.snapshot); final buffer = load == null || active == null @@ -3918,13 +4073,36 @@ class WorkspaceController extends Notifier { load, mode: _settingsController.state.documentViewMode, ); + // The service model and this validated reread can straddle an external + // edit. Prepare the reread revision before publishing it, so the initial + // editor never synchronously reparses a mismatching model during build. + final markdown = loadedWorkspace.markdown; + if (buffer != null && + (_writersideSourceMismatch(loadedWorkspace, buffer) || + (markdown != null && + p.equals(markdown.filePath, active!) && + markdown.source != buffer.text))) { + loadedWorkspace = await _service.prepareDocument( + loadedWorkspace, + buffer, + {active!: buffer.text}, + ); + } final preview = buffer == null ? null : _safePreview(loadedWorkspace, buffer); if (!_isCurrentActiveDocumentOperation(operationRevision)) { return; } + if (acceptNavigation?.call() == false) { + await _restoreCancelledNavigation( + prior, + recoveryOwners: recoveryOwners, + ); + return; + } state = WorkspaceState( + isLoading: true, workspace: loadedWorkspace, activeText: text, preview: preview, @@ -3938,15 +4116,23 @@ class WorkspaceController extends Notifier { _requestDerivedRefresh(rebuildPreview: true); } await _startMonitoring(loadedWorkspace); + if (!_isCurrentActiveDocumentOperation(operationRevision)) return; + state = state.copyWith(isLoading: false); _schedulePersistence(); _resetSaveTracking(); final recentPath = workspace.kind == WorkspaceKind.singleMarkdown ? workspace.activeFilePath ?? workspace.rootPath : workspace.rootPath; - await _settingsController.recordOpenedWorkspace( - path: recentPath, - kind: workspace.kind.name, + await _publishRecent( + () => _settingsController.recordOpenedWorkspace( + path: recentPath, + kind: workspace.kind.name, + ), ); + if (buffer?.filePath != null && + p.equals(p.absolute(path), p.absolute(buffer!.filePath!))) { + await _recordOpenedDocument(); + } } on Object catch (error, stackTrace) { busyMarkDebugLogError( '[BusyMark] Open failed', @@ -3966,6 +4152,26 @@ class WorkspaceController extends Notifier { } } + Future _restoreCancelledNavigation( + WorkspaceState prior, { + Map? recoveryOwners, + }) async { + if (recoveryOwners != null) { + _restoredRecoveryOwnerIdsByBuffer + ..clear() + ..addAll(recoveryOwners); + } + state = prior; + _recordActivePreviewRevision(); + if (prior.workspace case final workspace?) { + await _startMonitoring(workspace); + } + for (final buffer in prior.documentBuffers) { + unawaited(_localHistory.observeOpened(buffer)); + _scheduleAutoSave(buffer.id); + } + } + Future createWritersideProject( WritersideProjectCreateRequest request, ) async { @@ -4025,9 +4231,11 @@ class WorkspaceController extends Notifier { await _startMonitoring(loadedWorkspace); _schedulePersistence(); _resetSaveTracking(); - await _settingsController.recordOpenedWorkspace( - path: workspace.rootPath, - kind: workspace.kind.name, + await _publishRecent( + () => _settingsController.recordOpenedWorkspace( + path: workspace.rootPath, + kind: workspace.kind.name, + ), ); return true; } on Object catch (error, stackTrace) { @@ -4147,15 +4355,99 @@ class WorkspaceController extends Notifier { Future openFolder(String path) => openPath(path); - Future openActiveFile(String path) async { - return _openActiveFile(path); + Future openActiveFile( + String path, { + bool Function()? acceptNavigation, + }) async { + if (acceptNavigation?.call() == false || !propertyEdits.flush()) { + return false; + } + final opened = await _openActiveFile( + path, + acceptNavigation: acceptNavigation, + ); + if (opened) unawaited(_recordOpenedDocument()); + return opened; + } + + Future recordRecentResource(RecentResource resource) => + _publishRecent(() => _settingsController.recordRecent(resource)); + + Future _publishRecent(Future Function() publish) async { + final workspaceId = state.workspace?.id; + try { + await publish(); + } on Object catch (error, stackTrace) { + busyMarkDebugLogError( + '[BusyMark] Could not update recent resources', + error, + stackTrace, + ); + // Content and its required persistence have their own completion. A + // failed ancillary Recent write must not turn a successful open into an + // opening failure, or publish into a different/disposed workspace. + if (ref.mounted && state.workspace?.id == workspaceId) { + state = state.copyWith( + message: WorkspaceMessage( + WorkspaceMessageCode.recentWorkspaceUpdateFailed, + error: error, + ), + ); + } + } + } + + Future _recordOpenedDocument([DocumentBuffer? opened]) async { + final workspace = state.workspace; + final buffer = opened ?? state.activeBuffer; + if (workspace == null || buffer == null) return; + final remote = buffer.remoteNote; + if (remote != null) { + await _publishRecent( + () => _settingsController.recordRecent( + RecentResource( + path: '', + kind: 'nextcloudNote', + document: true, + accountId: remote.accountId, + accountLabel: _notesRepository + ?.accountById(remote.accountId) + ?.server + .toString(), + localId: remote.localId, + title: buffer.remoteTitle, + lastOpenedAt: DateTime.now(), + ), + ), + ); + } else if (buffer.filePath case final path?) { + final context = resolveWorkspaceDocumentContext(workspace, buffer); + await _publishRecent( + () => _settingsController.recordRecent( + RecentResource( + path: p.normalize(p.absolute(path)), + kind: workspace.kind == WorkspaceKind.singleMarkdown + ? workspace.kind.name + : context.kind.name, + document: true, + ownerRoot: context.isWritersideOwned ? workspace.rootPath : null, + title: context.writersideTopic?.title, + lastOpenedAt: DateTime.now(), + ), + ), + ); + } } Future activateNextOpenFileTab() => _activateOpenFileTab(1); Future activatePreviousOpenFileTab() => _activateOpenFileTab(-1); - Future activateDocumentBuffer(String bufferId) async { + Future activateDocumentBuffer( + String bufferId, { + bool Function()? acceptNavigation, + }) async { + if (acceptNavigation?.call() == false) return false; final workspace = state.workspace; final buffer = state.documentBuffers .where((candidate) => candidate.id == bufferId) @@ -4165,14 +4457,18 @@ class WorkspaceController extends Notifier { } if (state.activeBufferId == bufferId && workspace.activeFilePath == buffer.filePath) { + await _recordOpenedDocument(); return true; } - return _activateBuffer( + final opened = await _activateBuffer( workspace, buffer, documentBuffers: state.documentBuffers, openFilePaths: workspace.openFilePaths, + acceptNavigation: acceptNavigation, ); + if (opened) unawaited(_recordOpenedDocument()); + return opened; } Future closeActiveOpenFileTab() async { @@ -4398,6 +4694,7 @@ class WorkspaceController extends Notifier { onOperationStaged: (value) => historyOperationId = value, ); committed = true; + await _settingsController.moveRecentPath(path, target); _remapOpenWorkspacePaths(workspace, path, target); await _localHistory.acknowledgeStagedPathOperation(historyOperationId); return _remapMovedPath(activeFilePath, path, target); @@ -4451,6 +4748,7 @@ class WorkspaceController extends Notifier { onOperationStaged: (value) => historyOperationId = value, ); committed = true; + await _settingsController.moveRecentPath(sourcePath, target); _remapOpenWorkspacePaths(workspace, sourcePath, target); await _localHistory.acknowledgeStagedPathOperation(historyOperationId); return _remapMovedPath(activeFilePath, sourcePath, target); @@ -4921,6 +5219,7 @@ class WorkspaceController extends Notifier { ); final target = result.newTopicPath; committed = true; + await _settingsController.moveRecentPath(plan.oldTopicPath, target); _remapOpenWorkspacePaths(workspace, plan.oldTopicPath, target); await _localHistory.acknowledgeStagedPathOperation(historyOperationId); return _remapMovedPath(activeFilePath, plan.oldTopicPath, target); @@ -5351,7 +5650,9 @@ class WorkspaceController extends Notifier { String path, { List? openFilePaths, List? documentBuffers, + bool Function()? acceptNavigation, }) async { + if (acceptNavigation?.call() == false) return false; final workspace = state.workspace; if (workspace == null) { return false; @@ -5374,6 +5675,7 @@ class WorkspaceController extends Notifier { existing, documentBuffers: buffers, openFilePaths: requestedOpenFilePaths, + acceptNavigation: acceptNavigation, ); } _cancelPendingDerivedRefresh(); @@ -5392,7 +5694,8 @@ class WorkspaceController extends Notifier { activeFileSnapshot: load.snapshot, openFilePaths: requestedOpenFilePaths, ); - if (!_isCurrentActiveDocumentOperation(operationRevision) || + if (acceptNavigation?.call() == false || + !_isCurrentActiveDocumentOperation(operationRevision) || state.workspace?.id != workspaceId) { return false; } @@ -5418,7 +5721,8 @@ class WorkspaceController extends Notifier { buffers: state.documentBuffers, ); } - if (!_isCurrentActiveDocumentOperation(operationRevision) || + if (acceptNavigation?.call() == false || + !_isCurrentActiveDocumentOperation(operationRevision) || state.workspace?.id != workspaceId) { return false; } @@ -5501,7 +5805,9 @@ class WorkspaceController extends Notifier { DocumentBuffer buffer, { required List documentBuffers, required List openFilePaths, + bool Function()? acceptNavigation, }) async { + if (acceptNavigation?.call() == false) return false; if (!propertyEdits.flush()) return false; _cancelPendingDerivedRefresh(); final operationRevision = _invalidateActiveDocumentOperations(); @@ -5595,7 +5901,8 @@ class WorkspaceController extends Notifier { parsedBuffer, buffers: documentBuffers, ); - if (!_isCurrentActiveDocumentOperation(operationRevision)) { + if (acceptNavigation?.call() == false || + !_isCurrentActiveDocumentOperation(operationRevision)) { return false; } if (state.workspace?.id != workspaceId) return false; @@ -5612,7 +5919,8 @@ class WorkspaceController extends Notifier { parsedBuffer, buffers: currentRequestedBuffers(), ); - if (!_isCurrentActiveDocumentOperation(operationRevision) || + if (acceptNavigation?.call() == false || + !_isCurrentActiveDocumentOperation(operationRevision) || state.workspace?.id != workspaceId) { return false; } @@ -7174,10 +7482,15 @@ class WorkspaceController extends Notifier { snapshot: activeAfterSave.diskSnapshot, ); } - await _settingsController.recordOpenedWorkspace( - path: path, - kind: savedWorkspace.kind.name, - ); + if (savedWorkspace.kind == WorkspaceKind.singleMarkdown) { + await _publishRecent( + () => _settingsController.recordOpenedWorkspace( + path: path, + kind: savedWorkspace.kind.name, + ), + ); + } + unawaited(_recordOpenedDocument(savedBuffer)); _schedulePersistence(); return true; } on Object catch (error) { @@ -7497,6 +7810,7 @@ class WorkspaceController extends Notifier { final existed = await _service.pathExists(path); if (existed != overwriteExisting) return false; WorkspaceFileSnapshot? createdSnapshot; + void Function()? stopWatchingTarget; try { if (!existed) { createdSnapshot = await _service.saveNewFormattedText( @@ -7547,6 +7861,18 @@ class WorkspaceController extends Notifier { format: destination.format, editorState: destination.editorState, ); + var targetChanged = false; + // Latch actual target changes, including switching away and back. A + // workspace metadata refresh may invalidate other document operations + // without changing this destination or its protected source. + stopWatchingTarget = listenSelf((previous, next) { + if (next.workspace?.id != target.workspaceId || + next.activeBufferId != target.bufferId || + next.activeBuffer?.filePath != target.path || + next.activeBuffer?.editorState.mode != target.editorState.mode) { + targetChanged = true; + } + }); if (!await _localHistory.captureProtective( LocalHistoryBufferSnapshot.fromBuffer(destination), LocalHistoryCaptureReason.beforeRestore, @@ -7556,8 +7882,10 @@ class WorkspaceController extends Notifier { } return false; } - if (!_isLocalHistoryRestoreTargetCurrent(target) || + if (targetChanged || + !_isLocalHistoryRestoreTargetCurrent(target) || await _service.fileChangedSince(path, target.snapshot) || + targetChanged || !_isLocalHistoryRestoreTargetCurrent(target)) { if (createdSnapshot != null) { await _rollbackMissingHistoryDestination(path, null, createdSnapshot); @@ -7595,6 +7923,8 @@ class WorkspaceController extends Notifier { await _rollbackMissingHistoryDestination(path, null, createdSnapshot); } return false; + } finally { + stopWatchingTarget?.call(); } } @@ -7618,19 +7948,33 @@ class WorkspaceController extends Notifier { } bool _isLocalHistoryRestoreTargetCurrent(_LocalHistoryRestoreTarget target) { + if (!ref.mounted) return false; final current = state.documentBuffers .where((buffer) => buffer.id == target.bufferId) .firstOrNull; + // This restores a whole revision, independently of caret/viewport state. + // Mounting Editor initializes its rich selection and viewport while the + // protective capture is pending. Those session reports do not change the + // target source. Navigation, mode, source/undo and disk changes still reject + // the restore; use the live session when constructing its Undo entry. return current != null && state.workspace?.id == target.workspaceId && + state.activeBufferId == target.bufferId && current.filePath != null && p.equals(current.filePath!, target.path) && current.revision == target.revision && current.text == target.text && _sameFileSnapshot(current.diskSnapshot, target.snapshot) && current.diskState == target.diskState && - identical(current.format, target.format) && - identical(current.editorState, target.editorState); + current.format.hasUtf8Bom == target.format.hasUtf8Bom && + current.format.lineEnding == target.format.lineEnding && + current.format.hasFinalNewline == target.format.hasFinalNewline && + current.format.lfCount == target.format.lfCount && + current.format.crlfCount == target.format.crlfCount && + current.format.crCount == target.format.crCount && + !current.readonly && + current.editorState.mode == target.editorState.mode && + identical(current.editorState.undoState, target.editorState.undoState); } Future localHistoryRestorePathExists(String path) { @@ -9240,3 +9584,15 @@ class _RemovalBufferAuthorization { final List buffers; bool committed = false; } + +/// Application commands use the same sidebar destination handler as browsing. +final workspaceNavigationCommandProvider = + NotifierProvider( + WorkspaceNavigationCommandController.new, + ); + +class WorkspaceNavigationCommandController extends Notifier<(String, int)?> { + @override + (String, int)? build() => null; + void request(String id) => state = (id, (state?.$2 ?? 0) + 1); +} diff --git a/lib/src/workspace/workspace_glyphs.dart b/lib/src/workspace/workspace_glyphs.dart index 511e0bfa..0afb4b6d 100644 --- a/lib/src/workspace/workspace_glyphs.dart +++ b/lib/src/workspace/workspace_glyphs.dart @@ -18,7 +18,9 @@ abstract final class WorkspaceGlyphs { }; } - static IconData forRecent(RecentWorkspace recent) { + static IconData forRecent(RecentResource recent) { + if (recent.accountId != null) return BusyMarkGlyphs.documentHistory; + if (recent.ownerRoot != null) return BusyMarkGlyphs.document; return forKindName(recent.kind, fallbackPath: recent.path); } diff --git a/lib/src/workspace/workspace_message.dart b/lib/src/workspace/workspace_message.dart index 0ca247e8..cb290131 100644 --- a/lib/src/workspace/workspace_message.dart +++ b/lib/src/workspace/workspace_message.dart @@ -5,6 +5,7 @@ import '../core/busymark_exception.dart'; enum WorkspaceMessageCode { openFailed, + recentWorkspaceUpdateFailed, createWritersideProjectFailed, createWritersideTopicFailed, couldNotOpenFile, @@ -32,6 +33,8 @@ String localizeWorkspaceMessage( final error = _localizeWorkspaceError(context, message.error); return switch (message.code) { WorkspaceMessageCode.openFailed => l10n.workspaceErrorOpenFailed(error), + WorkspaceMessageCode.recentWorkspaceUpdateFailed => + l10n.workspaceErrorRecentWorkspaceUpdateFailed(error), WorkspaceMessageCode.createWritersideProjectFailed => l10n.workspaceErrorCreateWritersideProjectFailed(error), WorkspaceMessageCode.createWritersideTopicFailed => diff --git a/lib/src/writerside/writerside_document_parser.dart b/lib/src/writerside/writerside_document_parser.dart index 79993152..98218d5d 100644 --- a/lib/src/writerside/writerside_document_parser.dart +++ b/lib/src/writerside/writerside_document_parser.dart @@ -13,6 +13,14 @@ class WritersideDocumentParser { WritersideDocument parseXml({ required String filePath, required String source, + }) => SourceLocationMapper.withSource( + source, + () => _parseXml(filePath: filePath, source: source), + ); + + WritersideDocument _parseXml({ + required String filePath, + required String source, }) { try { return WritersideDocument( diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 070476dd..05851558 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.13) project(runner LANGUAGES CXX) +include(cmake/BundleInstall.cmake) + # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "busymark") @@ -209,15 +211,8 @@ include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) +busymark_clean_default_bundle() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") diff --git a/linux/cmake/BundleInstall.cmake b/linux/cmake/BundleInstall.cmake new file mode 100644 index 00000000..114b4eb6 --- /dev/null +++ b/linux/cmake/BundleInstall.cmake @@ -0,0 +1,19 @@ +# Set the default before dependency discovery: a failed first configure must +# not leave /usr/local cached as the prefix for a later successful build. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH + "BusyMark installation directory" FORCE) +endif() + +function(busymark_clean_default_bundle) + # Clean only our ordinary build bundle. Custom prefixes and DESTDIR installs + # must preserve both their existing contents and the unstaged build bundle. + # Evaluate the prefix and DESTDIR at installation time. + install(CODE " + if(CMAKE_INSTALL_PREFIX STREQUAL \"${BUILD_BUNDLE_DIR}\" + AND \"\$ENV{DESTDIR}\" STREQUAL \"\") + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + endif() + " COMPONENT Runtime) +endfunction() diff --git a/linux/io.busystack.busymark.metainfo.xml b/linux/io.busystack.busymark.metainfo.xml index 9166eb5c..de562c23 100644 --- a/linux/io.busystack.busymark.metainfo.xml +++ b/linux/io.busystack.busymark.metainfo.xml @@ -88,6 +88,7 @@ https://github.com/busystack/busymark/issues + diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index a48642c2..2f3ce821 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -12,6 +12,7 @@ #include "flutter/generated_plugin_registrant.h" #include "gtk_accent_host.h" #include "linux_chrome_host.h" +#include "native_shortcut_metadata.h" #include "rich_clipboard_host.h" #include "secure_credential_host.h" #include "video_player_host.h" @@ -69,60 +70,15 @@ struct _MyApplication { }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) -static gchar* gtk_accelerator_from_shortcut_label(const gchar* shortcut) { - if (shortcut == nullptr || shortcut[0] == '\0') { - return nullptr; - } - - guint key = 0; - GdkModifierType modifiers = static_cast(0); - gtk_accelerator_parse(shortcut, &key, &modifiers); - if (key != 0) { - return g_strdup(shortcut); - } - - gchar** parts = g_strsplit(shortcut, "+", -1); - const gsize part_count = g_strv_length(parts); - GString* accelerator = g_string_new(nullptr); - gboolean valid = part_count > 0; - for (gsize index = 0; valid && index + 1 < part_count; index++) { - const gchar* part = g_strstrip(parts[index]); - if (g_strcmp0(part, "Ctrl") == 0 || - g_strcmp0(part, "Control") == 0) { - g_string_append(accelerator, ""); - } else if (g_strcmp0(part, "Alt") == 0) { - g_string_append(accelerator, ""); - } else if (g_strcmp0(part, "Shift") == 0) { - g_string_append(accelerator, ""); - } else if (g_strcmp0(part, "Super") == 0) { - g_string_append(accelerator, ""); - } else if (g_strcmp0(part, "Meta") == 0) { - g_string_append(accelerator, ""); - } else { - valid = FALSE; - } - } - if (valid) { - const gchar* key_label = g_strstrip(parts[part_count - 1]); - g_string_append(accelerator, - g_strcmp0(key_label, "Esc") == 0 ? "Escape" : key_label); - key = 0; - modifiers = static_cast(0); - gtk_accelerator_parse(accelerator->str, &key, &modifiers); - valid = key != 0; - } - - g_strfreev(parts); - return g_string_free(accelerator, !valid); -} - -static void set_menu_item_accelerator(GMenuItem* item, +static bool set_menu_item_accelerator(GMenuItem* item, const gchar* shortcut) { g_autofree gchar* accelerator = gtk_accelerator_from_shortcut_label(shortcut); if (accelerator != nullptr) { g_menu_item_set_attribute(item, kMenuAccelAttribute, "s", accelerator); + return true; } + return false; } static GdkPixbuf* load_application_icon_at_size(gint size) { @@ -665,6 +621,8 @@ static gboolean validate_native_menu_entries(FlValue* entries, static_cast(G_MAXUINT32))) || (selected && !checkable) || (children != nullptr && (separator || checkable)) || + (fl_value_lookup_string(entry, "gtkAccelerator") != nullptr && + fl_lookup_string_arg(entry, "gtkAccelerator") == nullptr) || (fl_value_lookup_string(entry, "shortcut") != nullptr && fl_lookup_string_arg(entry, "shortcut") == nullptr)) { respond_native_menu_argument_error( @@ -764,7 +722,10 @@ static void build_native_menu_model(FlValue* entries, // this native boundary; action indexes and clipboard payloads stay intact. g_autofree gchar* literal_label = escape_native_menu_label(label); const gchar* icon_name = fl_lookup_string_arg(entry, "icon"); - const gchar* shortcut = fl_lookup_string_arg(entry, "shortcut"); + // Accelerator metadata is generated from logical keys in Dart. GMenu uses + // it for display; Flutter remains the single command dispatch mechanism. + const gchar* shortcut = fl_lookup_string_arg(entry, "gtkAccelerator"); + if (shortcut == nullptr) shortcut = fl_lookup_string_arg(entry, "shortcut"); gboolean enabled = TRUE; gboolean checkable = FALSE; gboolean selected = FALSE; @@ -810,8 +771,13 @@ static void build_native_menu_model(FlValue* entries, g_autoptr(GIcon) icon = create_native_menu_icon(icon_name, entry); g_menu_item_set_icon(item, icon); } - if (shortcut != nullptr && shortcut[0] != '\0') { - set_menu_item_accelerator(item, shortcut); + if (shortcut != nullptr && shortcut[0] != '\0' && + !set_menu_item_accelerator(item, shortcut)) { + const gchar* display = fl_lookup_string_arg(entry, "shortcut"); + if (display != nullptr && display[0] != '\0') { + g_autofree gchar* fallback = g_strdup_printf("%s (%s)", literal_label, display); + g_menu_item_set_label(item, fallback); + } } g_menu_append_item(section, item); g_object_unref(action); diff --git a/linux/runner/native_shortcut_metadata.h b/linux/runner/native_shortcut_metadata.h new file mode 100644 index 00000000..8c9b78c3 --- /dev/null +++ b/linux/runner/native_shortcut_metadata.h @@ -0,0 +1,90 @@ +#ifndef BUSYMARK_NATIVE_SHORTCUT_METADATA_H_ +#define BUSYMARK_NATIVE_SHORTCUT_METADATA_H_ + +#include +#include + +inline gchar* gtk_accelerator_from_shortcut_label(const gchar* shortcut) { + if (shortcut == nullptr || shortcut[0] == '\0') { + return nullptr; + } + + // Dart sends logical Unicode identity, never a translated shortcut badge. + // GDK supplies legacy keysym names such as eacute or Cyrillic_a. + const gchar* unicode = strstr(shortcut, "U+"); + if (unicode != nullptr) { + const gchar* digits = unicode + 2; + const gsize count = strlen(digits); + if (count == 0 || count > 6 || + strspn(digits, "0123456789abcdefABCDEF") != count) { + return nullptr; + } + const guint64 codepoint = g_ascii_strtoull(digits, nullptr, 16); + if (codepoint == 0 || codepoint > 0x10ffff || + !g_unichar_validate(codepoint)) { + return nullptr; + } + g_autofree gchar* prefix = g_strndup(shortcut, unicode - shortcut); + g_autofree gchar* sample = g_strconcat(prefix, "a", nullptr); + guint sample_key = 0; + GdkModifierType modifiers = static_cast(0); + gtk_accelerator_parse(sample, &sample_key, &modifiers); + if (sample_key != GDK_KEY_a) return nullptr; + // GTK normalizes alphabetic accelerator keys to lowercase. Shift remains + // a separate modifier, as it is in the structured application binding. + const guint expected_key = gdk_keyval_to_lower( + gdk_unicode_to_keyval(codepoint)); + g_autofree gchar* canonical = gtk_accelerator_name(expected_key, modifiers); + guint verified = 0; + GdkModifierType verified_modifiers = static_cast(0); + gtk_accelerator_parse(canonical, &verified, &verified_modifiers); + // Some Unicode keys have no GTK 3 accelerator name. The caller retains the + // truthful display label instead of publishing invalid accelerator syntax. + return verified == expected_key && verified_modifiers == modifiers + ? g_strdup(canonical) + : nullptr; + } + + guint key = 0; + GdkModifierType modifiers = static_cast(0); + gtk_accelerator_parse(shortcut, &key, &modifiers); + if (key != 0) { + return g_strdup(shortcut); + } + + gchar** parts = g_strsplit(shortcut, "+", -1); + const gsize part_count = g_strv_length(parts); + GString* accelerator = g_string_new(nullptr); + gboolean valid = part_count > 0; + for (gsize index = 0; valid && index + 1 < part_count; index++) { + const gchar* part = g_strstrip(parts[index]); + if (g_strcmp0(part, "Ctrl") == 0 || + g_strcmp0(part, "Control") == 0) { + g_string_append(accelerator, ""); + } else if (g_strcmp0(part, "Alt") == 0) { + g_string_append(accelerator, ""); + } else if (g_strcmp0(part, "Shift") == 0) { + g_string_append(accelerator, ""); + } else if (g_strcmp0(part, "Super") == 0) { + g_string_append(accelerator, ""); + } else if (g_strcmp0(part, "Meta") == 0) { + g_string_append(accelerator, ""); + } else { + valid = FALSE; + } + } + if (valid) { + const gchar* key_label = g_strstrip(parts[part_count - 1]); + g_string_append(accelerator, + g_strcmp0(key_label, "Esc") == 0 ? "Escape" : key_label); + key = 0; + modifiers = static_cast(0); + gtk_accelerator_parse(accelerator->str, &key, &modifiers); + valid = key != 0; + } + + g_strfreev(parts); + return g_string_free(accelerator, !valid); +} + +#endif diff --git a/linux/runner/video_player_host.cc b/linux/runner/video_player_host.cc index 42e4d80b..a12f26d1 100644 --- a/linux/runner/video_player_host.cc +++ b/linux/runner/video_player_host.cc @@ -45,7 +45,9 @@ void video_player_free(gpointer data) { return; } if (player->web_view != nullptr && GTK_IS_WIDGET(player->web_view)) { + g_signal_handlers_disconnect_by_data(player->web_view, player); gtk_widget_destroy(player->web_view); + g_clear_object(&player->web_view); } g_free(player->allowed_local_uri); g_free(player); @@ -354,6 +356,79 @@ gboolean context_menu_cb(WebKitWebView*, return TRUE; } +gboolean local_file_chooser_cb(WebKitWebView* view, + WebKitFileChooserRequest* request, + gpointer user_data) { + auto* player = static_cast(user_data); + // Only BusyMark's fixed local-player shell may obtain the one file that + // the user already authorized with Play. Never open a chooser or grant a + // hosted document access to this path. + if (player->kind != PlayerKind::kLocalFile || + g_strcmp0(webkit_web_view_get_uri(view), "file:///") != 0 || + webkit_file_chooser_request_get_select_multiple(request)) { + webkit_file_chooser_request_cancel(request); + return TRUE; + } + g_autofree gchar* path = + g_filename_from_uri(player->allowed_local_uri, nullptr, nullptr); + if (path == nullptr || !g_file_test(path, G_FILE_TEST_IS_REGULAR)) { + webkit_file_chooser_request_cancel(request); + return TRUE; + } + const gchar* files[] = {path, nullptr}; + webkit_file_chooser_request_select_files(request, files); + return TRUE; +} + +void local_source_script_finished_cb(GObject* object, + GAsyncResult* result, + gpointer) { + g_autoptr(GError) error = nullptr; + g_autoptr(JSCValue) value = webkit_web_view_evaluate_javascript_finish( + WEBKIT_WEB_VIEW(object), result, &error); + if (error != nullptr && !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warning("The local video source could not be initialized: %s", error->message); + } +} + +void local_source_load_changed_cb(WebKitWebView* view, + WebKitLoadEvent event, + gpointer) { + if (event != WEBKIT_LOAD_FINISHED || + g_strcmp0(webkit_web_view_get_uri(view), "file:///") != 0) { + return; + } + // A direct file: URI makes GStreamer's filesrc open the path inside the + // WebProcess sandbox, where it is deliberately unavailable. WebKit's File + // API instead brokers the selected file and streams an existing allowed + // blob: URL. This neither copies the video nor widens filesystem access. + // Native evaluation carries the already-authorized Play gesture. + constexpr char script[] = R"JS((() => { + if (document.getElementById('busymark-local-media')) return; + const video = document.querySelector('video'); + const input = document.createElement('input'); + input.id = 'busymark-local-media'; + input.type = 'file'; + input.accept = 'video/*'; + input.hidden = true; + const playFile = file => { + const url = URL.createObjectURL(file); + video.src = url; + window.addEventListener('pagehide', () => URL.revokeObjectURL(url), {once: true}); + void video.play(); + }; + // Cancellation (for example, a file removed during opening) must produce + // the normal media error rather than leave a source-less player pending. + input.onchange = () => playFile(input.files[0] || new Blob()); + input.oncancel = () => playFile(new Blob()); + document.body.append(input); + input.click(); + })())JS"; + webkit_web_view_evaluate_javascript(view, script, -1, nullptr, nullptr, + nullptr, local_source_script_finished_cb, + nullptr); +} + WebKitSettings* create_player_settings() { WebKitSettings* settings = webkit_settings_new(); webkit_settings_set_enable_javascript(settings, TRUE); @@ -379,7 +454,6 @@ WebKitSettings* create_player_settings() { gchar* player_html(PlayerKind kind, const gchar* value, - const gchar* local_uri, gboolean mini_player, const gchar* play_label, const gchar* pause_label, @@ -401,14 +475,13 @@ gchar* player_html(PlayerKind kind, constexpr char kTail[] = ""; g_autofree gchar* head = g_strdup_printf(kHeadTemplate, border_effect); if (kind == PlayerKind::kLocalFile) { - g_autofree gchar* escaped_uri = g_markup_escape_text(local_uri, -1); if (mini_player) { g_autofree gchar* escaped_play_label = g_markup_escape_text(play_label, -1); g_autofree gchar* escaped_pause_label = g_markup_escape_text(pause_label, -1); return g_strdup_printf( - "%s