* fix(reader): fill the drawing dock's bottom safe area with its own background
The dock was lifted above the home indicator by the bottom inset, leaving a
transparent gap where the reader footer showed through. It now sits at the
bottom edge and pads the inset with its own background.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(library): title the bookshelf editor with the selected shelf on phones
On phones the selected shelf's name becomes the page title beside the back
button and the tabs, icons only, get a centered row of their own, so the
built-in shelves and the add button fit on one line. Wrapped rows are now
centered on the screen rather than within the slot beside the back button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): keep a hidden mini player hidden through brief stalls between paragraphs
The buffering probe catches the short wait while the next paragraph loads,
which re-showed the card and re-armed its linger, and the end of buffering
showed it again. A hidden card now comes back only when a stall lasts 500ms,
and the end of buffering just restarts the countdown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): only full-height sheets clear the status bar and show a back button
A content-height sheet such as a confirmation, or a sheet snapped short of
the screen, rests at the bottom: it no longer pads its drag handle for the
status bar or shows the back button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): stop the word highlight stepping back at a sentence start on iOS
chunk-start fires as the item is handed to AVPlayer, which can take a few
hundred ms to open it. The media clock ran ahead from the event and lit the
second word, then the next poll pulled it back to the first. The clock now
holds until a poll sees the item playing, and the position reports playing
from timeControlStatus rather than the rate, which is set while loading.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): preload the next chapter's opening for Edge TTS and custom voices
Only windowed engines carried preloads past a chapter end, so with Edge TTS
or a custom service a chapter change started with nothing synthesized. A new
preloadsAudio capability marks engines whose preloads keep the audio; they
now fill their usual handful of paragraphs from the next chapter too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: run selected unit test files with pnpm test --run
pnpm test -- <files> forwards the separator and runs more than the files
asked for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(agent): update agent memories
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): keep the iOS word clock from stepping back on pause or running ahead on resume
Pausing froze the clock at the last poll, up to 250ms behind where it had
got, so the highlight could step back a word. Resuming restarted it before
AVPlayer was rendering again. Pause now freezes the extrapolated position,
and resume polls and lets the player report when it is playing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): preload the next chunk's opening at the end of a chunk
A section over 512 KB of HTML is read in chunks, and the live document ends
at each chunk's end. The cross-boundary preload took that for the chapter's
end and preloaded the next section, so moving to the next chunk found nothing
synthesized. It now starts a throwaway reader at the next chunk's anchor in a
fresh copy of the section, and only goes on to the next section after the
last chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ui): drag a content-height sheet up instead of setting its height
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): use the contrast button for play/pause in the full player
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): preload the next section in the language it is read in, and more
- A section file with only xml:lang is rendered with the book's language as
its lang, which is what the live read speaks it in. The fresh copy used to
preload the next section kept xml:lang (zh-CN vs zh), so its audio was
cached under keys playback never looked up and each new section started
with a wait. The fresh copy now takes the book's language the same way.
- E-ink drops the tinted sentence under the word highlight in word mode.
- The lyric sheet follows the document read, not only the section index, so
a section read in chunks no longer shows the first chunk's lines under the
next chunk's position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(tts): resize the full player on desktop and size the lyric text
- A corner grip resizes the full Read Aloud player with the pointer on
desktop, up to the whole window, and the size is kept for the next time
it opens. Once sized, the lyric sheet takes the height it is given.
- A−/A+ in the lyric view step the line size from 12 to 38px, kept per
device.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tts): keep the lyric size and player size working when storage is blocked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(linux): debug the CEF build over the DevTools protocol
The Linux CEF runtime is Chromium, so the app window is a normal CDP
target — the only way to inspect the running desktop app on Linux, since
WebKitWebDriver (pnpm tauri:dev:test) drives the wry runtime instead.
`READEST_CDP_PORT=9222` passes the port to CEF as `--remote-debugging-port`
through the runtime's command-line args. CEF honours that switch straight
off argv too, but tauri-plugin-cli parses the same argv for open-with paths
and warns about the unknown argument on every launch, so the env var is the
supported way in. It works on any CEF build — a packaged .deb or the
Flatpak, not just `pnpm tauri:dev:cdp`.
scripts/cdp.mjs is the client, dependency-free (node's global fetch and
WebSocket): `pnpm cdp <targets|eval|click|screenshot|logs>`. The endpoint
listens on loopback only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(desktop): open the first window at 1280x800
macOS has always opened at 1280x800; Windows and Linux opened at 800x600,
which is cramped for the library grid and a two-page spread. Give every
desktop platform the same default, and clamp it to 90% of the monitor's
work area so it cannot open partly off-screen on a 1366x768 laptop — a
window whose title bar lands past the bottom edge can be impossible to
resize back. An unreported or empty work area keeps the default: a window
that is too large stays reachable, one sized from a bogus zero-height
screen does not.
Only the first launch is affected; later ones restore the saved geometry
through tauri_plugin_window_state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(opds): size the add catalog dialog like the catalog manager
The Add/Edit Catalog form took daisyUI's bare `modal-box` default: 512x889
against the 640x756 manager it opens on top of, so it sat narrower than the
window behind it and ran 89px off the bottom of a 1280x800 window, putting
its Cancel / Add Catalog row out of reach.
Both now size from one exported class string, so they cannot drift apart
again. `sm:max-h-full sm:max-w-full` is what defeats daisyUI's own max-w-lg
and max-height caps; everything is sm:-prefixed, leaving each dialog's
mobile treatment alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(library): stop the native scrollbar flashing before OverlayScrollbars
Reloading the library showed the platform scrollbar for ~1.8s and then lost
it, shifting the grid 15px sideways as it went.
Virtuoso owns the scroller inside the OverlayScrollbars root and gives it
`overflow-y: auto` inline, so the native scrollbar paints for as long as
OverlayScrollbars' deferred init takes, then vanishes when OS marks that
same element `data-overlayscrollbars-viewport="scrollbarHidden ..."`.
OverlayScrollbars ships hiding rules for `[data-overlayscrollbars-initialize]`,
but that attribute sits on the root we hand it, never on the child that
actually scrolls.
Extend the same pre-init convention to the scroller child. Keyed on
attributes Virtuoso and OS already set, so it also covers the reader
sidebar's TOC and book-note lists, and `:not([data-overlayscrollbars])`
scopes it to the pre-init window — once OS owns the root, its own viewport
styling takes over. It lives in globals.css so it is there at first paint,
unlike the overlayscrollbars.css import inside the component chunk.
Measured on reload: scroller gutter went 0 -> 15 -> 0 before, and stays 0
throughout after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cdp): clear the request timeout so the client exits when it is done
Every send() armed a 30s timer and nothing cleared it when the reply
arrived. A pending timer keeps node's event loop alive, so `pnpm cdp eval`
printed its answer and then sat there until the timer fired — 30.1s for a
command whose work took 0.1s. Clear it on reply, and drop the pending entry
when it does fire.
Measured against a stub CDP endpoint: `eval` went from 30.143s to 0.124s,
and a server that never answers still rejects with "timed out" at 30.1s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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