chore: merge upstream jocmp/capyreader main#2117
Closed
Shengqiang-Zhang wants to merge 118 commits into
Closed
Conversation
Bundled article fonts (Inter, Jost, Literata, etc.) are Latin-only, so CJK glyphs fall back to whatever sans-serif font WebView ships with — which ignores OEM system font customizations (e.g. Mi Sans on MIUI, HarmonyOS Sans). Add a new FontOption.SYSTEM_UI that maps to the CSS system-ui stack so articles can render in the actual device system font, including its CJK coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Triggered on v* tag push or manual dispatch. Uses repository signing secrets when available and falls back to the committed debug keystore so forks can publish releases without additional configuration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous SYSTEM_UI option mapped to CSS system-ui, but Chromium's system-ui on Android resolves to a fixed Roboto/Noto stack and never reaches OEM-registered families like Mi Sans (MIUI), HarmonyOS Sans, OPPO Sans, etc. Native Compose UI honors Typeface.DEFAULT and picks them up correctly; only WebView was stuck on Roboto for Latin and Noto for CJK. Resolve an OEM-branded or CJK-capable font via SystemFonts.getAvailableFonts(), serve the file through a new /system-font/ WebViewAssetLoader handler, and point an @font-face ("AndroidSystem") at it. The SYSTEM_UI family stack now leads with AndroidSystem and also enumerates the common OEM family names as backups before falling through to system-ui. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Samsung S24+, the previous resolver returned null for devices whose default font filename lacks the "samsung" substring (e.g. SECSans*, SamsungOneUI-Regular.otf), and the path handler always served "font/ttf" — so an .otf or .ttc face came back with the wrong MIME and Chromium rejected it, falling through @font-face to the Roboto/Noto stack. The CSS `format("truetype")` hint compounded this by instructing browsers to skip non-truetype sources outright. Broaden OEM_FILE_HINTS (oneui, secsans, seccjk, sansation, honor, magicui, nubia), prefer non-script-specific OEM faces, and add a Latin fallback that picks any sans-serif whose filename isn't a stock Roboto/Noto/Droid or a script/emoji/symbol/serif/mono variant. Drop the CSS format hint so browsers auto-detect, and select the response MIME from the file extension (otf → font/otf, ttc → font/collection, else font/ttf). Log the resolved file and unresolved candidates via CapyLog so future devices can be diagnosed from the event stream. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Samsung's Galaxy Themes installs user-selected fonts (e.g. Palatino) in a private directory that app processes can't read, so the `Real system default` option can only reach the built-in OEM family. Add a CUSTOM FontOption that lets the user point the article WebView at any font file on the device. The user taps the new Custom font… item in the font dropdown, which opens a SAF picker. CustomFontManager copies the chosen file into `filesDir/article-custom-font` (avoiding brittle content-URI permissions) and records the display name so the menu can echo it back. CustomFontPathHandler serves the saved file to WebView under /custom-font/, detecting OTF/TTC from magic bytes and falling back to TTF. A new @font-face ("CustomFont") is wired into the SCSS alongside a `custom` slug, so selecting CUSTOM swaps the body class to `article__body--font-custom` and the title class accordingly when titleFollowsBodyFont is on. Also revert the short-lived hidden-API reflection in SystemFontResolver — it didn't help the custom-font case and added a sketchy VMRuntime bypass. The expanded OEM hints, Latin fallback, mime-type-by-extension, and dropped format() hint from the prior commit stay; they still help OEM defaults. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the title font class from the h1 up to the article__header container so the byline and feed name inherit the same font. The titleFollowsBodyFont toggle now controls all three elements together. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align the displayed version with the release tag format (v2.0) and add a button that hits the GitHub releases API; if the latest tag is newer the release page opens, otherwise a snackbar reports the status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New "Apply article font to whole app" toggle under Display & Appearance reuses the article font selection (including bundled and custom fonts) for MaterialTheme typography across the entire app. - Article list now shows a small scroll-to-top FAB in the bottom right once the list has been scrolled. - Bump versionName to 2.2 ahead of the v2.2 release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AnimatedVisibility check held a stale LazyListState reference because
`remember { derivedStateOf { ... } }` was not keyed. When the article list
transitioned from empty to populated, a new LazyListState was produced but
the derived state kept observing the original (empty) instance, so the FAB
never became visible after scrolling.
Keying `remember` on `listState` rebuilds the derived state when the
reference changes. Also bumps versionName to 2.2.1 for the patch release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
transactionWithErrorHandling now returns Result<Unit> so callers can observe transaction failures instead of silently treating them as success. OPMLImportWorker returns Result.failure() on exception so WorkManager records the failure. Bumps version to 2.2.2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pull-to-refresh indicator and the drawer refresh button could remain stuck "refreshing" forever when a refresh job was cancelled (e.g. by starting another refresh) or when the sync returned a failure. The old flow only reset state inside the success path of the coroutine body, and used invokeOnCompletion for refreshingAll — neither ran if the body threw or was cancelled mid-flight. Wrap the refresh coroutine in try/finally so the state-clearing callback always runs, and fold the refreshingAll reset back into the same callback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers deploy, emulator, screen, and docs subcommands so future build/test workflows can use the CLI where it complements Gradle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some CDNs (e.g. img2.jintiankansha.me, used by WeChat/Kindle4RSS feeds) apply hotlink protection that returns 403 when the Referer points to an unrelated host. PR jocmp#1879 always set Referer to the article URL, which tripped these checks and broke images for feeds like feedmaker.kindle4rss.com. For media sub-resources, send the request URL's own origin as Referer instead. That still satisfies CDNs that simply require some Referer (the jocmp#1878 need), passes same-origin hotlink checks, and avoids leaking the article URL to third-party image hosts. Iframe and CORS proxies keep the article URL as Referer since embeds can depend on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-1776690802786 Add Claude Code GitHub Workflow
- Reword KDoc to clarify refererFor can return null (Issue jocmp#1878 is only satisfied when pageUrl is available, not unconditionally) - Fix originOf to use uri.host + uri.port instead of uri.authority to prevent leaking user-info credentials into the Referer header Co-authored-by: Shengqiang Zhang <Shengqiang-Zhang@users.noreply.github.com>
fix: Use image origin as Referer for proxied media requests
The workflow was copied from a Flutter repo and still instructed Claude to run `ego_sdk/flutter/bin/flutter analyze|test`, which does not exist here. Swap in this project's actual checks — `./gradlew :<module>:testDebugUnitTest`, `./gradlew assembleFreeDebug`, and `make` / `make check` for JS/Liquid template changes — and update the allowed-tools list accordingly so the loop can actually verify its fixes.
The Claude auto-fix prompt still referenced `ego_sdk/flutter/bin/flutter` commands inherited from the source repo. Swap those for this project's Gradle/make commands (assembleFreeDebug, module unit tests, `make` / `make check` for JS/Liquid assets) and update the allowed-tools list accordingly so the Copilot review-fix loop actually runs the right checks on capyreader PRs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Feeds like AI_era.weixin.xml render 18+ images per article, all lazy beyond the first. Two failure modes could leave images stuck at the opacity:0 default: - WebView occasionally completes an image's network fetch without firing `load` or `error` (cache hits and lazy-loaded images), so the `.loaded` class was never added. Poll `img.complete` every 500ms for up to 30s and flip any resolved images visible; force-mark anything still pending at the deadline so broken images fall back to the browser's broken-image rendering instead of blank space. Also switch to `addEventListener` so the handler isn't clobbered if another script assigns onload/onerror. - `proxyRequest` only had one try/catch covering the whole flow. If an exception occurred after `execute()` returned, the OkHttp Response was never closed, leaking connections back to the pool. With maxRequestsPerHost=5 and 18 images on the same host, subsequent requests could stall. Split into two try blocks and explicitly close the Response on the response-processing failure path. Also pass null as the encoding for non-text MIME types, log the failing URL, and skip the default UTF-8 charset for binary image bodies. Also repoint tsconfig.json include paths from the non-existent capy/src/main/assets/ location to the real app/src/main/assets/ one so `make check` actually validates the JS assets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…IME types Expand the UTF-8 default to cover application/json, application/javascript, application/ecmascript, application/xml, and application/*+xml / *+json types, not just text/* — matching WebView expectations for proxied textual responses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The auto-fix step had no timeout, so when Claude invoked `./gradlew assembleFreeDebug` on a bare `ubuntu-latest` runner (no Android SDK, cold Gradle cache) the action stalled until GitHub's 6-hour job ceiling. Observed on PR #3: the first review arrived at 08:29Z, the Claude step started at 08:41Z, and was still running an hour later with no progress. Changes: - Add `timeout-minutes: 30` to the Claude step so hangs self-resolve. - Update the prompt to explicitly forbid full Gradle builds in this job and defer Kotlin build verification to the existing `test.yml` CI workflow. JS/Liquid `make` checks stay in-scope. - Drop `Bash(./gradlew:*)` from the allowed-tools list so Claude can't regress into invoking it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Handle null OkHttp response body explicitly (close response and return null) instead of relying on NPE caught by the surrounding try/catch - Hoist isTextMimeType's set literal to a companion object val to avoid per-call allocation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…prompt
- Use { once: true } on load/error listeners in setupImageLoadHandler so
duplicate calls before image resolves do not register stacked handlers
- Remove stale ./gradlew hint from workflow prompt; Bash(./gradlew:*) is
not in claude_args so the suggestion was misleading
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…orkflow prompt" This reverts commit 310179b.
Use { once: true } on load/error event listeners in setupImageLoadHandler
so they auto-remove after firing, preventing duplicate handlers if the
function is called more than once for the same image before it resolves.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…concile fix(webview): reconcile image load state for WeChat feeds
… + auth Introduce the `web/` top-level directory for a browser-based Capy Reader client that syncs with Android via a shared Miniflux server. Phase 1 of the plan in docs: Vite + React 18 + TypeScript shell with Tailwind, a small in-repo component kit (Button, Input, Card, Label) styled for a polished Inoreader-like UI, a custom-font pipeline sourced from the Android `app/src/main/res/font/*.ttf` assets, a typed Miniflux API wrapper + TanStack Query hooks, an auth context persisting the API token in localStorage, and a login route that verifies the token via `GET /v1/me` before signing the user in. No Android code is touched; cross-device sync is enabled by pointing both clients at the same Miniflux instance. Verified: pnpm typecheck, pnpm test (5 passing), pnpm build succeed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, marking entries above as read kept the scroll container's scrollTop unchanged while the unread-filtered list shrank, so the viewport ended up past the anchor row (or empty) and the user had to scroll back to the top to find unread articles. Track the clicked row's entry id when invoking the action and re-anchor the virtualizer to that entry on each entries update (optimistic + refetch), then forget the anchor after a short delay so unrelated future updates don't yank the scroll position. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gate the re-anchor scroll on the entries list actually shrinking below its pre-action length, so the All-articles filter no longer jumps the viewport when "mark above as read" is used there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track the anchor article's initial index instead of the list length to decide when re-anchoring is needed after "mark above as read". The previous length-based guard failed when the unread list was at the page cap (100 entries): the refetch could return 100 entries again (older items filling in), so the length never shrank, but the anchor article had already moved upward and the scroll correction was skipped. Using the index correctly handles both the partial-page case (fewer entries, anchor moves earlier) and the full-page case (same entry count, but anchor index is lower), while still avoiding spurious jumps in the All-articles filter where the anchor index stays unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clear markAboveAnchorId immediately when entries refresh but the anchor index is unchanged (All-articles filter), preventing a stale scroll if a later refetch or filter change happens to move the row upward. Guard the first effect run (before refetch) with an entries-reference comparison so the unread-filter scroll path is still reached after the real refresh. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The optimistic cache update in useMarkEntriesAsRead creates a new entries reference with statuses changed but all entries still present, so the anchor stays at the same index. The previous guard cleared markAboveAnchorId on that first post-initial change, dropping the anchor before the real network refetch returned the shrunken unread list. Track how many entries changes have occurred since the anchor was set; only clear the anchor in the same-index case on the second change (the real refetch) so the Unread-filter scroll correction still fires. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the change-counter guard (>= 2) with a timeout-based cleanup. In the All filter, the optimistic update makes the cache match the server response, so React Query structurally shares the refetched array and never delivers a second entries reference — leaving markAboveAnchorId armed indefinitely. The timeout fires 3 s after the same-index entries change and clears the anchor; in the Unread filter the real refetch arrives first and its effect cleanup cancels the timer before it fires. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-after-mark-above fix(web): scroll to anchor article after "mark above as read"
Adds a small Copy icon button next to each feed entry that copies the feed_url to the clipboard, swapping to a Check icon for ~1.5s as visual confirmation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(web): copy-to-clipboard icon for feed URLs in Manage Feeds
Previously the scroll-to-top after refresh was gated on markReadOnScrollEnabled, so users with that setting off would stay at their prior scroll position and miss the newly-fetched articles at the top. Always jump to the top once a pull-to-refresh completes so the newest items are visible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p-after-refresh fix(android): scroll article list to top after pull-to-refresh
…alog
Adds a per-row category dropdown to the Manage feeds dialog. Selecting a
different category sends PUT /v1/feeds/{id} with the new category_id and
invalidates feeds/categories/counters/entries on success.
Per-feed pending state is tracked in a Set so concurrent moves on
different rows lock independently; finishing one move never re-enables
another row that's still in flight.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-entry status changes (auto-mark on click, the read/unread toggle in ArticleView, and the m keyboard shortcut) used to invalidate the entries query, which refetched ?status=unread from Miniflux and made the just-read row vanish from the list. useUpdateEntryStatus now skips the entries invalidation. The optimistic onMutate already flips the row's status in the cached list, so it re-renders with read styling but stays in place. Counters and the per-entry cache still invalidate. Bulk "Mark above as read" (useMarkEntriesAsRead) keeps its entries invalidation, which is the explicit way to clear out read items above the current article. Refresh button and window-focus refetch behave unchanged. Tests pin both behaviors: useUpdateEntryStatus does not invalidate ["entries"], useMarkEntriesAsRead does. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…icle list
Mirror the Android app's favicon treatment in the web companion. Sidebar
feed rows and article list rows now render each feed's Miniflux-cached
favicon (via /v1/icons/{id}, returned as a base64 data URL) with a
first-letter fallback when no icon has been fetched yet.
Each feed also gets a deterministic soft background tint. The palette is
built by spreading hues evenly inside each category and rotating the
starting hue per category by the golden angle, so feeds within the same
category stay visually distinct even when the global hue wheel
inevitably reuses hues across hundreds of feeds. The same feed colour
appears in both columns so an article's source is recognisable at a
glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… fresh When useUpdateEntryStatus marks an entry as unread, the Unread list cache must be invalidated so the article appears there even if it was not already cached. The previous change skipped all entries invalidation, but that breaks the mark-unread path. Now only the mark-read path skips invalidation (keeping the article visible in-place during the reading session). Mark-unread still invalidates so newly-unread articles are fetched into the Unread view. Add a test that pins the mark-unread invalidation contract. Co-authored-by: Shengqiang Zhang <Shengqiang-Zhang@users.noreply.github.com>
main (PR #18-#20) added Copy URL functionality to ManageFeedsDialog (Check/Copy icons, copiedFeedId state, useEffect cleanup, handleCopyFeedUrl). This branch added the category move dropdown (useUpdateFeed, movingFeedIds, handleMoveFeed, <select>). Manually merge both so no changes are lost. Feed rows now show: feed info | category selector | copy-URL button | delete button. Co-authored-by: Shengqiang Zhang <Shengqiang-Zhang@users.noreply.github.com>
Commit 924667e manually duplicated the copy-URL feature from main into this branch, causing a 3-way merge conflict: both main and this branch independently added code in the same file region (around the delete button). Removing it lets GitHub's merge cleanly combine copy-URL from main with move-feeds from this branch. Co-authored-by: Shengqiang Zhang <Shengqiang-Zhang@users.noreply.github.com>
…ints feat(web): real feed favicons + per-feed colour tint in sidebar and article list
…d-keep-unread-on-click # Conflicts: # web/src/features/subscriptions/ManageFeedsDialog.tsx
When marking an article read, onMutate had already optimistically rewritten all cached ['entries'] lists (including inactive Unread caches). The prior code skipped all invalidation, so a cached Unread list could show a read article for up to 10 s if the user switched back within the stale window. Now we call removeQueries with type:"inactive" for the read case, which evicts any inactive entry caches while leaving the active list untouched — preserving the "article stays visible during reading session" UX without leaking stale data into other views. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace removeQueries({ type: "inactive" }) with invalidateQueries({
refetchType: "none" }) when marking an entry read. This marks all
["entries"] caches stale without triggering an immediate refetch, so:
- The active list keeps its optimistic update visible during the
current reading session.
- Both active and inactive Unread caches will refetch fresh data the
next time they are observed, preventing a read article from
re-appearing after navigation within the 10 s stale window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test incorrectly expected no ["entries"] invalidation when marking read, but onSettled calls invalidateQueries with refetchType:"none" to mark entries stale without triggering a refetch. Update the assertion to verify the no-refetch behaviour instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…focus refetch
Switch from invalidateQueries({refetchType:"none"}) to removeQueries({type:"inactive"})
when marking an entry read. The previous approach marked the active Unread query stale,
so refetchOnWindowFocus would refetch it and remove the article mid-session. Evicting
only inactive caches leaves the active list untouched while still ensuring stale caches
are cleaned up when the user navigates away.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The seeded entries query had no active observer, so onSettled's
removeQueries({ type: "inactive" }) evicted it before the assertion
ran. Added a QueryObserver subscription to simulate an active list
view, matching the real-app scenario where a component observes the
query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The QueryObserver was created with `enabled: false`, but TanStack Query's
isActive() check ignores observers whose enabled resolves to false. As a
result, removeQueries({ queryKey: ["entries"], type: "inactive" }) in
useUpdateEntryStatus.onSettled was evicting the seeded unread cache
before the assertion could read it, which broke the deploy preview CI.
Replace with an enabled observer that has a no-op queryFn returning the
seed and staleTime: Infinity, so the query is genuinely active and the
seeded/optimistic data is preserved through settlement.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eep-unread-on-click feat(web): move feeds between categories; keep articles in Unread after click
Bring in 94 upstream commits through 0e42752 (incremental Miniflux save). Highlights: M3 Expressive toolbar, sync outbox for article statuses, SQLite cursor window reduction, mercury-parser-kt for full content, mark-read-on-scroll fixes, and many translations. For the 6 conflicting files, kept the local version to preserve fork changes (unread cache, web feed move/keep-unread-on-click, .claude/worktrees gitignore, version line, tsconfig). Conflicts resolved with --ours: - .gitignore - app/build.gradle.kts - app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt - app/src/test/java/com/capyreader/app/ui/articles/ArticleScreenViewModelTest.kt - capy/src/main/java/com/jocmp/capy/accounts/local/LocalAccountDelegate.kt - tsconfig.json
Author
|
Opened against the wrong repo by mistake — this should land on the fork (Shengqiang-Zhang/capyreader), not upstream. Closing. Apologies for the noise. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merge 94 upstream commits from
jocmp/capyreader:main(through0e42752f) into the fork. Resolves divergence accumulated since last upstream sync.Upstream highlights
Conflict resolution
Six files conflicted; resolved as follows:
.gitignore,app/build.gradle.kts,app/.../ArticleScreen.kt,app/.../ArticleScreenViewModelTest.kt,tsconfig.json--ours) — has fork-specific changescapy/.../LocalAccountDelegate.kt0e17e7db) targeted a function upstream had since removed (#2035)Fixups applied to keep the build green
ArticleScreen.kt: addedonMarkAllReadtoFeedList,onRemoveFolder/currentFeed/sourcetoArticleListTopBar, removedrefreshingAllfromArticleList, collectedcurrentFeedfrom the view modelapp/build.gradle.kts: addedtestImplementation(libs.tests.work.testing)for the newRefreshSchedulerTestTest plan
./gradlew assembleFreeDebugpasses./gradlew :capy:test :rssparser:test :feedfinder:test :readerclient:test :feedbinclient:testall pass./gradlew :app:testFreeDebugUnitTest— could not run in sandbox (new upstream depandroidx.work:work-testing:2.11.2not cached anddl.google.comblocked); run locally before merging2026.05.1209🤖 Generated with Claude Code