Commit Graph
403 Commits
Author SHA1 Message Date
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
hectorqinandClaude Opus 5.5 3b1f8b46dc feat: 时长统计 — reading, watching and listening time by day, month, year and work
Reading time is counted by the reader: only while the page is visible and the
reader has turned, scrolled or tapped within three minutes, or while 朗读 plays.
It is kept per book per 15-minute UTC slot and per device install, saved
locally until sent, and stored as MAX(stored, reported) in the new
reading_time table, so a replayed report never counts twice.

Media time is derived on the server from the progress saves the web player and
Android Media3 already send: the smaller of the wall-clock gap and the position
advance between two saves of one session, gaps over 30s ignored. It is stored
in media.db (media_watch_time), so neither client changes.

我的 → 时长统计 shows a stacked bar per day (one month), month (one year) or
year, with a legend that filters by kind, a table view and the works ranked for
the range or the bar picked. Administrators also get every account's totals.
Chart colours are new mode-level tokens (--color-chart-1..4), validated for
colour-vision separation on both surfaces. The React UI review now covers the
page and names the overflowing elements when a screen overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:55:52 +00:00
hectorqin c72e32bd06 Merge branch 'feat/tap-zones' 2026-10-31 10:27:51 +00:00
hectorqinandClaude Opus 5.5 a904bb5a83 feat(web): 点击区域设置 — a 3×3 tap grid with a per-cell action
The two-way 左退右进 / 左进右退 switch becomes nine cells, each 上一页, 下一页,
菜单, 上一章, 下一章 or 无操作, edited as a small page in 阅读设置 → 翻页 with
左右互换 and 恢复默认. The default grid is the old standard layout and a stored
`tapZone: 'reversed'` is folded into its grid. At least one 菜单 cell is kept,
right-to-left books mirror the grid, and the gesture layer now reports the row
as well as the column, from layout geometry like before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:27:51 +00:00
hectorqinandClaude Opus 5.5 bbc0ec0ee9 Merge branch 'feat/progress-history'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:52:14 +00:00
hectorqinandClaude Opus 5.5 21e2509c86 feat(web): 阅读记录 in 书籍信息 brings back an overwritten reading position
Tapping the title opens 书籍信息, which now ends with 阅读记录: the places this
account has been in the book on any device, newest first, each with its
chapter, percentage, device and time. The position on screen is pushed
before the list is read, so its entry is marked 当前; pressing another entry
goes back there and saves it as the position. Offline the section says the
history cannot be read; a failed read offers 重试.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:51:42 +00:00
hectorqinandClaude Opus 5.5 842331684a feat(server): keep a per-book reading-progress history
reading_progress keeps one row per book and last-writer-wins, which answers
"where am I" and loses "where was I": a stale device, a mistapped 目录 entry
or a clock running ahead overwrote the place for good (P0: 进度历史).

Every valid progress write now also lands in reading_progress_history,
including a write that lost the merge. A device reading on through one
chapter updates its latest entry (same chapter, within 5% and 30 minutes)
instead of adding rows; another chapter, a jump, another device or an
out-of-order write is a new entry. The newest 50 per book are kept.
GET /api/v1/sync/progress/:bookId/history lists them, newest first, for the
signed-in account only.

