Replaces the URL-only placeholder extension with a full MV3 page-clipper
that builds a self-contained EPUB on the user's machine and uploads it
to the inbox.
- Captures the rendered DOM in a content script, then runs Readability,
asset bundling, and EPUB build through the shared
`convertToEpub({kind: 'page'})` pipeline inside a Chrome offscreen
document (the SW lacks DOMParser).
- Uploads the resulting EPUB directly from the offscreen page to the
new `POST /api/send/inbox/file` endpoint — keeps the bytes in one
realm because `runtime.sendMessage` JSON-serialises ArrayBuffer to
`{}` between extension contexts.
- Adds a long-lived Port + ping handshake between SW, offscreen, and
the on-demand capture content script so neither idle-eviction nor
load-order races can hang the popup.
- Localised popup, badge feedback, key-as-content i18n (`_('English source')`)
with an extract script that seeds locale stubs from i18n-langs.json
and writes a static-imports map for the runtime. All 33 locales
fully translated.
- Server: `pages/api/send/inbox/file.ts` accepts a raw EPUB body
(Content-Type: application/epub+zip), enforces the inbox pending cap,
stores to the existing send-inbox R2 bucket as `kind='file'`.
`assetBundler` now sets `credentials: 'include'` in the non-Tauri
branch so the extension SW carries paywalled-CDN cookies.
- 47 vitest cases for the extension shell (upload, badge, auth, lazy,
popup state machine, auth-bridge token sync) + 8 cases for the new
server endpoint. CI's `test_web_app` invokes both via the extended
`test:pr:web` plus a `build-browser-ext` step that catches webpack
alias / Tauri-stub regressions.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users can now tap "Share → Readest" in Safari, Chrome, or any other
browser on iOS / Android and the article URL flows through the same
clip-and-import pipeline the in-app "From Web URL" entry uses.
Android
`MainActivity.handleIncomingIntent` already routed file shares via
`ACTION_SEND` + `EXTRA_STREAM`. Extend it to also pick up URL shares
via `ACTION_SEND` + `EXTRA_TEXT`: parse the first http(s) token out of
the text payload and dispatch it on the existing `shared-intent` event
channel. No new event channel needed — `useAppUrlIngress` already
listens and re-broadcasts as `app-incoming-url`.
The existing `<intent-filter>` for `ACTION_SEND` with `*/*` MIME type
already accepts `text/plain` from browsers — no manifest change
required.
iOS
`gen/apple/` gains a new ShareExtension target. The extension's
`ShareViewController` extracts a URL from `NSExtensionContext.inputItems`
(prefers `public.url`, falls back to first http(s) token in
`public.plain-text`) and forwards it to the main app as
`readest://clip?url=<encoded>` via the responder-chain `openURL:`
selector — the standard share-extension trick used by Pocket,
Instapaper, Matter, etc.
`project.yml` adds the ShareExtension target and switches the main app's
Info.plist / entitlements references to `INFOPLIST_FILE` /
`CODE_SIGN_ENTITLEMENTS` build settings instead of xcodegen's `info:` /
`entitlements:` blocks. That way the hand-tuned `Readest_iOS/Info.plist`
(CFBundleDocumentTypes, UTExportedTypeDeclarations, locales,
CFBundleURLTypes for readest://, applesignin, associated-domains for
Universal Links) is treated as an opaque input — xcodegen won't
regenerate it.
JS
New `useClipUrlIngress` hook subscribes to `app-incoming-url`,
unwraps `readest://clip?url=<encoded>` into the inner URL (the iOS
forwarding path), filters out file URIs and annotation deep links,
and runs each remaining http(s) URL through `clip_url` →
`convertToEpubWithWorker` → `ingestFile` — the same path `/send` uses.
Mounted alongside `useOpenWithBooks` and `useOpenAnnotationLink` in
both `app/library/page.tsx` and `app/reader/page.tsx` so shares
arriving while the user is reading still process.
Notes
- The PR targets `feat/send-clip-mobile` (PR #4252) since the share
pipeline depends on `clip_url` being available on mobile.
- iOS Share Extension built locally via xcodegen; the regenerated
pbxproj is tracked because gen/apple is gitignored.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(send): Send to Readest — multi-channel capture into your library
A Send-to-Kindle equivalent: email, web-upload, share, or one-click
capture books and articles into the cloud library; they sync to every
device.
Architecture (client-side processing): out-of-app channels drop a raw
payload into a per-user send_inbox; Readest clients drain it through one
shared ingestService.ingestFile(). The server never parses or converts.
- ingestService.ingestFile() — channel-agnostic import orchestration
extracted from library/page.tsx (DI-based, forceUpload support).
- send_addresses / send_allowed_senders / send_inbox tables + RLS + 4
SECURITY DEFINER claim/lease RPCs (migration 012_send_to_readest.sql).
- Conversion subsystem (DOCX/RTF/HTML/article/TXT -> EPUB) in a Web Worker.
- send-email Cloudflare Email Worker; inbox-drainer controller +
useInboxDrainer hook; /api/send/* routes.
- Send to Readest settings panel: inbound address, approved-sender
allowlist, recent activity, per-device drain toggle.
- /send web page (file drop + article URL) + SSRF-guarded fetch-url proxy.
- OS-shared files routed through ingestFile; Manifest V3 browser extension.
Security: inbox state changes only via SECURITY DEFINER RPCs (clients get
SELECT-only on send_inbox); approved-sender allowlist gates email;
SSRF guard on the one server-side URL fetch; inbox payload signed URLs
authorize against send_inbox.user_id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: run format:check in the pre-push hook
Biome format checking is fast (~0.4s), so gate pushes on it too — catches
mis-formatted files that bypassed the staged-only pre-commit hook before
they reach CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(send): address CodeQL security findings
- ReDoS (senders.ts): the email regex had ambiguous quantifiers around
the literal dot. Rewrote it linear-time (domain labels exclude '.')
and cap the input at 254 chars.
- XSS (convertToEpub.ts): run untrusted HTML through DOMPurify
(sanitizeForParsing — keeps document structure) before DOMParser, so
title extraction and Readability never parse executable markup.
- SSRF (fetch-url.ts): harden the host guard — block bare single-label
hostnames, IPv4-mapped IPv6, CGNAT/benchmark/multicast ranges, and the
unspecified address. DNS rebinding stays a documented residual risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace Prettier with Biome for formatting JS/TS/JSX/CSS/JSON. The CI
format check drops from ~23s to ~0.4s.
- Unify config into a single root biome.json (formatter + linter); the
former apps/readest-app/biome.json was linter-only
- Mirror the old .prettierrc.json style: 100 line width, 2-space indent,
LF, single quotes, trailing commas
- Enable the CSS tailwindDirectives parser for @apply in globals.css
- Convert // prettier-ignore comments to // biome-ignore format:
- Root scripts and lint-staged now run biome; apps/readest-app lint runs
`biome lint` (lint-only) so formatting stays a separate CI step
- Drop prettier + prettier-plugin-tailwindcss dependencies
Markdown/YAML are no longer format-checked (Biome does not format them)
and Tailwind class sorting is no longer enforced.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(e2e): add Playwright web e2e lane
Adds a web-layer end-to-end suite that drives the Next.js web build
(`pnpm dev-web`) in a real browser, complementing the existing
WebdriverIO suite that drives the Tauri shell.
- playwright.config.ts: single Chromium project, auto-starts dev-web
- e2e/pages: BasePage/LibraryPage/ReaderPage page objects
- e2e/fixtures/base.ts: suppresses demo-book auto-import for a
deterministic empty library
- e2e/tests: library shell + search, book import, reader open +
pagination smoke specs
- e2e/fixtures/books: synthetic sample book for import tests
- scripts: test:e2e:web, test:e2e:web:ui, test:e2e:web:report
Tests run unauthenticated against isolated browser contexts;
authenticated/sync flows are out of scope until a test account is
provisioned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): cover reading and annotation flows
Expands the Playwright web e2e lane beyond library/import smoke tests to
exercise the major reading and annotation features against the real
sample-alice.epub fixture (src/__tests__/fixtures/data/).
Reading (reading.spec.ts): open + page turn, TOC chapter navigation,
in-book search, font-size change via the settings dialog, bookmark
toggle.
Annotation (annotation.spec.ts): selection popup, create highlight,
change highlight color, add a note, delete an annotation.
- ReaderPage POM gains sidebar/TOC, search, settings, bookmark and
annotation actions; text selection is driven inside the section
iframe (synthetic drags do not produce a selection through nested
paginated foliate iframes)
- openBook fixture imports and opens a book so specs skip boilerplate
- books.ts centralises fixture book paths
- replaces the old reader.spec.ts smoke
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(e2e): add headed run script and always write HTML report
- test:e2e:web:headed runs the suite in a visible browser, one test at
a time, with traces captured
- the HTML reporter now runs for local runs too, so every run writes
playwright-report/ for test:e2e:web:report to open
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): fix headed-run flakes in reading and annotation specs
The headed run (slower rendering) surfaced two races that the headless
run happened to pass:
- TOC navigation read reading progress before the section's async
progress update landed — now polls with expect.poll.
- visibleSectionFrame required a paragraph fully inside the viewport,
which intermittently matched nothing — now accepts any paragraph
intersecting the viewport and tolerates frames detaching mid-navigation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: run the Playwright web e2e suite in test_web_app
Adds `pnpm test:e2e:web` to the test_web_app job, after the unit/browser
tests. The job already installs the Chromium browser, and `.env.web` is
committed so the auto-started `pnpm dev-web` server has its config. On
failure the HTML report is uploaded as an artifact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): exclude e2e specs from the vitest run
vitest's default glob matches `*.spec.ts`, so it picked up the new
Playwright `e2e/tests/*.spec.ts` files and crashed. Exclude `e2e/`
from vitest — those specs run via `pnpm test:e2e:web`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(e2e): run the web e2e suite against a production build
`next dev` renders a full-screen error overlay when the app emits its
`next-view-transitions` "Transition was aborted" unhandled rejection,
and the overlay intercepts pointer events — making the suite flaky on
CI. CI now builds the web app (`pnpm build-web`) and the Playwright
webServer serves it via `pnpm start-web`; local runs still use
`pnpm dev-web`. Verified: 14/14 pass against the production build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(e2e): run the web e2e suite in the build_web_app job
build_web_app already runs `pnpm build-web`, so the e2e suite belongs
there — it reuses that build (the CI Playwright webServer serves it via
`pnpm start-web`) instead of building a second time in test_web_app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): run the web e2e suite with 4 workers
Specs are isolated (a fresh browser context per test), so they are
safe to parallelize. `test:e2e:web:headed` keeps --workers=1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(koplugin/library): data layer + busted harness + design doc
- LibraryStore: per-user SQLite index merging cloud + local books by
partial-md5 hash. listBooks/listBookshelfBooks/listBookshelfGroups,
upsertBook with cloud_present/local_present OR-merge + _force/clear
sentinels, parseSyncRow, getChangedBooks for tombstone push.
- EXTS table mirroring web's document.ts.
- busted harness with KOReader stubs (G_reader_settings, DataStorage,
lua-ljsqlite3 against :memory:); spec_helper, exts_spec, smoke_spec,
librarystore_spec covering schema, sort, group nesting, dedupe.
- Library design doc + spec README.
- pnpm test:lua wired through root + app package.json; lint-koplugin
recurses into library/ + spec/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(koplugin/library): cloud sync — push, pull, upload, delete, downloads
- Spore methods: pullBooks (incremental /sync), getDownloadUrl,
getUploadUrl, listFiles, deleteFile.
- syncbooks.lua: pushBook + pushChangedBooks (advances watermark to
max(updated_at, deleted_at)), syncBooks(opts, mode) for push/pull/both,
downloadBook + downloadCover (sync socket.http with file sink; cover
download via fork+poll with single-slot queue + visible-page filter +
coalesced refresh), uploadBook (presigned-PUT flow + best-effort
cover), deleteCloudFiles (list-then-delete-each, mirrors
cloudService.deleteBook).
- SyncAuth.withFreshToken wrapper resolves the ensureClient race; 401/403
unified across syncconfig + syncannotations.
- Cloud + local book covers shared by partial-md5 hash; cover.png cached
at <settings>/readest_covers/<hash>.png with sentinel for known 404s.
- syncbooks_spec covers row-to-wire conversion + file_key shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(koplugin/library): local discovery — sidecar scanner + cover provider
- localscanner.lightScan iterates ReadHistory entries, reads
partial_md5_checksum from .sdr sidecars, and upserts local rows.
Slow filesystem walks deferred to fullSidecarWalk (24h-gated).
- coverprovider wraps BookInfoManager:getBookInfo for local books with
graceful FakeCover fallback when coverbrowser is absent.
- localscanner_spec + coverprovider_spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(koplugin/library): UI — widget, item, view menu, FileManager hooks
- librarywidget: full-screen Menu mixed in with CoverMenu + Mosaic/List
per zen_ui's group_view pattern. Title-bar tap → view menu, search via
left icon, drill-in/back for grouping. Async cloud sync deferred via
scheduleIn so the menu paints before HTTP fires.
- libraryitem: BIM patch for cloud-only (readest-cloud://) and group
(readest-group://) URIs; group-cover composer (2x2 mosaic for grid,
same in list) with cache key derived from the actual first-N hashes
for content-based invalidation; ListMenuItem update + paintTo patches
for wider list-mode cover strip and cloud-up/down icon overlay.
- libraryviewmenu: ButtonDialog with View/Group by/Sort by/Actions.
Default Group by = Groups (parity with web), values authors/groups.
- librarypaint: partial-page e-ink repaint shim adapted from zen_ui.
- main.lua: Library menu entry, dispatcher actions (Open Library / Push
/ Pull as general; progress + annotations as reader-only),
"Add to Readest" button in FileManager's long-press file dialog
(dedupe by partial_md5; bumps updated_at when present, inserts a
fresh local-only row otherwise; un-tombstones via _clear_fields).
Push books on Library open when auto_sync is on, pull-only otherwise.
- Long-press action sheet with Readest BookDetailView parity:
Remove from Cloud & Device / Cloud Only / Device Only,
Upload to Cloud, Download Book / Cover / All.
- Cloud-down + cloud-up SVG icons (LiaCloudDownloadAltSolid /
LiaCloudUploadAltSolid) painted in the right-side wpageinfo slot.
- i18n catalog updated for new strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(koplugin/library): split libraryitem into focused modules
libraryitem.lua had grown to 1018 lines mixing five unrelated
concerns. Split along the natural seams:
cloud_covers — readest-cloud:// URI scheme, on-disk <hash>.png
cache, single-slot async download queue, visible-page
filter
group_covers — readest-group:// URI scheme, 2x2 mosaic composer with
content-derived cache key (first-N hashes), cell
layout table
cloud_icons — bundled cloud-up/cloud-down SVG loader, IconWidget
cache, paint-overlay positioning
list_strip — list-mode group row builder (4-cover wider strip
replacing ListMenu's square cover slot)
bim_patch — BookInfoManager:getBookInfo router (cloud / group /
local) + ListMenuItem update + paintTo patches; owns
the _library_local_paths set and orig BIM reference
libraryitem.lua is now 141 lines: just the entry-table constructors
(entry_from_row, entry_from_group, entry_back) plus thin install /
set_visible_hashes delegates. Each new module is 88-216 lines.
No behavior change — same 113 specs pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(koplugin): co-locate dev tooling, ship-zip exclusions, CI job split
- apps/readest.koplugin/scripts/build-koplugin.mjs: local sideloading
build with the same exclusions the release workflow uses.
- Move lint-koplugin + test-koplugin from apps/readest-app/scripts/ to
apps/readest.koplugin/scripts/. All koplugin dev tooling now lives
with the koplugin and is excluded from the published release zip.
- Rename to .mjs so Node treats them as ESM without the reparse warning
(the i18n CommonJS scripts stay .js).
- Release workflow: zip -r exclusions for scripts/, docs/, spec/,
.busted so dev artifacts don't ship to end users.
- PR workflow: split build_web_app into build_web_app + test_web_app
for parallelism. The test job installs luarocks + busted +
lsqlite3complete and runs pnpm test:lua. test-koplugin.mjs now
hard-fails (instead of soft-skipping) when CI=true and a tool is
missing — a broken CI toolchain previously exited 0 silently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- i18n loader at apps/readest.koplugin/i18n.lua: isolated callable,
falls back to KOReader's gettext when a string is untranslated
- Translation catalog at locales/<i18next-code>/translation.po for 31
languages, mirroring apps/readest-app/public/locales/
- scripts/extract-i18n.js (scan _("...") and _([[...]]); preserve
existing, drop obsolete, add new) and scripts/apply-translations.js
(bulk import from /tmp/koplugin-translations/<lang>.json)
- Mirror apps/readest-app SyncInfoDialog: rename showMetaHashInfo to
showSyncInfo, dialog title "Sync Info", new Last Synced row computed
as max(last_synced_at_config, last_synced_at_notes) from doc_settings
- syncconfig.lua / syncannotations.lua mark per-book sync timestamps
on push/pull success
- Rename "Meta Hash" -> "Book Fingerprint" in koplugin and
apps/readest-app SyncInfoDialog.tsx; translations propagated to
all readest-app locales
- "book config" -> "reading progress" wording across user-facing
strings (matches QiuYukang fork terminology)
- Replace "Log out as " / "Login failed: " concat prefixes with
T(_("...%1..."), arg) placeholder pattern (RTL / verb-final friendly)
- pnpm lint:lua: luajit -b syntax check across koplugin .lua files;
soft-skips when luajit is missing locally; CI installs luajit and
runs the check unconditionally
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(reader): custom dictionaries (StarDict + MDict)
Adds a pluggable dictionary provider system. Built-in Wiktionary +
Wikipedia (extracted from the legacy single-popup model into a tabbed
shell) plus user-importable StarDict (.ifo/.idx/.dict.dz/.syn) and MDict
(.mdx/.mdd) bundles.
Settings → Language → Dictionaries: import / enable / drag-reorder /
delete (delete-mode toggle mirrors CustomFonts). Drag uses @dnd-kit with
pointer/touch/keyboard sensors. Reader popup: tabbed UI, per-tab lookup
history, scroll-aware back button, last-active tab persists. Tabs grow
to natural width up to a cap, truncate with ellipsis when crowded; phantom
bold layer prevents layout shift on focus.
StarDict reader is self-contained (replaces unused foliate-js/dict.js),
with lazy random-access binary search on .idx + .syn (~420 KB Int32Array
of byte offsets vs ~10 MB of parsed JS objects), lazy DictZip chunk
decompression via fflate streaming Inflate (cmudict/eng-nld both
chunked), and an optional .idx.offsets sidecar generated at import to
skip the init scan. Cmudict 105K-entry init drops from ~10 MB heap and
2 MB IO to ~1.7 MB heap and ~500 KB IO.
MDict uses the readest/js-mdict fork (added as a submodule, consumed via
tsconfig paths so deps stay out of readest's pnpm-lock) which adds a
browser-friendly BlobScanner reading via blob.slice(...).arrayBuffer()
— slices are lazy when the Blob is Readest's NativeFile / RemoteFile.
encrypt=2 (key-info-only) MDX is fully supported via ripemd128-based
mdxDecrypt; encrypt=1 (record-block, needs user passcode) surfaces as
unsupported.
Wikipedia annotation tool removed (Wikipedia is now a tab inside the
unified popup); legacy WiktionaryPopup / WikipediaPopup deleted. Stale
annotationQuickAction === 'wikipedia' coerced to 'dictionary' on settings
load. iOS-friendly external links: skip target="_blank" on Tauri to
avoid the WebView's "open externally" path triggering the shell scope
error; the popup's container click handler routes through openUrl.
i18n: 939 strings translated across 31 locales (30 base keys + CLDR
plural forms for ar/he/sl/pl/ru/uk/ro/it/pt/fr/es).
Test fixtures bundled: cmudict (StarDict, 105K entries), eng-nld
(StarDict, smaller), and a Longman Phrasal Verbs MDX (encrypt=2).
3396 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(stardict): resolve fflate from js-mdict source for vitest + Next
js-mdict is consumed as TypeScript source via tsconfig paths from
packages/js-mdict/src/. Its sources `import 'fflate'` directly, but
fflate is only installed under apps/readest-app/node_modules — so
vite's import-analysis (and Next/Turbopack's resolver) can't find
fflate when it walks up from the redirected js-mdict source location.
CI's fresh checkout exposes this; locally a leftover
packages/js-mdict/node_modules/fflate from the old workspace setup
masked it.
Pin fflate resolution to apps/readest-app/node_modules/fflate in:
- vitest.config.mts (Vite alias)
- next.config.mjs (webpack alias + Turbopack resolveAlias — Turbopack
rejects absolute paths so use a project-relative form)
- tsconfig.json (paths entry so tsgo / Biome see it)
Verified by deleting packages/js-mdict/node_modules locally and
re-running pnpm test (3396 pass), pnpm lint (clean), and both
pnpm build-web and a tauri-platform Next build (clean).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(deps): update stripe
* fix: filter and sort not affecting purchases
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* refactor: rename restoredPurchases to restoredSubscriptions
The filter excludes one-time purchases, so the array contains only
subscriptions. Naming reflects that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): restore highlight options layout and clean up color name editing
Restore the pre-#3741 layout for both the AnnotationPopup highlight
options row and the HighlightColorsEditor settings panel.
HighlightOptions: re-add `justify-between` on the outer container and
remove `flex-1` from the color strip so the gap between the style box
and color strip responds to the number of colors.
HighlightColorsEditor: restore `grid-cols-3 sm:grid-cols-5` grid,
remove always-visible name inputs, and add a click-to-edit popover on
each color circle with hover tooltip for the label.
Add a browser screenshot test that renders the real AnnotationPopup
component with Tailwind CSS and compares against baseline PNGs for
5, 10, and 15 colors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ~290 new unit tests covering utils (lru, diff, css, a11y, walk,
usage, txt-worker), services (EdgeTTSClient, transformers), and
suppress noisy console output across all test files. Rename 27
camelCase test files to dash-case for consistency.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(rsvp): add persistent context, display settings, and layout improvements, closes#3333
Add always-visible collapsible context panel, font size adjustment,
ORP color selection, and stable context window that only rebuilds on
scroll. Refactor speed controls into playback row with collapsible
settings behind a gear icon. Translate new i18n keys across 29 locales.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(selfhost): update docker db schema to match FileRecords (#3527)
In #2636, FileRecord was defined to have updated_at field
which is used when it is accessed from the database. But the
local dev setup is missing this field.
This diff adds an updated_at column to match the expectation
* chore(ci): cache system dependency and rustc outputs in ci
* chore(ci): lint script changed from tsc to tsgo
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Azeem Bande-Ali <me@azeemba.com>
Set up WebDriver-based testing for the Tauri app with two tiers:
- Vitest browser-mode tests (*.tauri.test.ts) running inside the Tauri WebView
for plugin IPC testing (libsql, smoke tests)
- WDIO E2E tests (*.e2e.ts) for UI-level interaction testing
Key changes:
- Add webdriver Cargo feature gating tauri-plugin-webdriver
- Add runtime capability for remote URLs (webdriver builds only)
- Add vitest.tauri.config.mts and wdio.conf.ts connecting to embedded
WebDriver server on port 4445
- Add shared tauri-invoke helper for IPC from Vitest iframe context
- Add testing documentation in docs/testing.md
* added current time to desktop bar
* added time prototype to footer, needs code cleanup and settings toggle
* fixed settings toggle, added translations and code cleanup
* added battery support and moved Statusbar to own Component
* #3306 added 24 hour clock support
* refactored code styling and getting rid of any type in battery hook
* Add battery info for Tauri Apps
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
* feat: enable linking to settings items
* feat: integrate command palette with global state and styles
* feat: add command pallete ui and fuzzy search
* feat: add command palette with global state and styles
* feat: add command registry and search dependencies
* fix: open command palette with shortcuts in reader page
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>