forked from akai/readest
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01bc015985 | |||
| 781a297993 | |||
| a23427ccc6 |
@@ -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:
|
||||
@@ -381,6 +387,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: |
|
||||
|
||||
@@ -122,6 +122,10 @@ jobs:
|
||||
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:
|
||||
@@ -267,6 +271,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:
|
||||
@@ -309,6 +321,7 @@ jobs:
|
||||
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
|
||||
id: tauri
|
||||
if: matrix.config.release != 'android'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -332,6 +345,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 +407,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:
|
||||
|
||||
@@ -14,6 +14,7 @@ Durable, non-obvious gotchas (each cost a debugging round):
|
||||
- **iOS widget missing from gallery = stale `.xcodeproj`.** `gen/apple/project.yml` defines the `ReadestWidget` target, but **Tauri's iOS build does NOT re-run xcodegen**, so a newly-added target is silently omitted from the build. Fix: `cd src-tauri/gen/apple && xcodegen generate`. Also: iOS builds from the **MAIN repo** `/Users/chrox/dev/readest` (complete gen/apple), NOT the `pnpm worktree:new` worktree (its gen/apple is incomplete — missing `Sources/`, `Assets.xcassets`, `Externals`, `LaunchScreen.storyboard` — so xcodegen fails there).
|
||||
- **Android RemoteViews allow only @RemoteView widgets.** Plain `<View>` (and `<Space>`) is NOT allowed → launcher inflate fails → "Can't load widget". Use an empty `FrameLayout` for spacers. Covers: badge + progress bar are **baked into the bitmap** (Canvas in `writeThumbnail`) because RemoteViews can't clip/overlay reliably; shown via `fitCenter`. Responsive sizing by grid cells: `n = (minWidthDp + 30) / 70` (Android cell formula); one book per column, cap 3.
|
||||
- **Background TTS progress freeze.** `book.progress` (libraryStore) AND `readerProgressStore` are both written by the same `setProgress`, inside `commitRelocate` → **`requestAnimationFrame`**, which Android pauses for a backgrounded WebView → both freeze during background TTS. No store-only fix (page-based progress needs rendering). Fix: in `FoliateViewer.progressRelocateHandler`, commit synchronously when `document.visibilityState === 'hidden'` (relocate still fires; only the rAF commit was deferred). Confirmed working on device.
|
||||
- **Android crash: "cannot use a recycled source in createBitmap" (exact-2:3 covers).** In `ReadingWidgetStore.writeThumbnail`, `Bitmap.createBitmap(src, x, y, w, h)` returns the SAME instance when the crop covers the whole *immutable* source (`decodeFile` bitmaps are immutable) — which happens when the cover decodes to exactly 2:3 (height==width*3/2), making the center-crop a no-op. The old code then did `bitmap.recycle()`, recycling `cropped` too, so the next `createScaledBitmap(cropped, …)` threw. Fix: `if (cropped !== bitmap) bitmap.recycle()` — mirror the `if (scaled !== cropped) cropped.recycle()` guard already 4 lines below. Trace was R8-obfuscated + ran inside `update_reading_widget`'s `pluginScope.launch { withContext(Dispatchers.IO) }`, surfacing as `FATAL EXCEPTION: main` with a `Dispatchers.Main` cancelled-coroutine suppressed frame. **iOS is unaffected** — `ReadingWidgetWriter.writeThumbnail` uses ARC-managed immutable `UIImage` + `UIGraphicsImageRenderer`, no manual recycle/aliasing.
|
||||
- **iOS TTS controls deferred** — interactive widget buttons need iOS 17 App Intents; widget min target is iOS 15 (15/16 widgets can only deep-link, no buttons). Android uses `MediaButtonReceiver.buildMediaButtonPendingIntent` (any version). Follow-up only.
|
||||
- **`.superpowers/` is NOT gitignored** in this repo → a subagent's `git add` can sweep SDD scratch (`*-report.md`) into a commit; check `git ls-files '.superpowers/*'` before squashing/pushing.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.11.16",
|
||||
"version": "0.11.17",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.11.16": {
|
||||
"0.11.17": {
|
||||
"date": "2026-06-29",
|
||||
"notes": [
|
||||
"Sync: Added WebDAV and Google Drive sync for your books and reading data",
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.readest.native_bridge
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Instrumented test (runs on an Android device/emulator).
|
||||
*
|
||||
* Regression for the widget cover crash: a cover that decodes to exactly 2:3
|
||||
* makes the center-crop a no-op, so Bitmap.createBitmap returns the SAME
|
||||
* immutable instance as the source. The pre-fix code recycled the source right
|
||||
* after, then passed the now-recycled bitmap to createScaledBitmap, throwing
|
||||
* "cannot use a recycled source in createBitmap" and killing the app.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ReadingWidgetStoreTest {
|
||||
@Test
|
||||
fun writeThumbnail_exact2to3Cover_doesNotCrash() {
|
||||
val ctx = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
// 240x360 is exactly 2:3 and small enough to skip downsampling, so the
|
||||
// decoded cover hits the createBitmap same-instance path.
|
||||
val src = Bitmap.createBitmap(240, 360, Bitmap.Config.ARGB_8888)
|
||||
val srcFile = File(ctx.cacheDir, "widget-cover-2x3.png")
|
||||
srcFile.outputStream().use { src.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||
src.recycle()
|
||||
|
||||
val hash = "regression2x3"
|
||||
try {
|
||||
// Pre-fix: throws IllegalArgumentException. Post-fix: writes the PNG.
|
||||
ReadingWidgetStore.writeThumbnail(ctx, hash, srcFile.absolutePath, 42)
|
||||
|
||||
val out = File(ReadingWidgetStore.coversDir(ctx), "$hash.png")
|
||||
assertTrue("thumbnail should be written", out.exists())
|
||||
val decoded = BitmapFactory.decodeFile(out.absolutePath)
|
||||
assertNotNull("thumbnail should decode to a valid bitmap", decoded)
|
||||
out.delete()
|
||||
} finally {
|
||||
srcFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -57,7 +57,11 @@ object ReadingWidgetStore {
|
||||
val cropX = (srcW - cropW) / 2
|
||||
val cropY = (srcH - cropH) / 2
|
||||
val cropped = Bitmap.createBitmap(bitmap, cropX, cropY, cropW, cropH)
|
||||
bitmap.recycle()
|
||||
// createBitmap returns the SAME instance when the crop covers the whole
|
||||
// (immutable) source — i.e. covers already at exactly 2:3. Recycling here
|
||||
// would recycle `cropped` too and crash createScaledBitmap below with
|
||||
// "cannot use a recycled source". Mirror the scaled !== cropped guard.
|
||||
if (cropped !== bitmap) bitmap.recycle()
|
||||
|
||||
// Scale the cropped bitmap to the target size.
|
||||
val scaled = Bitmap.createScaledBitmap(cropped, THUMB_WIDTH, THUMB_HEIGHT, true)
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</supports>
|
||||
|
||||
<releases>
|
||||
<release version="0.11.16" date="2026-06-29">
|
||||
<release version="0.11.17" date="2026-06-29">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Sync: Added WebDAV and Google Drive sync for your books and reading data</li>
|
||||
|
||||
@@ -1 +1 @@
|
||||
509767206c4727e8f7a60d2b94879129f0edfa0bbbbcea34a748391b1113a073 ../../data/metainfo/appdata.xml
|
||||
c0fae8cab8e75871573d679d28bf0c1b778ef9e8f7c0b42da678396d1749fd96 ../../data/metainfo/appdata.xml
|
||||
|
||||
@@ -13,14 +13,6 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
||||
|
||||
# Available Actions
|
||||
|
||||
### verify_paths
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane verify_paths
|
||||
```
|
||||
|
||||
|
||||
|
||||
### release_ios
|
||||
|
||||
```sh
|
||||
|
||||
Reference in New Issue
Block a user