The table is new (CREATE TABLE IF NOT EXISTS), so the schema change is
additive and older builds sharing the database ignore it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:51:42 +00:00
hectorqinandClaude Opus 5.5 67954ba592 Merge branch 'fix/reading-continuity'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:13:44 +00:00
hectorqinandClaude Opus 5.5 0115dbd81a feat(web): in scroll mode, pulling past a chapter's end opens the next one
Taps and keys already crossed chapters, but a reader scrolling by hand reached
the last line and had to find 下一章 (#702, #698). A touch pull of more than
72px that starts at the end of a chapter, or wheel scrolling past it, now
opens the next chapter; the same at the top goes back. Only a gesture that
begins at the edge counts, so arriving at the end does not turn by itself,
and a cooldown keeps trackpad momentum from turning a second chapter. Touch
events are used because the browser cancels pointers once it scrolls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:56:36 +00:00
hectorqinandClaude Opus 5.5 5cddc9ef1e fix(web): 首页 and the shelf show the progress just read when the reader closes
Leaving the reader left the new position to the sync debounce, and 首页's
继续阅读 and the shelf's 最近阅读 order were served from a query cache that stays
fresh for a minute, so coming back from a book showed the old chapter and
percentage (#663). Closing the reader now saves the position, pushes the
outbox, and then re-reads the home and shelf queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:56:36 +00:00
hectorqinandClaude Opus 5.5 20a85c33fe fix(web): restore the ⋯ menu row styles removed as dead CSS
a894bdd deleted .action-menu-item/-label/-icon/-check/-description and the
.sheet-list-icon/-check/-description rules as unused, but ActionMenu builds
those names as `${base}-item` (base 'action-menu' or 'sheet-list'), which the
dead-class scan did not recognise. Every ⋯ dropdown and its bottom-sheet form
lost their row layout, hover, danger colour and check mark. Both files are
restored to their state before that commit.

The class-usage test now covers every stylesheet (not only media) and
understands built names: `${expr}-suffix` with a literal prefix, and
`prefix-${expr}`. Library markup (Notyf, DPlayer) is exempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:47:19 +00:00
hectorqin 4c20771e31 Merge branch 'ui-ux-optimization' 2026-10-30 10:41:22 +00:00
hectorqinandClaude Opus 5.5 a894bdd198 chore(web): remove CSS for classes nothing renders outside the media styles
32 class names in reader.css, action-menu.css and sheet.css had no reference
in the source (the old quick-settings row, manager screen and selection bar,
catalog source heading, source capabilities and editor, action-menu and
sheet-list item parts, …); their rules, or their members of selector lists,
are removed. Classes built at runtime (indicator-*, reading-indicator-*) and
Notyf's own are kept.

The reader review no longer checks for a .library-preview-hint the browsing
page stopped drawing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:49:37 +00:00
hectorqinandClaude Opus 5.5 cdd9abd813 chore(web): remove unused app and media exports
LoadingPage and isApiError; the media search's back/forward restore
(restoreSearchHistory, its history tagging and saved-entry map,
clearSearchReturn, restoreSearchReturn), which nothing ever called — the
in-memory return to search still works; SeriesPlayback and its styles;
useMediaRequest, useMediaDetail, useMediaPlayback and searchQuery; and the
short route aliases at the end of MediaItemManagementPages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:49:37 +00:00
hectorqinandClaude Opus 5.5 d8b3491e95 chore(web): remove the reading engine and helpers nothing runs
An import graph from main.tsx shows a previous reader still in the tree:
render/{reader,paged,pdf,text,container,theme}.ts with lib/{events,locator}.ts,
ui/settings.ts and bridge.ts — superseded by ReaderScreen/ReaderView and
never imported. Also unreachable: render/paginator.ts, render/window.ts and
ui/shelf-order.ts (only their own tests used them; the shelf sorts on the
server), formats/remote-zip.ts (re-exported, never called; chapters are
fetched per section), ui/source-capabilities.tsx and the sources pages barrel.

Inside live modules: PrefetchCache (only shouldPrefetch is used),
textToParagraphs / paragraphElement / endsAtParagraphBreak (the streaming
TXT reader's carry), txt.ts's textToHtml alias, and ChunkCursor. Their tests
go with them; the node:test list, the Vitest excludes and the docs that
named these files are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:49:37 +00:00
hectorqinandClaude Opus 5.5 12fbe0cbcd chore(web): remove the icon font toolchain and unused icon names
The UI draws Lucide SVGs through ui/icon.tsx and has not loaded the icon
font for a while; the font, its code-point table and the generator only fed
their own tests. Removed: tools/icons (generator, glyph paths, stroker),
src/styles/reader-icons.ttf, src/ui/icon-names.ts, the `icons` npm script and
the font/metrics tests. icons.test.ts keeps the contracts that still apply:
no borrowed names (now checked against ICONS), only icon.tsx imports
lucide-react, the .icon rule and icon buttons. The .icon rule drops its
font-era declarations.

ICONS loses the names nothing draws (the Font Awesome spellings kept as
aliases, add-circle, film, line-height, indent, sign-out); the reader's 朗读
button is `read-aloud` rather than `volume`. Docs and CLAUDE.md describe
adding an icon in icon.tsx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:48:04 +00:00