增加漫画展馆功能
This commit is contained in:
+323
-3
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { getAuthInfoFromBrowserCookie, clearAuthCookie } from './auth';
|
||||
import { MangaReadRecord, MangaShelfItem } from './manga.types';
|
||||
import { DanmakuFilterConfig, EpisodeFilterConfig,SkipConfig } from './types';
|
||||
|
||||
// 全局错误触发函数
|
||||
@@ -81,6 +82,8 @@ interface CacheData<T> {
|
||||
interface UserCacheStore {
|
||||
playRecords?: CacheData<Record<string, PlayRecord>>;
|
||||
favorites?: CacheData<Record<string, Favorite>>;
|
||||
mangaShelf?: CacheData<Record<string, MangaShelfItem>>;
|
||||
mangaReadRecords?: CacheData<Record<string, MangaReadRecord>>;
|
||||
searchHistory?: CacheData<string[]>;
|
||||
skipConfigs?: CacheData<Record<string, SkipConfig>>;
|
||||
danmakuFilterConfig?: CacheData<DanmakuFilterConfig>;
|
||||
@@ -90,6 +93,8 @@ interface UserCacheStore {
|
||||
// ---- 常量 ----
|
||||
const PLAY_RECORDS_KEY = 'moontv_play_records';
|
||||
const FAVORITES_KEY = 'moontv_favorites';
|
||||
const MANGA_SHELF_KEY = 'moontv_manga_shelf';
|
||||
const MANGA_HISTORY_KEY = 'moontv_manga_history';
|
||||
const SEARCH_HISTORY_KEY = 'moontv_search_history';
|
||||
const MUSIC_PLAY_RECORDS_KEY = 'moontv_music_play_records';
|
||||
|
||||
@@ -236,6 +241,14 @@ class HybridCacheManager {
|
||||
if (cache.favorites && now - cache.favorites.timestamp > maxAge) {
|
||||
delete cache.favorites;
|
||||
}
|
||||
|
||||
if (cache.mangaShelf && now - cache.mangaShelf.timestamp > maxAge) {
|
||||
delete cache.mangaShelf;
|
||||
}
|
||||
|
||||
if (cache.mangaReadRecords && now - cache.mangaReadRecords.timestamp > maxAge) {
|
||||
delete cache.mangaReadRecords;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,6 +343,52 @@ class HybridCacheManager {
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
getCachedMangaShelf(): Record<string, MangaShelfItem> | null {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return null;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
const cached = userCache.mangaShelf;
|
||||
|
||||
if (cached && this.isCacheValid(cached)) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
cacheMangaShelf(data: Record<string, MangaShelfItem>): void {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
userCache.mangaShelf = this.createCacheData(data);
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
getCachedMangaReadRecords(): Record<string, MangaReadRecord> | null {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return null;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
const cached = userCache.mangaReadRecords;
|
||||
|
||||
if (cached && this.isCacheValid(cached)) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
cacheMangaReadRecords(data: Record<string, MangaReadRecord>): void {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
userCache.mangaReadRecords = this.createCacheData(data);
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的搜索历史
|
||||
*/
|
||||
@@ -503,7 +562,7 @@ const cacheManager = HybridCacheManager.getInstance();
|
||||
* 立即从数据库刷新对应类型的缓存以保持数据一致性
|
||||
*/
|
||||
async function handleDatabaseOperationFailure(
|
||||
dataType: 'playRecords' | 'favorites' | 'searchHistory',
|
||||
dataType: 'playRecords' | 'favorites' | 'searchHistory' | 'mangaShelf' | 'mangaHistory',
|
||||
error: any
|
||||
): Promise<void> {
|
||||
console.error(`数据库操作失败 (${dataType}):`, error);
|
||||
@@ -535,6 +594,16 @@ async function handleDatabaseOperationFailure(
|
||||
cacheManager.cacheSearchHistory(freshData);
|
||||
eventName = 'searchHistoryUpdated';
|
||||
break;
|
||||
case 'mangaShelf':
|
||||
freshData = await fetchFromApi<Record<string, MangaShelfItem>>(`/api/manga/shelf`);
|
||||
cacheManager.cacheMangaShelf(freshData);
|
||||
eventName = 'mangaShelfUpdated';
|
||||
break;
|
||||
case 'mangaHistory':
|
||||
freshData = await fetchFromApi<Record<string, MangaReadRecord>>(`/api/manga/history`);
|
||||
cacheManager.cacheMangaReadRecords(freshData);
|
||||
eventName = 'mangaHistoryUpdated';
|
||||
break;
|
||||
}
|
||||
|
||||
// 触发更新事件通知组件
|
||||
@@ -1519,6 +1588,229 @@ export async function clearAllFavorites(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ---------------- 漫画书架 / 历史 API ----------------
|
||||
|
||||
export async function getAllMangaShelf(): Promise<Record<string, MangaShelfItem>> {
|
||||
if (typeof window === 'undefined') return {};
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const cachedData = cacheManager.getCachedMangaShelf();
|
||||
if (cachedData) {
|
||||
fetchFromApi<Record<string, MangaShelfItem>>('/api/manga/shelf')
|
||||
.then((freshData) => {
|
||||
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
|
||||
cacheManager.cacheMangaShelf(freshData);
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: freshData }));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('后台同步漫画书架失败:', err);
|
||||
});
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
try {
|
||||
const freshData = await fetchFromApi<Record<string, MangaShelfItem>>('/api/manga/shelf');
|
||||
cacheManager.cacheMangaShelf(freshData);
|
||||
return freshData;
|
||||
} catch (err) {
|
||||
console.error('获取漫画书架失败:', err);
|
||||
triggerGlobalError('获取漫画书架失败');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(MANGA_SHELF_KEY);
|
||||
if (!raw) return {};
|
||||
return JSON.parse(raw) as Record<string, MangaShelfItem>;
|
||||
} catch (err) {
|
||||
console.error('读取漫画书架失败:', err);
|
||||
triggerGlobalError('读取漫画书架失败');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveMangaShelf(sourceId: string, mangaId: string, item: MangaShelfItem): Promise<void> {
|
||||
const key = generateStorageKey(sourceId, mangaId);
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const cached = cacheManager.getCachedMangaShelf() || {};
|
||||
cached[key] = item;
|
||||
cacheManager.cacheMangaShelf(cached);
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: cached }));
|
||||
|
||||
try {
|
||||
await fetchWithAuth('/api/manga/shelf', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ key, item }),
|
||||
});
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaShelf', err);
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const allItems = await getAllMangaShelf();
|
||||
allItems[key] = item;
|
||||
localStorage.setItem(MANGA_SHELF_KEY, JSON.stringify(allItems));
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: allItems }));
|
||||
}
|
||||
|
||||
export async function deleteMangaShelf(sourceId: string, mangaId: string): Promise<void> {
|
||||
const key = generateStorageKey(sourceId, mangaId);
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const cached = cacheManager.getCachedMangaShelf() || {};
|
||||
delete cached[key];
|
||||
cacheManager.cacheMangaShelf(cached);
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: cached }));
|
||||
|
||||
try {
|
||||
await fetchWithAuth(`/api/manga/shelf?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaShelf', err);
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const allItems = await getAllMangaShelf();
|
||||
delete allItems[key];
|
||||
localStorage.setItem(MANGA_SHELF_KEY, JSON.stringify(allItems));
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: allItems }));
|
||||
}
|
||||
|
||||
export async function clearAllMangaShelf(): Promise<void> {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
cacheManager.cacheMangaShelf({});
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: {} }));
|
||||
try {
|
||||
await fetchWithAuth('/api/manga/shelf', { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaShelf', err);
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(MANGA_SHELF_KEY);
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: {} }));
|
||||
}
|
||||
|
||||
export async function getAllMangaReadRecords(): Promise<Record<string, MangaReadRecord>> {
|
||||
if (typeof window === 'undefined') return {};
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const cachedData = cacheManager.getCachedMangaReadRecords();
|
||||
if (cachedData) {
|
||||
fetchFromApi<Record<string, MangaReadRecord>>('/api/manga/history')
|
||||
.then((freshData) => {
|
||||
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
|
||||
cacheManager.cacheMangaReadRecords(freshData);
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: freshData }));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('后台同步漫画历史失败:', err);
|
||||
});
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
try {
|
||||
const freshData = await fetchFromApi<Record<string, MangaReadRecord>>('/api/manga/history');
|
||||
cacheManager.cacheMangaReadRecords(freshData);
|
||||
return freshData;
|
||||
} catch (err) {
|
||||
console.error('获取漫画历史失败:', err);
|
||||
triggerGlobalError('获取漫画历史失败');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(MANGA_HISTORY_KEY);
|
||||
if (!raw) return {};
|
||||
return JSON.parse(raw) as Record<string, MangaReadRecord>;
|
||||
} catch (err) {
|
||||
console.error('读取漫画历史失败:', err);
|
||||
triggerGlobalError('读取漫画历史失败');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveMangaReadRecord(sourceId: string, mangaId: string, record: MangaReadRecord): Promise<void> {
|
||||
const key = generateStorageKey(sourceId, mangaId);
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const cached = cacheManager.getCachedMangaReadRecords() || {};
|
||||
cached[key] = record;
|
||||
cacheManager.cacheMangaReadRecords(cached);
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: cached }));
|
||||
|
||||
try {
|
||||
await fetchWithAuth('/api/manga/history', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ key, record }),
|
||||
});
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaHistory', err);
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const allRecords = await getAllMangaReadRecords();
|
||||
allRecords[key] = record;
|
||||
localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(allRecords));
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: allRecords }));
|
||||
}
|
||||
|
||||
export async function deleteMangaReadRecord(sourceId: string, mangaId: string): Promise<void> {
|
||||
const key = generateStorageKey(sourceId, mangaId);
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const cached = cacheManager.getCachedMangaReadRecords() || {};
|
||||
delete cached[key];
|
||||
cacheManager.cacheMangaReadRecords(cached);
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: cached }));
|
||||
|
||||
try {
|
||||
await fetchWithAuth(`/api/manga/history?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaHistory', err);
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const allRecords = await getAllMangaReadRecords();
|
||||
delete allRecords[key];
|
||||
localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(allRecords));
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: allRecords }));
|
||||
}
|
||||
|
||||
export async function clearAllMangaReadRecords(): Promise<void> {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
cacheManager.cacheMangaReadRecords({});
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: {} }));
|
||||
try {
|
||||
await fetchWithAuth('/api/manga/history', { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaHistory', err);
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(MANGA_HISTORY_KEY);
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: {} }));
|
||||
}
|
||||
|
||||
// ---------------- 混合缓存辅助函数 ----------------
|
||||
|
||||
/**
|
||||
@@ -1542,10 +1834,12 @@ export async function refreshAllCache(): Promise<void> {
|
||||
// 使用 Promise 缓存防止并发重复刷新
|
||||
await cacheManager.getOrCreateRequest('refresh-all-cache', async () => {
|
||||
// 并行刷新所有数据
|
||||
const [playRecords, favorites, searchHistory, skipConfigs] =
|
||||
const [playRecords, favorites, mangaShelf, mangaHistory, searchHistory, skipConfigs] =
|
||||
await Promise.allSettled([
|
||||
fetchFromApi<Record<string, PlayRecord>>(`/api/playrecords`),
|
||||
fetchFromApi<Record<string, Favorite>>(`/api/favorites`),
|
||||
fetchFromApi<Record<string, MangaShelfItem>>(`/api/manga/shelf`),
|
||||
fetchFromApi<Record<string, MangaReadRecord>>(`/api/manga/history`),
|
||||
fetchFromApi<string[]>(`/api/searchhistory`),
|
||||
fetchFromApi<Record<string, SkipConfig>>(`/api/skipconfigs`),
|
||||
]);
|
||||
@@ -1568,6 +1862,24 @@ export async function refreshAllCache(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
if (mangaShelf.status === 'fulfilled') {
|
||||
cacheManager.cacheMangaShelf(mangaShelf.value);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaShelfUpdated', {
|
||||
detail: mangaShelf.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (mangaHistory.status === 'fulfilled') {
|
||||
cacheManager.cacheMangaReadRecords(mangaHistory.value);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaHistoryUpdated', {
|
||||
detail: mangaHistory.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (searchHistory.status === 'fulfilled') {
|
||||
cacheManager.cacheSearchHistory(searchHistory.value);
|
||||
window.dispatchEvent(
|
||||
@@ -1601,6 +1913,8 @@ export function getCacheStatus(): {
|
||||
hasFavorites: boolean;
|
||||
hasSearchHistory: boolean;
|
||||
hasSkipConfigs: boolean;
|
||||
hasMangaShelf: boolean;
|
||||
hasMangaHistory: boolean;
|
||||
username: string | null;
|
||||
} {
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
@@ -1609,6 +1923,8 @@ export function getCacheStatus(): {
|
||||
hasFavorites: false,
|
||||
hasSearchHistory: false,
|
||||
hasSkipConfigs: false,
|
||||
hasMangaShelf: false,
|
||||
hasMangaHistory: false,
|
||||
username: null,
|
||||
};
|
||||
}
|
||||
@@ -1619,6 +1935,8 @@ export function getCacheStatus(): {
|
||||
hasFavorites: !!cacheManager.getCachedFavorites(),
|
||||
hasSearchHistory: !!cacheManager.getCachedSearchHistory(),
|
||||
hasSkipConfigs: !!cacheManager.getCachedSkipConfigs(),
|
||||
hasMangaShelf: !!cacheManager.getCachedMangaShelf(),
|
||||
hasMangaHistory: !!cacheManager.getCachedMangaReadRecords(),
|
||||
username: authInfo?.username || null,
|
||||
};
|
||||
}
|
||||
@@ -1629,7 +1947,9 @@ export type CacheUpdateEvent =
|
||||
| 'playRecordsUpdated'
|
||||
| 'favoritesUpdated'
|
||||
| 'searchHistoryUpdated'
|
||||
| 'skipConfigsUpdated';
|
||||
| 'skipConfigsUpdated'
|
||||
| 'mangaShelfUpdated'
|
||||
| 'mangaHistoryUpdated';
|
||||
|
||||
/**
|
||||
* 用于 React 组件监听数据更新的事件监听器
|
||||
|
||||
Reference in New Issue
Block a user