电子书架
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { BookAcquisitionLink, BookDetail, BookListItem, BookReadRecord, BookShelfItem } from './book.types';
|
||||
|
||||
const BOOK_ROUTE_CACHE_KEY = 'moontv_books_route_cache_v1';
|
||||
const MAX_CACHE_ITEMS = 300;
|
||||
|
||||
export interface BookRouteCacheItem {
|
||||
sourceId: string;
|
||||
bookId: string;
|
||||
sourceName?: string;
|
||||
title?: string;
|
||||
author?: string;
|
||||
cover?: string;
|
||||
summary?: string;
|
||||
detailHref?: string;
|
||||
acquisitionHref?: string;
|
||||
acquisitionLinks?: BookAcquisitionLink[];
|
||||
format?: 'epub' | 'pdf';
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
function buildKey(sourceId: string, bookId: string) {
|
||||
return `${sourceId}+${bookId}`;
|
||||
}
|
||||
|
||||
function readCache(): Record<string, BookRouteCacheItem> {
|
||||
if (typeof window === 'undefined') return {};
|
||||
try {
|
||||
const raw = localStorage.getItem(BOOK_ROUTE_CACHE_KEY);
|
||||
return raw ? (JSON.parse(raw) as Record<string, BookRouteCacheItem>) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(cache: Record<string, BookRouteCacheItem>) {
|
||||
if (typeof window === 'undefined') return;
|
||||
const entries = Object.entries(cache)
|
||||
.sort(([, a], [, b]) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
||||
.slice(0, MAX_CACHE_ITEMS);
|
||||
localStorage.setItem(BOOK_ROUTE_CACHE_KEY, JSON.stringify(Object.fromEntries(entries)));
|
||||
}
|
||||
|
||||
export function getBookRouteCache(sourceId: string, bookId: string): BookRouteCacheItem | null {
|
||||
const item = readCache()[buildKey(sourceId, bookId)];
|
||||
return item || null;
|
||||
}
|
||||
|
||||
export function saveBookRouteCache(item: Omit<BookRouteCacheItem, 'updatedAt'> & { updatedAt?: number }) {
|
||||
if (typeof window === 'undefined' || !item.sourceId || !item.bookId) return;
|
||||
const cache = readCache();
|
||||
const key = buildKey(item.sourceId, item.bookId);
|
||||
const prev = cache[key];
|
||||
cache[key] = {
|
||||
...prev,
|
||||
...item,
|
||||
acquisitionLinks: item.acquisitionLinks || prev?.acquisitionLinks || [],
|
||||
updatedAt: item.updatedAt || Date.now(),
|
||||
};
|
||||
writeCache(cache);
|
||||
}
|
||||
|
||||
export function cacheBookListItem(item: BookListItem) {
|
||||
saveBookRouteCache({
|
||||
sourceId: item.sourceId,
|
||||
bookId: item.id,
|
||||
sourceName: item.sourceName,
|
||||
title: item.title,
|
||||
author: item.author,
|
||||
cover: item.cover,
|
||||
summary: item.summary,
|
||||
detailHref: item.detailHref,
|
||||
acquisitionLinks: item.acquisitionLinks,
|
||||
});
|
||||
}
|
||||
|
||||
export function cacheBookDetail(detail: BookDetail) {
|
||||
const readable = detail.acquisitionLinks.find((item) => item.type.toLowerCase().includes('epub') || item.type.toLowerCase().includes('pdf'));
|
||||
saveBookRouteCache({
|
||||
sourceId: detail.sourceId,
|
||||
bookId: detail.id,
|
||||
sourceName: detail.sourceName,
|
||||
title: detail.title,
|
||||
author: detail.author,
|
||||
cover: detail.cover,
|
||||
summary: detail.summary,
|
||||
detailHref: detail.detailHref,
|
||||
acquisitionHref: readable?.href,
|
||||
format: readable?.type.toLowerCase().includes('pdf') ? 'pdf' : readable ? 'epub' : undefined,
|
||||
acquisitionLinks: detail.acquisitionLinks,
|
||||
});
|
||||
}
|
||||
|
||||
export function cacheBookShelfItem(item: BookShelfItem) {
|
||||
saveBookRouteCache({
|
||||
sourceId: item.sourceId,
|
||||
bookId: item.bookId,
|
||||
sourceName: item.sourceName,
|
||||
title: item.title,
|
||||
author: item.author,
|
||||
cover: item.cover,
|
||||
detailHref: item.detailHref,
|
||||
acquisitionHref: item.acquisitionHref,
|
||||
format: item.format,
|
||||
});
|
||||
}
|
||||
|
||||
export function cacheBookReadRecord(item: BookReadRecord) {
|
||||
saveBookRouteCache({
|
||||
sourceId: item.sourceId,
|
||||
bookId: item.bookId,
|
||||
sourceName: item.sourceName,
|
||||
title: item.title,
|
||||
author: item.author,
|
||||
cover: item.cover,
|
||||
detailHref: item.detailHref,
|
||||
acquisitionHref: item.acquisitionHref,
|
||||
format: item.format,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildBookDetailPath(sourceId: string, bookId: string) {
|
||||
return `/books/detail?sourceId=${encodeURIComponent(sourceId)}&bookId=${encodeURIComponent(bookId)}`;
|
||||
}
|
||||
|
||||
export function buildBookReadPath(sourceId: string, bookId: string) {
|
||||
return `/books/read?sourceId=${encodeURIComponent(sourceId)}&bookId=${encodeURIComponent(bookId)}`;
|
||||
}
|
||||
+10
-8
@@ -363,20 +363,22 @@ export class OPDSClient {
|
||||
href: normalizeUrl(source.url, href || source.url),
|
||||
entries: bookEntries.map((entry) => mapEntryToItem(source, entry)),
|
||||
navigation: [
|
||||
...feed.links.filter((link) => isNavigationLink(link)).map((link) => ({
|
||||
title: link.title || '目录',
|
||||
href: link.href,
|
||||
rel: link.rel,
|
||||
type: link.type,
|
||||
})),
|
||||
...feed.links
|
||||
.filter((link) => isNavigationLink(link) && link.rel !== 'next' && link.rel !== 'previous' && !!(link.title || '').trim())
|
||||
.map((link) => ({
|
||||
title: (link.title || '').trim(),
|
||||
href: link.href,
|
||||
rel: link.rel,
|
||||
type: link.type,
|
||||
})),
|
||||
...navigationEntries
|
||||
.map((entry) => ({
|
||||
title: entry.title,
|
||||
title: (entry.title || '').trim(),
|
||||
href: pickDetailHref(entry.links) || entry.links.find((link) => isNavigationLink(link))?.href || '',
|
||||
rel: entry.links.find((link) => isNavigationLink(link))?.rel,
|
||||
type: entry.links.find((link) => isNavigationLink(link))?.type,
|
||||
}))
|
||||
.filter((item) => !!item.href),
|
||||
.filter((item) => !!item.href && !!item.title && item.title !== '目录'),
|
||||
],
|
||||
nextHref: feed.links.find((link) => link.rel === 'next')?.href,
|
||||
previousHref: feed.links.find((link) => link.rel === 'previous')?.href,
|
||||
|
||||
Reference in New Issue
Block a user