diff --git a/README.md b/README.md index a37e48b..2a47103 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ volumes: ### 界面与图标 阅读页以手机浏览器为优先:顶部显示返回、书名和章节,底部集中目录、朗读、界面、设置。 -工具栏和半屏面板覆盖正文,显隐不改变正文区域尺寸;沉浸时只留底部书名、章内页码和全书进度。 +工具栏和半屏面板覆盖正文,显隐不改变正文区域尺寸。界面设置可分别选择顶部左/右、底部左/右显示书名、章节名、章节进度、时间或不显示;默认顶部左侧章节名、底部左侧章内页码与全书百分比、底部右侧时间,按设备保存。 底栏上方保留一个日夜切换按钮,随工具栏一起收起。 图标使用 Lucide SVG,通过统一的 Icon 组件控制尺寸、线宽和无障碍标签,不再加载字体图标。 diff --git a/docs/ui.md b/docs/ui.md index 484d6d0..c28a173 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -543,7 +543,7 @@ TXT 是唯一一种**没有作者排版**的书,所以它也是唯一一处「 - 顶部:返回、书名、当前章节。 - 底部:上一章、本章页数滑杆、下一章;下一行是目录、朗读、界面、设置。 - 中间滑杆始终控制本章页数,不能变成全书进度。固定页章节只有一页时禁用滑杆。 -- 工具栏隐藏后只保留底部书名、章内页码和全书百分比。日夜圆形快捷按钮随工具栏一起显隐。 +- 顶部左/右、底部左/右阅读信息可在“界面”分别选择书名、章节名、章节进度(章内页码与全书百分比)、时间或不显示。默认顶部左侧章节名,底部左侧章节进度,底部右侧时间。配置按设备保存,工具栏隐藏后仍显示;长文本省略。关闭信息仍保留固定边界,避免改变正文分页。日夜圆形快捷按钮随工具栏一起显隐。 - 正文宿主的上下边界不依赖工具栏状态。工具栏覆盖正文,显隐不得改变页码、滚动位置、正文尺寸。 - 四个面板使用统一底部半屏布局;界面放排版与主题,设置放翻页行为,朗读放语音设置和播放控制。 - TXT 默认字号为基准的 1.125 倍、行距 1.8,支持两字缩进与段间距;EPUB 默认保留作者排版。 diff --git a/web/src/store/settings.ts b/web/src/store/settings.ts index c42e18b..4486af7 100644 --- a/web/src/store/settings.ts +++ b/web/src/store/settings.ts @@ -29,7 +29,26 @@ export type ShelfDensity = 'compact' | 'cozy' | 'comfortable'; */ export type ShelfSort = 'recent' | 'added' | 'title' | 'author'; +export type ReadoutMode = 'none' | 'book' | 'chapter' | 'progress' | 'time'; +export const READOUT_FIELDS = [ + { key: 'readoutTopLeft', label: '顶部左侧' }, + { key: 'readoutTopRight', label: '顶部右侧' }, + { key: 'readoutBottomLeft', label: '底部左侧' }, + { key: 'readoutBottomRight', label: '底部右侧' }, +] as const; +export const READOUT_OPTIONS: Array<{ value: ReadoutMode; label: string }> = [ + { value: 'none', label: '不显示' }, + { value: 'book', label: '书名' }, + { value: 'chapter', label: '章节名' }, + { value: 'progress', label: '章节进度' }, + { value: 'time', label: '时间' }, +]; + export interface AppSettings extends ViewSettings { + readoutTopLeft: ReadoutMode; + readoutTopRight: ReadoutMode; + readoutBottomLeft: ReadoutMode; + readoutBottomRight: ReadoutMode; /** UTF-8 by default; a reader can force another for an odd TXT file. */ txtEncoding: string; /** Only meaningful for fixed-layout books. */ @@ -72,6 +91,10 @@ export interface AppSettings extends ViewSettings { } export const DEFAULT_APP_SETTINGS: AppSettings = { + readoutTopLeft: 'chapter', + readoutTopRight: 'none', + readoutBottomLeft: 'progress', + readoutBottomRight: 'time', mode: 'scroll', fontScale: 1, lineHeight: 'inherit', @@ -113,6 +136,11 @@ export class SettingsStore { try { const parsed = JSON.parse(raw) as Partial & { shelfSort?: string }; this.settings = { ...DEFAULT_APP_SETTINGS, ...parsed }; + for (const { key } of READOUT_FIELDS) { + if (!READOUT_OPTIONS.some(option => option.value === this.settings[key])) { + this.settings[key] = DEFAULT_APP_SETTINGS[key]; + } + } /* * Two stored values are folded forward, and both are the same repair. * diff --git a/web/src/styles/reader.css b/web/src/styles/reader.css index 723ff4e..f77e723 100644 --- a/web/src/styles/reader.css +++ b/web/src/styles/reader.css @@ -3660,29 +3660,30 @@ book-content { svg.icon { display: inline-block; width: 1em; height: 1em; flex-shrink: 0; vertical-align: middle; } .reader-screen { --reader-footer-height: calc(8.5rem + var(--safe-bottom)); } .reader-screen .book-host { - inset-block-start: var(--safe-top); + inset-block-start: calc(1.8rem + var(--safe-top)); inset-block-end: calc(1.8rem + var(--safe-bottom) + var(--reader-page-trim, 0px)); } .reading-indicator { position: absolute; inset-inline: 0; - bottom: 0; - height: calc(1.8rem + var(--safe-bottom)); - padding: 0.35rem max(1.25rem, var(--safe-right)) var(--safe-bottom) max(1.25rem, var(--safe-left)); + padding-inline: max(1.25rem, var(--safe-left)) max(1.25rem, var(--safe-right)); display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; color: var(--reader-muted); background: var(--reader-paper); - border-top: 1px solid color-mix(in srgb, var(--reader-line) 50%, transparent); font-size: 0.72rem; line-height: 1.2; z-index: var(--reader-layer-readout); pointer-events: none; } -.indicator-chapter { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } -.indicator-progress { white-space: nowrap; font-variant-numeric: tabular-nums; } +.reading-indicator-top { top: 0; height: calc(1.8rem + var(--safe-top)); padding-top: calc(0.5rem + var(--safe-top)); } +.reading-indicator-bottom { bottom: 0; height: calc(1.8rem + var(--safe-bottom)); padding-top: 0.35rem; padding-bottom: var(--safe-bottom); } +.reading-indicator > span { min-width: 0; flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-variant-numeric: tabular-nums; } +.indicator-left { text-align: start; } +.indicator-right { text-align: end; } +.reading-indicator > span:empty { flex: 0; } .reader-screen .topbar { display: flex; align-items: center; diff --git a/web/src/ui/reader-chrome.tsx b/web/src/ui/reader-chrome.tsx index c4825fa..5f2b732 100644 --- a/web/src/ui/reader-chrome.tsx +++ b/web/src/ui/reader-chrome.tsx @@ -16,6 +16,8 @@ */ import type { AppSettings } from '../store/settings.ts'; +import { DEFAULT_APP_SETTINGS, READOUT_FIELDS, READOUT_OPTIONS, type ReadoutMode } from '../store/settings.ts'; +import { ReaderIndicators } from './reader-indicators.tsx'; import type { SpeechEngineKind } from '../render/speech.ts'; import { type ComponentChildren, type JSX } from './vendor/preact.ts'; import { IconButton, SectionTitle } from './toolkit.tsx'; @@ -49,6 +51,10 @@ export interface SpeechBarState { } export interface ChromeState { + readoutTopLeft?: ReadoutMode; + readoutTopRight?: ReadoutMode; + readoutBottomLeft?: ReadoutMode; + readoutBottomRight?: ReadoutMode; title: string; author: string; chromeVisible: boolean; @@ -167,10 +173,7 @@ export function ReaderChrome({ state, stage, handlers }: ReaderChromeProps): JSX