fix(css): multiply mix blend for images in dark override color mode (#4763)

This commit is contained in:
Huang Xin
2026-06-24 22:12:10 +08:00
committed by GitHub
parent 005aa2d615
commit f7124cbeea
9 changed files with 168 additions and 3 deletions
+7 -1
View File
@@ -65,12 +65,15 @@
- [Android hyphen selection (#1553)](android-hyphen-selection-bounds-1553.md) — Blink paints start handle on last hyphen; repair anchor + custom handles
- [NativeFile vs RemoteFile I/O](android-nativefile-remotefile-io.md) — NativeFile slow; RemoteFile can't replace (asset Range broken); handle-reuse 2.3×
- [Window-state sanitizer (#4398)](window-state-sanitize-4398.md) — invalid `.window-state.json` crashes WebView2; sanitizer plugin before window-state
- [Android Open-with intent (#4521)](android-open-with-intent-flow.md) — VIEW=transient→reader, SEND=library; Telegram fails cold-start + foreign-file read
- [Android Open-with intent (#4521)](android-open-with-intent-flow.md) — VIEW/SEND pipeline; VIEW routing now gated by `autoImportBooksOnOpen` (#4747, mobile default ON → import); Telegram cold-start + foreign-file read
- [Dict lookup browser hijack (#4559)](dict-lookup-browser-hijack-4559.md) — missing `<queries>` ACTION_PROCESS_TEXT sdk36; filter browsers in `decideLookupDispatch`
- [Large-PDF OOM range flood (#3470)](pdf-oom-range-flood-3470.md) — makePDF fires all ranges un-awaited → OOM; MAX_CONCURRENT_RANGES=6
- [Android themed icon (#4733)](android-themed-icon-4733.md) — `tauri icon` emits no monochrome → force-commit; tint=SRC_IN, negative-space gap
## Reader Features & UI
- [Search modes #4560 + spoiler-bound bug](search-modes-4560-and-spoiler-bound-bug.md) — regex + nearby-words in foliate search.js (phased); deferred cache/searchBook; lookupPassage spoiler bound (page vs chunk ordinal) already wrong
- [OPDS groups carousel (#4750)](opds-groups-carousel-4750.md) — >=2 groups → horizontal react-virtuoso carousel; lazy covers; `handle.scrollBy` vertical-only so page by `scrollToIndex`; card covers rounded + badge dropped (kept in detail)
- [Image zoom trackpad flicker (#4742)](image-zoom-trackpad-flicker-4742.md) — macOS trackpad pinch = `ctrl+wheel` stream; `0.05s` transform transition restarts each event → flicker; `isWheelZooming` debounce gates transition off
- [Instant Highlight ate tap/swipe (Android)](instant-highlight-tap-paginate.md) — `handlePointerDown` preventDefault killed tap-paginate; touch still-hold gate `INSTANT_HOLD_MS=300`
- [Keyboard selection adjust (#4728)](keyboard-selection-adjust-4728.md) — Shift+←/→ char, Ctrl/Alt+Shift word; `onAdjustTextSelection` in useBookShortcuts
- [Annotator onLoad listener leak (#4735)](annotator-onload-listener-leak-paragraph-mode.md) — per-section onLoad leaked listeners; `useRendererInputListeners` once-per-view
@@ -90,6 +93,7 @@
- [Biometric app-lock (#4645)](biometric-app-lock-4645.md) — read flag from `appLockStore` not settingsStore; plugin `cfg(mobile)`
- [Reference Pages (#4542)](reference-pages-672-4542.md) — 'reference' progressStyle from foliate pageList; per-book `referencePageCount`
- [Share intent + toolbar (#4014)](annotation-share-toolbar-4014.md) — Share tool gated mobile+macOS; drag-drop customizer; `annotationToolbarItems`
- [Customize Toolbar global (serializeConfig) #4760](customize-toolbar-global-serializeconfig.md) — was per-book; root cause `serializeConfig` `!==` ref-compares array viewSettings → always stored as stale per-book override shadowing global; fix = value compare (`isSameViewSettingValue`) in viewSettings reduce only; field stays `annotationToolbarItems` in AnnotatorConfig (no exception/rename/move); limitation: pre-existing v0.11.12 overrides not retro-cleared
- [Native iOS TTS (#4676)](native-ios-tts-4676.md) — AVSpeechSynthesizer plugin; pause==stop, never `end` on didCancel; rate `pow^(1/2.5)`
- [Native TTS offline halt (#4613)](native-tts-offline-autoadvance-4613.md) — `#speak` advances only on `end`; native SKIP-on-error via `forward()` + cap
- [Edge TTS word highlight (#4017)](edge-tts-word-highlighting-4017.md) — `audio.metadata` WordBoundary synced by rAF; gates on UA not Origin
@@ -109,6 +113,7 @@
- [OPDS Firefox strict-XML (#4479)](opds-firefox-strict-xml-4479.md) — junk after `</feed>` → parsererror; `parseOPDSXML` slices to last close tag
- [OPDS2 JSON search greyed (#4502)](opds2-json-search-4502.md) — `isSearchLink` ignored templated opds+json; expand `{?query}` BEFORE resolveURL
- [OPDS HTML description (#4503)](opds-html-description-4503.md) — double-escaped into unsanitized HTML; decode-once + sanitize
- [OPDS self-link metadata (#4749)](opds-self-link-metadata-4749.md) — summary pubs need `rel:self` deref (`opds-publication+json`); JSON `description` is HTML → `getOPDSDescriptionHtml(content ?? description)`
- [D-pad Navigation](dpad-navigation.md) — Android TV remote / arrow-key nav design + pitfalls
- [koplugin cover upload (#4374)](koplugin-cover-upload.md) — local covers uploaded blank; `extractLocalCover` via `getCoverImage`
@@ -119,6 +124,7 @@
- [TXT chapter measure-word FP (#4658)](txt-chapter-measure-word-4658.md) — strong `[章节回讲篇话]` vs weak `[卷本册部封]` needs separator
- [Cover stale (in-place mutation)](cover-stale-inplace-mutation-memo.md) — mutated book in place → memo skip; pure `getBookWithUpdatedMetadata`
- [Series/author back no-op (#4437)](series-folder-back-noop-4437.md) — Next 16.2 empty-search `router.replace` no-op; `handleBack` `group=''` workaround
- [Library/reader separate texture (#4743)](library-reader-separate-texture-4743.md) — shared `#background-texture` style; `libraryBackground*` device-local + inherit; `none` must unmount; store inits `{}`
## Architecture & Patterns
- foliate-js is a git submodule at `packages/foliate-js/`; multiview paginator preloads adjacent sections (multiple View/Overlayer per book)
@@ -11,7 +11,7 @@ Android "Open with Readest" / "Send to Readest" file-intent pipeline and the #45
**Pipeline (ACTION_VIEW = "Open with", ACTION_SEND = "Share"):**
- `NativeBridgePlugin.kt::handleIntent` is the real handler (NOT `MainActivity.kt` — its ACTION_SEND branch is legacy/redundant). → `emitSharedIntent("VIEW"|"SEND", uris)` → JS `useAppUrlIngress` `shared-intent` plugin listener → `app-incoming-url` event → `useOpenWithBooks`.
- VIEW `openTransient` → straight to reader (ephemeral book, `deletedAt` set, `filePath` = the content:// URI, no library write/upload). SEND`window.OPEN_WITH_FILES``library/page.tsx::processOpenWithFiles` (full ingest + force cloud upload on mobile).
- VIEW routing is now gated by `autoImportBooksOnOpen` (PR #4747, issue #4746): `shouldOpenTransient(action, autoImportBooksOnOpen)` in `helpers/openWith.ts` → only `VIEW` with the setting OFF goes `openTransient` (ephemeral book, `deletedAt` set, `filePath` = content:// URI, no library write/upload); `VIEW` with it ON falls through to the SEND path. SEND (and VIEW-with-import-on)`window.OPEN_WITH_FILES``library/page.tsx::processOpenWithFiles` (full ingest + force cloud upload on mobile). The setting defaults TRUE on mobile (`DEFAULT_MOBILE_SYSTEM_SETTINGS`, desktop default still false) and its "Auto Import on File Open" toggle is now shown on mobile too. `useOpenWithBooks.handle` reads it via `appService.loadSettings()` (disk), NOT the settings store — the store is unhydrated during the cold-start intent replay and would wrongly fall back to transient. So the Telegram default is now import-to-library (persists past the dying URI grant); transient is opt-out.
- content:// read: `nativeAppService.openFile` → if URI contains `com.android.externalstorage` → direct `NativeFile` (real path); else `copyURIToPath``contentResolver.openInputStream` → copy to Cache → `NativeFile`. `basename` here is LEXICAL (`@tauri-apps/api/path`), not a ContentResolver `DISPLAY_NAME` query — but EPUB format is sniffed by zip magic (`document.ts isZip()`), so an extension-less content URI still opens.
- The Tauri deep-link plugin's `getCurrent()`/`onOpenUrl` only fire for configured deep-link domains (`https://web.readest.com`, `readest:`); `content://`/`file://` VIEW intents are filtered out by `DeepLinkPlugin.isDeepLink()`, so file opens flow ONLY through the native `shared-intent` channel, never the deep-link plugin.
@@ -0,0 +1,48 @@
---
name: customize-toolbar-global-serializeconfig
description: Customize Toolbar applied per-book not global; root cause = serializeConfig compared viewSettings by reference (!==) so array values were always stored as stale per-book overrides
metadata:
node_type: memory
type: project
originSessionId: c6601464-9463-4ac3-99c0-e7527e4051b5
---
Customize Toolbar (annotation bar, #4014, shipped v0.11.12) changes only applied
to the book where edited, not globally. Fixed in PR #4760 (MERGED, squashed onto
main as 7da5f8321).
**Root cause:** `serializeConfig` (`src/utils/serializer.ts`) decides which per-book
viewSettings to persist as overrides via `globalViewSettings[key] !== value` — a
*reference* compare. It deep-clones the config first (`JSON.parse(JSON.stringify)`),
so any **array/object** viewSettings value (`annotationToolbarItems`, and latently
`paragraphMode`, `proofreadRules`, `ttsHighlightOptions`, `noteExportConfig`) is a
fresh reference ≠ global → stored as a per-book override on **every** save (progress
autosave serializes with settings each relocate). On reopen the merge
`{ ...globalViewSettings, ...perBookOverrides }` lets the stale override shadow
global → a global toolbar change never reaches already-saved books.
**Fix (final — minimal, general, no special-casing):** compare viewSettings values
by content, not reference. Added `isSameViewSettingValue(a,b) = a===b ||
JSON.stringify(a)===JSON.stringify(b)`, used in the viewSettings reduce ONLY
(searchConfig left on `!==` — it holds functions / large `results`). The field
stays `annotationToolbarItems` in `AnnotatorConfig` (normal per-book viewSettings,
honors the isGlobal "Apply to This Book" toggle). PR diff is just serializer.ts +
serializer.test.ts.
**Iteration history (user steered):** (1) a `GLOBAL_ONLY_VIEW_SETTINGS` exception
forcing global save + strip/ignore per-book — rejected "don't make it an exception";
(2) move field to `SystemSettings.globalReadSettings` — rejected "too much";
(3) rename `annotationToolbarItems``annotationToolbar` for a clean slate — rejected,
keep the original name (it's synced in globalViewSettings). Landing point: keep the
name, fix only the serializer reference-compare bug.
**Known limitation (no rename clean-slate):** existing books may carry a per-book
`annotationToolbarItems` override from the buggy v0.11.12 build. The value compare
stops new ones and drops an existing one on next save when it matches global, but
does NOT retroactively clear an override whose content differs from current global —
those books keep the stale toolbar until re-saved while equal to global. A follow-up
one-time migration (clear persisted per-book toolbar overrides) would close this if
needed.
Tests: `src/__tests__/utils/serializer.test.ts` — array setting equal to global is
not persisted; differing array still persisted.
@@ -0,0 +1,16 @@
---
name: image-zoom-trackpad-flicker-4742
description: "Trackpad pinch-zoom flickered the image viewer; macOS pinch = ctrl+wheel stream, disable CSS transition during continuous gestures"
metadata:
node_type: memory
type: project
originSessionId: affbfa14-0152-4d69-8fce-f7e0b9ee97a3
---
ImageViewer (`src/app/reader/components/ImageViewer.tsx`) flickered when zooming an open image with a MacBook trackpad pinch (#4742, PR #4748).
**Root cause:** on macOS a trackpad pinch-to-zoom is delivered to the WebView as a rapid stream of `wheel` events with `ctrlKey: true` (NOT touch events), so it flows through `handleWheel`. The zoomed `<img>` kept its `transition: transform 0.05s ease-out` whenever `isDragging` was false. Pinch wheel events fire faster than 50ms apart, so each event restarted the in-flight transition from its interpolated mid-point — the transform constantly lagged and caught up = visible flicker. Same root cause as the #4451 pan flicker, which only fixed the pan path and (via `isDragging` set in `onTouchStart`) the touch-pinch path; the wheel-zoom path was the only continuous gesture left with the transition on. That's why touch pinch on iPhone was smooth but trackpad pinch flickered.
**Fix:** added an `isWheelZooming` state set on each `handleWheel` event and cleared on a 200ms debounce (wheel has no explicit gesture-end). Transition is `isDragging || isWheelZooming ? 'none' : 'transform 0.05s ease-out'`. Discrete zoom (buttons, double-click, keyboard) keeps the smoothing.
**General pattern:** never run a CSS `transition` on a transform that's being updated by a high-frequency continuous input stream (drag, touch pinch, trackpad/`ctrl+wheel` pinch) — the interrupted-transition restart flickers. Gate the transition off for the duration of the gesture. Maintainer couldn't repro on macOS 15.6.1 (WebKit) while reporter hit it on macOS 26.5.1 / WebKit 605.1.15; the fix is version-independent. Related: [[instant-highlight-tap-paginate]].
@@ -0,0 +1,18 @@
---
name: library-reader-separate-texture-4743
description: "Separate library vs reader background texture (#4743); shared-style-element + two gotchas"
metadata:
node_type: memory
type: project
originSessionId: dfdb7b38-1869-4fb4-b869-c32301c80128
---
#4743: library and reader shared one background texture; split so each is set independently.
**Architecture**: ONE global `<style id="background-texture">` paints `body::before` (covers library) plus reader containers (`.foliate-viewer/.sidebar-container/.notebook-container ::before`). Library and reader are separate routes (only one mounted), so the split = store two values + have each page apply its own on activation, not separate style elements. New device-local `SystemSettings.libraryBackground{TextureId,Opacity,Size}` (NOT in settings sync whitelist — texture *selection* is per-device like reader's `backgroundTextureId`; only image binaries sync via `texture` replica kind). `getLibraryViewSettings(settings)` in `helpers/settings.ts` resolves each field with `?? globalViewSettings.<field>` so the bookshelf inherits the reader texture until decoupled (no migration). `ColorPanel` is context-aware via `isLibraryContext = !bookKey`: library context writes `libraryBackground*` via `saveSysSettings`, reader context unchanged via `saveViewSettings`. Applied at boot (`Providers`) + on every library mount (`library/page.tsx` effect).
**Gotcha 1 — `useBackgroundTexture` early-returned on `'none'` WITHOUT unmounting.** Since library+reader share the one style element, switching a page to None must actively clear a texture the OTHER page mounted. Fixed: always delegate to `applyTexture(envConfig, textureId || 'none')` (it unmounts on 'none'); only set CSS vars / addTexture for a real texture. Also fixes the symmetric reader case (opening a 'none' book after a textured one).
**Gotcha 2 — `useSettingsStore` initializes `settings: {} as SystemSettings`.** So `settings.globalViewSettings` is `undefined` on the first renders before `appService.loadSettings()` runs. Any NEW effect/deps that deep-derefs `settings.globalViewSettings.<x>` crashes the library with "Cannot read properties of undefined (reading 'backgroundTextureId')". Caught only in a hard reload (HMR kept old store state, so first nav didn't repro). Fix = optional-chain in effect deps + make the resolver tolerate missing globalViewSettings (fallback to 'none'). Relates to [[cover-stale-inplace-mutation-memo]].
Verified end-to-end in dev-web: library moon texture, reader stays none, round-trip persists, None clears live. Related: [[wordlens-feature]] i18n (recent feature commits ship `_()` strings WITHOUT running `i18n:extract`; translations are batched separately — don't commit locale churn in a feature PR).
@@ -0,0 +1,23 @@
---
name: opds-groups-carousel-4750
description: OPDS feed groups (>=2) render as horizontal virtualized carousels with lazy cover loading
metadata:
node_type: memory
type: project
originSessionId: 3073b2b0-8219-42cc-8e3f-547715b86b01
---
#4750 (PR #4755, merged): when an OPDS `feed.groups.length >= 2`, `FeedView` renders each group's publications/navigation as a horizontal carousel (`src/app/opds/components/GroupCarousel.tsx`) instead of the grid; single-group feeds keep the grid. Matches Thorium.
`GroupCarousel` wraps a horizontal `react-virtuoso` `Virtuoso` (`horizontalDirection`), so only in-view items mount → covers load lazily as you scroll (verified via network: ~12 covers/group fetched regardless of group size; far-right items fetch only after scrolling to them).
Gotchas (cost real debugging):
- `VirtuosoHandle.scrollBy({left})` is a **no-op** in horizontal mode (the handle maps to the vertical axis). Page the arrows by **index** via `scrollToIndex({index, align, behavior})`, tracking the visible range from `rangeChanged`.
- Virtuoso sizes the horizontal track **lazily**, so a pixel `scrollBy` on the scroller element clamps to the currently-rendered width — another reason to scroll by index.
- Arrow visibility comes from `atTopStateChange`/`atBottomStateChange` (top=left, bottom=right). Row height is measured from the first `[data-carousel-item]`; arrows are vertically centered on the cover by measuring the first `<figure>` (cards have title/author below, so centering on the whole row looks low).
- Scrollbar hidden via a scoped `.no-scrollbar` util in `globals.css`; arrows use `eink-bordered`.
- Tests must mock `react-virtuoso` (jsdom has no layout) like the TOCView/BooknoteView tests — render all items via `itemContent`.
`PublicationCard` (shared by carousel + grids) got rounded covers (`overflow-hidden rounded`, matching the library bookshelf) and dropped the inline acquisition/price badge — that badge still renders on the detail page (`PublicationView`).
Related: [[virtuoso_overlayscrollbars]].
@@ -0,0 +1,19 @@
---
name: opds-self-link-metadata-4749
description: OPDS 2.0 summary publications need self-link dereference for full metadata; JSON description is HTML
metadata:
node_type: memory
type: project
originSessionId: 0e1e6ec0-38c1-45a2-aab6-52b78a5ad38a
---
Readest issue #4749 (pglaf/Gutenberg test feed `https://opds-test.pglaf.org/opds/`). Two related OPDS bugs, both fixed together.
**1. Summary publications need a `self`-link dereference.** OPDS 2.0 feeds may list a publication with only minimal metadata + a `rel:"self"` link of type `application/opds-publication+json` (no acquisition links, no description) — the server sends the full record only when the client follows that link on click. Thorium does this; Readest did not.
- New `src/app/opds/utils/opdsPublication.ts`: `getPublicationDetailHref(pub)` finds the `rel:"self"` link whose type is `application/opds-publication+json` or Atom `application/atom+xml;type=entry`; `parsePublicationDocument(text, docURL)` parses JSON or Atom-entry XML (reuses foliate `getPublication`) and **absolutizes** links/images hrefs against `docURL` so downloads/cover resolve regardless of the feed's `baseURL`.
- `page.tsx`: renamed derived `publication``basePublication`; an effect fetches the detail doc (via `fetchWithAuth` + proxy refs) when `selectedPublication` is set AND a detail link exists (skip directly-loaded entry docs — already full); merges as `{ metadata: resolved.metadata, links/images: resolved.* || base.* }` keyed by `source===basePublication` so a stale fetch can't bleed into the next selection. Summary renders immediately, upgrades in place.
**2. JSON `description` is HTML.** OPDS 2.0 keeps the summary in plain `metadata.description` (no typed `<content>`), and pglaf fills it with `<p>...</p>`. `PublicationView` rendered `<p>{description}</p>` → literal tags. Fix: `getOPDSDescriptionHtml(content ?? description)` so the (sanitized) markup renders. See [[bug-patterns]] and prior [[OPDS HTML description (#4503)]] decode-once+sanitize.
**Why:** less data per feed page + faster load; client dereferences on demand.
**How to apply:** when an OPDS publication looks under-populated, check for a `rel:"self"` publication-type link before assuming the feed is the whole record. Related OPDS notes: opds-firefox-strict-xml-4479, opds2-json-search-4502, opds-html-description-4503.
@@ -0,0 +1,34 @@
---
name: search-modes-4560-and-spoiler-bound-bug
metadata:
node_type: memory
type: project
originSessionId: c416114a-72e6-40ed-a3ed-4b2d5fd7d5f4
---
**#4560 (Calibre-parity search)** was scoped down via `/autoplan` review (both Codex + Claude
agreed the original "foundational Turso-cached engine + searchBook agent tool" was over-scoped).
Decision = **phase it**.
**PR-1 (done, branch `feat/search-modes-regex-nearby-4560`, worktree `/Users/chrox/dev/readest-feat-search-modes-regex-nearby-4560`):**
adds `regex` + `nearby-words` modes INSIDE the foliate submodule `packages/foliate-js/search.js`
(`regexSearch`, `nearbyWordsSearch`, `mode` dispatch in `search()`/`searchMatcher`); per-word `cfis`
+ annotation dedupe in `view.js`; `BookSearchConfig.mode`/`nearbyWords` + `BookSearchMatch.cfis` +
`SearchExcerpt.segments` in `types/book.ts` (schema v2→v3 in `serializer.ts`, `utils/searchConfig.ts`
helper); sidebar mode selector + greyed modifiers + "within N words" stepper + `searchError` state +
segmented excerpt. Nearby distance = **words** (default 10), via a control — NOT chars, NOT a trailing
number in the query. **foliate-js is a submodule** — search.js/view.js changes must be committed in
the submodule first, then the parent pointer updated.
**Deferred:** PR-2 = perf cache (only if measured; neutral `search.db`, NEVER `reedy.db` — that DB is
opt-in/desktop-gated and its delete-cleanup wouldn't run for non-AI users; FTS ngram is NOT a
guaranteed superset so it must fall back to full scan; run regex in a Web Worker for real backtracking
isolation). PR-3 = `searchBook` agent tool.
**Pre-existing bug to fix in PR-3:** `lookupPassage` spoiler protection is already wrong — it passes
`currentPage` (a rendered page ordinal, `AIAssistant.tsx`) as `spoilerBoundPosition`, but `ReedyDb`
compares it to `c.position_index`, a **global chunk ordinal** (`positionIndex: all.length`,
`BookIndexer.ts`). Page count ≠ chunk count, so the bound is off. Fix searchBook (and lookupPassage)
to spoiler-bound by the current **CFI → (sectionIndex, charOffset)**, not a position integer.
Related: [[koplugin-stats-sync]] is unrelated; see plan at
`~/.claude/plans/the-search-might-be-glistening-mccarthy.md`.
+2 -1
View File
@@ -265,7 +265,8 @@ const getColorStyles = (
}
img {
${isDarkMode && invertImgColorInDark ? 'filter: invert(100%);' : ''}
${!isDarkMode && overrideColor ? 'mix-blend-mode: multiply;' : ''}
${isDarkMode && overrideColor ? 'filter: grayscale(100%) contrast(1.2) brightness(1.2);' : ''}
${overrideColor ? 'mix-blend-mode: multiply;' : ''}
}
svg, img {
${overrideColor ? `background-color: transparent !important;` : ''};