mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 13:06:36 +08:00
优化
This commit is contained in:
@@ -3,12 +3,15 @@ import { Loader2 } from 'lucide-react'
|
||||
|
||||
import { useSmoothWheelScroll } from '../../hooks/useSmoothWheelScroll'
|
||||
import { rememberImageSize } from '../../utils/comicSpread'
|
||||
import { comicImageSizing, isSelfSizedComicFit } from '../../utils/comicImageFit'
|
||||
import type { ComicImageFit } from '../../stores/readerSettings'
|
||||
|
||||
// 漫画/图片阅读器(仿 legado MangaMenu 能力面):
|
||||
// 上下滚动(默认)/ 左右单页 / 左右双页三种呈现;点击分区翻页/呼出菜单;图片懒加载。
|
||||
//
|
||||
// 双页铺开由外层算好「这一屏显示哪几张」传进来(见 utils/comicSpread.ts),
|
||||
// 这里只负责把它们并排摆好、各自撑满视口高度。
|
||||
// 滚动模式的显示尺寸由 imageFit 档位决定(见 utils/comicImageFit.ts)。
|
||||
|
||||
interface ReaderComicProps {
|
||||
images: string[]
|
||||
@@ -22,6 +25,8 @@ interface ReaderComicProps {
|
||||
onScrolled: () => void
|
||||
/** 双页模式:本屏要并排显示的图片序号(1 张=独占的宽图/单页,2 张=左右一对)。 */
|
||||
spread?: number[] | null
|
||||
/** 滚动模式的图片显示尺寸档位(翻页模式不适用,传 default 即可)。 */
|
||||
imageFit?: ComicImageFit
|
||||
}
|
||||
|
||||
function ComicImage({
|
||||
@@ -29,13 +34,19 @@ function ComicImage({
|
||||
theme,
|
||||
fit = false,
|
||||
half = false,
|
||||
imageFit = 'default',
|
||||
viewportHeight = 0,
|
||||
}: {
|
||||
src: string
|
||||
theme: { bg: string; text: string; accent: string }
|
||||
// fit:整页缩放至视口内,长图不再被 overflow-hidden 的容器裁掉。
|
||||
// fit:整页缩放至视口内,长图不再被 overflow-hidden 的容器裁掉(翻页模式)。
|
||||
fit?: boolean
|
||||
// half:双页并排时占位不超过半屏;容器收窄到图片本身宽度,两页之间不留缝。
|
||||
half?: boolean
|
||||
// imageFit:滚动模式的显示尺寸档位。
|
||||
imageFit?: ComicImageFit
|
||||
// viewportHeight:滚动容器高度(px),height/long 档位按它换算。
|
||||
viewportHeight?: number
|
||||
}) {
|
||||
const [state, setState] = useState<'loading' | 'ok' | 'error'>('loading')
|
||||
|
||||
@@ -45,6 +56,15 @@ function ComicImage({
|
||||
setState('loading')
|
||||
}, [src])
|
||||
|
||||
const loaded = state === 'ok'
|
||||
// 翻页模式:整页缩放进视口。滚动模式:按用户选的尺寸档位摆(外层列宽已放开)。
|
||||
const sizing = fit
|
||||
? {
|
||||
className: 'block max-h-full w-auto max-w-full object-contain',
|
||||
style: loaded ? {} : { minHeight: '10rem' },
|
||||
}
|
||||
: comicImageSizing(imageFit, viewportHeight, loaded)
|
||||
|
||||
// <img> 必须始终留在渲染树里(不能 display:none):浏览器不会去拉取
|
||||
// display:none 的 loading="lazy" 图片,onLoad 就永远不会触发,于是更没机会
|
||||
// 脱离 loading —— 之前用 hidden 藏图就死在这里,漫画只剩一个转圈。
|
||||
@@ -71,8 +91,8 @@ function ComicImage({
|
||||
setState('ok')
|
||||
}}
|
||||
onError={() => setState('error')}
|
||||
className={fit ? 'block max-h-full w-auto max-w-full object-contain' : 'block w-full'}
|
||||
style={state === 'ok' ? undefined : { minHeight: '10rem' }}
|
||||
className={sizing.className}
|
||||
style={sizing.style}
|
||||
/>
|
||||
{state === 'loading' && (
|
||||
<div
|
||||
@@ -94,10 +114,27 @@ function ComicImage({
|
||||
)
|
||||
}
|
||||
|
||||
export function ReaderComic({ images, theme, mode, page, onZone, initialImage, onProgress, scrollTo, onScrolled, spread }: ReaderComicProps) {
|
||||
export function ReaderComic({ images, theme, mode, page, onZone, initialImage, onProgress, scrollTo, onScrolled, spread, imageFit = 'default' }: ReaderComicProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const imgRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const restoredRef = useRef(false)
|
||||
// 滚动容器的高度:「一屏多高」以这个容器为准,而不是 window.innerHeight——正文区是
|
||||
// root(fixed inset-0)里的 flex-1,将来上下再挂条之类的布局变化就不等于窗口高度了。
|
||||
// 用 ResizeObserver 跟着它,改窗口大小时 height/long 档位自动重算。
|
||||
const [viewportHeight, setViewportHeight] = useState(0)
|
||||
|
||||
const selfSized = isSelfSizedComicFit(imageFit)
|
||||
useEffect(() => {
|
||||
// 只有 height/long/original 用得到,默认档位不挂观察器,免得每次改窗口都重渲染整章。
|
||||
if (mode !== 'scroll' || !selfSized) return
|
||||
const el = scrollRef.current
|
||||
if (!el) return
|
||||
const update = () => setViewportHeight(el.clientHeight)
|
||||
update()
|
||||
const observer = new ResizeObserver(update)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}, [mode, selfSized])
|
||||
|
||||
// 滚动模式:恢复进度(图片序号)并上报当前图
|
||||
useEffect(() => {
|
||||
@@ -183,7 +220,7 @@ export function ReaderComic({ images, theme, mode, page, onZone, initialImage, o
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ComicImage src={src} theme={theme} />
|
||||
<ComicImage src={src} theme={theme} imageFit={imageFit} viewportHeight={viewportHeight} />
|
||||
<p className="pb-1 text-center text-2xs opacity-40" style={{ color: theme.text }}>
|
||||
{i + 1} / {images.length}
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||
import { readerAPI, type ReaderBook, type ReaderChapter, type ReaderChapterContent, type ReaderSearchOrigin } from '../../api/reader'
|
||||
import { useComicSpreads } from '../../hooks/useComicSpreads'
|
||||
import { useSmoothWheelScroll } from '../../hooks/useSmoothWheelScroll'
|
||||
import { READER_THEMES, getReaderTheme, useReaderSettingsStore } from '../../stores/readerSettings'
|
||||
import { COMIC_IMAGE_FITS, READER_THEMES, getReaderTheme, useReaderSettingsStore } from '../../stores/readerSettings'
|
||||
import { buildChapterGroups, chapterGroupIndexOf } from '../../utils/chapterGroups'
|
||||
import { ReaderAudioPanel } from './ReaderAudioPanel'
|
||||
import { ReaderComic } from './ReaderComic'
|
||||
@@ -149,6 +149,19 @@ export default function ReaderViewPage() {
|
||||
? Math.min(comicSpreadIndexOf[comicPage] ?? 0, comicSpreads.length - 1)
|
||||
: 0
|
||||
|
||||
// ── 漫画滚动模式的图片显示尺寸 ──
|
||||
// 档位只在上下滚动模式生效:翻页模式是整页缩放进视口,没有「太大/太小」的问题,
|
||||
// 掺进来反而会和双页铺开的排版打架。所以翻页模式一律按 default 处理。
|
||||
const comicScrollFit = contentType === 'image' && settings.pageMode === 'scroll' ? settings.comicImageFit : 'default'
|
||||
/** 图片尺寸档位只在漫画的滚动模式下起作用,别的场景不显示这一行免得点了没反应。 */
|
||||
const showComicImageFit = contentType === 'image' && settings.pageMode === 'scroll'
|
||||
/**
|
||||
* 正文列是否放开 900px 上限。两种情况:漫画双页铺开要吃满窗口宽度;
|
||||
* 或者用户把图片尺寸调成了「适应宽度/高度/长边/原图」——这些档位由图片自己定尺寸,
|
||||
* 900px 的列会把它们再压回去。
|
||||
*/
|
||||
const comicFullWidth = contentType === 'image' && (comicDoublePage || comicScrollFit !== 'default')
|
||||
|
||||
// ── 加载书籍与章节 ──
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -711,7 +724,7 @@ export default function ReaderViewPage() {
|
||||
<div ref={readerRef} className="fixed inset-0 z-40 flex flex-col" style={{ backgroundColor: theme.bg, color: theme.text }}>
|
||||
{/* 正文视口 */}
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<div className={`mx-auto h-full w-full ${contentType === 'image' && comicDoublePage ? '' : 'max-w-[900px]'}`}>
|
||||
<div className={`mx-auto h-full w-full ${comicFullWidth ? '' : 'max-w-[900px]'}`}>
|
||||
{contentType === 'audio' ? (
|
||||
media && media.tracks && media.tracks.length > 0 ? (
|
||||
<ReaderAudioPanel
|
||||
@@ -746,6 +759,7 @@ export default function ReaderViewPage() {
|
||||
theme={theme}
|
||||
mode={settings.pageMode}
|
||||
page={comicPage}
|
||||
imageFit={comicScrollFit}
|
||||
spread={comicDoublePage ? (comicSpreads[comicSpreadIndex] ?? null) : null}
|
||||
onZone={(zone) => {
|
||||
if (zone === 'center') setMenuOpen((v) => !v)
|
||||
@@ -975,7 +989,7 @@ export default function ReaderViewPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 界面设置面板(主题 / 字号 / 行距 / 段距) */}
|
||||
{/* 界面设置面板(主题 / 字号 / 行距 / 段距 / 漫画图片尺寸) */}
|
||||
{panel === 'style' && (
|
||||
<div
|
||||
className="absolute bottom-full inset-x-0 border-t px-4 py-4"
|
||||
@@ -1029,6 +1043,30 @@ export default function ReaderViewPage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* 漫画滚动模式的图片显示尺寸。默认档就是老样子(900px 居中),
|
||||
「适应宽度」铺满窗口,「原图」按原始像素 1:1 看细节。 */}
|
||||
{showComicImageFit && (
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs">
|
||||
<span className="opacity-70">图片尺寸</span>
|
||||
{COMIC_IMAGE_FITS.map((item) => {
|
||||
const active = settings.comicImageFit === item.id
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => settings.setComicImageFit(item.id)}
|
||||
className={`rounded-lg border px-2 py-0.5 ${active ? 'font-bold' : 'opacity-70'}`}
|
||||
style={{
|
||||
borderColor: active ? theme.accent : theme.text + '44',
|
||||
color: active ? theme.accent : theme.text,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user