diff --git a/docs/CHANGELOG_web.md b/docs/CHANGELOG_web.md index 7d21458..7af61e5 100644 --- a/docs/CHANGELOG_web.md +++ b/docs/CHANGELOG_web.md @@ -13,11 +13,11 @@ The format loosely follows Keep a Changelog and can be adapted to the team's hab - CBZ reader is now chapter-scoped: a chapter's images flow as one continuous strip and scrolling stops at an 本章完 · 下一章 card (only chapter buttons / TOC / slider / bookmarks cross the boundary); a 连读 toggle makes scrolling flow across chapters, and a configurable 0–3-chapter prefetch pre-mounts upcoming chapters for instant switching — the old 连读/整页/适高 mode cycle is gone. - CBZ 阅读器改为以章为单位:一章图片是一段连续长卷,滚动止于「本章完 · 下一章」卡片(跨章只能靠章节按钮/目录/滑条/书签);「连读」开关让滑动贯穿章节,预读 0–3 章可配置、提前挂载解码实现切章零白屏;移除原 连读/整页/适高 三档循环。 -- Bookmarks in all four readers: a 书签 button in the bar captures the current position (page / chapter+offset / EPUB CFI) with an optional note; the right-side panel lists them — click jumps straight back to that spot, ✎ edits the note in place, ✕ deletes. Bookmarks are per-user and server-side, so they follow you across devices. -- 四类阅读器新增书签:工具条「书签」按钮把当前位置(页码/章节+章内偏移/EPUB CFI)连同可选备注存下;右侧书签面板点一下即跳回原位,✎ 行内改备注,✕ 删除。书签按用户存在服务端,换设备同账号即见。 +- Bookmarks in all four readers: a 书签 button in the bar captures the current position (page / chapter+offset / EPUB CFI) with an optional note; the 书签 tab of the left navigation drawer lists them — click jumps straight back to that spot, ✎ edits the note in place, ✕ deletes. Bookmarks are per-user and server-side, so they follow you across devices. +- 四类阅读器新增书签:工具条「书签」按钮把当前位置(页码/章节+章内偏移/EPUB CFI)连同可选备注存下;左侧导航抽屉的「书签」选项卡内点一下即跳回原位,✎ 行内改备注,✕ 删除。书签按用户存在服务端,换设备同账号即见。 -- Folder-structured comics gain a 目录 sidebar in the CBZ reader: each archive folder (第N話…) becomes a chapter that jumps to its first page, with 上一章/下一章 buttons, a current-chapter counter, and the active row highlighted and scrolled into view — flat archives show no extra UI. -- 按目录组织的漫画在 CBZ 阅读器新增「目录」侧栏:压缩包内每个文件夹(第N話…)即一章,点击直达该话首页;工具条配上一章/下一章与当前章计数,目录内高亮当前章并自动居中;平铺无目录的包不显示多余控件。 +- Folder-structured comics gain a 目录 tab in the CBZ reader's navigation drawer: each archive folder (第N話…) becomes a chapter that jumps to its first page, with 上一章/下一章 buttons, a current-chapter counter, and the active row highlighted and scrolled into view — flat archives show no extra UI. +- 按目录组织的漫画在 CBZ 阅读器导航抽屉内新增「目录」选项卡:压缩包内每个文件夹(第N話…)即一章,点击直达该话首页;工具条配上一章/下一章与当前章计数,目录内高亮当前章并自动居中;平铺无目录的包不显示多余控件。 - Library uploads over 16MB now auto-switch to the resumable chunked protocol (8MB parts): failed parts retry in place up to 3 times and already-sent parts are skipped, so a flaky upload continues instead of starting over; small files keep the original single request. - 书库上传超过 16MB 自动切换为可续传分片协议(每片 8MB):失败的分片原地重试至多 3 次,已传片自动跳过,中断后继续而不是从头再来;小文件仍走原单次上传。 @@ -31,6 +31,9 @@ The format loosely follows Keep a Changelog and can be adapted to the team's hab ### Changed / 变更 +- Reader navigation tightened into one left-side drawer: 目录 and 书签 are now tabs of a single sidebar shared by all four readers, following the reader theme's colors (books without a TOC — flat archives, EPUB, PDF — show the bookmarks panel alone); the separate right-side bookmark panel and the per-reader TOC popovers are gone. +- 阅读器导航收紧为统一左侧抽屉:目录与书签成为四个阅读器共用的同一侧边栏内的两个选项卡,配色跟随阅读器主题(无目录的书——平铺包、EPUB、PDF——仅显示书签面板);原右侧书签面板与各阅读器自绘目录浮层移除。 + - The web UI was rebuilt on a shadcn/Radix design system: semantic light/dark theme tokens with a system/light/dark switcher (remembered), a persistent app shell (desktop sidebar, tablet icon rail, mobile bottom tabs), a shelf driven by URL-state search/sort/group controls with a flat-first grid and menu-based card actions (confirm dialogs replace native popups), and rebuilt login/admin pages. Readers keep their current chrome and every reading interaction this round; their restyle lands next. - 网页 UI 以 shadcn/Radix 设计系统重做:语义化明暗主题 token + 系统/浅色/深色切换(记忆);常驻应用壳(桌面侧栏、平板图标栏、手机底部标签);书架改为 URL 状态驱动的搜索/排序/分组工具条,默认平铺网格,卡片操作收进菜单(删除改为对话框确认);登录页与 admin 两页同步重做。阅读器保持现状,改版随下一期到达。 diff --git a/frontend/src/components/Bookmarks.tsx b/frontend/src/components/Bookmarks.tsx index 3b74b82..2a421c8 100644 --- a/frontend/src/components/Bookmarks.tsx +++ b/frontend/src/components/Bookmarks.tsx @@ -1,19 +1,22 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useState, type ReactNode } from "react"; +import { useState } from "react"; import { api } from "../api/client"; import type { Book } from "../api/types"; import { toast } from "./Toaster"; type Locator = Record; -export function useBookmarks(opts: { +/** 书签面板内容(供 ReaderNav 的「书签」选项卡承载);跳转后由调用方负责关抽屉. */ +export function BookmarksTab({ + book, + capture, + seek, +}: { book: Book; capture: () => { locator: Locator; percent: number }; seek: (locator: Locator) => void; -}): { btn: ReactNode; panel: ReactNode } { - const { book, capture, seek } = opts; +}) { const qc = useQueryClient(); - const [open, setOpen] = useState(false); const [note, setNote] = useState(""); const [editId, setEditId] = useState(null); const [editVal, setEditVal] = useState(""); @@ -48,107 +51,84 @@ export function useBookmarks(opts: { onError: fail, }); - const btn = ( - - ); - - const panel = open ? ( + return ( <> - - {bm.panel} + {nav && ( + setNav(false)} + bookmarks={ + { + const cfi = String(l.cfi ?? ""); + setNav(false); + if (!cfi) return; + Promise.resolve(rendRef.current?.display(cfi)).catch(() => toast("err", "书签定位失败")); + }} + /> + } + /> + )} ); } diff --git a/frontend/src/readers/PdfReader.tsx b/frontend/src/readers/PdfReader.tsx index 32bfcaa..a3904e1 100644 --- a/frontend/src/readers/PdfReader.tsx +++ b/frontend/src/readers/PdfReader.tsx @@ -1,7 +1,8 @@ import { useEffect, useRef, useState, type KeyboardEvent } from "react"; import * as pdfjs from "pdfjs-dist"; import { apiRaw } from "../api/client"; -import { useBookmarks } from "../components/Bookmarks"; +import { BookmarksTab } from "../components/Bookmarks"; +import { ReaderNav } from "../components/reader-nav"; import { btn } from "../components/ui"; import { useProgressSaver, type ReaderProps } from "../lib/useProgress"; @@ -21,14 +22,7 @@ export default function PdfReader({ book, initialLocator, initialPercent }: Read const taskRef = useRef(null); const restored = useRef(false); const saver = useProgressSaver(book.id, { locator: initialLocator, percent: initialPercent }); - const bm = useBookmarks({ - book, - capture: saver.capture, - seek: (l) => { - const p = Number(l.page); - if (Number.isInteger(p) && p >= 0 && p < num) setPage(p); - }, - }); + const [nav, setNav] = useState(false); const [num, setNum] = useState(0); const [page, setPage] = useState(0); // 0 基 const [err, setErr] = useState(""); @@ -160,9 +154,27 @@ export default function PdfReader({ book, initialLocator, initialPercent }: Read - {bm.btn} + - {bm.panel} + {nav && ( + setNav(false)} + bookmarks={ + { + const p = Number(l.page); + setNav(false); + if (Number.isInteger(p) && p >= 0 && p < num) setPage(p); + }} + /> + } + /> + )} ); } diff --git a/frontend/src/readers/TextReader.tsx b/frontend/src/readers/TextReader.tsx index 72dbb98..569e17a 100644 --- a/frontend/src/readers/TextReader.tsx +++ b/frontend/src/readers/TextReader.tsx @@ -2,7 +2,8 @@ import DOMPurify from "dompurify"; import { marked } from "marked"; import { useEffect, useMemo, useRef, useState, type MouseEvent, type ReactNode } from "react"; import { apiRaw } from "../api/client"; -import { useBookmarks } from "../components/Bookmarks"; +import { BookmarksTab } from "../components/Bookmarks"; +import { ReaderNav, type NavTab } from "../components/reader-nav"; import { btn } from "../components/ui"; import { chapterText, seekChapter, splitChapters } from "../lib/chapters"; import { decodeText } from "../lib/encoding"; @@ -134,7 +135,7 @@ export default function TextReader(props: ReaderProps) { function MdView({ book, initialLocator, initialPercent, chrome }: ReaderProps) { const boxRef = useRef(null); const saver = useProgressSaver(book.id, { locator: initialLocator, percent: initialPercent }); - const bm = useBookmarks({ book, capture: saver.capture, seek: (l) => seek(Number(l.scrollFraction) || 0) }); + const [nav, setNav] = useState(false); const restored = useRef(false); const pr = useReaderPrefs(); const [frac, setFrac] = useState(0); @@ -183,8 +184,34 @@ function MdView({ book, initialLocator, initialPercent, chrome }: ReaderProps) { dangerouslySetInnerHTML={{ __html: html ?? "" }} /> - {chrome.on && {bm.btn}} - {bm.panel} + {chrome.on && ( + + + + )} + {nav && ( + setNav(false)} + bookmarks={ + { + seek(Number(l.scrollFraction) || 0); + setNav(false); + }} + /> + } + /> + )} ); } @@ -199,14 +226,9 @@ function TxtView({ book, initialLocator, initialPercent, chrome }: ReaderProps) const activeRow = useRef(null); const [ci, setCi] = useState(0); const [frac, setFrac] = useState(0); - const [toc, setToc] = useState(false); + const [nav, setNav] = useState(null); const pr = useReaderPrefs(); const { text, err, retry } = useFileText(book.file_url ?? ""); - const bm = useBookmarks({ - book, - capture: saver.capture, - seek: (l) => goChapter(Number(l.ch) || 0, Number(l.scrollFraction) || 0), - }); const chapters = useMemo(() => (text == null ? null : splitChapters(text)), [text]); @@ -238,8 +260,8 @@ function TxtView({ book, initialLocator, initialPercent, chrome }: ReaderProps) }, [ci, chapters]); useEffect(() => { - if (toc) activeRow.current?.scrollIntoView({ block: "center" }); - }, [toc]); + if (nav === "toc") activeRow.current?.scrollIntoView({ block: "center" }); + }, [nav]); function goChapter(i: number, intra = 0) { if (!chapters) return; @@ -287,14 +309,21 @@ function TxtView({ book, initialLocator, initialPercent, chrome }: ReaderProps) {chrome.on && ( - {bm.btn} + {chapters.length > 1 && ( <> @@ -311,35 +340,44 @@ function TxtView({ book, initialLocator, initialPercent, chrome }: ReaderProps) )} )} - {/* ponytail: 目录一次渲染全部章节,数千章级书若卡顿再上虚拟列表 */} - {toc && chapters.length > 1 && ( - <> - ); }