forked from akai/readest
chore(agent): update agent memories (#4833)
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
# Readest Project Memory
|
||||
|
||||
## Key Reference Documents
|
||||
- [Bug Fixing Patterns](bug-patterns.md) — categories, root causes, fixes
|
||||
- [CSS & Style Fixes](css-style-fixes.md) — EPUB CSS + style.ts
|
||||
- [TTS Fixes](tts-fixes.md) — TTS architecture + bugs
|
||||
- [Layout & UI Fixes](layout-ui-fixes.md) — safe insets, z-index, platform UI
|
||||
- [Platform Compat Fixes](platform-compat-fixes.md) — Android/iOS/Linux/macOS
|
||||
- [Annotator & Reader Fixes](annotator-reader-fixes.md) — highlight, selection, a11y
|
||||
## Key Reference Documents (aggregators)
|
||||
- [Bug Patterns](bug-patterns.md) · [CSS & Style](css-style-fixes.md) — EPUB CSS + style.ts · [TTS](tts-fixes.md) — architecture + bugs
|
||||
- [Layout & UI](layout-ui-fixes.md) — insets/z-index · [Platform Compat](platform-compat-fixes.md) — Android/iOS/Linux/macOS · [Annotator & Reader](annotator-reader-fixes.md) — highlight/selection/a11y
|
||||
## Safety & Security
|
||||
- [In-place delete wiped originals](in-place-delete-wiped-originals.md) — never `fs.removeFile` `external` source; managed copy + sidecars only
|
||||
- [Backup zip Windows paths (#4703)](backup-windows-zip-paths-4703.md) — `\` entry names broke restore; normalize separators
|
||||
@@ -26,29 +22,25 @@
|
||||
- [PDF scrolled wheel double (#4727)](pdf-scroll-mode-wheel-double-4727.md) — drop manual `scrollBy` over native chaining
|
||||
- [Scrolled header title center (#4436)](scrolled-header-title-center-4436.md) — view covering `renderedStart + size/2`
|
||||
- [Duokan fullscreen cover scroll](duokan-fullscreen-cover-scroll.md) — #4379 cover collapses; gate on `this.#column`
|
||||
- [TOC table heading clip (#4439)](toc-table-heading-clip-4439.md) — #4400 scroll-wrapper `overflow:auto` clips decorative table's `margin:-1em` bleed; `hoistNegativeMargins` onto wrapper
|
||||
- [Page-turn bg-replace reflow (#4785)](pageturn-bg-replace-reflow-4785.md) — `#replaceBackground` rebuilt context per frame (getComputedStyle + per-view getBCR), worst at boundaries; snapshot `#bgAnimContext` once per gesture (drag+snap); defer mid-drag preload via `!#touchScrolled`
|
||||
## Critical Files (Most Bug-Prone)
|
||||
- `src/utils/style.ts` — central EPUB CSS transformation hub
|
||||
- `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` layout/image/bg · `src/services/tts/TTSController.ts` TTS state machine
|
||||
- `src/hooks/useSafeAreaInsets.ts` insets · `src/app/reader/components/FoliateViewer.tsx` view orchestration · `.../annotator/Annotator.tsx` annotation lifecycle
|
||||
## Sync Notes
|
||||
- [Grimmory native sync](grimmory-native-sync.md) — Booklore-fork REVERTED; id by ISBN/ASIN + koreader-hash
|
||||
- [KOSync CFI spine resolution](kosync-cfi-spine-resolution.md) — convert via CFI's own spine, not `new XCFI(primaryDoc,...)`
|
||||
- KOSync: [CFI spine resolution](kosync-cfi-spine-resolution.md) convert via CFI's own spine; [connect() false-positive #4692](kosync-connect-false-positive-4692.md) validate koreader JSON not any 2xx
|
||||
- [Empty-start CFI sync](empty-start-cfi-sync.md) — `isMalformedLocationCfi` → discard synced value
|
||||
- [Custom fonts vanish on sync (#4410)](custom-fonts-reincarnation-4410.md) — CRDT remove-wins; re-import `reincarnation` token
|
||||
- [koplugin note deletion sync](koplugin-note-deletion-sync.md) — `recordDeletion` tombstone; `index_modified < 0`
|
||||
- [koplugin stats sync (#4666)](koplugin-stats-sync.md) — statistics.sqlite3 delta; LuaSettings/required/optional_params
|
||||
- koplugin: [note deletion](koplugin-note-deletion-sync.md) `recordDeletion` tombstone; [stats #4666](koplugin-stats-sync.md) statistics.sqlite3 delta LuaSettings; [bulk download #4751](koplugin-bulk-download-4751.md) `downloadAll()`/`coroutine.status`
|
||||
- [Statusless books re-pin top (#4677)](sync-statusless-book-rebump-4677.md) — `(a??null)!==(b??null)`
|
||||
- [Pull cursor via synced_at (#4678)](sync-synced-at-cursor-4678.md) — books `synced_at` + BEFORE trigger
|
||||
- [KOSync connect() false-positive (#4692)](kosync-connect-false-positive-4692.md) — validate koreader JSON, not any 2xx
|
||||
- [koplugin bulk download (#4751)](koplugin-bulk-download-4751.md) — `downloadAll()`; `listCloudOnlyBooks()`; `coroutine.status`
|
||||
- [WebDAV metadata sync (#4756)](webdav-metadata-sync-4756.md) — PR #4776; LWW on `book.updatedAt` + `updateBookMetadata`
|
||||
- [File-sync refactor (#4784)](webdav-filesync-refactor-plan.md) — `FileSyncEngine`/`FileSyncProvider`/`merge.ts`; incremental + Full Sync
|
||||
- [WebDAV connect nullified (#4780)](webdav-connect-nullified-4780.md) — catch+finally saved STALE closure; `getState().settings`
|
||||
- [Google Drive provider research](gdrive-sync-provider-research.md) — Drive as `FileSyncProvider`; token persist + resumable upload
|
||||
- [Google Drive provider multi-PR status](gdrive-provider-multipr-status.md) — PR1+PR2+PR3-so-far DONE on `feat/gdrive-sync-core` (5 commits thru `5efbe6b2f`, UNPUSHED, suite 6411 green); native KV + desktop OAuth runner + connect + ingress filter shipped; REMAINING (UI generalization + tauri.conf scheme + live verify) blocked on official Google client id (ops)
|
||||
- [WebDAV credential sync (#4810)](webdav-credential-sync-4810.md) — `webdav.*` missing from `SETTINGS_WHITELIST`/`ENCRYPTED_FIELDS`; new nested whitelist group NEEDS matching `mergeSettings` deep-merge or shallow merge clobbers per-device siblings
|
||||
- [Multi-window settings clobber (#4580)](multiwindow-settings-clobber-4580.md) — stale window overwrites shared settings.json; cross-window broadcast `settingsSync.ts`
|
||||
- Google Drive: [research](gdrive-sync-provider-research.md) Drive as `FileSyncProvider` token-persist+resumable; [multi-PR status](gdrive-provider-multipr-status.md) PR1+2+3 DONE on `feat/gdrive-sync-core` (5 commits thru `5efbe6b2f`, UNPUSHED, suite 6411 green); native KV+desktop OAuth+connect+ingress shipped; REMAINING (UI generalization, tauri.conf scheme, live verify) blocked on official Google client id (ops)
|
||||
- [Hardcover progress edition_id (#4792)](hardcover-progress-edition-id-4792.md) — `edition_id` fell back to `book_id`; resolve real edition; old bug via #4614
|
||||
## Build, Testing & CI
|
||||
- [format:check separate gate](verify-format-check-gate.md) — `pnpm format:check` own gate before push
|
||||
@@ -72,6 +64,8 @@
|
||||
- [Large-PDF OOM range flood (#3470)](pdf-oom-range-flood-3470.md) — un-awaited ranges OOM; MAX_CONCURRENT_RANGES=6
|
||||
- [Android themed icon (#4733)](android-themed-icon-4733.md) — no monochrome → force-commit; tint=SRC_IN
|
||||
## Reader Features & UI
|
||||
- [PDF scrolled-mode lag (#4795/#4031)](pdf-scroll-lag-preload-4795.md) — 415ms/page render vs 50% margin; widen to 200% + `planScrollModePages` nearest-first bounded scheduler; CDP-verified Xiaomi
|
||||
- [Scrolled-PDF pinch-zoom (#4817)](scrolled-pdf-pinch-zoom-4817.md) — live pinch + no-shift (gap×`--scroll-zoom` + rect-match anchor); cross-page pinch vs native selection MUTUALLY EXCLUSIVE (Android serializes cross-iframe touches), chose native selection (iframes interactive when idle); zoom store/attr desync gotcha
|
||||
- [Search modes #4560 + spoiler-bound bug](search-modes-4560-and-spoiler-bound-bug.md) — regex + nearby-words; lookupPassage spoiler bound (page vs chunk)
|
||||
- [OPDS groups carousel (#4750)](opds-groups-carousel-4750.md) — >=2 groups → virtuoso carousel; `scrollToIndex`
|
||||
- [WebDAV browser sort + search (#4724)](webdav-browse-sort-search-4724.md) — PR #4786; `sortWebDAVEntries`/`filterWebDAVEntries`
|
||||
@@ -87,54 +81,47 @@
|
||||
- [Tap to open image/table (#4600)](tap-to-open-image-table-4600.md) — `iframe-open-media` + `detectMediaTarget`
|
||||
- [PDF/CBZ Contrast view-menu](pdf-cbz-contrast-view-menu.md) — per-book `contrast`; ONE `filter:` (invert+contrast); `skipGlobal=true`
|
||||
- [iOS instant-dict double popup](ios-instant-dict-double-popup.md) — once-per-gesture latch; `isLongPressHold` 300ms
|
||||
- [Dict popup font size (#4443)](dict-popup-font-size-4443.md) — `--dict-font-scale`; MDict `::part(dict-content)`
|
||||
- [Dictionary lemmatization (#4574)](dict-lemmatization-4574.md) — inflected→lemma; `-ses→-sis` before `-es`
|
||||
- [Word Lens inline gloss](wordlens-feature.md) — CFI-safe `<ruby cfi-skip>…<rt cfi-inert>`; TTS/search isolation
|
||||
- [Word Lens en-en](wordlens-en-en.md) — simplest WordNet synonym; same-lang manifest-driven
|
||||
- Dict: [popup font size #4443](dict-popup-font-size-4443.md) `--dict-font-scale` MDict `::part(dict-content)`; [lemmatization #4574](dict-lemmatization-4574.md) inflected→lemma `-ses→-sis` before `-es`
|
||||
- Word Lens: [inline gloss](wordlens-feature.md) CFI-safe `<ruby cfi-skip>…<rt cfi-inert>` TTS/search isolation; [en-en](wordlens-en-en.md) WordNet synonym manifest-driven
|
||||
- [Stripe highest-active plan (#4694)](stripe-plan-highest-active-4694.md) — `plans.plan` = MAX over active subs
|
||||
- [Save image to gallery (#4680)](save-image-to-gallery-android.md) — MediaStore; sharekit 0-byte self-copy (Temp==cacheDir)
|
||||
- [Webtoon Mode (#3647)](webtoon-mode-3647.md) — no-gap scrolled images; FXL fit-width; `--scroll-page-gap`
|
||||
- [Biometric app-lock (#4645)](biometric-app-lock-4645.md) — flag from `appLockStore`; plugin `cfg(mobile)`
|
||||
- [Reference Pages (#4542)](reference-pages-672-4542.md) — 'reference' progressStyle; `referencePageCount`
|
||||
- [E-ink screen refresh page-turner (#4687)](eink-screen-refresh-pageturner-4687.md) — bindable 'refresh' action; generic `EinkRefreshController.kt` reflection (BOOX/Tolino/Rockchip), NO Onyx SDK bundle; gate Android+`viewSettings.isEink`
|
||||
- [Share intent + toolbar (#4014)](annotation-share-toolbar-4014.md) — Share gated mobile+macOS; `annotationToolbarItems`
|
||||
- [Instant highlight delete orphan (#4773)](instant-highlight-delete-orphan-4773.md) — stale memoized index + in-place `deletedAt`; re-check at READ
|
||||
- [Empty highlight leak on annotate cancel (#4791)](empty-highlight-leak-on-annotate-cancel-4791.md) — Annotate eagerly creates placeholder; track `notebookNewHighlightId`, presentation-driven cleanup effect
|
||||
- [Customize Toolbar global (#4760)](customize-toolbar-global-serializeconfig.md) — `serializeConfig` ref-compare → stale override; value compare
|
||||
- [Native iOS TTS (#4676)](native-ios-tts-4676.md) — AVSpeechSynthesizer; pause==stop; rate `pow^(1/2.5)`
|
||||
- [Native TTS offline halt (#4613)](native-tts-offline-autoadvance-4613.md) — advance only on `end`; SKIP-on-error via `forward()`
|
||||
- [Edge TTS word highlight (#4017)](edge-tts-word-highlighting-4017.md) — `audio.metadata` WordBoundary by rAF; gate on UA
|
||||
- [Edge TTS word-highlight drift](tts-word-highlight-singletextnode-drift.md) — TEXT_NODE fast path ignored offsets; slice `[start,end]`
|
||||
- Native TTS: [iOS #4676](native-ios-tts-4676.md) AVSpeechSynthesizer pause==stop rate `pow^(1/2.5)`; [offline halt #4613](native-tts-offline-autoadvance-4613.md) advance only on `end`, SKIP-on-error via `forward()`
|
||||
- Edge TTS: [word highlight #4017](edge-tts-word-highlighting-4017.md) `audio.metadata` WordBoundary by rAF gate on UA; [drift](tts-word-highlight-singletextnode-drift.md) TEXT_NODE fast path ignored offsets, slice `[start,end]`
|
||||
- [TTS highlight granularity setting](tts-highlight-granularity-setting.md) — Word/Sentence; gate `prepareSpeakWords` on granularity (NOT supportsWordBoundaries); suppress = wordBoundaries && word
|
||||
- [TTS start-from-selection](tts-start-from-selection.md) — use last mark at/before sel; cloneRange+deselect
|
||||
- [Reuse TTS session on mode entry](tts-reuse-session-mode-entry.md) — `redispatchPosition()` + `tts-sync-request` replay
|
||||
- [RSVP control bar overlap = REVERT](rsvp-control-bar-overlap-revert.md) — #4585 fixed; stale #4589 reverted it
|
||||
- [RSVP font face/family (#4519)](rsvp-font-settings-4519.md) — `getBaseFontFamily`; overlay in top doc
|
||||
- [RSVP RTL word display (#4630)](rsvp-rtl-word-display-4630.md) — ORP breaks Arabic; `isRTLText` → whole `dir=rtl`
|
||||
- RSVP: [control-bar overlap REVERT](rsvp-control-bar-overlap-revert.md) #4585 fixed/#4589 stale-reverted; [font face/family #4519](rsvp-font-settings-4519.md) `getBaseFontFamily` overlay top doc; [RTL word #4630](rsvp-rtl-word-display-4630.md) ORP breaks Arabic→whole `dir=rtl`
|
||||
- [Overlay z-index scale](zindex-overlay-scale.md) — RSVP 100 / Settings 110 / ModalPortal 120 / toast 130 / app-lock 200
|
||||
- [Global annotation page-turn lag (#4575)](global-annotation-pageturn-perf-4575.md) — `global` re-fanned every turn; `WeakMap<Document>` memo
|
||||
- [Overlayer splitRange text nodes](overlayer-splitrange-textnodes.md) — `'p,h1-h4'` dropped `li`; walk text nodes + img/svg
|
||||
- [Android image callout freeze](android-image-callout-freeze.md) — `.no-context-menu` on ANCESTOR
|
||||
- [Table dark-mode tint (#4419)](table-dark-mode-tint-4419.md) — `blockquote, table *` tint gated on `overrideColor`
|
||||
- [Footnote aside border line (#4438)](footnote-aside-namespace-order-4438.md) — @font-face before @namespace; hoist @namespace
|
||||
- [Proofread enhancements (#4700)](proofread-enhancements-4700.md) — regex UI; Ctrl+P reuse; `wholeWord` no-op
|
||||
- [Proofread per-book CRDT sync (#4781)](proofread-per-book-crdt-sync.md) — `mergeProofreadRules` by id; `ensureRuleId` backfill
|
||||
- Proofread: [enhancements #4700](proofread-enhancements-4700.md) regex UI/Ctrl+P reuse/`wholeWord` no-op; [per-book CRDT #4781](proofread-per-book-crdt-sync.md) `mergeProofreadRules` by id, `ensureRuleId` backfill
|
||||
- [Russian hanging-preposition NBSP (#4769)](russian-hanging-prepositions-nbsp-4769.md) — generic `nbsp` + `NBSP_LANGUAGES` (`ru`); after `whitespace`/`simplecc`
|
||||
- [OPDS Firefox strict-XML (#4479)](opds-firefox-strict-xml-4479.md) — junk after `</feed>`; `parseOPDSXML` slices to last close tag
|
||||
- [OPDS2 JSON search greyed (#4502)](opds2-json-search-4502.md) — expand `{?query}` BEFORE resolveURL
|
||||
- [OPDS HTML description (#4503)](opds-html-description-4503.md) — double-escaped; decode-once + sanitize
|
||||
- [OPDS self-link metadata (#4749)](opds-self-link-metadata-4749.md) — `rel:self` deref; `getOPDSDescriptionHtml`
|
||||
- [OPDS popular catalog dedup (#4782)](opds-popular-catalog-dedup-4782.md) — `getUnaddedPopularCatalogs` normalized-URL
|
||||
- OPDS: [Firefox strict-XML #4479](opds-firefox-strict-xml-4479.md) junk after `</feed>` slice to last close; [JSON search #4502](opds2-json-search-4502.md) expand `{?query}` BEFORE resolveURL; [HTML desc #4503](opds-html-description-4503.md) decode-once+sanitize; [self-link meta #4749](opds-self-link-metadata-4749.md) `rel:self` deref; [popular dedup #4782](opds-popular-catalog-dedup-4782.md) normalized-URL
|
||||
- [D-pad Navigation](dpad-navigation.md) — Android TV remote / arrow-key nav
|
||||
- [koplugin cover upload (#4374)](koplugin-cover-upload.md) — `extractLocalCover` via `getCoverImage`
|
||||
## Library Fixes
|
||||
- [Book action platform surfaces](book-actions-platform-surfaces.md) — context menu Tauri-desktop-only; cross-platform in `BookDetailView`
|
||||
- [Tauri menu append race (#4389)](tauri-menu-append-race-4389.md) — single `await Menu.new({ items })`
|
||||
- [TXT author recognition (#4390)](txt-author-recognition-4390.md) — `parseLabeledAuthor` + `isPlausibleAuthorName`
|
||||
- [TXT chapter measure-word FP (#4658)](txt-chapter-measure-word-4658.md) — strong `[章节回讲篇话]` vs weak `[卷本册部封]`
|
||||
- TXT: [author recognition #4390](txt-author-recognition-4390.md) `parseLabeledAuthor`+`isPlausibleAuthorName`; [chapter measure-word FP #4658](txt-chapter-measure-word-4658.md) strong `[章节回讲篇话]` vs weak `[卷本册部封]`
|
||||
- [Cover stale (in-place mutation)](cover-stale-inplace-mutation-memo.md) — pure `getBookWithUpdatedMetadata`
|
||||
- [Series/author back no-op (#4437)](series-folder-back-noop-4437.md) — Next 16.2 empty-search no-op; `handleBack` `group=''`
|
||||
- [Library/reader separate texture (#4743)](library-reader-separate-texture-4743.md) — `libraryBackground*` device-local; `none` unmounts
|
||||
- [List view series overflow (#4796)](list-view-series-overflow-4796.md) — fixed `h-28` clipped series+description (Android font scale); `min-h-28`
|
||||
- [Recently-read shelf (#3797)](recent-read-shelf-3797.md) — PR #4829; reuse `BookItem` + shared `useOpenBook`; flex strip needs grid's gap-aware `flexBasis` calc + `min-w-0` to align with bookshelf columns
|
||||
## Architecture & Patterns
|
||||
- foliate-js submodule at `packages/foliate-js/`; multiview paginator preloads adjacent sections
|
||||
- [Markdown .md support (#774)](markdown-md-support-774.md) — in-memory foliate book (no EPUB) in `src/utils/md.ts`; split-at-H1; `isMd()` before `isTxt()`; nav contract: `section.id`↔`splitTOCHref` SAME type (string), `loadText` needed for fragment CFIs, `XMLSerializer` for void-tag XHTML
|
||||
- Style: `getLayoutStyles()` always, `getColorStyles()` when overriding color; `transformStylesheet()` rewrites EPUB CSS at load
|
||||
- TTS independent section tracking (`#ttsSectionIndex`); safe insets: native plugin → useSafeAreaInsets → styles; Dropdowns `DropdownContext`
|
||||
- Stale settings closure: store-hook `settings` stale across `await`; persist `useSettingsStore.getState().settings` ([#4780](webdav-connect-nullified-4780.md))
|
||||
@@ -152,5 +139,6 @@
|
||||
- [No test seams in prod](feedback_no_test_seams_in_prod.md) — prod never imports `__reset*ForTests`
|
||||
- [No lookbehind regex](feedback_no_lookbehind_regex.md) — never `(?<=)`/`(?<!)`; build check rejects
|
||||
- [en plurals manual](feedback_en_plurals_manual.md) — only plural variants + proper nouns; `_one`/`_other`
|
||||
- [i18n:extract prunes keys](i18n-extract-prunes-keys.md) — `removeUnusedKeys` deletes valid keys; revert churn, add new keys manually
|
||||
- [Dependabot transitive fixes](dependabot-pnpm-overrides.md) — pin in `pnpm-workspace.yaml` `overrides:`; alert#≠issue#
|
||||
- [Upgrade gstack locally](feedback_gstack_upgrade.md) — upgrade from project `.claude/skills/gstack`
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: eink-screen-refresh-pageturner-4687
|
||||
description: "Page-turner \"Refresh Page\" action that deep-refreshes the e-ink panel (clear ghosting) on Android, via generic reflection across BOOX/Tolino/Rockchip"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 742b1517-392b-4735-8355-32b57fbfa400
|
||||
---
|
||||
|
||||
Issue #4687 — added a bindable **"Refresh Page"** page-turner action that triggers a deep e-ink full refresh (GC16) to clear ghosting. Shipped as **PR #4822 (MERGED)** (`feat/eink-screen-refresh-pageturner` → main, 55 files +470/-41), built in an isolated worktree off origin/main (worktree + branch since removed). Rebase note: origin/main's Drive-sync PR #4821 added `secure_item` native-bridge commands at the exact anchors I used (end of COMMANDS / handler list / structs / impls), so all 7 plugin files (build.rs, default.toml, commands.rs, desktop.rs, lib.rs, mobile.rs, models.rs) conflicted on apply — resolved "keep both" by re-adding `refresh_eink_screen` after the secure_item code; locales re-derived via script on main's current files; autogenerated permission files regenerated via `cargo check -p tauri-plugin-native-bridge`.
|
||||
|
||||
**Frontend** (reuses the existing hardware page-turner binding machinery — see [[keyboard-selection-adjust-4728]] / `src/utils/keybinding.ts`):
|
||||
- `keybinding.ts`: `'refresh'` added to `PageTurnAction` + `PAGE_TURN_ACTIONS` (so `resolvePageTurn` matches it). `matchesBinding` now accepts `undefined`.
|
||||
- `types/settings.ts`: `HardwarePageTurnerSettings.bindings.refresh?: KeyBinding | null` — OPTIONAL (older persisted settings lack it; never migrate, optional-chaining handles absence). Default `refresh: null` in `constants.ts`.
|
||||
- `PageTurnerSettings.tsx`: refresh slot rendered ONLY when `appService?.isAndroidApp && viewSettings.isEink` (the user-facing Eink-mode view setting, not just hardware detection).
|
||||
- `usePagination.ts` `handleHardwarePageTurn`: branch `if (action === 'refresh') { if (appService?.isAndroidApp) refreshEinkScreen().catch(()=>{}); return true; }` BEFORE the page/section side/mode logic. Also added `bindings.refresh?.source === 'native'` to `hasNativeBinding` + the effect dep array so a media key bound to refresh still acquires page-turner key interception.
|
||||
- `bridge.ts`: `refreshEinkScreen()` → `invoke('plugin:native-bridge|refresh_eink_screen')`.
|
||||
|
||||
**Native generic refresh** (`EinkRefreshController.kt`, new) — the answer to "compatible with most e-ink devices, generic interface not brand SDK". Android has NO public e-ink API; each vendor patches `android.view.View`. Probe via reflection, stop at first success (patterns from KOReader android-luajit-launcher EPD controllers):
|
||||
1. **Onyx BOOX (Qualcomm)**: `View.refreshScreen(0,0,w,h, 34)` instance method. `34 = FULL(32)+GC16(2)`.
|
||||
2. **Tolino/Nook (NTX/Freescale)**: `View.postInvalidateDelayed(0L,0,0,w,h, 34)`.
|
||||
3. **Rockchip (Boyue clones)**: `View.requestEpdMode(View$EINK_MODE.EPD_FULL, true)`.
|
||||
|
||||
Deliberately do NOT bundle the Onyx SDK (`com.onyx.android.sdk.*` classes aren't on-device unless bundled — reflection would always fail) and do NOT call Onyx `setWaveformAndScheme`/None (KOReader does, but it owns the update loop; Readest leaves system auto-update in place, so switching to manual mode could FREEZE later updates). Run on UI thread against `activity.window.decorView`; `success:false` (no controller) is a soft no-op, not an error. iOS Swift stub resolves `{success:false}`.
|
||||
|
||||
**Plugin wiring** added across `models.rs`/`commands.rs`/`mobile.rs`/`desktop.rs`/`lib.rs` + `build.rs` COMMANDS + `permissions/default.toml` `allow-refresh-eink-screen` (build regenerates `reference.md`/`schema.json`/`commands/refresh_eink_screen.toml`). App uses `native-bridge:default` so no capability edit needed.
|
||||
|
||||
**Verified on real hardware**: ONYX BOOX Leaf5 (`ro.product.manufacturer=ONYX`). `pnpm dev-android` build+install; via adb+CDP invoked `plugin:native-bridge|refresh_eink_screen` directly in the WebView → `{success:true}`, logcat `EinkRefresh: onyx full refresh requested` (the Onyx/Qualcomm `View.refreshScreen` path, decor view), and the user visually confirmed 5/5 full GC16 screen flashes in the reader. So the onyx path works on modern BOOX without SDK bundling or `setWaveformAndScheme` priming. (CDP socket is pid-bound `webview_devtools_remote_<pid>`; re-forward when the WebView process recycles — see [[cdp-android-webview-profiling]].)
|
||||
|
||||
**i18n**: ran into [[i18n-extract-prunes-keys]] (scanner `removeUnusedKeys:true` deleted ~314 dynamic keys / huge churn). REVERTED the scanner output and added the single `"Refresh Page"` key MANUALLY to all 33 non-en locales (en is key-as-content, needs no entry), aligning each translation with the locale's existing `"Reload Page"`/`"Next Page"` terminology. `check:translations` green.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: empty-highlight-leak-on-annotate-cancel-4791
|
||||
description: Annotate eagerly creates a highlight placeholder; cancelling the note must tear it down
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 1c75c865-8e1b-4641-ac20-81692d3ff20b
|
||||
---
|
||||
|
||||
#4791 — clicking **Annotate** on a selection eagerly creates a highlight (`note:''`)
|
||||
as the note anchor (`handleAnnotate` → `handleHighlight(true)` in `Annotator.tsx`),
|
||||
so the selection stays visible while the NoteEditor is open. Cancelling the note
|
||||
(Cancel button, overlay, Escape, switching books, closing the notebook) left that
|
||||
empty highlight leaked into config → showed as a stale card in the left-sidebar
|
||||
Booknotes list + a phantom yellow highlight.
|
||||
|
||||
**Fix:**
|
||||
- `handleHighlight` now returns the created `BookNote` only when it pushes a NEW
|
||||
record (returns `null` when it restyles/toggles an EXISTING one — that record
|
||||
predates the flow and must survive a cancel).
|
||||
- `handleAnnotate` stores `created?.id` via `setNotebookNewHighlightId` (new
|
||||
`notebookStore` field). This tracked id is what distinguishes a removable
|
||||
placeholder from a pre-existing highlight; do NOT identify it by cfi (a fresh
|
||||
selection can collide with an existing highlight's cfi).
|
||||
- `removeEmptyAnnotationPlaceholder(booknotes, id, now)` in `annotatorUtil.ts`
|
||||
tombstones (`deletedAt`) the live annotation with that id ONLY if it still has
|
||||
no note text, and returns it so the caller tears the overlay down with
|
||||
`removeBookNoteOverlays` across ALL views (`getViewsById`, symmetric with how
|
||||
`handleHighlight` drew it).
|
||||
- Cleanup is **presentation-driven**, not threaded through every cancel path:
|
||||
`Notebook.tsx` runs `handleCancelNewAnnotation` from an effect whenever the
|
||||
creation editor stops being presented (`!(isNotebookVisible && notebookNewAnnotation)`)
|
||||
— catches Cancel/Escape/overlay/close/swipe/navigate — plus a second effect's
|
||||
cleanup on `sideBarBookKey` change / unmount for book-switch (pinned) and
|
||||
reader-close.
|
||||
- Save survives the guard (placeholder gains note text) and also clears the
|
||||
tracked id. `handleCancelNewAnnotation` has stable identity (empty deps) so the
|
||||
effects don't re-fire mid-edit; it reads settings fresh via
|
||||
`useSettingsStore.getState().settings` (stale-closure guard, see [[webdav-connect-nullified-4780]]).
|
||||
|
||||
**Why id-set-LAST in handleAnnotate matters:** `setNotebookNewHighlightId` is
|
||||
called after `setNotebookVisible(true)` + `setNotebookNewAnnotation`, so no
|
||||
intermediate render has (editing=false AND a fresh placeholder id) — prevents the
|
||||
presentation effect from deleting the placeholder it just created.
|
||||
|
||||
Related: [[instant-highlight-delete-orphan-4773]], [[customize-toolbar-global-serializeconfig]].
|
||||
@@ -38,6 +38,34 @@ Adding **Google Drive as a second `FileSyncProvider`** for the merged file-sync
|
||||
|
||||
**Drive UI DONE (commit `c657c34f0`):** `FileSyncForm` (shared sync controls extracted from WebDAVForm, parameterized by kind, builds provider via registry; WebDAVForm refactored to use it, behavior unchanged) + `GoogleDriveForm` (OAuth Connect/account/Disconnect + FileSyncForm) + `googleDriveConnect.ts` (assembles env client id + keychain + desktop runner) + IntegrationsPanel "Google Drive" row gated on `appService.isDesktopApp`. Full suite 6412 green.
|
||||
|
||||
**Cloud Sync redesign DONE (commit `1a31a8cbd`):** new "Third-party Cloud Sync" Integrations section with a unified "Cloud Sync" sub-page (`CloudSyncForm`) — WebDAV + Google Drive MUTUALLY EXCLUSIVE via `withActiveCloudProvider` (enabling one disables the other). Radio picker (AIPanel pattern) + shared `FileSyncForm`. WebDAVForm/GoogleDriveForm refactored to embeddable panels; Drive has a "configured-but-inactive" state (`accountLabel` present, `enabled=false`) with frictionless "Use Google Drive" re-activate (no re-OAuth); explicit Disconnect clears the keychain token. Temp concurrency probe removed (upload was already concurrency-4, confirmed).
|
||||
|
||||
**Reader auto-sync DONE (commit `f5e07e50b`):** `useWebDAVSync`→`useFileSync` — the reader auto-syncs the single ACTIVE provider per-book while reading (pull-on-open, debounced push, cover/file). Async engine build (Drive keychain probe) held in state, pull-on-open waits for it; engine keyed on connection-relevant settings (not lastSyncedAt) to avoid re-probing keychain; deviceId/lastSyncedAt write the active provider slice; events renamed `*-file-sync`. WebDAV reader behavior unchanged.
|
||||
|
||||
**Drive feature is functionally complete on desktop:** connect, manual Sync now, auto-sync while reading, exclusive provider switching. Live-verified: connected + synced a 675-book library.
|
||||
|
||||
**DESKTOP PR OPENED: readest/readest#4821** (`feat/gdrive-sync-core`, rebased onto origin/main, all gates green incl. rust). Covers provider + OAuth + native KV + redesign (exclusive Third-party Cloud Sync section, inline radio switch) + reader auto-sync + **premium gating** (any paid plan via `isCloudSyncInPlan`; free sees upgrade CTA; reader auto-sync off for free). Rebase needed `git -c protocol.file.allow=always submodule update --init packages/foliate-js` (foliate-js drift, index wanted `6f1a190`).
|
||||
|
||||
**PR #4821 review fix (pushed `5769682c5`):** CodeQL flagged `escapeDriveLiteral` (driveRest.ts) for not escaping backslashes — fixed (escape `\`→`\\` FIRST, then `'`→`\'`). Was the only review comment.
|
||||
|
||||
**Both branches REBASED onto origin/main `324bb8a36` (was `7e78f80e1`). UNPUSHED, both gates green (lint+format+full suite: mobile 6483, resumable 6486). foliate-js submodule drift on rebase: origin/main now wants `0fa407c4c` (not in local submodule clone whose origin is the main checkout's modules dir); fix `git -C packages/foliate-js fetch https://github.com/readest/foliate-js.git 0fa407c4c... && git -C packages/foliate-js checkout 0fa407c4c...` (the `submodule update --init` shortcut FAILS here — local origin lacks the commit; must fetch from GitHub URL). Current commits: mobile `6728c94f0`(Android)+`8b3dd1cd5`(iOS); resumable `f7a1e5117`.**
|
||||
|
||||
**Branch `feat/gdrive-mobile-oauth` (Android+iOS OAuth) — no longer stacked, off main. PR not opened yet.**
|
||||
|
||||
**Android OAuth (PR4) DONE (commit `eb8e22081`, was `5583c9b38` pre-rebase).** `auth/oauthAndroid.ts` (`runAndroidOAuth` via existing `authWithCustomTab`, DI, 2 tests) + platform dispatch in `googleDriveConnect` (`osType()==='android'`→Custom Tab, else desktop) + Drive row shown on Android. NATIVE (device-verify pending, no Android toolchain in CI): `NativeBridgePlugin.kt` `handleIntent` resolves `com.googleusercontent.apps.<id>:/oauthredirect` via the same `pendingInvoke` as the Supabase callback; matching BROWSABLE intent-filter added to `gen/android/.../AndroidManifest.xml`.
|
||||
|
||||
**iOS OAuth (PR5) DONE (commit `1230fb291`).** `auth/oauthIos.ts` (`runIosOAuth` via `authWithSafari({authUrl, callbackScheme})`; callbackScheme = `deriveReverseDnsRedirectScheme(clientId)` = bare `com.googleusercontent.apps.<id>` — ASWebAuthenticationSession matches on SCHEME not path; DI, 2 tests) + `AuthRequest.callbackScheme?` (nativeAuth.ts; Supabase keeps native `readest` default) + `resolveOAuthRunner` `os==='ios'`→runIos + Drive row on iOS (`isDesktopApp||isAndroidApp||isIOSApp`). `createDriveTokenPersistence` already works on iOS (Keychain via secure-KV). NATIVE (device-verify pending, no iOS toolchain in CI): Swift `auth_with_safari` uses `args.callbackScheme ?? "readest"` (`SafariAuthRequestArgs.callbackScheme: String?`); `Info-ios.plist` CFBundleURLTypes gains the reverse-DNS scheme. macOS Drive uses the desktop deep-link runner (NOT authWithSafari), so no macOS native change. Full suite 6477 green + lint + format + plutil OK.
|
||||
|
||||
**Drive streaming upload/download DONE — own branch `feat/gdrive-resumable-upload` off origin/main (commit `0c9cc1a22`, UNPUSHED).** `uploadStream`+`downloadStream` on GoogleDriveProvider so book files stream from/to disk instead of buffering the whole file in the JS heap (buffered marshal of a large book across the WebView↔Rust bridge crashes the renderer on mobile — this unlocks Drive book sync on Android/iOS and flattens heap on desktop too). `driveRest.resumableCreateUrl`/`resumableUpdateUrl`; `uploadStream` opens a Drive resumable session (POST new `{name,parents}` / PATCH existing `{name}`, metadata in initiation so NO reparent follow-up), reads `Location` session URI, PUTs bytes via `tauriUpload`; `downloadStream` GETs `alt=media` to disk via `tauriDownload` + bearer. Attached **Tauri-only** (`isTauriAppPlatform()`); web keeps buffered fallback. Both swallow→`false` per provider contract (engine retries once). REUSES `@tauri-apps/plugin-upload` already shipped for WebDAV — NO new native code. Single-shot streaming PUT (not chunked mid-stream resume) — sufficient for the heap/OOM fix; chunked-resume-on-failure is a further enhancement. Full suite 6484 green + lint + format. **NOTE: changes desktop Drive book sync from buffered → streaming (previously live-verified buffered); device-verify the streaming path on desktop + mobile.**
|
||||
|
||||
**ALL PRs MERGED to main/dev (dev @ `c6f2a83d9`).** Worktree `feat/gdrive-*` branches no longer needed; work continues in the MAIN repo `/Users/chrox/dev/readest` on `dev` (tracks `origin/main`; there is NO `origin/dev`).
|
||||
- **#4821** desktop Drive cloud sync + premium Third-party Cloud Sync section.
|
||||
- **#4824** Drive resumable streaming upload/download.
|
||||
- **#4823** mobile OAuth (Android Custom Tab + iOS ASWebAuthenticationSession).
|
||||
- **#4827** Android sync fix: retry THROWN transport errors in `withBackoff` (was 429/5xx only); `mapDriveError` classifies transport throws (incl. Tauri plugin's plain `error sending request` Error) as NETWORK. Root cause: Android pooled keep-alive connection to googleapis.com goes stale mid-sync → every files.list after the first batch threw; sync recovered on its own after ~3-4 min (reqwest evicting dead conns). The retry forces a fresh connection so recovery is fast + kills the error spam.
|
||||
|
||||
**CODE COMPLETE + MERGED.** REMAINING (human/ops-only): (1) on-device re-verify with #4827 in the build — Android sync should no longer stall ~3-4 min / spam `failed to inspect hash dir`; iOS OAuth sign-in; desktop streaming book-sync re-check; (2) Google consent screen → Production (testing caps 100 users). NOTE: Android build auto-generates a deep-link intent-filter for the gdrive reverse-DNS scheme in `gen/android/.../AndroidManifest.xml` (duplicates the manual `gdrive-oauth` filter) — benign build drift, don't commit.
|
||||
|
||||
**PR3 REMAINING:**
|
||||
- **LIVE VERIFICATION (needs the user — real Google sign-in):** `pnpm tauri dev` → add own Google account as a Test user in the consent screen (Testing mode caps + gates) → Settings → Integrations → Google Drive → Connect → browser → grant → "Connected as <email>" → add book / Sync now → confirm `Readest/books/<hash>/{config.json,cover.png}` in Drive. Windows cold-browser fallback.
|
||||
- **Reader-hook auto-sync (deferred):** generalize `useWebDAVSync`→`useFileSync` (per-provider state maps, async Drive provider build in the hook) so Drive auto-syncs per-book while reading like WebDAV. Manual Sync-now already works without it; do after live-verifying the base.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: i18n-extract-prunes-keys
|
||||
description: "pnpm i18n:extract (removeUnusedKeys) deletes valid keys not statically in the branch; don't commit that churn"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: feedback
|
||||
originSessionId: afe50e44-d394-4301-bd81-1368df66f90b
|
||||
---
|
||||
|
||||
`pnpm run i18n:extract` (i18next-scanner, `i18next-scanner.config.cjs` has
|
||||
`removeUnusedKeys: true`) can DELETE ~30+ valid-looking keys from every non-`en`
|
||||
locale on a feature branch — keys whose source usage isn't statically present in
|
||||
the current branch (e.g. `"Sync History"`, `"downloaded {{n}} book(s)"`,
|
||||
`"Match Whole Words"`). The extract diff then shows huge churn (~1000 +/- lines)
|
||||
unrelated to your change.
|
||||
|
||||
**Why:** the committed locales can be ahead of the branch's source (strings from
|
||||
features not yet on this base, or built dynamically/in non-scanned modules), and
|
||||
`removeUnusedKeys` strips anything the scanner can't find. `en/translation.json`
|
||||
is a tiny key-as-content file (~70 lines, only plural/proper-noun overrides), so
|
||||
new keys never land there anyway — it stays out of the diff.
|
||||
|
||||
**How to apply:** for a feature that adds a few strings, do NOT commit the
|
||||
scanner's deletions into an unrelated PR.
|
||||
1. Run `pnpm run i18n:extract` (optional — only confirms which keys are new).
|
||||
2. `git checkout -- apps/readest-app/public/locales` to drop ALL the churn.
|
||||
3. Add ONLY your new keys manually to each locale in `i18n-langs.json` with real
|
||||
translations. The files are exactly `JSON.stringify(obj, null, 2) + "\n"`, so
|
||||
a Node script that `JSON.parse`s, appends new keys (insertion order preserved),
|
||||
and rewrites that way yields a zero-extra-diff result. Skip `en` (key-as-content).
|
||||
|
||||
Match each locale's existing terminology (grep the file for a related key, e.g.
|
||||
`"Export Annotations"` / `"Annotations"`, before translating). Verify with
|
||||
`grep -rn '"<Your Key>"' apps/readest-app/public/locales | wc -l` == number of locales.
|
||||
|
||||
Related: [[feedback_en_plurals_manual]].
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: markdown-md-support-774
|
||||
description: Markdown (.md) reading via in-memory foliate book (no EPUB); split-at-H1; foliate book-object contract gotchas
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: a82e979b-0edb-4964-91fd-3677ecfe5679
|
||||
---
|
||||
|
||||
Issue #774: render standalone `.md` files at runtime (NO EPUB conversion). **MERGED as
|
||||
PR #4816** (branch `feat/markdown-support`). Built test-first via `/autoplan` (CEO+Eng
|
||||
dual-voice review). Suite green (6365), lint + format clean; live-verified in web app
|
||||
(import via drop, split TOC, cross-section nav, GFM rendering, pagination).
|
||||
|
||||
**Where:** `src/utils/md.ts` `makeMarkdownBook(file)` builds an in-memory foliate book
|
||||
modeled on `packages/foliate-js/fb2.js`. Routed in `src/libs/document.ts` `open()` via
|
||||
`isMd()` **before `isTxt()`** (a `.md` served as `text/plain` would otherwise hit TXT→EPUB).
|
||||
`'md'` added to `SUPPORTED_BOOK_EXTS` (constants.ts). `sanitize.ts` `sanitizeHtml` gained
|
||||
`'class'` (code `language-*` theming) + `del`/`ins` tags. Pipeline: strip YAML frontmatter →
|
||||
`marked`(gfm) → `sanitizeHtml` → split at `<h1>` (preamble = pre-first-H1 content) → nested
|
||||
heading-outline TOC.
|
||||
|
||||
**Non-obvious foliate book-object contract (cost us the CRITICAL review finding):**
|
||||
- `section.id` and `splitTOCHref()` output MUST be the SAME type. readest nav
|
||||
(`services/nav/index.ts:133`) does `new Map(sections.map(s=>[s.id,s]))` then `.get(sectionId)`
|
||||
where sectionId = `splitTOCHref(href)[0]`. `SectionItem.id` is typed `string`, so use
|
||||
STRING ids + `splitTOCHref => href.split('#')`. fb2.js uses numbers consistently (works
|
||||
only because it's untyped JS); do NOT copy fb2's `Number(x)`.
|
||||
- Fragment CFIs / TOC sub-anchors require `section.loadText` (nav skips sections without it,
|
||||
index.ts:153). Provide it.
|
||||
- `SectionItem.cfi` is non-optional → set `cfi: ''` (foliate falls back to `CFI.fake.fromIndex`).
|
||||
- `createDocument()` parses `application/xhtml+xml`; marked's HTML5 void tags (`<br><hr><img>`)
|
||||
are parse errors there → serialize sections with `XMLSerializer` (not innerHTML). `load()`
|
||||
and `createDocument()` must derive from the SAME string (CFI round-trip).
|
||||
- `resolveHref` returns `null` for unresolved anchors (never index 0). jsdom lacks
|
||||
`URL.createObjectURL` → `load()` is lazy + tests stub it.
|
||||
|
||||
**Deferred follow-ups (open issues):** relative image resolution (web File-objects have no
|
||||
sibling access — needs the bundle model); Markdown folder/zip "package" model; footnotes/math/
|
||||
Mermaid/wikilinks; syntax-highlight token colors. Plan: `.claude/plans/2026-06-26-markdown-md-support-774.md`.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: multiwindow-settings-clobber-4580
|
||||
description: Pagination/global settings revert with multiple desktop windows; cross-window broadcast fix
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 4df1808d-e106-4316-9206-b4e606b4b9bf
|
||||
---
|
||||
|
||||
Issue #4580 (fix: PR #4803, branch `fix/multiwindow-settings-revert-4580`): on desktop (Tauri) global view settings (Click/Swipe to Paginate, Show Page Navigation Buttons) "revert to default" — only when multiple windows are open (OP ran `1 + n_opened_books` windows).
|
||||
|
||||
**Root cause:** each Tauri window keeps its own in-memory `useSettingsStore.settings`, loaded once at window open. Global settings persist to ONE shared `settings.json`, and every window writes the WHOLE object via the store's `saveSettings`. A window opened before the user customized a global setting holds the default (e.g. `disableClick=false`); when it later saves (notably `handleCloseBooks` on reader-window close in `ReaderContent.tsx`, but ANY settings write) it clobbers the user's value back to default. Explains "reverts to *default*, only with multiple windows". Note: `replicaCursorStore` avoids this by load-modify-saving from disk each time.
|
||||
|
||||
**Fix:** cross-window broadcast. `src/utils/settingsSync.ts` (`broadcastGlobalSettings` emits `global-settings-window-sync` with `sourceLabel` + the two global blobs; `subscribeSettingsSync` ignores self; `mergeSyncedGlobalSettings` adopts `globalViewSettings`/`globalReadSettings` and preserves all device/window-local fields). Store `saveSettings` calls `broadcastGlobalSettings` after persisting. `useSettingsSync` (mounted in `Providers.tsx`, the shared root for both library + reader windows) adopts broadcasts via `setSettings`. No-op off Tauri.
|
||||
|
||||
Only the two global objects are synced (minimal scope) — covers the reported bug + sibling read settings; top-level scalars left window-local. No save/broadcast loop: receive calls `setSettings` only; the replica publisher subscriber pushes to network (no disk write) and pagination fields aren't in `SETTINGS_WHITELIST` anyway. Live cross-window view update of already-open books is intentionally NOT done (bug is persistence, not live propagation). Related: [[webdav-connect-nullified-4780]] (stale settings closure), [[window-state-sanitize-4398]].
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: pageturn-bg-replace-reflow-4785
|
||||
description: "Page-turn frame drops at chapter boundaries (#4785) — per-frame"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: e42ea03e-cda7-4e59-b398-1a28f589b37e
|
||||
---
|
||||
|
||||
Issue #4785: swipe page-turn animation drops frames, worst crossing .xhtml
|
||||
section boundaries, "first open" (Android/Xiaomi). Repro book is a Taiwan light
|
||||
novel with custom fonts + `.bg`/`background-attachment:fixed` front-matter.
|
||||
|
||||
Root cause (in `packages/foliate-js/paginator.js`, a submodule):
|
||||
`#replaceBackground()` rebuilt its whole paint context **every frame** of both
|
||||
swipe phases — `getComputedStyle(<html>)` + `this.size` + one
|
||||
`getBoundingClientRect()` **per rendered view** + a per-view background-reset
|
||||
write loop + full `#background` DOM rebuild. Those forced reads scale with the
|
||||
number of loaded views, which **peaks at a chapter boundary** because adjacent
|
||||
sections are preloaded there — hence "worst at boundaries". Two callers ran it
|
||||
per-frame: the snap `syncBackground` rAF loop (`#scrollTo`) and the drag-phase
|
||||
container `scroll` listener (`#onTouchMove`→`scrollBy`→scroll event).
|
||||
|
||||
Everything `#replaceBackground` reads is **invariant for one gesture** (theme/
|
||||
texture, bg+container geometry, each view size+bg) — only scroll offset changes.
|
||||
|
||||
Fix:
|
||||
- Split into `#readBackgroundStyle` / `#computePaginatedBgContext` (the reads) +
|
||||
`#paintPaginatedBackground(ctx, atPosition)` (writes only; calls the unchanged
|
||||
pure `computeBackgroundSegments`).
|
||||
- `#bgAnimContext` field snapshots context once: set in `#onTouchStart` (drag)
|
||||
and at the start of the animated branch in `#scrollTo` (snap); cleared in
|
||||
`#onTouchEnd` and both animation `.then()`s. `#replaceBackground` uses
|
||||
`this.#bgAnimContext ?? this.#computePaginatedBgContext()`.
|
||||
- Also deferred the heavy mid-drag forward preload: added `&& !this.#touchScrolled`
|
||||
to the scroll-listener `#loadAdjacentSection` gate (columnize/expand on the main
|
||||
thread janked the drag). The scroll that settles the gesture re-fires the gate
|
||||
with the finger up, so the buffer still tops up.
|
||||
|
||||
Tests: `src/__tests__/document/paginator-background-anim-perf.browser.test.ts`
|
||||
(real Chromium) drives `next()` (snap) and a synthetic touch drag, spying on the
|
||||
primary iframe `<html>` getComputedStyle. Pre-fix: 39 reads (snap) / 7 (drag);
|
||||
post-fix ≤3 / ≤1. Existing `paginator-background-segments.test.ts` (pure
|
||||
`computeBackgroundSegments`) stays green — visual output unchanged.
|
||||
|
||||
Scrolled-mode branch kept inline in `#replaceBackground` (never the per-frame hot
|
||||
path). Behavior preserved: scrolled set every view bg so the old reset loop was
|
||||
redundant; `containerSize = containerRect[sideProp]` == old `this.size`.
|
||||
|
||||
NOT the cause (ruled out): `computeBookNav`/`nav.json` is awaited before the view
|
||||
renders, so first/second-open in-memory state is identical — it can't explain
|
||||
reading-time swipe jank. See [[booknote-view-autoscroll-4352]] neighbors in
|
||||
Paginator & Scroll. Related: [[paginator-swipe-bg-flash]],
|
||||
[[global-annotation-pageturn-perf-4575]], [[paginated-texture-occlusion-4399]].
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: pdf-scroll-lag-preload-4795
|
||||
description: "PDF scrolled-mode rendering lag on Android (#4795/#4031) — fix via widened preload margin + bounded prioritized load scheduler in fixed-layout.js"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 902324ba-94ee-4c88-804e-ea9f796681f9
|
||||
---
|
||||
|
||||
PDF **scrolled mode** showed blank pages while scrolling on Android (#4795, resurfacing #4031). Reproduced + fixed + CDP-verified on Xiaomi 13 (fuxi).
|
||||
|
||||
**Root cause (measured via CDP on-device):** per-page render (`pdf.js` `onZoom`→`render()`: canvas raster + text layer + annotation layer) ≈ **415 ms** uncached / ~65 ms cached, but the scrolled-mode IntersectionObserver used `rootMargin: '50% 0px'` (~0.5 page of lead). Loads also had **unbounded concurrency** (observer fired `#loadScrollPage` for every intersecting page) and **no viewport prioritization**, so the slow render never finished before the page scrolled into view, and a fling spawned dozens of competing renders. Per-page canvas ≈ **7 MB** at dpr 3 (screen-res, NOT the ~50 MB I first feared) → memory headroom existed; the #3470 OOM was byte-range *parsing* flood (`MAX_CONCURRENT_RANGES`, orthogonal).
|
||||
|
||||
**Fix** (`packages/foliate-js/`):
|
||||
- `fixed-layout.js`: widened observer to `rootMargin: '200% 0px'` (~2 viewports lead); observer now only **flags `page.visible`** and calls new `#scheduleScrollPages()`.
|
||||
- New pure exported `planScrollModePages({pages, currentIndex, maxLoaded, maxConcurrent, loadingCount})` → `{load, evict}`: loads **visible+idle pages nearest currentIndex first, bounded by `maxConcurrent - loadingCount`**; evicts **farthest non-visible loaded** beyond `maxLoaded`; **never evicts a visible page** (distance = `|index - currentIndex|`). Unit-tested in `src/__tests__/document/fixed-layout-scroll-scheduler.test.ts`.
|
||||
- `#scrollMaxLoaded 8→12` (live-canvas cap = memory ceiling), `#scrollMaxConcurrent=3`, `#scrollLoadingCount` tracked in `#loadScrollPage` (inc on start, dec in `finally`, then reschedule so a freed slot pulls the next nearest page). Removed `#evictScrollPages` (scheduler handles it).
|
||||
- **Terminal `error` state**: a load that throws or returns no src sets `state='error'` (not `'idle'`) so the post-completion reschedule can't retry a persistently failing page in a tight async loop (regression I introduced with reschedule-on-completion).
|
||||
- `pdf.js`: `MAX_CACHED_PAGES 8→16` (page objects + render blobs are cheap, not the canvas) so back-scroll within the wider window doesn't re-parse.
|
||||
|
||||
**Verified (CDP + screenrecord, identical 9-swipe reading-pace test, fresh region):** baseline = mostly blank frames, settled forward lead **+2** (span [-9,+2]); fix = **every frame fully rendered**, forward lead **+4** (span [-7,+4]). Extreme 8-fling (240 pages/2s) still blanks mid-fling (inherent) but settles to rendered content and **no crash**.
|
||||
|
||||
**Best-practice cache strategy for scrolled PDF on mobile** (asked during this work): two bounded tiers — live-canvas cap = the hard memory ceiling (sized to window+lead), decoded-page cache a bit larger (cheap); distance/viewport-aware LRU never evicting visible; bound+prioritize loads; release bitmaps eagerly (`canvas.width=0`); the biggest unused lever for low-end devices is **capping effective DPR** (canvas mem ∝ DPR²) — not applied here since 12×7 MB≈84 MB is fine on the Xiaomi.
|
||||
|
||||
**CDP on release builds:** the installed Play/release 0.11.12 has **no `webview_devtools_remote_<pid>` socket** — WebView debugging is gated behind the `devtools` Cargo feature (`src-tauri/Cargo.toml`); must build+install `pnpm dev-android` (release + `--features devtools`, same keystore so it updates over the store build, library preserved). CDP `webSocketDebuggerUrl` comes back as `ws://localhost/devtools/...` **with no port** (echoes Host header) → rewrite to `ws://127.0.0.1:9222<path>`; `ws` npm pkg is CJS so import default + destructure. See [[cdp-android-webview-profiling]], [[pdf-oom-range-flood-3470]].
|
||||
|
||||
**WIP caveat:** during this work the foliate-js submodule had unrelated uncommitted `paginator.js` WIP (background-anim perf, #4785) — exclude it from any #4795 commit.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: recent-read-shelf-3797
|
||||
description: "Recently-read carousel at library top (#3797 / PR"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: d5f79cf1-9e58-4ae4-9f8a-46a8e8ca625f
|
||||
---
|
||||
|
||||
Opt-in "Recently read" strip in the library Virtuoso header (PR #4829, issue #3797). `selectRecentShelfBooks(books, count)` in `libraryUtils.ts` (filter `!deletedAt && progress != null`, sort by `updatedAt` desc, slice 12). Setting `libraryRecentShelfEnabled` (default false) + View-menu toggle. Rendered via the Virtuoso `Header` through `BookshelfListContext` (stable identity → no grid re-render churn); list `<Virtuoso>` needs explicit `context={listContext}`.
|
||||
|
||||
**Reuse, don't reimplement:** each slide renders the real `BookItem` (identical cover/title/progress/badges). The open path was extracted to `src/app/library/hooks/useOpenBook.ts` (in-place stale-record probe + `makeBookAvailable` on-demand download for cloud-only synced books + navigate) and is shared by `BookshelfItem` AND the recent shelf. Do NOT open via the select-mode `navigateToReader` path — it skips the download, so a recently-read book that synced (progress + `updatedAt`) without its blob fails to open on a second device.
|
||||
|
||||
**Alignment gotcha (cost several iterations):** a horizontal flex strip with `basis-1/N` does NOT match a CSS-grid column when the grid has a row gap — CSS Grid subtracts the gap from each track, flex `basis` does not (covers come out too wide at 2/3 cols where `BOOKSHELF_GRID_CLASSES` uses `gap-x-4`; matches at `sm+` where `gap-x-0`). Fix: size each slide with the grid's own formula `flexBasis: calc((100% - (var(--rs-cols) - 1) * var(--rs-gap)) / var(--rs-cols))`, with `--rs-cols` (responsive `3/4/6/8/12` ladder when auto, else `libraryColumns`) and `--rs-gap` (`1rem` base / `0px` sm+, mirroring `gap-x-4 sm:gap-x-0`) set on the row. Also `min-w-0` on each flex item, else image covers expand to intrinsic width. Verified 0.00-0.02px edge diff vs a real CSS grid at N=2/3/4/5 (standalone HTML repro + getBoundingClientRect).
|
||||
|
||||
Arrows: plain scroll div + `scrollBy`, shown on overflow (`scrollLeft`/`scrollWidth`, `ResizeObserver`), centered on `.bookitem-main` via measure; `start-2`/`end-2` + `rtl:rotate-180`. Swipe never opens (useLongPress moveThreshold). i18n: `i18n:extract` churns every locale (see [[i18n-extract-prunes-keys]]) — added the 2 keys manually; bo/si/ta/bn best-effort.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: scrolled-pdf-pinch-zoom-4817
|
||||
description: Scrolled-PDF live pinch-zoom + the cross-page-pinch vs native-selection tradeoff (readest
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 902324ba-94ee-4c88-804e-ea9f796681f9
|
||||
---
|
||||
|
||||
Live pinch-zoom for scrolled PDF (fixed-layout scrolled mode). The real fix is entirely in foliate-js: PR **readest/foliate-js#43 MERGED** to foliate main as `0fa407c` (on top of #42 `8bcb61e` which already had live pinch + the rect-match anchor + the interactive-when-idle idle-toggle). readest **PR #4817** is therefore **minimal — just the submodule bump to `0fa407c` + one unit test** (`fixed-layout-pinch-zoom.test.ts`, single clean commit `dd39837af`, +39/-1). readest needs NO touch-handling change: `origin/main`'s `useIframeEvents` already detects a two-finger gesture per page (`event.touches` forwarded via `iframeEventHandlers`) and calls `renderer.pinchZoom`. Builds on the scroll-lag scheduler [[pdf-scroll-lag-preload-4795]].
|
||||
|
||||
**Abandoned detour (do not re-add):** a host-level cross-page-pinch approach (`multiTouch.ts` `updateSourceTouches`/`flattenSourceTouches`, per-iframe `sourceIndex` binding, `allActiveTouches` reading `e.touches`, and a `usePagination` host-click tap fix) was built then fully reverted. It is unnecessary once cross-page pinch is dropped, and same-page pinch + centre-tap toggle both work through existing `origin/main` code (tap goes iframe -> `iframe-single-click` centre zone; the host-click path is never hit when iframes are interactive).
|
||||
|
||||
**Core architectural finding (the crux):** in scrolled FXL, **cross-page pinch and native text selection are mutually exclusive**. Each page is its own iframe; Android **serializes touches across iframe documents** (finger1 on page A gets `touchcancel` the instant finger2 lands on page B — proven via forwarded-touch logs), so a pinch spanning two pages can only be recognized if the *host* owns all touches, which requires `.scroll-page iframe { pointer-events: none }`. But inert iframes kill native selection/taps. So you pick one. User chose **native selection, drop cross-page pinch.**
|
||||
|
||||
**Final design (foliate `fixed-layout.js`):**
|
||||
- `pinchZoom(ratio)` in scroll mode scales the whole `.scroll-container` live (`computeScrollPinchTransform`, transform-origin at viewport centre). `pinchEnd` snapshots the centre page's `getBoundingClientRect` and the commit re-render (`#renderScrollMode`) scrolls it back to that exact rect (`#restorePinchAnchor`) — no jump.
|
||||
- **No-shift fix:** the inter-page gap must scale with zoom or the committed gaps don't match the transform-scaled preview. `margin: calc(var(--scroll-page-gap,4px) * var(--scroll-zoom,1))` and `#renderScrollMode` sets `--scroll-zoom = scaleFactor`. Verified preview->commit scale MATCH + position jump <=2px.
|
||||
- Iframes interactive **when idle** (restored `#setScrollIframeInteraction(true)` in `#handleScrollEvent` settle; `#scrolling` flag + interactive-on-load in `#loadScrollPage` so selection works without scrolling first), inert only **during active scroll** (native-smooth). Same-page pinch flows through the per-iframe forwarded-touch path; pdf.js `setupPanningEvents` handles pan (empty-area drag scrolls host) + native selection (text drag). `overflow-x:auto` + `width:max-content` enable horizontal pan of a zoomed page.
|
||||
|
||||
**Gotcha — zoom store/attribute desync:** setting the `scale-factor` attribute directly (e.g. a test reset) does NOT update readest's `viewSettings.zoomLevel`. Pinch commit = `round(zoomLevel * lastPinchRatio)`, so a desynced store makes commit diverge from the live transform preview. Real pinches keep them in sync; only direct `setAttribute` breaks it. Cost me a long false-positive "shift" chase — reset zoom via a synthetic pinch, never `setAttribute`.
|
||||
|
||||
**Selection re-impl (NOT taken):** host-level selection via `caretRangeFromPoint` + dispatch `selectionchange` on the iframe doc IS viable (readest `handleSelectionchange` -> `makeSelection` -> popup; `getPosition` returns valid coords for scroll-page selections), but loses native OS selection handles/magnifier, and the popup is deferred until a real `touchend` sets `androidTouchEndRef` (Annotator.tsx). Abandoned in favour of native selection.
|
||||
|
||||
CDP-verified on Xiaomi (tap toggle, same-page pinch in/out no-shift, vertical scroll, horizontal pan, iframes `pointer-events:auto` when idle). Native selection itself needs a real finger (CDP synthetic touches don't engage the WebView long-press selection gesture).
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: toc-table-heading-clip-4439
|
||||
description: "#4400 scroll-wrapper overflow:auto clips negative-margin bleed of decorative layout tables; hoist negative margins onto wrapper"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 6d1d7362-d152-4248-93c0-76f6aef92329
|
||||
---
|
||||
|
||||
#4439: on a decorative TOC page (nested layout tables), the **top half of the
|
||||
`CONTENTS` heading is clipped** in paginated mode (0.11.4 regression; 0.11.2 fine).
|
||||
Reporter blamed [[table-dark-mode-tint-4419]] but that's dark-mode only — this is
|
||||
light mode. Real cause is **#4400** (`scrollable.ts` + `getPageLayoutStyles`):
|
||||
|
||||
- v0.11.2 sized wide tables with `transform: scale()` — **never clipped**.
|
||||
- #4391 then #4400 replaced that with wrapping every `<table>` (and display
|
||||
`<math>`) in `.scroll-wrapper { overflow: auto }` + `table { max-height: var(--available-height) }`.
|
||||
- These EPUBs lay the contents out as a nested `<table class="bc" style="margin: -1em 0 0 1em">`
|
||||
with `<p class="lh em16 ...">CONTENTS</p>` (`line-height:1em`, inside `div.em06`=0.6em).
|
||||
The **negative top margin** pulls the table (and the heading's first line) above
|
||||
the wrapper's `overflow:auto` content box, which clips it. Measured: heading top
|
||||
~12.8px above the clip box = exactly `-1em` in the 0.6em context (~58% of the line).
|
||||
- The `-fit` escape (`SCROLL_WRAPPER_FIT_CLASS` → `overflow:visible`) only checks
|
||||
**horizontal** fit (`scrollWidth-clientWidth`). The table's positive `margin-left:1em`
|
||||
inflates scrollWidth so it never gets `-fit`, stays `overflow:auto`, and clips.
|
||||
|
||||
**Fix** (PR for #4439): `hoistNegativeMargins(el, wrapper, win)` in `applyScrollableStyle`'s
|
||||
`wrap()` — move any NEGATIVE computed margins from the wrapped element onto the wrapper
|
||||
and zero them on the element. Keeps the box in place, lets the element sit flush so the
|
||||
overflow box can't clip it; also de-inflates scrollWidth so a genuinely-fitting table
|
||||
gets `-fit`. Positive/auto margins are left alone (over-wide tables still scroll; centered
|
||||
tables stay centered). CSS can't do per-axis `overflow-x:auto; overflow-y:visible`
|
||||
(spec coerces `visible`→`auto`), so margin-hoisting is the route, not per-axis overflow.
|
||||
|
||||
Repro is metric-sensitive (whether the inner table is `-fit`). Tests: browser test
|
||||
`src/__tests__/document/paginator-table-toc-clip.browser.test.ts` + fixture
|
||||
`repro-4439.epub` (real foliate paginator, asserts heading top not above its clip box);
|
||||
unit cases in `scrollable.test.ts`. Verified against the literal book (`321123.epub`,
|
||||
content-7.xhtml spine idx 12): clipped without fix, clean with it. Related:
|
||||
[[paginated-texture-occlusion-4399]], [[inline-block-column-overflow]].
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: tts-highlight-granularity-setting
|
||||
description: TTS highlight granularity (word/sentence) user setting and its two-point gating in TTSController
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 33ddd196-7404-4af2-99ac-0d3b19b39b4e
|
||||
---
|
||||
|
||||
Settings → TTS → "TTS Highlighting" boxed list has a **Granularity** select (first row, before Style): `Word` (default) / `Sentence`. Field `ttsHighlightGranularity: TTSHighlightGranularity` on `TTSConfig` (`src/services/tts/types.ts`, `src/types/book.ts`), default `'word'` in `DEFAULT_TTS_CONFIG`. UI in `TTSHighlightStyleEditor.tsx` (props `granularity`/`onGranularityChange`), persisted from `TTSPanel.tsx` via `saveViewSettings(..., false, false)` + a value-watching `useEffect` (mirrors `ttsMediaMetadata`).
|
||||
|
||||
Word-by-word highlighting only ever happens on **Edge TTS** (`supportsWordBoundaries() === true`); Web/Native always highlight per sentence. We assume every engine supports sentence highlighting, so picking `word` on a non-word-boundary engine naturally falls back to sentence.
|
||||
|
||||
**Gating lives at two points in `TTSController` (NOT one helper):**
|
||||
1. `dispatchSpeakMark` suppression: `#suppressMarkHighlight = ttsClient.supportsWordBoundaries() && #highlightGranularity === 'word'`. With `sentence`, don't suppress → the sentence highlight is drawn at mark dispatch.
|
||||
2. `prepareSpeakWords` early-return: `if (#highlightGranularity === 'sentence') return;` — **gated on granularity only, NOT on `supportsWordBoundaries()`**. Reason: `prepareSpeakWords` is only called by EdgeTTSClient in prod (boundaries present), but `tts-controller.test.ts` calls it directly with the *web* client active (supportsWordBoundaries=false) and expects word highlighting. Adding a `supportsWordBoundaries()` check there would break those existing tests.
|
||||
|
||||
Controller learns the value via `setHighlightGranularity()` (called at creation in `useTTSControl.ts` next to `updateHighlightOptions`, and from a `useEffect` on `viewSettings.ttsHighlightGranularity`). Mock `TTSController` in `useTTSControl.test.tsx` must include `setHighlightGranularity: vi.fn()` or the speak path throws and emits no position/state.
|
||||
|
||||
Related: [[edge-tts-word-highlighting-4017]], [[tts-word-highlight-singletextnode-drift]], [[tts-sync-paragraph-rsvp-3235]].
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: webdav-credential-sync-4810
|
||||
description: "WebDAV credentials weren't synced cross-device; add to settings whitelist + encrypted fields + mergeSettings deep-merge"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: 1a4937d3-05a4-4e1e-b917-d56304b66f2b
|
||||
---
|
||||
|
||||
Issue #4810: WebDAV credentials never synced across devices despite the
|
||||
"Credentials" sync toggle being ON. WebDAV (`webdav.serverUrl/username/password/rootPath`)
|
||||
was simply absent from the settings replica whitelist — only kosync/readwise/hardcover
|
||||
were there.
|
||||
|
||||
Fix (`src/services/sync/adapters/settings.ts`):
|
||||
- Added `webdav.serverUrl`, `webdav.username`, `webdav.password`, `webdav.rootPath`
|
||||
to `SETTINGS_WHITELIST`.
|
||||
- Added `webdav.username`, `webdav.password` to `SETTINGS_ENCRYPTED_FIELDS`.
|
||||
- Deliberately EXCLUDED per-device bookkeeping: `enabled`, `deviceId`,
|
||||
`lastSyncedAt`, sync sub-toggles. Mirrors KOSync, which syncs credentials
|
||||
but NOT its `enabled` flag — a fresh device pre-fills the connect form and
|
||||
the user clicks Connect (avoids auto-arming sync / rotating deviceId).
|
||||
|
||||
**Gotcha (the real trap):** `mergeSettings` in `replicaSettingsSync.ts` does a
|
||||
top-level shallow merge (`{...current, ...patch}`) plus single-level deep
|
||||
merges ONLY for explicitly-listed nested groups (globalViewSettings,
|
||||
globalReadSettings, kosync, readwise, hardcover, dictionarySettings). Adding a
|
||||
new nested whitelist group WITHOUT a matching deep-merge case there means the
|
||||
shallow merge REPLACES the whole sub-object with the partial patch, wiping
|
||||
sibling per-device fields (here: `webdav.enabled`/`deviceId`/sub-toggles would
|
||||
be clobbered to undefined on every pull). Always add the `if (patch.X)` deep
|
||||
merge when adding `X.*` paths to `SETTINGS_WHITELIST`.
|
||||
|
||||
Encrypted-path plumbing is generic: `ENCRYPTED_PATHS` in replicaSettingsSync.ts
|
||||
derives from `SETTINGS_ENCRYPTED_FIELDS`, and replicaPublish uses
|
||||
`adapter.encryptedFields` — so listing the two webdav fields there is enough
|
||||
to gate them behind the credentials toggle + crypto session.
|
||||
|
||||
Also updated the credentials-category description (SyncCategoriesSection.tsx)
|
||||
to list WebDAV; migrated the i18n key in all 33 locale JSONs manually (NOT via
|
||||
`pnpm i18n:extract`, which prunes valid keys — see [[i18n-extract-prunes-keys]]).
|
||||
|
||||
Related: [[multiwindow-settings-clobber-4580]], [[webdav-metadata-sync-4756]],
|
||||
[[webdav-connect-nullified-4780]].
|
||||
@@ -76,6 +76,7 @@ docs/superpowers
|
||||
.context/
|
||||
.claude/settings.local.json
|
||||
.claude/skills
|
||||
.claude/plans
|
||||
|
||||
# Playwright web e2e
|
||||
/test-results/
|
||||
|
||||
@@ -137,6 +137,19 @@
|
||||
|
||||
|
||||
|
||||
</intent-filter>
|
||||
<intent-filter >
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- ChromeOS ARC++ uses a different action for deep links -->
|
||||
<action android:name="org.chromium.arc.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="com.googleusercontent.apps.209390247301-ctpmep68ppfa56r1b8tr35e4qi4p60kq" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</intent-filter>
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
<!-- tauri-file-associations. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
|
||||
Reference in New Issue
Block a user