Browsing shared the 搜书 card: a row of plain buttons (back and paging
among the categories) above the search results area. It now has its own
tab, shown when a source can be browsed: a breadcrumb of the levels
walked, the levels below as wrapping chips with a filter for long lists,
and books as a cover list that grows with 加载更多. 搜书 only searches.
Co-Authored-By: Claude <noreply@anthropic.com>
Every shelf book now has 更换封面. A local book searches a source library
the reader picks by the book's title and lists same-title editions that
have a cover. The chosen image is read through the source, validated and
stored as a manual `cover` override, so a rescan never replaces it; the
book can go back to the cover its file carries.
Co-Authored-By: Claude <noreply@anthropic.com>
Server: catalog entries may carry `coverRef`, read through the source as
a validated raster image (`GET /sources/:id/cover`), saved as the book's
cover on acquisition (and adopted on a manual refresh for older books),
and replaceable from another edition (`POST /books/:id/cover-source`,
`GET /books/:id/cover-candidate`). Cover URLs carry a version so a
changed cover is not served from the immutable cache. Directory items
keep optional `volume` and `note`.
Web: the directory draws a heading above each volume's first chapter and
a quiet note under a chapter title; search results show a lazily loaded
cover; shelf books from a source get 更换封面, which lists the covers
other sources have for the same book.
Co-Authored-By: Claude <noreply@anthropic.com>
A source that browses through navigation entries (the Legado explore
tree) returns pages with links and no books; those showed "没有找到匹配书籍"
and never said where the reader was.
Co-Authored-By: Claude <noreply@anthropic.com>
- MediaSelect moves to ui/select.tsx as Select (old name kept as an alias);
its colours fall back to the theme tokens outside 影音
- sources, reader panels and admin pages use it; business settings default
to it
- a test rejects a native <select> anywhere but the two select components
- review scripts target the hidden native element
Co-Authored-By: Claude <noreply@anthropic.com>
- shelf and library restore the scroll position when coming back from a
book; the scroll-memory hook moves to shared/ and takes the container
- every native select draws the same currentColor arrow instead of none
or the platform's own
- service configuration: an explained empty state instead of an empty
select when the server returns no groups
- home page test waits for the series title instead of a fixed 20ms
Co-Authored-By: Claude <noreply@anthropic.com>
- search page: searches as you type (debounced, replace history), keeps
recent searches per account on the device, "/" focuses search, the
sidebar field steps aside on the search page, and phones get an entry
from the home header (there was none)
- shelf on phones: 书库/书源 move into the title row so the first cover
starts ~50px higher
- back button of 书库, 文件管理 and 书源 uses the media page frame's geometry
- reader settings: 点击区域 and 电纸书预设 fold away below the rows that
change between books
- ui:review now covers login, home, search and a dark shelf, and asserts
the back button geometry and the phone shelf's first cover position
Co-Authored-By: Claude <noreply@anthropic.com>
- describeError: show the server's own Chinese wording, replace transport
and proxy diagnostics with what kind of failure it was; used by search,
shelf, library, sources and the media page frame
- account page gains a sync panel (state, reason, last success, retry);
the sidebar and 我的 link to it when sync is offline or failing
- per-route document titles; the reader uses the book title
- reader says when the restored position came from another device
- file manager: pick a move destination by walking folders instead of
typing a library-relative path
Co-Authored-By: Claude <noreply@anthropic.com>
Scan IO
- The walk decides whether a path is absorbed (page of a directory book,
stray image on a shelf) before stat'ing it, replacing the separate
recursive listing of every claimed directory.
- Ancestor checks are set lookups along the path instead of prefix scans
over every shelf/claimed directory (quadratic on large libraries).
- Directory handlers may provide fingerprint(); an unchanged comic folder
is settled from its listing instead of re-parsing and re-reading its
cover every scan.
Memory
- File handlers may provide parseFile(ctx, {size, contentHash}). The
scanner then hashes the file as a stream and settles identical bytes
under a touched mtime before parsing. PDF, CBZ and EPUB implement it;
EPUB metadata/cover/spine are read entry by entry through ZipArchive
(falling back to the buffered path for archives it cannot open).
parse(buf) is unchanged for acquisitions.
Synthetic library (1k shelves, 100 comics / 20k pages, one 300MB PDF):
unchanged rescan ~3.6s -> ~0.9s; first-scan peak RSS +352MB -> +78MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- comic-directory: `!isBookCollection()` negated a Promise, so chapter
folders inside a volume (第01卷/第001话/*.jpg) were never collected.
Await it, count only real book files as a collection (ComicInfo.xml
and other sidecars no longer veto a chapter), and keep the root's
loose-page pass non-recursive so volume pages are not counted twice.
- scanner: remember the handler that claimed a directory instead of
re-resolving it with Array.find over an async predicate; drop the
unused directory signature; list the tree once per scan and memoise
"holds a book file" across discovery and the shelf pass.
- ai: one splitPathBatches for jobs and direct scans; folders larger
than batchSize are split instead of overflowing a prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SwitchRow styles were scoped to `.sheet`, so on 偏好设置 › 书架 the
显示作者/显示阅读进度 switches rendered as empty transparent buttons. Unscope
them, and restore the pill over the media page's generic button rule.
主题已保存 and theme save errors were inline <p> lines; they now go through
the global notice (ui.md §3.6), one notice updated in place per panel.
ui:review captures the shelf section and measures the switches and the
floating theme notice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The real cause of the CI failure: createMemoryRouter builds a fetch Request for
every navigation, and on Node 24 (CI) undici rejects jsdom's AbortSignal
("RequestInit: Expected signal to be an instance of AbortSignal"), so no click or
Back ever navigated. Locally Node 22 accepts it. The test now mounts a plain
MemoryRouter, like every other test, and reads location / navigation type /
navigate through a probe component.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed every test in media-list-return.test.tsx: each ran ~550-650ms, the whole
budget of settle()'s 50 x 10ms polls, because the first render is slower on the
runner than locally. settle() now polls for up to 4s (under vitest's 5s test
timeout); media-catalog-page.test.tsx had the same helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 影音浏览 preferences finally take effect: cover density (phone 3/2 columns),
default catalog sort, the continue card (channel and 首页) and the 来源库
selector, read live from the per-device preferences.
- A catalog or favorites page past the end clamps to the last page instead of
showing nothing / claiming there are no favorites.
- 演播者 shows the 有声书 category tabs (shared CatalogTabs).
- The channel ⋯ menu offers 媒体库管理 only to administrators.
- History and search restore their scroll on back (also back after forward);
a detail opened from search returns to the search; browser Back between two
searches shows the earlier search again.
- The player's queue re-renders after 上移 / 移除.
- Media writes invalidate the cached queries they change (editions, metadata,
hierarchy, libraries, recognition…); item details are always rechecked, so
刷新资源状态 sees a rescan.
- Aborts surface as ApiError('aborted') everywhere (including body reads), so a
shared request aborted by one caller is retried for a live one and never shows
"signal is aborted without reason".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
外观与主题, 书架, 影音浏览 and 播放 were four stacked sections on one long page.
They are now tabs (role=tablist, arrow keys, one tab stop): only the selected
panel renders, ?section= picks it (外观与主题 when absent) and switching replaces
the history entry, so back still leaves for 我的.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every script under web/tools and web/tools/ui-review had drifted from the app
(login lands on 首页, AppNav instead of the channel menu, 我的 pages instead of
sheets, query-string paging and folders, the redesigned plugin, source and
library screens). Each is updated to the current flows; checks for features that
no longer exist are removed (the v2 settings home and media palettes, the
channel menu, the 刷新 button, debounced library search, the sheet-based 添加书源
and 书源登录, the per-script track filters, …).
media-smoke and media-review-smoke run to the end but still fail on genuine app
regressions they record (listed in their output) rather than hiding them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 偏好设置 › 打开播放器 always opened the video player; it now opens the playing
channel's player (playerPath, shared with the mini player's link).
- The player's panel and back navigation dropped item/part from the address, so a
refresh could not restore playback; the address now follows the playing part, and
a restoring link shows 正在恢复播放… instead of flashing 当前没有播放内容.
- A link that cannot be restored no longer leaves another video expanded over its
explanation; the other playback stays in the mini player.
- The first category tab of 音乐 and 有声书 was never marked current (the bare
channel path redirects to it).
- A navigation query shared by two callers handed the first caller's abort to the
second (a detail opened right after login showed "request aborted").
- The metadata and match pages' fixed header covered the sidebar at >=701px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A book downloaded for offline reading did not open offline: nothing ever writes
books into the offline mirror, so the fallback found nothing. The reader page now
falls back to the book record the download stored in its manifest.
- 书源登录 reopened on the cached credential status after saving; saving now
refetches that source's status.
- A newly installed/upgraded/enabled/removed plugin's source types did not appear
in 添加书源 for up to five minutes; plugin changes now refetch the type list.
- The extension page did not scroll its selected tab into view on narrow screens.
- The shelf settings sheet's sticky headings stopped 14px below the header again
(the padding rule was lost when .panel-body became .sheet-body).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
我的 mixed sheets (书架显示, 系统设置, OPDS nested in the shelf sheet) with pages
(外观与主题, 影音浏览, 播放), and the personal preferences were four separate rows.
- 偏好设置 (#/me/settings) holds 外观与主题, 书架, 影音浏览 and 播放 as sections of
one page, with a jump bar and ?section=; every control applies at once (影音浏览
loses its save button and unsaved-changes prompt). The shelf's gear still opens
the same fields (ShelfSettingsFields) as a sheet shortcut.
- 系统设置 becomes three admin pages of 我的 › 管理: 用户管理, 注册与邀请, 服务配置
(#/me/admin/*, lazily loaded).
- 连接外部阅读器 (OPDS) is a page (#/me/opds) under 账号, also reached from the
书架 section and the shelf sheet; back returns to whoever opened it.
- Old /me/theme|browse|playback and /media/:channel/settings[/:panel] redirect to
the matching section; /me/account keeps its page.
- Drops MediaSettingsPage and the CSS only it used; docs follow the new paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Books, video and audiobooks in 「继续」 are the same card (cover, work title,
episode/chapter, progress), most recent first, up to two books. An episode
resolves to its series for title and poster instead of reading "第 1 集".
- New rows 看点什么 / 听本书 / 听张专辑, each switching between 最近添加 and
随便看看 (with 换一批); hidden when the channel is empty or unavailable.
- Server: browse accepts sort=random and sort=added (newest available file
mtime across the work and its episodes/tracks; no schema change).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page borrowed media-utility-workspace, whose layout expects the settings
screens' own .media-settings-heading, so the frame's header was left unstyled:
on desktop it sat ~100px down inside the content column instead of pinned at
the top like 影音浏览 and 播放历史. It is a content page, so it now wears
media-secondary-page as 播放历史 does.
The React UI review also stubs /api/v1/ai/capabilities, which 扫描与刮削 asks
since the AI-switch change and the media fixture does not serve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main.ts registers the routes with a placeholder context and attaches the
services afterwards, so the stats route captured ctx.shelf while it was still
undefined and every summary failed with "Cannot read properties of undefined
(reading 'getMany')". Writes never touched the shelf and were unaffected. The
service is now built on first request; a new test builds the app the way
main.ts does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/api/v1/ai/capabilities now also answers `scan`: 启用 AI and 启用 AI 扫描 on and
Base URL, key and model filled in. 扫描与刮削 shows the AI 扫描 tab only then; while
the answer is unknown there is no tab, a ?tab=ai link falls back to 媒体库扫描,
and a server without the endpoint keeps the tab as before. The answer is
re-asked each time the page opens, so a switch changed in 系统设置 takes effect
on the next visit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>