diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml
new file mode 100644
index 000000000..5a2ecf807
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml
@@ -0,0 +1,93 @@
+name: Report a bug
+description: Report a bug or a functional regression
+title: "BUG DESCRIPTION"
+labels: ["type: bug"]
+body:
+ - type: "markdown"
+ attributes:
+ value: |
+
+
+
+
+ Please replace the **`BUG DESCRPTION`** template above with a clear and concise description of what the current behvaior is.
+
+ An example of a good bug report title would look like:
+ ```plaintext
+ In DarkMode, a blank square appears in bottom right corner while scrolling
+ ```
+
+ ---
+ - type: "textarea"
+ id: "description"
+ attributes:
+ label: "Bug Description"
+ description: |
+ A clear and concise description of what the current behavior is.
+
+ Please also add **screenshots** of the issue if applicable.
+ placeholder: "E.g: In DarkMode, when scrollbar are displayed, we see a blank square in the bottom right corner"
+ validations:
+ required: true
+ - type: "textarea"
+ id: "steps_to_reproduce"
+ attributes:
+ label: "Steps to Reproduce"
+ description: |
+ Please include a step-by-step guide for reproducing this issue. Consider writing in concise, numbered bullet points.
+ validations:
+ required: true
+ - type: "textarea"
+ id: "expected_behvaior"
+ attributes:
+ label: "Expected Behavior"
+ description: |
+ A clear and concise description of what the expected behavior is.
+ placeholder: "The blank square should be transparent (invisible)"
+ validations:
+ required: true
+ - type: "input"
+ id: "reproduction_file"
+ attributes:
+ label: "Reproduction File"
+ description: "If applicable, upload a link to download the specific file that is causing the issue, so as to allow us to easily reproduce the bug."
+ - type: "dropdown"
+ id: "os_kind"
+ attributes:
+ label: "Operating System"
+ description: "Select the operating system(s) that this bug occurs on."
+ multiple: true
+ options:
+ - "macOS"
+ - "Windows"
+ - "Linux"
+ - "iOS/iPadOS"
+ - "Android"
+ - "Web"
+ validations:
+ required: true
+ - type: "input"
+ id: "os_version"
+ attributes:
+ label: "OS Version"
+ description: |
+ The OS version you are running.
+ placeholder: "E.g: Windows 11 24H2, macOS 15.5, Ubuntu 24.04, Android 14, iOS 18.5"
+ validations:
+ required: true
+ - type: "input"
+ id: "readest_version"
+ attributes:
+ label: "Readest Version"
+ description: |
+ The Readest version you are running.
+
+ To find this, go to **☰ -> About Readest**.
+ placeholder: "Version x.xx.xx (WebView xxx.x.xx)"
+ validations:
+ required: true
+ - type: "textarea"
+ id: "custom_css"
+ attributes:
+ label: "Custom CSS"
+ description: "If you have custom CSS, please paste it here, as it could be affecting the issue."
diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml
new file mode 100644
index 000000000..bfe4eacf3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml
@@ -0,0 +1,46 @@
+name: Feature request
+description: Share an idea or suggestion
+title: "FR: FEATURE REQUEST"
+labels: ["type: enhancement"]
+body:
+ - type: "markdown"
+ attributes:
+ value: |
+
+
+
+
+ Please replace the **`FEATURE REQUEST`** template above describing your feature request.
+
+ ---
+ - type: "textarea"
+ id: "problem_being_solved"
+ attributes:
+ label: "What problem would this feature solve?"
+ description: |
+ A clear and concise of what problem this feature solves.
+ placeholder: "E.g: I think it takes too many steps to [...]"
+ validations:
+ required: true
+ - type: "textarea"
+ id: "feature_request"
+ attributes:
+ label: "Describe the solution you'd like."
+ description: |
+ A clear and concise of what problem this feature solves.
+ placeholder: "E.g: I think it takes too many steps to [...]"
+ validations:
+ required: true
+ - type: "textarea"
+ id: "alternatives"
+ attributes:
+ label: "Describe workarounds you may have used to acheive the same results."
+ description: |
+ A clear and concise description of any workarounds you've used.
+ placeholder: "E.g: This functionality can be changed in the settings menu, but I want a keyboard shortcut as well."
+ - type: "textarea"
+ id: "additional_context"
+ attributes:
+ label: "Additional Context"
+ description: |
+ Add any additional context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
deleted file mode 100644
index 146afe177..000000000
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-name: Feature request
-about: Share an idea or suggestion
-title: 'FR: describing your feature request'
-labels: enhancement
-assignees: ''
----
-
-**Does your feature request involve difficulty for you to complete a task? Please describe.**
-
-> A clear and concise description of what the problem is. Ex. I think it takes too many steps to [...]
-
-**Describe the solution you'd like**
-
-> A clear and concise description of what you'd like to happen.
-
-**Describe alternatives you've considered**
-
-> A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-
-> Add any additional context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/report-bug.md b/.github/ISSUE_TEMPLATE/report-bug.md
deleted file mode 100644
index e9b30b13a..000000000
--- a/.github/ISSUE_TEMPLATE/report-bug.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-name: Report a bug
-about: Report a bug or a functional regression
-title: 'Example: In DarkMode, a blank square appears in bottom right corner while scrolling'
-labels: ['type: bug']
-assignees: ''
----
-
-## Bug Description
-
-A clear and concise description of what the current behavior is.
-Please also add **screenshots** of the existing application.
-
-> **Example:**
-> In DarkMode, when scrollbar are displayed (for example on Companies page, with enough companies in the list), we see a blank square in the bottom right corner
-> [screenshot]
-
-## Expected behavior
-
-A clear and concise description of what the expected behavior is.
-
-> **Example:**
-> The blank square should be transparent (invisible)
-
-## Technical inputs
-
-Operating System:
-
-Readest Version:
-
-> **Example:**
-> Operating System: Android 14 (WebView 135.0)
-> Readest Version: 0.9.0
-> We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
-> Probably fixable with CSS
diff --git a/.github/workflows/android-e2e.yml b/.github/workflows/android-e2e.yml
index 6621bcffa..909978a1c 100644
--- a/.github/workflows/android-e2e.yml
+++ b/.github/workflows/android-e2e.yml
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: initialize git submodules
run: git submodule update --init --recursive
@@ -46,7 +46,7 @@ jobs:
cache: pnpm
- name: setup Java
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
+ uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
with:
distribution: 'zulu'
java-version: '17'
@@ -97,7 +97,7 @@ jobs:
test -n "$APK"
- name: cache AVD snapshot
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: avd-cache
with:
path: |
@@ -107,7 +107,7 @@ jobs:
- name: create AVD snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
- uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2
+ uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
with:
api-level: 34
arch: x86_64
@@ -118,7 +118,7 @@ jobs:
script: echo "AVD snapshot created"
- name: run Android e2e lane
- uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2
+ uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
with:
api-level: 34
arch: x86_64
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index cb124687a..28d78c415 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -61,7 +61,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
@@ -71,7 +71,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
+ uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -100,6 +100,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
+ uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
with:
category: '/language:${{matrix.language}}'
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 69700047d..24ea76583 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -39,15 +39,15 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: recursive
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to GHCR
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -55,13 +55,13 @@ jobs:
- name: Docker meta (for labels)
id: meta
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
+ uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository_owner }}/readest
- name: Build and push by digest
id: build
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
+ uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Dockerfile
@@ -102,10 +102,10 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to GHCR
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -122,7 +122,7 @@ jobs:
- name: Log in to Docker Hub
if: steps.dockerhub.outputs.enabled == 'true'
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -130,7 +130,7 @@ jobs:
- name: Extract image metadata (GHCR only)
id: meta-ghcr
if: steps.dockerhub.outputs.enabled != 'true'
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
+ uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository_owner }}/readest
tags: |
@@ -144,7 +144,7 @@ jobs:
- name: Extract image metadata (GHCR + Docker Hub)
id: meta-all
if: steps.dockerhub.outputs.enabled == 'true'
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
+ uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
ghcr.io/${{ github.repository_owner }}/readest
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 1f94b1984..29b03bee5 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -28,7 +28,7 @@ jobs:
outputs:
nightly_version: ${{ steps.v.outputs.nightly_version }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: main
persist-credentials: false
@@ -40,6 +40,12 @@ jobs:
build:
needs: compute-version
+ permissions:
+ contents: read
+ # Required by actions/attest-build-provenance: id-token mints the Sigstore
+ # OIDC identity, attestations writes the provenance to the repo's store.
+ id-token: write
+ attestations: write
strategy:
fail-fast: false
matrix:
@@ -72,9 +78,11 @@ jobs:
args: '--target aarch64-pc-windows-msvc --bundles nsis'
runs-on: ${{ matrix.config.os }}
- timeout-minutes: 60
+ # Backstop only — must stay ABOVE setup time + the per-step timeouts below,
+ # because a job-level timeout reports `cancelled` and skips assemble-manifest.
+ timeout-minutes: 75
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: main
persist-credentials: false
@@ -93,7 +101,7 @@ jobs:
- name: setup Java (for Android build only)
if: matrix.config.release == 'android'
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
+ uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
with:
distribution: 'zulu'
java-version: '17'
@@ -134,6 +142,10 @@ jobs:
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
+ echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env.local
+ echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env.local
+ echo "SENTRY_ORG=readest" >> .env.local
+ echo "SENTRY_PROJECT=readest" >> .env.local
cp .env.local apps/readest-app/.env.local
- name: install rclone
@@ -167,6 +179,7 @@ jobs:
- name: build and sign Android apks
if: matrix.config.release == 'android'
shell: bash
+ timeout-minutes: 55
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
@@ -207,9 +220,30 @@ jobs:
if: matrix.config.release == 'linux'
run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
+ # The truly-portable AppImage bundler downloads quick-sharun.sh from
+ # Anylinux-AppImages@main ONLY when it is not already in the tauri tools
+ # cache. An upstream strace-mode change (2026-06-29) made bundling launch
+ # the app under Xvfb and hang forever, timing out the Linux legs (#4906).
+ # Seed the cache with the last known-good revision so the bundler never
+ # fetches the moving main-branch script.
+ - name: pin quick-sharun.sh for AppImage bundling (Linux)
+ if: matrix.config.release == 'linux'
+ run: |
+ set -euo pipefail
+ cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
+ mkdir -p "$cache_dir"
+ curl -fsSL --retry 3 -o "$cache_dir/quick-sharun.sh" \
+ "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/b3a9e985cdedf7efa81d172f182cd13983743147/useful-tools/quick-sharun.sh"
+ chmod +x "$cache_dir/quick-sharun.sh"
+
- name: build desktop bundles
if: matrix.config.release != 'android'
shell: bash
+ # A hung build must fail the STEP (step timeout -> job failure), not hit
+ # the job-level timeout: job timeouts report `cancelled`, which the
+ # assemble-manifest guard treats as run cancellation and skips promoting
+ # latest.json for ALL platforms (#4906).
+ timeout-minutes: 45
env:
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: 'true'
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
@@ -238,6 +272,7 @@ jobs:
- name: build and sign portable binaries (Windows only)
if: matrix.config.os == 'windows-latest'
shell: bash
+ timeout-minutes: 30
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
@@ -381,6 +416,18 @@ jobs:
;;
esac
+ # Attest the distributable binaries staged in nightly-out (apks, AppImage,
+ # app.tar.gz, setup/portable exe). gh attestation verify is digest-based,
+ # so it verifies these against readest/readest even though they ship via
+ # download.readest.com rather than a GitHub release. The .sig updater
+ # signatures are excluded — they are not binaries users run.
+ - name: attest nightly binaries
+ uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
+ with:
+ subject-path: |
+ nightly-out/Readest*
+ !nightly-out/*.sig
+
- name: upload artifacts + fragment to R2
shell: bash
run: |
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 390340bd1..c1a2c27b8 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -14,19 +14,19 @@ jobs:
SCCACHE_GHA_ENABLED: 'true'
RUSTC_WRAPPER: sccache
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: 'true'
- name: setup sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: Install minimal stable with clippy and rustfmt
- uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1
+ uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Cache apt packages
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: /var/cache/apt/archives
key: apt-rust-lint-${{ runner.os }}
@@ -47,7 +47,7 @@ jobs:
build_web_app:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: 'true'
@@ -84,7 +84,7 @@ jobs:
- name: cache playwright browsers
id: playwright-cache
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
@@ -122,7 +122,7 @@ jobs:
matrix:
shard: [1, 2]
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: 'true'
@@ -144,7 +144,7 @@ jobs:
- name: cache playwright browsers
if: matrix.shard == 1
id: playwright-cache
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
@@ -179,13 +179,13 @@ jobs:
contents: read
pull-requests: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: 'true'
- name: detect koplugin changes
id: changes
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
+ uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
with:
filters: |
koplugin:
@@ -215,7 +215,7 @@ jobs:
- name: cache apt packages
if: steps.changes.outputs.koplugin == 'true'
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: /var/cache/apt/archives
key: apt-test-koplugin-${{ runner.os }}
@@ -250,7 +250,7 @@ jobs:
SCCACHE_GHA_ENABLED: 'true'
RUSTC_WRAPPER: sccache
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: 'true'
@@ -266,7 +266,7 @@ jobs:
# The tauri tests run `next dev`, whose Turbopack cache lives in
# `.next/dev/cache` (a different path from the `next build` cache).
- name: cache Turbopack dev cache
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: apps/readest-app/.next/dev/cache
key: turbo-dev-tauri-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
@@ -282,7 +282,7 @@ jobs:
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: install Rust toolchain
- uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1
+ uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
with:
toolchain: stable
# Disable this action's built-in rust-cache so the explicit
@@ -307,7 +307,7 @@ jobs:
cache-workspace-crates: 'true'
- name: Cache apt packages
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: /var/cache/apt/archives
key: apt-tauri-${{ runner.os }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 302109c02..c4f4f8227 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ jobs:
release_version: ${{ steps.get-release-notes.outputs.release_version }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
- name: get version
@@ -89,7 +89,7 @@ jobs:
contents: write
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: create KOReader plugin zip
env:
@@ -118,10 +118,41 @@ jobs:
echo "Uploading ${plugin_zip} to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} ${plugin_zip} --clobber
+ build-calibre-plugin:
+ needs: get-release
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+
+ - name: create calibre plugin zip
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ version=${{ needs.get-release.outputs.release_version }}
+
+ # Stamp PLUGIN_VERSION in __init__.py with the release version
+ # (from apps/readest-app/package.json, mirroring the koplugin's
+ # _meta.lua stamp above); the committed value is a dev placeholder.
+ version_tuple=$(echo "${version}" | awk -F. '{printf "(%d, %d, %d)", $1, $2, $3}')
+ perl -i -pe "s/^PLUGIN_VERSION = \(\d+, \d+, \d+\)/PLUGIN_VERSION = ${version_tuple}/" \
+ apps/readest-calibre-plugin/__init__.py
+
+ make -C apps/readest-calibre-plugin zip
+ plugin_zip="apps/readest-calibre-plugin/dist/Readest-${version}.calibre-plugin.zip"
+
+ echo "Uploading ${plugin_zip} to GitHub release"
+ gh release upload ${{ needs.get-release.outputs.release_tag }} ${plugin_zip} --clobber
+
build-tauri:
needs: get-release
permissions:
contents: write
+ # Required by actions/attest-build-provenance: id-token mints the Sigstore
+ # OIDC identity, attestations writes the provenance to the repo's store.
+ id-token: write
+ attestations: write
strategy:
fail-fast: false
matrix:
@@ -156,7 +187,7 @@ jobs:
runs-on: ${{ matrix.config.os }}
timeout-minutes: 60
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: initialize git submodules
run: git submodule update --init --recursive
@@ -172,7 +203,7 @@ jobs:
- name: setup Java (for Android build only)
if: matrix.config.release == 'android'
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
+ uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
with:
distribution: 'zulu'
java-version: '17'
@@ -225,6 +256,10 @@ jobs:
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
+ echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env.local
+ echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env.local
+ echo "SENTRY_ORG=readest" >> .env.local
+ echo "SENTRY_PROJECT=readest" >> .env.local
cp .env.local apps/readest-app/.env.local
- name: build and upload Android apks
@@ -267,6 +302,14 @@ jobs:
gh release upload ${{ needs.get-release.outputs.release_tag }} $universial_apk.sig --clobber
gh release upload ${{ needs.get-release.outputs.release_tag }} $arm64_apk.sig --clobber
+ - name: attest Android apks
+ if: matrix.config.release == 'android'
+ uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
+ with:
+ subject-path: |
+ apps/readest-app/Readest_${{ needs.get-release.outputs.release_version }}_universal.apk
+ apps/readest-app/Readest_${{ needs.get-release.outputs.release_version }}_arm64.apk
+
- name: download and update latest.json for Android release
if: matrix.config.release == 'android'
env:
@@ -308,7 +351,24 @@ jobs:
if: matrix.config.release == 'linux'
run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
- - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0
+ # The truly-portable AppImage bundler downloads quick-sharun.sh from
+ # Anylinux-AppImages@main ONLY when it is not already in the tauri tools
+ # cache. An upstream strace-mode change (2026-06-29) made bundling launch
+ # the app under Xvfb and hang forever (#4906). Seed the cache with the
+ # last known-good revision so the bundler never fetches the moving
+ # main-branch script. Keep in sync with nightly.yml.
+ - name: pin quick-sharun.sh for AppImage bundling (Linux)
+ if: matrix.config.release == 'linux'
+ run: |
+ set -euo pipefail
+ cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
+ mkdir -p "$cache_dir"
+ curl -fsSL --retry 3 -o "$cache_dir/quick-sharun.sh" \
+ "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/b3a9e985cdedf7efa81d172f182cd13983743147/useful-tools/quick-sharun.sh"
+ chmod +x "$cache_dir/quick-sharun.sh"
+
+ - uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
+ id: tauri
if: matrix.config.release != 'android'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -332,6 +392,16 @@ jobs:
releaseBody: ${{ needs.get-release.outputs.release_note }}
args: ${{ matrix.config.args || '' }}
+ # Attest the freshly built desktop bundles (installers, AppImage, dmg,
+ # updater archives + their .sig). tauri-action reports their on-disk paths
+ # as a JSON array; fromJSON('"\n"') yields a real newline to join them into
+ # the newline-delimited list subject-path expects.
+ - name: attest desktop bundles
+ if: matrix.config.release != 'android' && steps.tauri.outputs.artifactPaths != ''
+ uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
+ with:
+ subject-path: ${{ join(fromJSON(steps.tauri.outputs.artifactPaths), fromJSON('"\n"')) }}
+
- name: upload release notes to GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -384,6 +454,15 @@ jobs:
echo "Uploading signature to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file.sig --clobber
+ # The portable rebuild above is not produced by tauri-action, so it is not
+ # covered by the "attest desktop bundles" step; attest it here. Exactly one
+ # portable exe is staged at the workspace root per Windows leg.
+ - name: attest Windows portable binary
+ if: matrix.config.os == 'windows-latest'
+ uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
+ with:
+ subject-path: Readest_*-portable.exe
+
- name: download and update latest.json for Windows portable release
if: matrix.config.os == 'windows-latest'
env:
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index e588b6ddb..9cfd52001 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -34,7 +34,7 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -73,6 +73,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
+ uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
sarif_file: results.sarif
diff --git a/.github/workflows/vercel-merge.yml b/.github/workflows/vercel-merge.yml
index 15ab32abe..2098725bc 100644
--- a/.github/workflows/vercel-merge.yml
+++ b/.github/workflows/vercel-merge.yml
@@ -11,7 +11,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
submodules: 'true'
- uses: amondnet/vercel-action@de09aeac2ace6599ec9b11ef87558759a496bac4 # v42
diff --git a/.zed/settings.json b/.zed/settings.json
new file mode 100644
index 000000000..77b4fc839
--- /dev/null
+++ b/.zed/settings.json
@@ -0,0 +1,103 @@
+{
+ "languages": {
+ "Rust": {
+ "language_servers": ["rust-analyzer"]
+ },
+ "JavaScript": {
+ "language_servers": ["biome", "...", "!eslint"],
+ "formatter": {
+ "language_server": {
+ "name": "biome"
+ }
+ },
+ "code_actions_on_format": {
+ "source.fixAll.biome": true,
+ "source.organizeImports.biome": true
+ }
+ },
+ "TypeScript": {
+ "language_servers": ["biome", "...", "!eslint"],
+ "formatter": {
+ "language_server": {
+ "name": "biome"
+ }
+ },
+ "code_actions_on_format": {
+ "source.fixAll.biome": true,
+ "source.organizeImports.biome": true
+ }
+ },
+ "TSX": {
+ "language_servers": ["biome", "...", "!eslint"],
+ "formatter": {
+ "language_server": {
+ "name": "biome"
+ }
+ },
+ "code_actions_on_format": {
+ "source.fixAll.biome": true,
+ "source.organizeImports.biome": true
+ }
+ },
+ "CSS": {
+ "language_servers": ["biome", "...", "!eslint"],
+ "formatter": {
+ "language_server": {
+ "name": "biome"
+ }
+ }
+ },
+ "JSON": {
+ "language_servers": ["biome", "..."],
+ "formatter": {
+ "language_server": {
+ "name": "biome"
+ }
+ }
+ },
+ "JSONC": {
+ "language_servers": ["biome", "..."],
+ "formatter": {
+ "language_server": {
+ "name": "biome"
+ }
+ }
+ }
+ },
+ "lsp": {
+ "rust-analyzer": {
+ "settings": {
+ "linkedProjects": ["packages/tauri/Cargo.toml", "apps/readest-app/src-tauri/Cargo.toml"]
+ }
+ },
+ "biome": {
+ "binary": {
+ "path": "node_modules/.bin/biome",
+ "arguments": ["lsp-proxy"]
+ }
+ },
+ "vtsls": {
+ "settings": {
+ "typescript": {
+ "inlayHints": {
+ "parameterNames": {
+ "enabled": "all"
+ },
+ "variableTypes": {
+ "enabled": true
+ },
+ "propertyDeclarationTypes": {
+ "enabled": true
+ },
+ "functionLikeReturnTypes": {
+ "enabled": true
+ },
+ "enumMemberValues": {
+ "enabled": true
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2dc9f3f5c..e6721c19e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,35 +20,33 @@ Basically you need to install or update the following development tools:
- **Rust** and **Cargo** for Tauri development
```bash
-nvm install v22
-nvm use v22
+nvm install v24
+nvm use v24
npm install -g pnpm
rustup update
```
-### Getting Started
+## Getting Started
To get started with Readest, follow these steps to clone and build the project.
-#### 1. Clone the Repository
+### 1. Clone the Repository
```bash
git clone https://github.com/readest/readest.git
cd readest
-git submodule update --init --recursive
```
-#### 2. Install Dependencies
+### 2. Install Dependencies
```bash
# might need to rerun this when code is updated
+git submodule update --init --recursive
pnpm install
# copy vendors dist libs to public directory
pnpm --filter @readest/readest-app setup-vendors
```
-#### 3. Verify Dependencies Installation
-
To confirm that all dependencies are correctly installed, run the following command:
```bash
@@ -59,21 +57,78 @@ This command will display information about the installed Tauri dependencies and
For Windows targets, “Build Tools for Visual Studio 2022” (or a higher edition of Visual Studio) and the “Desktop development with C++” workflow must be installed. For Windows ARM64 targets, the “VS 2022 C++ ARM64 build tools” and "C++ Clang Compiler for Windows" components must be installed. And make sure `clang` can be found in the path by adding `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\Llvm\x64\bin` for example in the environment variable `Path`.
-#### 4. Build for Development
+#### Using Nix
+
+If you have Nix installed, you can leverage the included flake to enter a
+development shell to install and run all the necessary dependencies and commands:
```bash
-pnpm tauri dev
+nix develop ./ops # enter a dev shell for the web app
+nix develop ./ops#ios # enter a dev shell for the ios app
+nix develop ./ops#android # enter a dev shell for the android app
```
-#### 5. Build for Production
+### 4. Build for Development
+
+```bash
+# Start development for the Tauri app
+pnpm tauri dev
+# or start development for the Web app
+pnpm dev-web
+# preview with OpenNext build for the Web app
+pnpm preview
+```
+
+#### Android
+
+The following must be run once before running the Android app. Note that this is done automatically if using the nix Android devshell:
+
+```bash
+rm apps/readest-app/src-tauri/gen/android
+pnpm tauri android init
+pnpm tauri icon ../../data/icons/readest-book.png
+git checkout apps/readest-app/src-tauri/gen/android
+```
+
+To run the Android app:
+
+```bash
+pnpm tauri android dev
+# or if you want to dev on a real device
+pnpm tauri android dev --host
+```
+
+#### iOS
+
+```bash
+# Set up the iOS environment (run once)
+pnpm tauri ios init
+pnpm tauri icon ../../data/icons/readest-book.png
+
+pnpm tauri ios dev
+# or if you want to dev on a real device
+pnpm tauri ios dev --host
+```
+
+### 5. Build for Production
```bash
pnpm tauri build
+pnpm tauri android build
+pnpm tauri ios build
```
+Please refer to our release script if you experience any issues:
+https://github.com/readest/readest/blob/main/.github/workflows/release.yml
+
+
+### 7. More information
+
+Please check the [wiki][link-gh-wiki] of this project for more information on development.
+
Now you're all setup and can start implementing your changes.
-### Implement your changes
+## Implement your changes
This project is a monorepo. The code for the `readest-app` is in the `apps/readest-app` directory. Here are some useful scripts for developing the frontend only without compiling Tauri:
@@ -82,12 +137,20 @@ This project is a monorepo. The code for the `readest-app` is in the `apps/reade
| `pnpm dev-web` | Starts the development server for the web app only |
| `pnpm build-web` | Builds the web app |
-Recommended Visual Studio Code plugins for development:
+### Editor-specific setup
-- JavaScript and TypeScript Nightly (ms-vscode.vscode-typescript-next)
-- VS Code ESLint extension (dbaeumer.vscode-eslint)
-- Biome - Code formatter and linter (biomejs.biome)
-- rust-analyzer (rust-lang.rust-analyzer) (for Tauri development only)
+#### VS Code
+
+Upon opening the project, you will be prompted to install the following recommended extensions:
+
+- JavaScript and TypeScript Nightly (`ms-vscode.vscode-typescript-next`)
+- VS Code ESLint extension (`dbaeumer.vscode-eslint`)
+- Biome - Code formatter and linter (`biomejs.biome`)
+- rust-analyzer (`rust-lang.rust-analyzer`) (for Tauri development only)
+
+#### Zed
+
+The only extension needed is [biome-zed](https://github.com/biomejs/biome-zed), for code formatting and linting.
### When you're done
diff --git a/Cargo.lock b/Cargo.lock
index 49153bd16..ccf94eb2e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -36,6 +36,7 @@ dependencies = [
"read-progress-stream",
"reqwest 0.12.28",
"semver",
+ "sentry",
"serde",
"serde_json",
"tauri",
@@ -57,6 +58,7 @@ dependencies = [
"tauri-plugin-os",
"tauri-plugin-persisted-scope",
"tauri-plugin-process",
+ "tauri-plugin-sentry",
"tauri-plugin-sharekit",
"tauri-plugin-shell",
"tauri-plugin-sign-in-with-apple",
@@ -71,9 +73,19 @@ dependencies = [
"tokio",
"tokio-util",
"walkdir",
+ "winreg 0.52.0",
"zip 2.4.2",
]
+[[package]]
+name = "addr2line"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
+dependencies = [
+ "gimli",
+]
+
[[package]]
name = "adler2"
version = "2.0.1"
@@ -544,6 +556,21 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "backtrace"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-link 0.2.1",
+]
+
[[package]]
name = "base64"
version = "0.21.7"
@@ -748,7 +775,7 @@ checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
dependencies = [
"borsh-derive",
"bytes",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
]
[[package]]
@@ -1030,6 +1057,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@@ -1340,6 +1373,30 @@ dependencies = [
"libc",
]
+[[package]]
+name = "crash-context"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "mach2",
+]
+
+[[package]]
+name = "crash-handler"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2066907075af649bcb8bcb1b9b986329b243677e6918b2d920aa64b0aac5ace3"
+dependencies = [
+ "cfg-if",
+ "crash-context",
+ "libc",
+ "mach2",
+ "parking_lot",
+]
+
[[package]]
name = "crc32c"
version = "0.6.8"
@@ -1621,6 +1678,16 @@ dependencies = [
"keyring-core",
]
+[[package]]
+name = "debugid"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
+dependencies = [
+ "serde",
+ "uuid 1.23.2",
+]
+
[[package]]
name = "default-net"
version = "0.22.0"
@@ -1964,7 +2031,7 @@ dependencies = [
"rustc_version",
"toml 1.1.2+spec-1.1.0",
"vswhom",
- "winreg",
+ "winreg 0.55.0",
]
[[package]]
@@ -2730,6 +2797,12 @@ dependencies = [
"weezl",
]
+[[package]]
+name = "gimli"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+
[[package]]
name = "gio"
version = "0.18.4"
@@ -2894,6 +2967,17 @@ dependencies = [
"system-deps 7.0.8",
]
+[[package]]
+name = "goblin"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
+dependencies = [
+ "log",
+ "plain",
+ "scroll",
+]
+
[[package]]
name = "gtk"
version = "0.18.2"
@@ -3068,6 +3152,17 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "hostname"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-link 0.2.1",
+]
+
[[package]]
name = "html5ever"
version = "0.29.1"
@@ -3983,6 +4078,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "mach2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -4126,6 +4230,75 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+[[package]]
+name = "minidump-common"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77"
+dependencies = [
+ "bitflags 2.11.1",
+ "debugid",
+ "num-derive",
+ "num-traits",
+ "range-map",
+ "scroll",
+ "smart-default",
+]
+
+[[package]]
+name = "minidump-writer"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d"
+dependencies = [
+ "bitflags 2.11.1",
+ "byteorder",
+ "cfg-if",
+ "crash-context",
+ "goblin",
+ "libc",
+ "log",
+ "mach2",
+ "memmap2",
+ "memoffset",
+ "minidump-common",
+ "nix 0.28.0",
+ "procfs-core",
+ "scroll",
+ "tempfile",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "minidumper"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4ebc9d1f8847ec1d078f78b35ed598e0ebefa1f242d5f83cd8d7f03960a7d1"
+dependencies = [
+ "cfg-if",
+ "crash-context",
+ "libc",
+ "log",
+ "minidump-writer",
+ "parking_lot",
+ "polling",
+ "scroll",
+ "thiserror 1.0.69",
+ "uds",
+]
+
+[[package]]
+name = "minidumper-child"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4f23f835dbe67e44ddf884d3802ff549ca5948bf60e9fd70e9a13c96324d1"
+dependencies = [
+ "crash-handler",
+ "minidumper",
+ "thiserror 1.0.69",
+ "uuid 1.23.2",
+]
+
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -4351,6 +4524,18 @@ dependencies = [
"libc",
]
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "cfg_aliases 0.1.1",
+ "libc",
+]
+
[[package]]
name = "nix"
version = "0.31.3"
@@ -4359,7 +4544,7 @@ checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.11.1",
"cfg-if",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"libc",
]
@@ -4486,6 +4671,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -4856,6 +5052,15 @@ dependencies = [
"objc",
]
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -5386,6 +5591,12 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
[[package]]
name = "plist"
version = "1.9.0"
@@ -5581,6 +5792,16 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "procfs-core"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29"
+dependencies = [
+ "bitflags 2.11.1",
+ "hex",
+]
+
[[package]]
name = "proptest"
version = "1.11.0"
@@ -5724,7 +5945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
@@ -5764,7 +5985,7 @@ version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"libc",
"once_cell",
"socket2",
@@ -5943,6 +6164,15 @@ dependencies = [
"rand_core 0.9.5",
]
+[[package]]
+name = "range-map"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "rapidhash"
version = "4.4.1"
@@ -6078,6 +6308,7 @@ dependencies = [
"cookie",
"cookie_store",
"encoding_rs",
+ "futures-channel",
"futures-core",
"futures-util",
"h2",
@@ -6269,6 +6500,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "rustc-demangle"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
+
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -6332,6 +6569,7 @@ version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
+ "log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -6514,6 +6752,26 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "scroll"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
+dependencies = [
+ "scroll_derive",
+]
+
+[[package]]
+name = "scroll_derive"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "sdd"
version = "3.0.10"
@@ -6596,6 +6854,113 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "sentry"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030"
+dependencies = [
+ "httpdate",
+ "reqwest 0.12.28",
+ "rustls",
+ "sentry-backtrace",
+ "sentry-contexts",
+ "sentry-core",
+ "sentry-panic",
+ "sentry-tracing",
+ "tokio",
+ "ureq",
+]
+
+[[package]]
+name = "sentry-backtrace"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68e299dd3f7bcf676875eee852c9941e1d08278a743c32ca528e2debf846a653"
+dependencies = [
+ "backtrace",
+ "regex",
+ "sentry-core",
+]
+
+[[package]]
+name = "sentry-contexts"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fac0c5d6892cd4c414492fc957477b620026fb3411fca9fa12774831da561c88"
+dependencies = [
+ "hostname",
+ "libc",
+ "os_info",
+ "rustc_version",
+ "sentry-core",
+ "uname",
+]
+
+[[package]]
+name = "sentry-core"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "deaa38b94e70820ff3f1f9db3c8b0aef053b667be130f618e615e0ff2492cbcc"
+dependencies = [
+ "rand 0.9.4",
+ "sentry-types",
+ "serde",
+ "serde_json",
+ "url",
+]
+
+[[package]]
+name = "sentry-panic"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b7a23b13c004873de3ce7db86eb0f59fe4adfc655a31f7bbc17fd10bacc9bfe"
+dependencies = [
+ "sentry-backtrace",
+ "sentry-core",
+]
+
+[[package]]
+name = "sentry-rust-minidump"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63964525bf74b16233dbcfb307e11485ebd8ff8f87f6ae212b07ca7937cd2db1"
+dependencies = [
+ "minidumper-child",
+ "sentry",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "sentry-tracing"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080"
+dependencies = [
+ "bitflags 2.11.1",
+ "sentry-backtrace",
+ "sentry-core",
+ "tracing-core",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "sentry-types"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e477f4d4db08ddb4ab553717a8d3a511bc9e81dde0c808c680feacbb8105c412"
+dependencies = [
+ "debugid",
+ "hex",
+ "rand 0.9.4",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+ "uuid 1.23.2",
+]
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -6988,6 +7353,17 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+[[package]]
+name = "smart-default"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "socket2"
version = "0.6.4"
@@ -7167,8 +7543,6 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
dependencies = [
"base64 0.21.7",
"serde",
@@ -7889,11 +8263,16 @@ name = "tauri-plugin-native-bridge"
version = "0.1.0"
dependencies = [
"apple-native-keyring-store",
+ "base64 0.22.1",
+ "block",
+ "cocoa",
"dbus-secret-service-keyring-store",
"font-enumeration",
"keyring-core",
+ "objc",
"schemars 0.8.22",
"serde",
+ "serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
@@ -7992,6 +8371,22 @@ dependencies = [
"tauri-plugin",
]
+[[package]]
+name = "tauri-plugin-sentry"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7432b519b6d2d027082a940783c61ba9b684b38d8df7558cd5f924d87009b295"
+dependencies = [
+ "base64 0.22.1",
+ "schemars 0.8.22",
+ "sentry",
+ "sentry-rust-minidump",
+ "serde",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "tauri-plugin-sharekit"
version = "0.3.1"
@@ -8894,7 +9289,7 @@ dependencies = [
"branches",
"bumpalo",
"bytemuck",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"cfg_block",
"chrono",
"crc32c",
@@ -9100,6 +9495,15 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+[[package]]
+name = "uds"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "uds_windows"
version = "1.2.1"
@@ -9111,6 +9515,15 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "uname"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "unarray"
version = "0.1.4"
@@ -9217,6 +9630,34 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "ureq"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
+dependencies = [
+ "base64 0.22.1",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-roots 1.0.7",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
+dependencies = [
+ "base64 0.22.1",
+ "http",
+ "httparse",
+ "log",
+]
+
[[package]]
name = "url"
version = "2.5.8"
@@ -9260,6 +9701,12 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -10099,6 +10546,15 @@ dependencies = [
"windows-targets 0.42.2",
]
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -10420,6 +10876,16 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "winreg"
version = "0.55.0"
diff --git a/Cargo.toml b/Cargo.toml
index 2ca3c71ff..1bb082c77 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,3 +40,7 @@ rust-version = "1.77.2"
[patch.crates-io]
tauri = { path = "packages/tauri/crates/tauri" }
tauri-plugin-fs = { path = "packages/tauri-plugins/plugins/fs" }
+# Xcode 26.2 (Swift 6.2) broke upstream swift-rs 1.0.7's per-swiftc target
+# override; the vendored copy cross-compiles via `--triple`/`--sdk` instead.
+# Upstream is unmaintained (last release 2024). See packages/swift-rs.
+swift-rs = { path = "packages/swift-rs" }
diff --git a/README.md b/README.md
index f7dd9fd7b..f94840672 100644
--- a/README.md
+++ b/README.md
@@ -121,115 +121,9 @@ Guides, tutorials, and FAQs for installing and using Readest live in the officia
📖 **[https://readest.com/docs][link-docs]**
-## Requirements
+## Building from Source
-- **Node.js** and **pnpm** for Next.js development
-- **Rust** and **Cargo** for Tauri development
-
-For the best experience to build Readest for yourself, use a recent version of Node.js and Rust. Refer to the [Tauri documentation](https://v2.tauri.app/start/prerequisites/) for details on setting up the development environment prerequisites on different platforms.
-
-```bash
-nvm install v24
-nvm use v24
-npm install -g pnpm
-rustup update
-```
-
-## Getting Started
-
-To get started with Readest, follow these steps to clone and build the project.
-
-### 1. Clone the Repository
-
-```bash
-git clone https://github.com/readest/readest.git
-cd readest
-```
-
-### 2. Install Dependencies
-
-```bash
-# might need to rerun this when code is updated
-git submodule update --init --recursive
-pnpm install
-# copy vendors dist libs to public directory
-pnpm --filter @readest/readest-app setup-vendors
-```
-
-### 3. Verify Dependencies Installation
-
-To confirm that all dependencies are correctly installed, run the following command:
-
-```bash
-pnpm tauri info
-```
-
-This command will display information about the installed Tauri dependencies and configuration on your platform. Note that the output may vary depending on the operating system and environment setup. Please review the output specific to your platform for any potential issues.
-
-For Windows targets, “Build Tools for Visual Studio 2022” (or a higher edition of Visual Studio) and the “Desktop development with C++” workflow must be installed. For Windows ARM64 targets, the “VS 2022 C++ ARM64 build tools” and "C++ Clang Compiler for Windows" components must be installed. And make sure `clang` can be found in the path by adding `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\Llvm\x64\bin` for example in the environment variable `Path`.
-
-### 4. Build for Development
-
-```bash
-# Start development for the Tauri app
-pnpm tauri dev
-# or start development for the Web app
-pnpm dev-web
-# preview with OpenNext build for the Web app
-pnpm preview
-```
-
-For Android:
-
-```bash
-# Initialize the Android environment (run once)
-rm apps/readest-app/src-tauri/gen/android
-pnpm tauri android init
-pnpm tauri icon ../../data/icons/readest-book.png
-git checkout apps/readest-app/src-tauri/gen/android
-
-pnpm tauri android dev
-# or if you want to dev on a real device
-pnpm tauri android dev --host
-```
-
-For iOS:
-
-```bash
-# Set up the iOS environment (run once)
-pnpm tauri ios init
-pnpm tauri icon ../../data/icons/readest-book.png
-
-pnpm tauri ios dev
-# or if you want to dev on a real device
-pnpm tauri ios dev --host
-```
-
-### 5. Build for Production
-
-```bash
-pnpm tauri build
-pnpm tauri android build
-pnpm tauri ios build
-```
-
-Please refer to our release script if you experience any issues:
-https://github.com/readest/readest/blob/main/.github/workflows/release.yml
-
-### 6. Setup dev environment with Nix
-
-If you have Nix installed, you can leverage flake to enter a development shell
-with all the necessary dependencies:
-
-```bash
-nix develop ./ops # enter a dev shell for the web app
-nix develop ./ops#ios # enter a dev shell for the ios app
-nix develop ./ops#android # enter a dev shell for the android app
-```
-
-### 7. More information
-
-Please check the [wiki][link-gh-wiki] of this project for more information on development.
+To build Readest from the latest commit, see [Getting Started](./CONTRIBUTING.md#getting-started).
## Troubleshooting
diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md
index 853a79aef..d0ccf2af9 100644
--- a/apps/readest-app/.claude/memory/MEMORY.md
+++ b/apps/readest-app/.claude/memory/MEMORY.md
@@ -1,150 +1,142 @@
# Readest Project Memory
-## Key Reference Documents
-- [Bug Fixing Patterns](bug-patterns.md) - Common bug categories, root causes, and fix strategies
-- [CSS & Style Fixes](css-style-fixes.md) - EPUB CSS override patterns and the style.ts pipeline
-- [TTS Fixes](tts-fixes.md) - Text-to-Speech architecture and bug patterns
-- [Layout & UI Fixes](layout-ui-fixes.md) - Safe insets, z-index, platform-specific UI issues
-- [Platform Compat Fixes](platform-compat-fixes.md) - Android, iOS, Linux, macOS platform-specific bugs
-- [Annotator & Reader Fixes](annotator-reader-fixes.md) - Highlight, selection, accessibility bugs
-
-## Security
-- [download_file scope Android regression](download-file-scope-android-regression.md) — #4639 strict `is_allowed` broke ALL Android downloads to app data dir (covers/dicts/books); `app.fs_scope()` lacks command-scoped capability globs; fix = `app.path()` base-dir membership. On-device CDP verify recipe + raw-invoke Channel trick
-- [Security advisories 2026-06](security-advisories-web-2026-06.md) — all 4 GHSA fixed in PR #4638 (web: A OPDS-proxy SSRF + canonical `isBlockedHost` in network.ts + `isLanAddress` merge, B storage `isSafeObjectKeyName`, D Stripe `metadata.userId` ownership) + PR #4639 (native C: `transfer_file.rs` fs_scope guard). OPDS proxy can't require auth (`` usage); strict `is_allowed` for C; shared-target worktree build-cache pollution gotcha
-
-## Paginator Scroll Knowledge
-- [Issue #4112 scroll-anchoring](issue-4112-scroll-anchoring.md) — RESOLVED (PR #4349). Scroll-anchoring suppressed at scrollTop 0 when prepending a section in scrolled mode; fix patterns (prepend compensation, eager backward preload, no-blank nav) + test & dev-server gotchas
-- [Reading ruler line/column-aware](reading-ruler-line-aware.md) — ruler snaps to real lines; multi-column band spans one column; Range.getClientRects() returns tall block boxes that must be dropped; iframe frame-offset mapping; synthetic-key throttling
-- [TOC expand + auto-scroll](toc-expand-and-autoscroll.md) — #4059 collapse-by-default policy in `tocTree.ts`; pinned-sidebar mounts before progress → dynamic expansion breaks scroll-to-current via (1) spurious onScroll clearing pending and (2) Virtuoso scrollToIndex landing short after row growth (re-assert on rAF)
-- [BooknoteView auto-scroll (#4352)](booknote-view-autoscroll-4352.md) — virtualizing the annotation/bookmark list dropped auto-scroll-to-nearest; two paths (reload: OverlayScrollbars resets scrollTop → re-apply in `initialized` via ref; tab-switch: synchronous scrollToIndex on fresh-mounted list wedges Virtuoso → use `initialTopMostItemIndex` + skip-gate). Mirrors TOCView. Includes dev-server/Fast-Refresh/screenshot-vs-DOM verification gotchas
-- [TOC current-position row](toc-current-position-row.md) — synthetic "Current position" row (open-book icon + live `progress.page`) injected one level deeper under the active TOC item via `buildTOCDisplayItems` in `TOCItem.tsx`. INVARIANT: insert AFTER the active item so its `flatItems` index stays valid for the auto-scroll effects
-- [Swipe page-turn bg flash](paginator-swipe-bg-flash.md) — white↔black flash on swipe+animation only; `#background` was static screen-space and didn't track content during drag/snap; fix = sliding per-view full-bleed segments (`computeBackgroundSegments`) rebuilt on scroll + per-rAF synced to the view transform during snap
-- [Duokan fullscreen cover hidden in scroll mode](duokan-fullscreen-cover-scroll.md) — #4379 `data-duokan-page-fullscreen` cover pinned `position:absolute height:100%` collapses against auto-height scroll container; gate fullscreen on `this.#column` + reset stale absolute props on toggle (`setImageSize` in paginator.js)
-- [Paginated texture occlusion](paginated-texture-occlusion-4399.md) — #4399 host `.foliate-viewer::before` texture absent in paginated (shown in scrolled); opaque `#background` container (`= fallbackBg`) from the swipe-flash fix occludes it; shared `textureAwareBackground` helper + `hasTexture ? '' : fallbackBg` container
-- [Dark-mode texture occluded by body bg (#4446)](dark-mode-texture-body-bg-4446.md) — RESOLVED (PR #4564): `body.theme-dark{bg !important}` from #4392 (v0.11.4, NOT foliate-js) painted iframe bodies opaque dark → occluded host texture + poisoned `docBackground` capture → opaque segments/view bgs; fix = `transparent !important` UNCONDITIONALLY (texture-gating would go stale: capture is once-per-section-load); CDP gotchas = patch ALL multiview iframes, stale preload views survive navigation ±2, load-listener sees exact capture-time state
-- [Background overflows column (#4394, PR #4429)](paginator-gutter-bleed-asymmetry-4394.md) — paginated page bg stretched into the outer `--_outer-min` gutter → mixed cover/title 2-up spread shifted off-centre (~250px at 1920px). KEEP the grid (`--_outer-min` keeps margins symmetric); fix = clamp `computeBackgroundSegments` to `[containerStart,containerEnd]` (Math.max/Math.min) so bg stays in its column. 2 wrong tries first (bleed-gating, "page shouldn't be yellow"); foliate submodule needs dev-server RESTART to pick up edits
-- [Inline-block column overflow](inline-block-column-overflow.md) — chapter skips to "Reference materials", clipping a large middle; EPUB wraps body in `display:inline-block` div → atomic-inline box can't fragment across columns → vertical overflow clipped (scrollHeight≫clientHeight). Fix = paginator `#demoteUnfragmentableBoxes` in `columnize` (col-mode, over-tall atomic-inline→fragmentable block). Renders via goTo/next but pages unreachable; scrolled mode unaffected
-
+## Key Reference Documents (aggregators)
+- [Bug Patterns](bug-patterns.md) · [CSS & Style](css-style-fixes.md) EPUB CSS + style.ts · [TTS](tts-fixes.md)
+- [Layout & UI](layout-ui-fixes.md) insets/z-index · [Platform Compat](platform-compat-fixes.md) · [Annotator & Reader](annotator-reader-fixes.md)
+## Safety & Security
+- [In-place delete wiped originals](in-place-delete-wiped-originals.md) never `fs.removeFile` `external` · [Backup zip Windows paths #4703](backup-windows-zip-paths-4703.md) normalize `\` · [download_file scope Android #4639](download-file-scope-android-regression.md)
+- [Security advisories 2026-06](security-advisories-web-2026-06.md) 4 GHSA #4638; SSRF guard broke dev-LAN OPDS, dev-only exemption 2026-07 PR#5002
+## Paginator & Scroll
+- Reading ruler: [line-aware](reading-ruler-line-aware.md) frame-offset map; [vertical-rl backwards #4865](reading-ruler-vertical-rtl-4865.md)
+- [Vertical-rl horizontal pagination (#624)](vertical-rl-horizontal-pagination-624.md) — horizontal inputs + two-phase slide; rtl gated `!vertical`
+- [Slide/curl turn styles via VT (#555)](page-turn-styles-viewtransitions-555.md) — VT turns gated on nested-VT-groups support (iOS 18 WebKit crashes despite having the API); Tauri fallback = `CapturedPageTurn` capture pipeline (WebGL mesh curl + flat canvas slide, full-gridcell capture, instant nav = drop `animated`); MERGED #4940 (2026-07-05), verified live mac/iOS/Android (mobile = JPEG capped 2x, PNG was 1.5s/turn); Win/Linux capture still open
+ - [Captured turn ignored instant-highlight hold](captured-turn-instant-highlight-scrolllock.md) — captured slide/curl swipe path (app interceptor, `no-swipe` set) didn't honor `renderer.scrollLocked`; fix = foliate `get scrollLocked()` + gate `useCapturedTurn` move on it (push/VT-slide already gated); PR#5000 + foliate#51
+- TOC: [expand + auto-scroll](toc-expand-and-autoscroll.md); [current-position row](toc-current-position-row.md); [table heading clip #4439](toc-table-heading-clip-4439.md); [BooknoteView auto-scroll #4352](booknote-view-autoscroll-4352.md)
+- Paginated bg: [swipe flash](paginator-swipe-bg-flash.md); [texture occlusion #4399](paginated-texture-occlusion-4399.md); [gutter bleed #4394](paginator-gutter-bleed-asymmetry-4394.md); [bg-replace reflow #4785](pageturn-bg-replace-reflow-4785.md)
+- [Inline-block column overflow](inline-block-column-overflow.md) `#demoteUnfragmentableBoxes`
+- FXL/PDF: [fit-width scroll reset #4683](fixed-layout-paginated-scroll-reset-4683.md); [PDF spread seam #4587](pdf-spread-canvas-seam-4587.md); [spine seam #4857](fxl-spread-spine-seam-4857.md); [portrait auto-spread off-center #4984](fxl-portrait-autospread-offcenter-4984.md) MERGED PR#4992+foliate#50 lone page kept one-sided auto margin -> stranded + taps turned page; `computeSpreadInlineMargins`
+- Scrolled: [PDF wheel double #4727](pdf-scroll-mode-wheel-double-4727.md); [header title center #4436](scrolled-header-title-center-4436.md); [Duokan fullscreen cover](duokan-fullscreen-cover-scroll.md)
## Critical Files (Most Bug-Prone)
-- `src/utils/style.ts` - Central EPUB CSS transformation hub (14+ bug fixes)
-- `packages/foliate-js/paginator.js` - Page layout, image sizing, backgrounds
-- `src/services/tts/TTSController.ts` - TTS state machine, section tracking
-- `src/hooks/useSafeAreaInsets.ts` - Safe area inset management
-- `src/app/reader/components/FoliateViewer.tsx` - Reader view orchestration
-- `src/app/reader/components/annotator/Annotator.tsx` - Annotation lifecycle
-
+- `src/utils/style.ts` EPUB CSS hub · `packages/foliate-js/paginator.js` · `src/services/tts/TTSController.ts`
+- `src/hooks/useSafeAreaInsets.ts` · `src/app/reader/components/FoliateViewer.tsx` · `.../annotator/Annotator.tsx`
## Sync Notes
-- [KOSync CFI spine resolution](kosync-cfi-spine-resolution.md) — convert via the CFI's own spine (`getXPointerFromCFI`/`getCFIFromXPointer`), never `new XCFI(primaryDoc, primaryIndex)`; primaryIndex lags during scroll → spine-mismatch throw
-- [Empty-start CFI sync bug](empty-start-cfi-sync.md) — `epubcfi(/6/24!/4,,/20/1:58)` (empty-start range) from the cfi-inert skip-link transitional window; jumps to wrong section end; `isMalformedLocationCfi` → discard the synced value in `useProgressSync` (NOT the local open path); foliate fix doesn't repair already-synced values
-- [Custom fonts disappear on cloud sync (#4410)](custom-fonts-reincarnation-4410.md) — CRDT remove-wins: re-import-after-delete needs a `reincarnation` token or the pull re-applies the tombstone; `addFont`/`addTexture` minted none; fix mirrors dictionary (both cases) + OPDS token style; coverage matrix per kind
-- [koplugin note deletion sync](koplugin-note-deletion-sync.md) — koplugin push only walked LIVE annotations so deletions never reached the server; fix = `recordDeletion` persists a `deletedAt` tombstone to `doc_settings.readest_sync.deleted_notes`, `push` folds+clears them; deletion signal in `onAnnotationsModified` is `items.index_modified < 0`
-- [koplugin stats sync (#4666)](koplugin-stats-sync.md) — reading-stats sync (pull on open / push on close, whole statistics.sqlite3 delta, cursor-based); 3-bug chain: plain-table-not-LuaSettings `settings:readSetting` crash; missing required books/notes/configs; statBooks/statPages need `optional_params` (Spore expected=required∪optional, `payload`≠accepted); large-backlog UI-stall + silent-retry risk unfixed
-- [Statusless books re-pinned to top (#4677)](sync-statusless-book-rebump-4677.md) — never-statused (locally-imported, never-pulled) books send `reading_status:undefined` vs server `null`; server POST `statusChanged` (`undefined!==null`) rewrites `updated_at=now()` every push → batch-identical ts pins them top of date-sort; 1-day re-sync window amplifies; fix = `(a??null)!==(b??null)`. On-device CDP PUSH_SENT-vs-RETURNED proof recipe
-
-## Testing
-- [Nightly updater Android E2E](nightly-updater-android-e2e.md) — real Xiaomi/HyperOS test of #4577 self-updater; `pnpm dev-android` (--features devtools) for CDP, raw-socket CDP discovery, nightly>stable comparator, MIUI 单次安装授权 install gates
-- [Android CDP e2e lane](android-cdp-e2e-lane.md) — `pnpm test:android`: adb+CDP drives the installed app on device/emulator; discover-don't-assume targeting, injected hyphenation, MediaStore VIEW transient open (canonical `_data` path gotcha), per-section frame restore; CI workflow with KVM emulator + debug x86_64 APK (no signing secrets)
-- [CDP Android WebView profiling](cdp-android-webview-profiling.md) — drive the on-device Readest WebView via adb+CDP to run JS probes/benchmarks inside the live app (no rebuild); gotchas: locked device freezes fetch (not invoke), visible:false throttles setTimeout, `__TAURI_INTERNALS__.convertFileSrc/invoke` always present, books in internal `/data/user/0/...`, fs `read{rid,len}` last-8-bytes=nread, `fs|close` not ACL-allowed, curl mishandles WebView HTTP framing
-- [Tauri Rust↔JS parser parity tests](tauri-parser-parity-tests.md) — #4369 native Rust EPUB/MOBI parser; how to cross-check vs foliate-js in the `.tauri.test.ts` WebView suite (CWD disk path for Rust, Vite URL for JS, normalizer-based compare, cover presence-only, desc whitespace-collapse); the `dcterms:modified`→`published` divergence fix
-- [TTS browser e2e harness](tts-browser-e2e-harness.md) — faithful auto-advance test (real `` + real `useTTSControl` + mock ONLY the 3 client modules; mock `speak()` yields `end` to drive the real `forward()` walk); seed readerStore/bookDataStore + `settings.globalViewSettings` (else `getMergedRules` crash stops TTS); reproduce FoliateViewer relocate→setProgress; sample-alice Ch4=section 6/Ch5=section 7; assert badge `false` BEFORE tts-stop
-- [TTS sync chrome verification](tts-sync-chrome-verification.md) — Edge TTS WORKS in claude-in-chrome (WebSpeech errors there with `InvalidStateError`); use an Edge voice to verify TTS-driven features live (RSVP followed at ~171 wpm). Synthetic-CFI debug recipe (expose controller, `syncToCfi(view.getCFI(docIndex, word.range))`). Exposed the #3235 cross-realm `instanceof Range` bug (frozen RSVP/paragraph follow) → `isRangeLike()` duck-type fix
-- [TTS sync paragraph+RSVP (#3235, PR #4576)](tts-sync-paragraph-rsvp-3235.md) — TTS-is-clock follow: canonical `tts-position{cfi,kind:word|sentence,sectionIndex,sequence}`; in-mode 🔊 audio toggle (`build{Paragraph,Rsvp}TtsSpeakDetail`, live-range gate); **current word/sentence highlight painted on the overlay CLONE via CSS Custom Highlight API** (no DOM mutation, spans inline; offsets relative to para-start map 1:1 to clone, `getTextSubRange` reuse, index-tagged vs stale); kind-gating `decideParagraphTtsHighlight` (Edge word wins, skip coarse sentence); `::highlight()` from `ttsHighlightOptions`
-
-## Build & Vendoring
-- [fastlane Apple App Store submission](fastlane-apple-appstore-submission.md) — `release_ios`/`release_macos` lanes (App Store review + TestFlight on the altool-uploaded build); gotchas = Tauri auto-notarizes if `APPLE_API_KEY_PATH` is in the macOS build env (keep it OUT, derive from key id), fastlane runs lanes from `./fastlane` so anchor paths via `repo_path`, npm-dotenv-cli vs Ruby-gem shadowing + `cd ../..`
-- [Turbopack build-cache OOM + gated Docker standalone (#4619)](turbopack-build-cache-oom-docker-standalone.md) — interrupted-build partial `turbopackFileSystemCacheForBuild` cache → 42 workers/18GB-swap freeze (clean build=~6.5GB); disabled the flag; `output:'standalone'` gated on `BUILD_STANDALONE` (Docker-only); tauri CI uses `next dev` (config-independent)
-- [Deps/security override workflow](deps-security-overrides-workflow.md) — fix transitive npm Dependabot alerts: main monorepo overrides live in `pnpm-workspace.yaml` (NOT root package.json); `packages/tauri-plugins` is a SEPARATE submodule project w/ own lockfile + `minimumReleaseAge` (main workspace has no age gate); bound 0.x overrides like `vite`; verify via test+lint+build-web. PR #4618 (esbuild 0.28.1, vitest 4.1.9)
-- [R2 rclone CreateBucket 403 (#4588)](r2-rclone-createbucket-403.md) — single-file `rclone copyto`/`moveto` probes CreateBucket → 403 on object-scoped R2 token; use a directory `rclone copy` (or `no_check_bucket=true`); broke nightly assemble, not the release flow
-- [Deploy workers.dev SNI-block + proxy](deploy-workers-dev-sni-proxy.md) — pnpm deploy crash (CN): workers.dev SNI-blocked (DoH useless), R2 populate WS hangs even via proxy; shipped fix = `dangerous.disableIncrementalCache:true` in open-next.config (stock deploy skips populate; readest has no ISR so runtime no-op)
-- [pdfjs vendor wasm decoders](pdfjs-vendor-wasm-decoders.md) — scanned PDFs blank in CI build only (0.11.2 regression); pdfjs 5.7.x moved JBIG2 to `jbig2.wasm`, `copy-pdfjs-wasm` allow-list dropped it; `cpx` no-errors on empty glob; local stale `public/vendor` (gitignored, not refreshed by `tauri build`) masked it; fix = copy `wasm/*`
-
+- [Cloud Sync provider selection #4959/#4380](cloud-sync-provider-selection-plan.md) MERGED #4971+#4973+#4975+#4976: derived provider, exclusive routing, syncBooks auto-enable, fleet probe, chooser; i18n pass + live verify pending
+- [Grimmory native sync](grimmory-native-sync.md) Booklore-fork REVERTED
+- KOSync: [CFI spine resolution](kosync-cfi-spine-resolution.md); [connect() false-positive #4692](kosync-connect-false-positive-4692.md)
+- [Empty-start CFI sync](empty-start-cfi-sync.md) · [Custom fonts vanish #4410](custom-fonts-reincarnation-4410.md) CRDT remove-wins
+- koplugin: [note deletion](koplugin-note-deletion-sync.md) tombstone; [stats #4666](koplugin-stats-sync.md); [bulk download #4751](koplugin-bulk-download-4751.md); [dup book rows #4861](koplugin-stats-duplicate-book-rows-4861.md)
+- [Statusless re-pin #4677](sync-statusless-book-rebump-4677.md) · [pull cursor synced_at #4678](sync-synced-at-cursor-4678.md)
+- [koplugin library stale #4934](koplugin-library-stale-synced-cursor-4934.md) pull cursor updated_at→synced_at + split push watermark + v2→v3 heal migration
+- WebDAV: [metadata #4756](webdav-metadata-sync-4756.md) LWW; [group membership #4942](webdav-group-membership-sync-4942.md) mergeBookMetadata dropped groupId/groupName; [credentials #4810](webdav-credential-sync-4810.md); [connect nullified #4780](webdav-connect-nullified-4780.md) stale closure
+- [WebDAV deletion + upload-after-enable (#4860/#4856)](webdav-deletion-and-upload-after-enable-4860-4856.md) edit-wins LWW + tombstone union
+- File sync: [refactor #4784](webdav-filesync-refactor-plan.md) `FileSyncEngine`; [third-party auto-sync #4835](third-party-library-autosync-4835.md)
+- [Transfer Queue clear not persisted](transfer-queue-clear-persistence.md) hook mutated store directly, skipped `persistQueue()`; route clears through `transferManager`
+- [Multi-window settings clobber (#4580)](multiwindow-settings-clobber-4580.md)
+- Google Drive: [research](gdrive-sync-provider-research.md); [multi-PR status](gdrive-provider-multipr-status.md); [full walk every sync](gdrive-fullwalk-every-sync-no-source-cursor.md) no-source books never recorded in uploadedHashes + focus refires pullLibrary
+- [S3/R2 provider](s3-r2-sync-provider.md) third backend, aws4fetch SigV4 path-style, full slice on dev uncommitted; live R2 verify pending
+- [OneDrive provider #5007](onedrive-sync-provider.md) 4th backend; shared `providers/oauth/` extraction + path-addressed Graph App Folder; branch `feat/onedrive-sync` 28 commits merge-ready, LOCAL; Azure client id = ops blocker; LESSON: settingsSync.ts #4580 slice + s3 backup-blacklist gap
+- [Hardcover edition_id (#4792)](hardcover-progress-edition-id-4792.md)
+## Build, Testing & CI
+- [Nightly quick-sharun hang #4906](nightly-quick-sharun-hang-4906.md) pin via cache pre-seed + step timeouts
+- [format:check separate gate](verify-format-check-gate.md) · [Worktree rebase submodule drift](worktree-rebase-submodule-drift.md)
+- Android CDP: [e2e lane](android-cdp-e2e-lane.md) `pnpm test:android`; [WebView profiling](cdp-android-webview-profiling.md); [double-tap gesture](android-e2e-doubletap-cdp-gesture.md)
+- [Tauri Rust↔JS parser parity](tauri-parser-parity-tests.md)
+- TTS tests: [browser e2e harness](tts-browser-e2e-harness.md); [paragraph+RSVP sync #3235](tts-sync-paragraph-rsvp-3235.md) TTS-is-clock
+- [fastlane App Store](fastlane-apple-appstore-submission.md) `APPLE_API_KEY_PATH` out of build env
+- [Turbopack cache OOM (#4619)](turbopack-build-cache-oom-docker-standalone.md)
+- [Deps override workflow](deps-security-overrides-workflow.md) `pnpm-workspace.yaml`
+- [Xcode 26.2 broke iOS builds (swift-rs)](xcode26-swiftrs-ios-build-broken.md) — phantom `Bundle.main`/`privacy:` errors; vendored `packages/swift-rs` `--triple`/`--sdk`; Package.swift platforms floor now enforced
+- [pdfjs vendor wasm](pdfjs-vendor-wasm-decoders.md) copy `wasm/*`
+- [CI/PR delivery + push keepalive](ci-pr-delivery-and-push.md)
## Platform Compat
-- [Android hyphen selection bounds (#1553)](android-hyphen-selection-bounds-1553.md) — Blink paints the start handle on the paragraph's LAST hyphen when a touch selection starts at the first word of a hyphenated paragraph (`ComputePaintingSelectionStateForCursor` lacks the generated-text offset remap, hyphen offsets {0,1}); drag-extend re-anchors base there. Fix = repair jumped anchor + suppress handles (empty-commit needs one painted frame) + `SelectionRangeEditor` custom handles; multicol NOT required; desktop/iOS unaffected
-- [Android NativeFile vs RemoteFile I/O](android-nativefile-remotefile-io.md) — why NativeFile is slow (4-IPC/chunk + bridge serialization, tauri#9190); RemoteFile CANNOT replace it on Android (asset-protocol Range broken: start>0 → "Failed to fetch", start-0 capped at 1,024,000; plain no-Range fetch returns full file at 281 MB/s); measured 44/100/281 MB/s; speedups = handle-reuse (2.3×), whole-file asset loader (6.3×), or fix wry upstream. Verified live via CDP.
-- [Window-state sanitizer (#4398)](window-state-sanitize-4398.md) — Windows launch crash (WebView2 0x80070057) from invalid `.window-state.json` (`-32000` minimized sentinel / `0×0`); our plugin already has upstream #253 fix so bad files are stale; defense-in-depth `window-state-sanitizer` plugin registered BEFORE window-state (plugin init = registration order); coord threshold `-16000` (~halfway to the -32000 sentinel; real desktops sit a few thousand px off origin) keeps multi-monitor negatives
-- [Android Open-with intent flow (#4521)](android-open-with-intent-flow.md) — "Open with"/"Send to" pipeline: `NativeBridgePlugin.kt::handleIntent` → `shared-intent` → `useAppUrlIngress` → `useOpenWithBooks` (VIEW=transient→reader, SEND=library+upload). Telegram fails where file-manager works on TWO axes: cold-start delivery (fixed by #4527, on dev NOT released v0.11.4) + foreign-private-file read (Telegram FileProvider non-persistable grant vs shared-storage FUSE real-path). adb MediaStore VIEW repro tests pipeline but CANNOT reproduce the read axis (MANAGE_EXTERNAL_STORAGE bypasses grant)
-- [Dict lookup → OEM browser hijack (#4559)](dict-lookup-browser-hijack-4559.md) — VIVO system-dict lookup opened the browser not Eudic. PRIMARY: no `` for `ACTION_PROCESS_TEXT` under targetSdk36 → dictionary apps invisible, only auto-visible browser returned (fix = add `` to plugin manifest). SECONDARY: browser registers PROCESS_TEXT + is default → filter browsers in pure `decideLookupDispatch` (explicit/chooser/unavailable). Remember-the-pick via `IntentSender`+`EXTRA_CHOSEN_COMPONENT`→`LookupChoiceReceiver`→SharedPreferences (`ACTION_CHOOSER` has no native Always); reset row in `CustomDictionaries.tsx`
-- [Android sideload same versionCode](android-sideload-same-versioncode.md) — sideloaded APK reinstall allows EQUAL versionCode (only strictly-lower blocked); Play Store's increment rule does NOT apply to sideload. Nightly APKs share base versionCode and still install. Corrects a plausible-but-wrong review claim
-- [Large-PDF OOM range flood (#3470)](pdf-oom-range-flood-3470.md) — 50MB+ PDF import/open crash = foliate makePDF firing ALL pdf.js range reads un-awaited (753 concurrent fetch→shouldInterceptRequest→Java byte[] → 512MB heap OOM), NOT whole-file load; official viewer survives via browser ~6-conn/host cap; fix = MAX_CONCURRENT_RANGES=6 queue in makePDF; on-device CDP recipe; Xiaomi13/WV147 won't OOM but flood 753→6 verified
-
-## Feature Notes
-- [Save image to gallery (Android, #4680)](save-image-to-gallery-android.md) — image-viewer Save button → MediaStore on Android (share sheet can't save-to-file: ACTION_SEND has no file-manager target); sharekit 0-byte self-copy bug (Temp==cacheDir); tsgo misses abstract-class conformance (real tsc catches); on-device CDP verify recipe
-- [Webtoon Mode (#3647)](webtoon-mode-3647.md) — seamless no-gap scrolled reading for image books (PRs #4662 + foliate-js#30); fixed-layout scroll mode is fit-width by construction (ignores `zoom`, only `scale-factor`); `scroll-gap` attr→`--scroll-page-gap` var; clear-on-leave in BOTH ViewMenu effect AND Shift+J; worktree submodule has local-path origin (push SHA direct to fork)
-- [Biometric app-lock (#4645)](biometric-app-lock-4645.md) — fingerprint/Face ID startup unlock layered over PIN (mobile); gate must read flag from `appLockStore` not un-seeded `settingsStore` (race); `tauri-plugin-biometric` is `#![cfg(mobile)]` (desktop clippy skips it; pin in root Cargo.lock); scope i18n manually (en unscanned, full extract churns drift)
-- [Tap to open image/table (#4600)](tap-to-open-image-table-4600.md) — single-tap opens gallery/table-zoom in **reflowable** EPUBs (long-press unchanged); `iframe-long-press` message renamed to `iframe-open-media`, hook `useLongPressEvent`→`useOpenMediaEvent`; shared `detectMediaTarget`; `handleClick` got `isFixedLayout`
-- [#4584 tap-death investigation](issue-4584-tap-death-investigation.md) — UNFIXED; `isPopuped` self-heals (RED HERRING, don't "fix" it); likely WebView-148-specific (emulator=133 can't repro); Android emulator/CDP gesture-verification gotchas (swiftshader ANR=artifact, CDP can't native-select, screenX=0)
-- [Dictionary lemmatization (#4574)](dict-lemmatization-4574.md) — inflected selections (`ran`/`mice`/`analyses`) resolve to base headwords (`run`/`mouse`/`analysis`) in dicts that store only lemmas (ODE). Pluggable `lemmatize/` registry (default English, explicit non-English no-op), English rules+irregulars, appended to tail of `buildLookupCandidates` so exact match wins; over-generate + dict-validates; `-ses→-sis` ordered before `-es`
-- [Word Lens inline gloss (feat/word-wise)](wordlens-feature.md) — Kindle-style native-language hint above hard words; CFI-safe via `…