418 Commits
Author SHA1 Message Date
hectorqin eaf5bfb04b Merge branch 'feat/explore-tab' 2026-11-13 02:30:31 +00:00
hectorqinandClaude 9875252d48 feat(web): 发现 is its own tab with a trail, category chips and a cover list
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>
2026-11-13 02:30:31 +00:00
hectorqin 4e9c9b7c30 Merge branch 'feat/local-book-cover' 2026-11-13 01:53:35 +00:00
hectorqinandClaude 1cf47d5c70 feat: change a local book's cover from a book source
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>
2026-11-13 01:53:35 +00:00
hectorqin 83a81c965d Merge branch 'feat/source-covers-volumes' 2026-11-13 01:27:32 +00:00
hectorqinandClaude 485ab38c2e feat: source covers, volume headings and chapter notes for chapter books
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>
2026-11-13 01:27:32 +00:00
hectorqin b9901fb0c5 Merge branch 'fix/source-browse-navigation' 2026-11-12 10:38:07 +00:00
hectorqinandClaude 56948900ac fix(web): show a browse page's title and no empty-results text on navigation pages
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>
2026-11-12 10:21:18 +00:00
hectorqin 67c4fbb2ab Merge branch 'feat/custom-select' 2026-11-11 06:01:08 +00:00
hectorqinandClaude 5eb38a974b feat(web): one themed drop-down everywhere instead of native selects
- 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>
2026-11-11 06:01:07 +00:00
hectorqin eff234749d Merge branch 'fix/ux-polish' 2026-11-10 08:23:34 +00:00
hectorqinandClaude 1aff7dfb8f feat(web): remember list scroll on shelf and library, one arrow for selects
- 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>
2026-11-10 04:32:24 +00:00
hectorqinandClaude d93a62f735 feat(web): live global search, compact phone shelf header, one back button
- 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>
2026-11-10 04:00:20 +00:00
hectorqinandClaude 99361a46dd feat(web): readable errors, sync status page, folder picker for moves
- 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>
2026-11-10 03:46:34 +00:00
hectorqin 6b85f7a0f0 Merge branch 'perf/project-optimization' 2026-11-08 05:20:02 +00:00
hectorqin dca46d909e perf: harden sync and authentication and defer route loading 2026-11-08 05:17:42 +00:00
hectorqin 74f617275b Merge branch 'fix/media-review-settings' 2026-11-07 12:13:52 +00:00
hectorqin 990cd20ee1 fix(test): initialize settings in media browser review fixture 2026-11-07 12:13:52 +00:00
hectorqin a4d3c08818 Merge branch 'feat/media-feature-controls' 2026-11-07 12:09:26 +00:00
hectorqin eeb508f880 feat(media): add global and per-user controls with reading-only mode 2026-11-07 12:09:26 +00:00
hectorqin 2db8464e17 Merge branch 'perf/pdfjs-range-loading' 2026-11-07 11:21:28 +00:00
hectorqin 7e66582f84 perf(pdf): render with PDF.js and authenticated range loading 2026-11-07 11:21:27 +00:00
hectorqin 813016d160 Merge branch 'perf/scan-io-and-streaming' 2026-11-05 09:18:23 +00:00
hectorqinandClaude Opus 5.5 84e04d84b3 perf(server): cheaper rescans, stream-hash large books
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>
2026-11-05 09:18:23 +00:00
hectorqinandClaude Opus 5.5 5c807c46d9 fix(server): nested comic chapters, directory handler dispatch, AI batch cap
- 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>
2026-11-05 09:07:46 +00:00
hectorqin dc6cbf5b2a Merge branch 'fix/settings-switches-notice' 2026-11-04 13:24:16 +00:00
hectorqinandClaude Opus 5.5 dd20dc6a87 fix(web): settings page draws shelf switches; theme saves report via Notyf
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>
2026-11-04 13:24:16 +00:00
hectorqinandClaude Opus 5.5 79c1c8e0aa Merge branch 'fix/ci-list-return-timing'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:23:58 +00:00
hectorqinandClaude Opus 5.5 d956723a83 test(web): list-return tests use MemoryRouter; data routers fail on Node 24
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>
2026-11-01 00:23:58 +00:00
hectorqinandClaude Opus 5.5 b58d2bc554 Merge branch 'fix/ci-list-return-timing'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:20:11 +00:00
hectorqinandClaude Opus 5.5 9eb1411017 test(web): give the list-return and catalog tests a time budget, not 50 polls
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>
2026-11-01 00:20:11 +00:00
hectorqinandClaude Opus 5.5 1410434a44 Merge branch 'fix/review-regressions'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:51:50 +00:00
hectorqinandClaude Opus 5.5 4b610a056f test(web): media reviews follow the tabbed 偏好设置 and record no stale regressions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:51:33 +00:00
hectorqinandClaude Opus 5.5 4ea2ea6096 fix(web): media browsing, lists, player queue and query cache regressions
- 影音浏览 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>
2026-10-31 23:51:33 +00:00
hectorqinandClaude Opus 5.5 a0a0fb5f2f feat(web): 偏好设置 shows its four kinds as tabs, one at a time
外观与主题, 书架, 影音浏览 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>
2026-10-31 23:51:33 +00:00
hectorqinandClaude Opus 5.5 401b4b8c01 chore: Task Master project config, env example and ignore rules
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:17:24 +00:00
hectorqinandClaude Opus 5.5 d247119ffe Merge branch 'feat/mine-settings'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:27:42 +00:00
hectorqinandClaude Opus 5.5 d2caf37634 test(web): bring the browser reviews up to date with the current UI
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>
2026-10-31 16:27:29 +00:00
hectorqinandClaude Opus 5.5 7044ab3ef7 fix(web): player, catalog and query regressions found by the media reviews
- 偏好设置 › 打开播放器 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>
2026-10-31 16:27:29 +00:00
hectorqinandClaude Opus 5.5 7810d5943e fix(web): five regressions the browser reviews caught
- 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>
2026-10-31 15:12:56 +00:00
hectorqinandClaude Opus 5.5 b8e5102669 feat(web): 我的 opens every row as a page; preferences on one page
我的 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>
2026-10-31 15:12:40 +00:00
hectorqin 6b58ffd93c Merge branch 'feat/home-recommendations' 2026-10-31 14:30:22 +00:00
hectorqinandClaude Opus 5.5 df93900236 feat: 首页 continue cards share one shape; recently added / random media rows
- 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>
2026-10-31 14:24:43 +00:00
hectorqin 71ce458acf Merge branch 'fix/stats-header' 2026-10-31 12:58:28 +00:00
hectorqinandClaude Opus 5.5 d505781cd2 fix(web): 时长统计 uses the standard secondary-page title bar
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>
2026-10-31 12:58:28 +00:00
hectorqin 77283c5171 Merge branch 'fix/stats-shelf' 2026-10-31 12:50:51 +00:00
hectorqinandClaude Opus 5.5 c0b8acd469 fix(server): 时长统计 reads 500 in production — resolve the shelf on first use
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>
2026-10-31 12:50:51 +00:00
hectorqin 8f2e415904 Merge branch 'feat/ai-switches' 2026-10-31 12:40:55 +00:00
hectorqinandClaude Opus 5.5 005b9be99e feat: AI 扫描 follows the AI switches, like AI 总结 already did
/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>
2026-10-31 11:55:19 +00:00
hectorqin eb54e0556b Merge branch 'feat/time-stats' 2026-10-31 11:45:48 +00:00