补充规则以及修改电子书馆来源选项卡为单行
This commit is contained in:
@@ -12419,7 +12419,7 @@ const OPDSConfigComponent = ({
|
||||
<div className='mb-3 flex items-center justify-between gap-3'>
|
||||
<div>
|
||||
<h4 className='text-sm font-medium text-amber-900 dark:text-amber-100'>Legado 订阅</h4>
|
||||
<p className='mt-1 text-xs text-amber-800 dark:text-amber-200'>输入订阅 URL 导入,支持大型书源订阅。</p>
|
||||
<p className='mt-1 text-xs text-amber-800 dark:text-amber-200'>目前处于实验性阶段,仅支持部分简单订阅。</p>
|
||||
</div>
|
||||
<button type='button' onClick={importLegadoSubscription} disabled={!legadoSubscriptionUrl.trim() || isLoading('importLegadoSubscription')} className={buttonStyles.primarySmall}>{isLoading('importLegadoSubscription') ? '导入中...' : '导入订阅'}</button>
|
||||
</div>
|
||||
|
||||
@@ -72,9 +72,12 @@ export default function BooksCatalogPage() {
|
||||
const [error, setError] = useState('');
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const loaderRef = useRef<HTMLDivElement | null>(null);
|
||||
const sourceScrollerRef = useRef<HTMLDivElement | null>(null);
|
||||
const navScrollerRef = useRef<HTMLDivElement | null>(null);
|
||||
const loadedPageHrefsRef = useRef<Set<string>>(new Set());
|
||||
const failedPageHrefsRef = useRef<Set<string>>(new Set());
|
||||
const sourceDragStateRef = useRef<{ pointerId: number; startX: number; startScrollLeft: number; moved: boolean; pointerType: string } | null>(null);
|
||||
const suppressSourceClickRef = useRef(false);
|
||||
const navDragStateRef = useRef<{ pointerId: number; startX: number; startScrollLeft: number; moved: boolean; pointerType: string } | null>(null);
|
||||
const suppressNavClickRef = useRef(false);
|
||||
|
||||
@@ -157,6 +160,64 @@ export default function BooksCatalogPage() {
|
||||
return () => observer.disconnect();
|
||||
}, [data, nextHref, loadingMore, loadCatalog]);
|
||||
|
||||
const handleSourcePointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||
const node = sourceScrollerRef.current;
|
||||
if (!node) return;
|
||||
sourceDragStateRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX,
|
||||
startScrollLeft: node.scrollLeft,
|
||||
moved: false,
|
||||
pointerType: event.pointerType,
|
||||
};
|
||||
suppressSourceClickRef.current = false;
|
||||
if (event.pointerType !== 'mouse') {
|
||||
node.setPointerCapture?.(event.pointerId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSourcePointerMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const node = sourceScrollerRef.current;
|
||||
const dragState = sourceDragStateRef.current;
|
||||
if (!node || !dragState || dragState.pointerId !== event.pointerId) return;
|
||||
const deltaX = event.clientX - dragState.startX;
|
||||
const moveThreshold = dragState.pointerType === 'mouse' ? 8 : 4;
|
||||
if (Math.abs(deltaX) > moveThreshold) {
|
||||
dragState.moved = true;
|
||||
suppressSourceClickRef.current = true;
|
||||
}
|
||||
node.scrollLeft = dragState.startScrollLeft - deltaX;
|
||||
}, []);
|
||||
|
||||
const handleSourcePointerUp = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const node = sourceScrollerRef.current;
|
||||
const dragState = sourceDragStateRef.current;
|
||||
if (!dragState || dragState.pointerId !== event.pointerId) return;
|
||||
if (dragState.moved) {
|
||||
event.preventDefault();
|
||||
window.setTimeout(() => {
|
||||
suppressSourceClickRef.current = false;
|
||||
}, 0);
|
||||
}
|
||||
sourceDragStateRef.current = null;
|
||||
if (dragState.pointerType !== 'mouse') {
|
||||
node?.releasePointerCapture?.(event.pointerId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSourcePointerLeave = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.pointerType === 'mouse') return;
|
||||
handleSourcePointerUp(event);
|
||||
}, [handleSourcePointerUp]);
|
||||
|
||||
const handleSourceWheel = useCallback((event: ReactWheelEvent<HTMLDivElement>) => {
|
||||
const node = sourceScrollerRef.current;
|
||||
if (!node) return;
|
||||
const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
|
||||
if (!delta) return;
|
||||
node.scrollLeft += delta;
|
||||
}, []);
|
||||
|
||||
const handleNavPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||
@@ -235,9 +296,30 @@ export default function BooksCatalogPage() {
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<div
|
||||
ref={sourceScrollerRef}
|
||||
className='flex flex-nowrap gap-2 overflow-x-auto pb-1 cursor-grab select-none touch-pan-x active:cursor-grabbing'
|
||||
onPointerDown={handleSourcePointerDown}
|
||||
onPointerMove={handleSourcePointerMove}
|
||||
onPointerUp={handleSourcePointerUp}
|
||||
onPointerCancel={handleSourcePointerUp}
|
||||
onPointerLeave={handleSourcePointerLeave}
|
||||
onWheel={handleSourceWheel}
|
||||
>
|
||||
{sources.map((source) => (
|
||||
<Link key={source.id} href={`/books/catalog?sourceId=${encodeURIComponent(source.id)}`} className={`rounded-full px-4 py-2 text-sm ${source.id === sourceId ? 'bg-sky-600 text-white' : 'border border-gray-200 dark:border-gray-700'}`}>
|
||||
<Link
|
||||
key={source.id}
|
||||
href={`/books/catalog?sourceId=${encodeURIComponent(source.id)}`}
|
||||
draggable={false}
|
||||
onDragStart={(event) => event.preventDefault()}
|
||||
onClick={(event) => {
|
||||
if (suppressSourceClickRef.current) {
|
||||
event.preventDefault();
|
||||
suppressSourceClickRef.current = false;
|
||||
}
|
||||
}}
|
||||
className={`shrink-0 whitespace-nowrap rounded-full px-4 py-2 text-sm ${source.id === sourceId ? 'bg-sky-600 text-white' : 'border border-gray-200 dark:border-gray-700'}`}
|
||||
>
|
||||
{source.name}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -46,7 +46,6 @@ export default function BooksHomePage() {
|
||||
<div className='space-y-6'>
|
||||
<section className='rounded-3xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
||||
<h1 className='text-lg font-semibold'>电子书源</h1>
|
||||
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>支持 OPDS 与 Legado 书源,提供搜索、书架与在线阅读。</p>
|
||||
</section>
|
||||
|
||||
{loading ? <BooksHomeSkeleton /> : null}
|
||||
|
||||
Reference in New Issue
Block a user