The media styles grew in layers and replaced screens left their rules behind
(old channel switcher, details-based menus, manager tabs/toolbar, match
confirm box, resume cover, settings groups, …). 56 class names had no
reference in the source; rules naming only them are gone, and dead members
of :is()/:where() lists are dropped while the live ones stay. Screenshots of
every ui:review scene match the previous build apart from timing noise.
media-css-classes.test.ts now fails when a media stylesheet names a class the
source never renders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- every typed field is at least --text-title on touch screens, so iOS does not
zoom on focus (guarded by design-tokens.test.ts); the search page too.
- notices float above the phone tab bar; shell and notices use --safe-* vars.
- unknown links land on 首页 instead of the video channel.
- 首页 shows loading and error states for 继续阅读 instead of 还没有在读的书.
- 我的 shows the sync engine's connection state, and 退出登录 asks first.
- SwitchRow names its button from its label and the whole row toggles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- a book that cannot open keeps saying why over the stage, with 重试 and 返回;
offline, a book downloaded for offline reading opens from its mirrored record.
- 返回 from a deep link or cold start goes to the shelf instead of leaving.
- keys are heard without focusing anything first; Space/Shift+Space page,
S toggles 朗读, and focused buttons keep Space/Enter.
- 右→左 mirrors taps, swipes, drags and arrow keys.
- the tap that closes a note card no longer also turns the page.
- 目录 opens at the current chapter; the 朗读 button shows 朗读中 while reading.
- the selection toolbar is placed by its measured height and flips above the
selection when it does not fit below.
- staged books fetch the next section while the current one is read, and a
section is never fetched twice at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 在线匹配 no longer opens the confirm sheet on load or after a search; only
pressing a candidate does. Candidate buttons name their candidate.
- 移除在线匹配 asks first and says a series' seasons and episodes lose their
online data too.
- admin pages show MediaScreenError's words (服务正在准备, 不可用, 离线, 无权限)
instead of raw error codes; the back link is named by its destination.
- 扫描全部媒体库 submits every library and reports which ones failed instead of
failing as a whole; scan records show a percentage like the library cards.
- metadata inputs are no longer shrunk to 13px on phones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- enable/pause/default, check interval, save and delete of a source catch
failures and say so; save/delete confirm with a notice.
- 自动追更 shows a load error with 重试 instead of the empty state.
- an empty credential field now keeps the stored value; each configured
credential has its own 清除 button behind a danger confirmation, and the
form is disabled while saving.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
书架:
- sort and filter are read from the URL on every navigation, so back/forward
and the nav's plain 书架 link clear a filter; ?sort= is validated.
- a page past the end clamps to the last page instead of showing 书架还没有书.
- ⋯ is an ActionMenu like every other ⋯, with 从书架拿掉 in the danger colour;
removal reports 从书架拿掉 N 本 or the error, and the confirmation uses the
same words and says the files on disk are untouched.
- the shared Pager replaces the unstyled hand-written one; load errors offer 重试.
书库 and 文件管理:
- 加入书架 reports its result or error; page numbers clamp; the folder listing
no longer follows the book page; the search box follows the URL.
- selection is cleared when the folder or page changes, so 删除 can no longer
act on files that are not on screen; delete confirmations name the files.
- each file action says what it did; both upload buttons report skipped files.
- members opening a 文件管理 link land on the same folder's browsing page, and
admins reloading it are not shown 无权访问 while /auth/me is pending.
- text glyphs (×, ›, +) are icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audit of README, docs/, android/README.md, android/contract.md and CLAUDE.md
against the current server, web and Android code: V3 navigation and settings
locations, Docker Hub release images, GitHub clone URL and CI, missing API
endpoints and error codes, architecture of lazy chunks/themes/sources, media
pages and routes, the Android bridge contract, and the new file-management and
member-upload permissions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Remove .cnb.yml and CNB references in the Dockerfile and Compose file; the
Compose default image is hectorqin/reader-pro:latest.
- Rename the Android applicationId/namespace and Kotlin package from
cool.cnb.reader to com.htmake.readerpro (shortcuts, ProGuard, shell check).
Installs under the old package are a different app and are not migrated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- browse move/rename/mkdir/delete/metadata now require an administrator
(403 ADMIN_REQUIRED for members).
- Members may upload, but only add books: unsupported formats are skipped,
zip/cbz are stored as comic archives rather than unpacked, and overwrite is
refused. The new business setting library.memberUpload (default on) turns
member uploads off (403 UPLOAD_DISABLED); GET /library/upload reports
`allowed`.
- The library page shows 上传书籍 to members when uploads are allowed.
- Shelf tests read user_books for the owner only, now that a second user exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
媒体库管理: 新建媒体库 and 扫描与刮削 are visible above the list, an empty
state offers 新建媒体库, and cards get a channel-coloured kind icon, a
指定用户 tag, a one-line directory, missing files in the warning colour,
开始扫描 for never-scanned libraries, and 扫描记录 in the ⋯ menu. Cards
take the full width when the server has no 扫描与元数据 switches.
扫描与刮削: scan and AI scan share one toolbar (library in ?library=,
refresh, a single primary action) instead of duplicated buttons; scan
records show their time, keep title/state/action on one line, put the
failure in a warning box followed by 重试, and have an empty state.
刮削 preselects the page's library, lays the form out in two columns
when wide, and keeps 已选 N and 开始批量匹配 pinned under the item list.
Styles live in features/media/styles/manager.css, loaded after v2.css.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Legado plugin now cleans by bookSourceType (text paragraphed, image
sources untouched); the reader only re-typesets text-only chapters, which
never includes a comic chapter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Source chapters are scraped fragments (`<div> text<br><br><br><br>`) served
as text/html, so the TXT cleanup never ran: every paragraph was followed by
three empty lines and the indent setting did nothing. Like legado-E, a
text-only chapter of a `chapters` book is read back as characters when it
is loaded (htmlToText) and typeset by the TXT path. Done at read time, so
chapters the server cached earlier are cleaned too; chapters with images
or tables keep their markup and EPUBs never take this path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Novels saved from a site keep the page's <div>, <br/> and in the
TXT. Escaped, the tags were drawn as text and the chapter ran together
as one paragraph. cleanTextMarkup turns block tags into line breaks,
drops inline tags, decodes common entities and treats whitespace-only
lines as blank, before paragraphs and the chapter heading are inferred.
Only real tag names match; other markup is still escaped as text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed 加入书架 (e.g. 502 EMPTY_RESULT) set an inline callout behind
the 书源列表 or detail sheet, so nothing appeared. Report failures,
authorization prompts and success with Notyf, which docs/ui.md uses for
source-page feedback and which stays above sheets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cd8f133 gave .sheet-body a zero flex basis so long lists scroll inside
full-height sheets. Compact sheets are content-sized, so the body then
contributed no height: confirmations, prompts and one-field forms showed
only their padding and clipped the text and inputs. Restore an auto basis
for compact sheets; they still shrink and scroll at the height cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"⋯ 管理" doubled the glyph with a label; row menus elsewhere are a bare
⋯, so keep 管理 as the accessible name only and give the trigger a 44px
target. A form with a single short field opens in a compact sheet, as
docs/ui.md specifies for single-line input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 我的来源 "管理" button and collapsible extension items (e.g. the
Legado source list) expanded an inline panel that changed the row; the
search options were a hand-built menu without Esc/outside-click handling.
All three now use ActionMenu, as docs/ui.md requires for row actions:
- 我的来源: plugin pages, 基本设置, 设为/取消默认, 暂停/启用.
- Extension items: a form without fields is a menu action (confirmed
first when it asks), a form with fields opens in a Sheet.
- 搜索选项: 继续搜索 / 重新搜索 keep their explanation via a new
MenuAction.description, exposed with aria-describedby.
Source rows keep one layout at every width now that nothing expands;
dead expansion and search-menu styles are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the React migration the input itself carries .catalog-source-search,
but the rules still targeted `.catalog-source-search input` from the old
label wrapper. The field fell back to the browser's inset box at 15px,
and the leftover `flex: 1 1 16rem` made it 256px tall in the sheet's
column layout. Style the input directly at 16px and drop the dead
.catalog-source-meta rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The source filter's searchable select is rendered inside .sources-card,
whose generic label/input/button rules stacked the search icon above a
second bordered 14px input (iOS zooms on focus) and centred the trigger
text. Its base styles also used media-only aliases (--border, --surface,
--media-selection), so outside media the trigger had no border and the
selected option no highlight. Fall back to the global tokens, scope the
picker's own controls, use a 16px input and hide the native clear button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mergePages replaced the accumulated page with the latest chunk, so only
the most recent chunk's errors were shown. Merge errors by source and
code, as the reader's alternatives panel already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Manifest: drop orientation "any" (Chrome maps it to the full sensor, so the
installed app ignored the phone's rotation lock); the service worker serves
the manifest network-first so installed apps can pick up manifest changes.
- Media: derive the channel from the path for static routes (演播者 showed 影视);
keep the tab bar on audiobook/narrators; a channel with no library swipes a
whole channel at a time.
- Reader (paged mode): the column strip follows the finger; release turns on a
flick or past 30% of the width, otherwise springs back; chapter edges fall
through to the normal turn.
- PWA smoke test stubs the current window.__readerFlush hook.
- Docs: ui.md §3.8 drag-follow and media swipe rules; pwa.zh-CN.md orientation
and manifest caching.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reader (scroll paging): mask the line the page's bottom edge cuts through
(clip-path, --reader-page-mask) so no page shows a half-sliced last line;
re-measured after page-turn animations.
- Landscape sidebar layout: the sidebar owns the left safe-area inset, the page
column the right one; fix .shelf-head's swapped left/right padding; reader
top/bottom bars respect left/right insets.
- Guard all :hover rules with (hover: hover) and (pointer: fine), enforced by
test/design-tokens.test.ts.
- PWA update banner sits above the phone tab bar and reader toolbar.
- Top safe-area on sources header and phone media pages; 16px sidebar search
on touch (iOS focus zoom); larger hit areas for chips and cover controls.
- Manifest: unified launch colour #FDF9F6, maskable icon, drop
window-controls-overlay; regenerate branding assets.
- Docs: ui.md safe-area/touch/hover/reader rules, pwa.zh-CN.md icons and
window notes, CLAUDE.md stale bundle notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The committed gradle-wrapper.jar was the real 8.11.1 wrapper jar nested
inside another jar, without the org/gradle/cli classes, so ./gradlew failed
everywhere with NoClassDefFoundError: org/gradle/cli/CommandLineParser
(CI was the first place it ran from a fresh checkout). gradlew was a
hand-written launcher and gradlew.bat, which the dev guide tells Windows users
to run, did not exist.
Regenerated with `gradle wrapper --gradle-version 8.11.1` from the official
distribution (sha256 f397b287…, matching services.gradle.org); the jar matches
the published gradle-8.11.1-wrapper.jar.sha256 (2db75c40…). The distribution
URL is unchanged. .gitattributes pins gradlew to LF and *.bat to CRLF.
The UI review job starts its media fixture through server/node_modules/tsx,
so it installs the server's dependencies too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- main is the long-lived development branch: release.yml no longer runs on
a push to it, only on v* tags and workflow_dispatch. ci.yml now runs on
every branch push, main included.
- android-actions/setup-android's default package list includes `tools`,
which current command-line tools no longer carry, so sdkmanager failed;
install exactly platform-tools, android-35 and build-tools 35.0.1.
- reader-chrome.test.ts left its React roots mounted; a scheduled commit
could land after jsdom teardown and fail the run with an unhandled
"Right-hand side of 'instanceof' is not an object" (all tests passing).
Unmount in afterEach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci.yml (PRs, non-main pushes, and called by release.yml):
- server: typecheck, test, build
- web: typecheck, test (with server deps, for search parity), build
- UI review: production bundle in Playwright's Chromium, screenshots kept
- Android: shell contract check, web assets staged and verified, debug APK
release.yml (main, v* tags, manual):
- runs CI first; nothing is published from a red build
- builds hectorqin/reader-pro for linux/amd64 and linux/arm64 on native
runners (no QEMU), pushes by digest, then one job creates the multi-arch
tags: main + sha-<short> for main; x.y.z, x.y, latest + sha for tags
- needs DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check-shell.sh failed on main in three places, none of them real defects:
- Every `fun` at method indentation counted as a bridge method, so the
Kotlin-only SpeechBridge.interruptForMedia (called by the media bridge,
not the page) was reported as undeclared. Only methods directly under
@JavascriptInterface are the page's contract now.
- configuredServerUrl was declared as an optional *property*
(`configuredServerUrl?: () => string`), which the declaration reader does
not see. Written as the equivalent optional method, `configuredServerUrl?(): string`.
- The package/directory check stripped only src/main/java/, so every unit
test under src/test/java/ was reported as misplaced. Any source set's java/
root is stripped now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reader
- The reading surface declared no touch-action, so the browser claimed every
horizontal drag (pointercancel): a swipe never turned a page on a phone, and
a rightward one triggered the browser's back gesture and left the book.
Paged and fixed layouts now leave every move to the gesture layer
(touch-action: none, on the host and on the paged column in the shadow
root); scroll mode keeps vertical panning only (pan-y), so it still scrolls
and no longer navigates away. Horizontal swipes in scroll mode stay inert.
Tabs
- useSwipeTabs (shared/ui): a clearly horizontal touch swipe moves one tab.
Mouse, short or diagonal moves, scrolls the browser took over, swipes that
start on a control or a sideways-scrolling row, and active selections are
ignored; the click the finger lifted over is suppressed. The entering body
slides in from the swipe side (header and tab row stay put; reduced motion
disables it).
- Media: the eight catalog tabs are one line across channels — 影视(首页 电影
剧集)→ 音乐(专辑 歌手 曲目)→ 有声书(有声书 演播者). Library and sort carry
over within a channel and reset across one.
- Shelf: 全部 · 在读 · 想读 · 已读.
Tests: swipe-tabs (gesture rules, catalog order and params) and the reader's
touch-action rules in reader-layout-css.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paged mode
- Restoring the position after the viewport height changes (address bar,
keyboard, rotation) wrote max * fraction, which is almost never a column
boundary: the screen showed half of one page and half of the next. Every
paged scrollLeft write (restore, TTS follow, revealBlock, search) now goes
through one page-snapping helper, and a scroll the browser makes on its own
(focus, selection autoscroll) is snapped back.
Highlights
- localSectionId prefixed `xhtml:` from the format alone, so a windowed book
whose sections have no prefix saved highlights that matched no chapter and
were never drawn (and its progress failed to restore). The loaded document's
own section ids are now the authority.
- Highlight boxes are measured once; font size, margins, mode switch, resize
or a late image moved the text under them. They are repainted after each.
Mobile bottom bar
- `.reader-route-host` had min-height: 100vh, which on a phone browser is
taller than the visible viewport while the address bar shows, pushing the
reader's bottom toolbar under it. Height is 100dvh with a 100vh fallback.
- Notices are bottom-centre, exactly over 朗读/外观; in the reader they now
stack above the bottom bar instead of swallowing the next tap.
Tap to turn hides the toolbar
- A tap on the outer thirds turns the page and puts a showing toolbar away;
the middle third still toggles it.
UI review scripts: page-turn-regression waits for login to finish rather than
for the shelf (the app now opens on media); reader-regression shows the
toolbar before pressing 下一章 after tap turns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Commands (per-package install/dev/test/build, single tests across the two web
runners, UI review) and the cross-file architecture: book identity, metadata
precedence, ShelfService as the only DTO builder, sync semantics, additive
schema, isolated media worker, hash routing and single-bundle constraints,
imperative reader stage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both screens showed an empty page with a floating "正在加载…" until the
first response. The .book-grid.is-skeleton styles (same grid and density as
the real one, a shimmer that goes flat under reduced motion) were already in
reader.css from the legacy shelf but nothing rendered them; BookGridSkeleton
does, so the first paint fills in instead of jumping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The side-nav global search and the shelf's own field sat side by side on
desktop and both read "搜索书名、…"; the shelf one was even labelled
搜索书库 while it only filters the shelf.
- Global (side nav, /search): 搜索书籍和影音 — also no longer truncated in
the 216px rail.
- Shelf: 在书架中筛选书名、作者、系列, labelled 筛选书架; results are 筛选结果.
- Library: 在书库中搜索书名、作者.
- ui.md §4 records the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui.md's four type roles only governed the reading side; the media sheets
used every integer from 8 to 24px plus dozens of rem values. Every font size
up to 24px now reads one of --text-display/title/body/ui/label/meta/micro.
- --text-ui (14px) is the media list/settings body; --text-display (22px) the
page hero; --text-micro (10px) only for cover/icon badges.
- --text-meta moves 0.72rem -> 0.75rem, so the 200+ media 12px lines land on
it unchanged and the reading side's meta grows by half a pixel.
- Inputs that were 16px land on --text-title (16.8px), keeping the iOS
focus-zoom floor; ui.md says so explicitly.
- Sizes above 24px (player initials, hero numerals) and the decorative serif
shorthands stay literal; the token test rejects literals <= 24px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>