diff --git a/docs/CHANGELOG_web.md b/docs/CHANGELOG_web.md index f64fe15..1f9f9f5 100644 --- a/docs/CHANGELOG_web.md +++ b/docs/CHANGELOG_web.md @@ -8,7 +8,17 @@ The format loosely follows Keep a Changelog and can be adapted to the team's hab ## [Unreleased] -### Fixed / 修复 +### Added / 新增 + +- Text (.txt/.md) reader upgrades: paper / sepia / night reading themes and adjustable font size, with a compact prefs bar; choices persist across books (localStorage); body text is now serif with a readable measure and justified lines. +- 文本阅读器(.txt/.md)升级:新增 纸/米/夜 三种阅读主题与字号调节,工具条常驻;偏好跨书持久化(localStorage);正文改为衬线字体、收窄行宽并两端对齐,更利于长时间阅读。 +- PDF reader: emoji skip icons replaced with 首页/末页 text buttons, framed page canvas, and an accessible label announcing arrow-key paging. +- PDF 阅读器:以文字按钮(首页/末页)替换 emoji 图标,页面画布加边框投影,并补充可访问的「方向键翻页」说明。 + +### Changed / 变更 + +- Whole UI restyled to a warm library palette (stone surfaces + amber accents, replacing cool zinc + emerald): warmer dark chrome, amber primary buttons/links, hover/press feedback on shelf cards (cover lift + glow), reader header with format badge and blur, pill-style CBZ page indicator, EPUB spread centered like a book page. +- 全站换为暖色「书房」配色(stone 深灰面 + amber 琥珀强调,替代冷色 zinc + emerald):书架封面悬停上浮高亮、阅读器顶栏带格式徽章与毛玻璃、CBZ 页码改为悬浮胶囊、EPUB 页面居中成书卷感,主按钮/链接统一琥珀色并补齐悬停/焦点反馈。 - .txt/.md files encoded in GBK/GB2312/GB18030 or UTF-16 (typical web-novel exports) no longer render as mojibake: the reader strictly tries UTF-8 first and falls back to GB18030, recognizing UTF-16 by BOM. - GBK/GB2312/GB18030 或 UTF-16 编码的 .txt/.md(网文导出常见)不再乱码:阅读器先按 UTF-8 严格解码,失败自动回退 GB18030,UTF-16 靠 BOM 识别。 diff --git a/frontend/index.html b/frontend/index.html index a158cc3..7c3f092 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - + BookLib — 个人书库 diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx index 1b45f6f..78e8bcd 100644 --- a/frontend/src/auth/AuthContext.tsx +++ b/frontend/src/auth/AuthContext.tsx @@ -77,7 +77,7 @@ export function RequireAuth({ children }: { children: ReactNode }) { const { user, ready } = useAuth(); const loc = useLocation(); if (!getToken()) return ; - if (!ready || !user) return
加载中…
; + if (!ready || !user) return
加载中…
; return <>{children}; } diff --git a/frontend/src/components/Cover.tsx b/frontend/src/components/Cover.tsx index 8406bf8..aa5a76b 100644 --- a/frontend/src/components/Cover.tsx +++ b/frontend/src/components/Cover.tsx @@ -29,12 +29,12 @@ export function Cover({ book, className }: { book: Book; className?: string }) { const { src, failed, retry } = useAuthedImage(book.cover_url); if (failed) return ( -
+
); - if (!src) return
; - return ; + if (!src) return
; + return ; } diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index 61ef37f..5793f32 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -13,8 +13,8 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { err: Err

页面出错了

-

{String(this.state.err)}

-
diff --git a/frontend/src/components/Toaster.tsx b/frontend/src/components/Toaster.tsx index affe5f5..a569d6d 100644 --- a/frontend/src/components/Toaster.tsx +++ b/frontend/src/components/Toaster.tsx @@ -31,7 +31,7 @@ export function Toaster() {
diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx index 8428b3f..bd5687f 100644 --- a/frontend/src/components/TopBar.tsx +++ b/frontend/src/components/TopBar.tsx @@ -6,23 +6,29 @@ import { btn } from "./ui"; export function TopBar({ right }: { right?: ReactNode }) { const { user, isAdmin, logout } = useAuth(); return ( -
- - BookLib +
+ + BookLib {isAdmin && ( - + 用户 )} {isAdmin && ( - + 库管理 )}
{right} - + {user?.username} {isAdmin ? "(admin)" : ""} diff --git a/frontend/src/components/ui.ts b/frontend/src/components/ui.ts index 5c56510..e9a37aa 100644 --- a/frontend/src/components/ui.ts +++ b/frontend/src/components/ui.ts @@ -1,7 +1,7 @@ export const btn = - "rounded-md bg-zinc-800 px-3 py-1.5 text-sm hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50"; + "rounded-lg bg-stone-800 px-3 py-1.5 text-sm transition-colors hover:bg-stone-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500 active:bg-stone-600 disabled:cursor-not-allowed disabled:opacity-50"; export const btnPrimary = - "rounded-md bg-emerald-700 px-3 py-1.5 text-sm hover:bg-emerald-600 disabled:cursor-not-allowed disabled:opacity-50"; + "rounded-lg bg-amber-600 px-3 py-1.5 text-sm text-stone-950 transition-colors hover:bg-amber-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500 active:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-50"; export const input = - "rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm outline-none focus:border-zinc-500"; -export const card = "rounded-xl border border-zinc-800 bg-zinc-900"; + "rounded-lg border border-stone-700 bg-stone-900 px-3 py-1.5 text-sm outline-none transition-colors placeholder:text-stone-500 focus:border-amber-500/70"; +export const card = "rounded-xl border border-stone-800 bg-stone-900"; diff --git a/frontend/src/index.css b/frontend/src/index.css index 5b28d2c..b9c1f27 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -6,15 +6,44 @@ body, height: 100%; } body { - @apply bg-zinc-950 text-zinc-100 antialiased; + @apply bg-stone-950 text-stone-100 antialiased; } ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-thumb { - @apply rounded bg-zinc-700; + @apply rounded-full border-2 border-transparent bg-clip-padding bg-stone-700/70; } +::-webkit-scrollbar-thumb:hover { + @apply bg-stone-600; +} + +/* 阅读页主题:纸/米/夜,文本类阅读器共用 —— 见 lib/readerPrefs.ts */ +[data-rd="paper"] { --rd-bg: #faf8f4; --rd-fg: #292524; --rd-link: #9a3412; } +[data-rd="sepia"] { --rd-bg: #f0e6d2; --rd-fg: #433a29; --rd-link: #92400e; } +[data-rd="night"] { --rd-bg: #171512; --rd-fg: #c6c0b6; --rd-link: #e0a458; } +.rd-surface { + background: var(--rd-bg); + color: var(--rd-fg); + font-family: ui-serif, "Songti SC", "Noto Serif CJK SC", "Source Han Serif SC", SimSun, Georgia, serif; +} +.rd-surface ::selection { + background: color-mix(in oklab, var(--rd-link) 30%, transparent); +} +/* 主题内的次级控件(章节条/翻页键),跟随当前阅读主题 */ +.rd-btn { + @apply rounded-md px-2.5 py-1 text-sm transition-colors; + background: color-mix(in oklab, currentColor 8%, transparent); +} +.rd-btn:hover { background: color-mix(in oklab, currentColor 16%, transparent); } +.rd-btn:disabled { @apply cursor-not-allowed opacity-40; } +.rd-select { + @apply rounded-md px-2 py-1 text-sm outline-none; + background: color-mix(in oklab, currentColor 8%, transparent); + color: inherit; +} +.rd-divider { border-color: color-mix(in oklab, currentColor 14%, transparent); } /* markdown 正文(仅 .md-body 作用域) */ .md-body h1 { @apply mt-8 mb-3 text-2xl font-semibold; } @@ -23,12 +52,12 @@ body { .md-body p { @apply my-3; } .md-body ul { @apply my-3 list-disc space-y-1 pl-6; } .md-body ol { @apply my-3 list-decimal space-y-1 pl-6; } -.md-body blockquote { @apply my-3 border-l-2 border-zinc-600 pl-4 text-zinc-400; } -.md-body code { @apply rounded bg-zinc-800 px-1 py-0.5 text-[13px]; } -.md-body pre { @apply my-3 overflow-x-auto rounded-lg bg-zinc-900 p-3; } +.md-body blockquote { @apply my-3 border-l-2 pl-4; border-color: color-mix(in oklab, currentColor 30%, transparent); opacity: 0.75; } +.md-body code { @apply rounded px-1 py-0.5 text-[0.85em]; background: color-mix(in oklab, currentColor 10%, transparent); } +.md-body pre { @apply my-3 overflow-x-auto rounded-lg p-3; background: color-mix(in oklab, currentColor 6%, transparent); } .md-body pre code { @apply bg-transparent p-0; } -.md-body a { @apply text-emerald-400 underline; } +.md-body a { color: var(--rd-link, #f59e0b); @apply underline underline-offset-2; } .md-body img { @apply my-3 max-w-full rounded; } -.md-body hr { @apply my-6 border-zinc-800; } +.md-body hr { @apply my-6; border-color: color-mix(in oklab, currentColor 15%, transparent); } .md-body table { @apply my-3 w-full border-collapse text-sm; } -.md-body th, .md-body td { @apply border border-zinc-700 px-2 py-1 text-left; } +.md-body th, .md-body td { @apply border px-2 py-1 text-left; border-color: color-mix(in oklab, currentColor 22%, transparent); } diff --git a/frontend/src/lib/readerPrefs.ts b/frontend/src/lib/readerPrefs.ts new file mode 100644 index 0000000..bc29246 --- /dev/null +++ b/frontend/src/lib/readerPrefs.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; + +export const RD_THEMES = ["paper", "sepia", "night"] as const; +export type RdTheme = (typeof RD_THEMES)[number]; +export const RD_THEME_LABEL: Record = { paper: "纸", sepia: "米", night: "夜" }; +export const FONT_SIZES = [16, 18, 20, 22, 25, 28]; + +const KEY = "reader-prefs"; + +export interface ReaderPrefs { + theme: RdTheme; + sizeIdx: number; +} + +export function useReaderPrefs() { + const [prefs, setPrefs] = useState(() => { + try { + const p = JSON.parse(localStorage.getItem(KEY) ?? "") as ReaderPrefs; + if (RD_THEMES.includes(p.theme) && Number.isInteger(p.sizeIdx) && p.sizeIdx >= 0 && p.sizeIdx < FONT_SIZES.length) + return p; + } catch { + /* 首次使用或存了坏值 */ + } + return { theme: "sepia", sizeIdx: 2 }; + }); + useEffect(() => localStorage.setItem(KEY, JSON.stringify(prefs)), [prefs]); + + return { + prefs, + fontSize: FONT_SIZES[prefs.sizeIdx], + cycleTheme: () => + setPrefs((p) => ({ ...p, theme: RD_THEMES[(RD_THEMES.indexOf(p.theme) + 1) % RD_THEMES.length] })), + bump: (d: number) => + setPrefs((p) => ({ ...p, sizeIdx: Math.min(FONT_SIZES.length - 1, Math.max(0, p.sizeIdx + d)) })), + }; +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 5402644..ed47ef0 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -30,9 +30,9 @@ export default function Login() {

BookLib 登录

setU(e.target.value)} - placeholder="用户名" autoFocus autoComplete="username" /> + aria-label="用户名" placeholder="用户名" autoFocus autoComplete="username" /> setP(e.target.value)} - placeholder="密码" autoComplete="current-password" /> + aria-label="密码" placeholder="密码" autoComplete="current-password" /> diff --git a/frontend/src/pages/Reader.tsx b/frontend/src/pages/Reader.tsx index dfd537a..6b3ac47 100644 --- a/frontend/src/pages/Reader.tsx +++ b/frontend/src/pages/Reader.tsx @@ -57,16 +57,21 @@ export default function Reader() { return (
-
- +
+ ← 书架 {book?.title ?? "…"} - - {book?.library ? book.library + " · " : ""} - {book?.format} - {book ? ` · ${Math.round(book.percent * 100)}%` : ""} - + {book && ( + + {book.library && {book.library}} + {book.format} + {Math.round(book.percent * 100)}% + + )} {isAdmin && book && ( @@ -38,13 +43,13 @@ function BookCard({ )} {book.percent > 0 && ( -
-
+
+
)} {isAdmin && ( - 白名单:cbz/zip/pdf/epub/txt/md + 白名单:cbz/zip/pdf/epub/txt/md
))} {(libsQ.data ?? []).length === 0 && !libsQ.isPending && ( -

还没有库,用上方表单创建。

+

还没有库,用上方表单创建。

)}
diff --git a/frontend/src/pages/admin/Users.tsx b/frontend/src/pages/admin/Users.tsx index 5153cd8..ae48636 100644 --- a/frontend/src/pages/admin/Users.tsx +++ b/frontend/src/pages/admin/Users.tsx @@ -55,7 +55,7 @@ export default function AdminUsers() {
- + @@ -66,11 +66,11 @@ export default function AdminUsers() { {(usersQ.data ?? []).map((x) => ( - - + + - +
ID 用户名
{x.id}
{x.id} {x.username} {x.role}{x.created_at.slice(0, 19).replace("T", " ")}{x.created_at.slice(0, 19).replace("T", " ")} diff --git a/frontend/src/readers/PdfReader.tsx b/frontend/src/readers/PdfReader.tsx index 238f065..62bc376 100644 --- a/frontend/src/readers/PdfReader.tsx +++ b/frontend/src/readers/PdfReader.tsx @@ -123,28 +123,33 @@ export default function PdfReader({ book, initialLocator }: ReaderProps) { ); - if (num === 0) return
PDF 加载中…
; + if (num === 0) return
PDF 加载中…
; return ( -
-
- +
+
+
-
- - + {page + 1}/{num} -
diff --git a/frontend/src/readers/TextReader.tsx b/frontend/src/readers/TextReader.tsx index 85506ba..99188b9 100644 --- a/frontend/src/readers/TextReader.tsx +++ b/frontend/src/readers/TextReader.tsx @@ -1,12 +1,47 @@ import DOMPurify from "dompurify"; import { marked } from "marked"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { apiRaw } from "../api/client"; -import { btn, input } from "../components/ui"; +import { btn } from "../components/ui"; import { chapterText, splitChapters } from "../lib/chapters"; import { decodeText } from "../lib/encoding"; +import { FONT_SIZES, RD_THEME_LABEL, useReaderPrefs } from "../lib/readerPrefs"; import { useProgressSaver, type ReaderProps } from "../lib/useProgress"; +type Prefs = ReturnType; + +function PrefsBar({ pr }: { pr: Prefs }) { + return ( +
+ + + +
+ ); +} + +function ReaderToolbar({ pr, children }: { pr: Prefs; children?: ReactNode }) { + return ( +
+ {children} +
+ +
+
+ ); +} + function useFileText(url: string) { const [text, setText] = useState(null); const [err, setErr] = useState(""); @@ -31,7 +66,7 @@ function LoadState({ text, onRetry }: { text: string; onRetry?: () => void }) {
{onRetry &&

{text}

} - {!onRetry &&

{text}

} + {!onRetry &&

{text}

} {onRetry && ( -
)} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index fca6d67..c413fe4 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -14,8 +14,8 @@ export default defineConfig({ name: "BookLib 个人书库", short_name: "BookLib", description: "个人书库/漫画库在线阅读", - theme_color: "#09090b", - background_color: "#09090b", + theme_color: "#0c0a09", + background_color: "#0c0a09", display: "standalone", icons: [{ src: "icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" }], },