增加播放记录批量删除功能
This commit is contained in:
+233
-67
@@ -17,7 +17,7 @@
|
||||
import { getAuthInfoFromBrowserCookie, clearAuthCookie } from './auth';
|
||||
import { normalizeEpisodeFilterConfig } from './episode-filter';
|
||||
import { MangaReadRecord, MangaShelfItem } from './manga.types';
|
||||
import { DanmakuFilterConfig, EpisodeFilterConfig,SkipConfig } from './types';
|
||||
import { DanmakuFilterConfig, EpisodeFilterConfig, SkipConfig } from './types';
|
||||
|
||||
// 全局错误触发函数
|
||||
function triggerGlobalError(message: string) {
|
||||
@@ -97,7 +97,8 @@ const FAVORITES_KEY = 'moontv_favorites';
|
||||
const MANGA_SHELF_KEY = 'moontv_manga_shelf';
|
||||
const MANGA_HISTORY_KEY = 'moontv_manga_history';
|
||||
const DEFAULT_MAX_MANGA_HISTORY_RECORDS = 100;
|
||||
const DEFAULT_MAX_MANGA_HISTORY_THRESHOLD = DEFAULT_MAX_MANGA_HISTORY_RECORDS + 10;
|
||||
const DEFAULT_MAX_MANGA_HISTORY_THRESHOLD =
|
||||
DEFAULT_MAX_MANGA_HISTORY_RECORDS + 10;
|
||||
const SEARCH_HISTORY_KEY = 'moontv_search_history';
|
||||
const MUSIC_PLAY_RECORDS_KEY = 'moontv_music_play_records';
|
||||
|
||||
@@ -141,10 +142,7 @@ class HybridCacheManager {
|
||||
/**
|
||||
* 获取或创建请求 Promise(防止并发重复请求)
|
||||
*/
|
||||
getOrCreateRequest<T>(
|
||||
key: string,
|
||||
fetcher: () => Promise<T>
|
||||
): Promise<T> {
|
||||
getOrCreateRequest<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
|
||||
// 如果已有正在进行的请求,直接返回
|
||||
if (this.pendingRequests.has(key)) {
|
||||
console.log(`[${key}] 复用进行中的请求`);
|
||||
@@ -153,11 +151,10 @@ class HybridCacheManager {
|
||||
|
||||
console.log(`[${key}] 创建新请求`);
|
||||
// 创建新请求
|
||||
const promise = fetcher()
|
||||
.finally(() => {
|
||||
// 请求完成后清除缓存
|
||||
this.pendingRequests.delete(key);
|
||||
});
|
||||
const promise = fetcher().finally(() => {
|
||||
// 请求完成后清除缓存
|
||||
this.pendingRequests.delete(key);
|
||||
});
|
||||
|
||||
this.pendingRequests.set(key, promise);
|
||||
return promise;
|
||||
@@ -249,7 +246,10 @@ class HybridCacheManager {
|
||||
delete cache.mangaShelf;
|
||||
}
|
||||
|
||||
if (cache.mangaReadRecords && now - cache.mangaReadRecords.timestamp > maxAge) {
|
||||
if (
|
||||
cache.mangaReadRecords &&
|
||||
now - cache.mangaReadRecords.timestamp > maxAge
|
||||
) {
|
||||
delete cache.mangaReadRecords;
|
||||
}
|
||||
}
|
||||
@@ -565,7 +565,12 @@ const cacheManager = HybridCacheManager.getInstance();
|
||||
* 立即从数据库刷新对应类型的缓存以保持数据一致性
|
||||
*/
|
||||
async function handleDatabaseOperationFailure(
|
||||
dataType: 'playRecords' | 'favorites' | 'searchHistory' | 'mangaShelf' | 'mangaHistory',
|
||||
dataType:
|
||||
| 'playRecords'
|
||||
| 'favorites'
|
||||
| 'searchHistory'
|
||||
| 'mangaShelf'
|
||||
| 'mangaHistory',
|
||||
error: any
|
||||
): Promise<void> {
|
||||
console.error(`数据库操作失败 (${dataType}):`, error);
|
||||
@@ -598,12 +603,16 @@ async function handleDatabaseOperationFailure(
|
||||
eventName = 'searchHistoryUpdated';
|
||||
break;
|
||||
case 'mangaShelf':
|
||||
freshData = await fetchFromApi<Record<string, MangaShelfItem>>(`/api/manga/shelf`);
|
||||
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`);
|
||||
freshData = await fetchFromApi<Record<string, MangaReadRecord>>(
|
||||
`/api/manga/history`
|
||||
);
|
||||
cacheManager.cacheMangaReadRecords(freshData);
|
||||
eventName = 'mangaHistoryUpdated';
|
||||
break;
|
||||
@@ -642,9 +651,16 @@ export async function fetchWithAuth(
|
||||
const text = await res.clone().text();
|
||||
|
||||
// 只有当响应体包含 "Unauthorized" 或 "Refresh token expired" 或 "Access token expired" 时才处理
|
||||
if (text.includes('Unauthorized') || text.includes('Refresh token expired') || text.includes('Access token expired')) {
|
||||
if (
|
||||
text.includes('Unauthorized') ||
|
||||
text.includes('Refresh token expired') ||
|
||||
text.includes('Access token expired')
|
||||
) {
|
||||
// 如果在登录页面,跳过刷新逻辑
|
||||
if (typeof window !== 'undefined' && window.location.pathname === '/login') {
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.pathname === '/login'
|
||||
) {
|
||||
console.log('[fetchWithAuth] On login page, skipping refresh logic');
|
||||
return res;
|
||||
}
|
||||
@@ -671,7 +687,9 @@ export async function fetchWithAuth(
|
||||
}
|
||||
} else {
|
||||
// 不是认证错误的401,直接返回
|
||||
console.log('[fetchWithAuth] Received 401 but not an auth error, skipping refresh');
|
||||
console.log(
|
||||
'[fetchWithAuth] Received 401 but not an auth error, skipping refresh'
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -679,9 +697,16 @@ export async function fetchWithAuth(
|
||||
if (res.status === 401) {
|
||||
const text2 = await res.clone().text();
|
||||
// 再次检查响应体
|
||||
if (text2.includes('Unauthorized') || text2.includes('Refresh token expired') || text2.includes('Access token expired')) {
|
||||
if (
|
||||
text2.includes('Unauthorized') ||
|
||||
text2.includes('Refresh token expired') ||
|
||||
text2.includes('Access token expired')
|
||||
) {
|
||||
// 检查当前页面是否已经是登录页,避免重复跳转
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
!window.location.pathname.startsWith('/login')
|
||||
) {
|
||||
// 调用 logout 接口
|
||||
try {
|
||||
await fetch('/api/logout', {
|
||||
@@ -824,7 +849,10 @@ export function getCachedPlayRecordsSnapshot(): Record<string, PlayRecord> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedMangaReadRecordsSnapshot(): Record<string, MangaReadRecord> {
|
||||
export function getCachedMangaReadRecordsSnapshot(): Record<
|
||||
string,
|
||||
MangaReadRecord
|
||||
> {
|
||||
if (typeof window === 'undefined') {
|
||||
return {};
|
||||
}
|
||||
@@ -995,6 +1023,66 @@ export async function deletePlayRecord(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除播放记录。
|
||||
* 数据库存储模式下只发起一次 API 请求,并进行一次缓存/事件更新。
|
||||
*/
|
||||
export async function deletePlayRecords(keys: string[]): Promise<void> {
|
||||
const uniqueKeys = Array.from(new Set(keys)).filter(Boolean);
|
||||
if (uniqueKeys.length === 0) return;
|
||||
|
||||
// 数据库存储模式:一次性乐观更新 + 一次 API 请求
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const cachedRecords = cacheManager.getCachedPlayRecords() || {};
|
||||
uniqueKeys.forEach((key) => {
|
||||
delete cachedRecords[key];
|
||||
});
|
||||
cacheManager.cachePlayRecords(cachedRecords);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('playRecordsUpdated', {
|
||||
detail: cachedRecords,
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await fetchWithAuth('/api/playrecords', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ keys: uniqueKeys }),
|
||||
});
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('playRecords', err);
|
||||
triggerGlobalError('删除播放记录失败');
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// localstorage 模式:一次性更新本地数据和事件
|
||||
if (typeof window === 'undefined') {
|
||||
console.warn('无法在服务端删除播放记录到 localStorage');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const allRecords = await getAllPlayRecords();
|
||||
uniqueKeys.forEach((key) => {
|
||||
delete allRecords[key];
|
||||
});
|
||||
localStorage.setItem(PLAY_RECORDS_KEY, JSON.stringify(allRecords));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('playRecordsUpdated', {
|
||||
detail: allRecords,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('批量删除播放记录失败:', err);
|
||||
triggerGlobalError('删除播放记录失败');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移播放记录到新的 source/id。
|
||||
* 用于换源时保留单一记忆点语义:当前进度迁移到新源后,再清理旧源记录。
|
||||
@@ -1038,9 +1126,12 @@ export async function migratePlayRecord(
|
||||
body: JSON.stringify({ key: toKey, record }),
|
||||
});
|
||||
|
||||
await fetchWithAuth(`/api/playrecords?key=${encodeURIComponent(fromKey)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
await fetchWithAuth(
|
||||
`/api/playrecords?key=${encodeURIComponent(fromKey)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
persistMove().catch((err) => {
|
||||
@@ -1307,7 +1398,8 @@ export async function deleteSearchHistory(keyword: string): Promise<void> {
|
||||
|
||||
// 模块级别的防重复请求机制
|
||||
let pendingFavoritesBackgroundRequest: Promise<void> | null = null;
|
||||
let pendingFavoritesFetchRequest: Promise<Record<string, Favorite>> | null = null;
|
||||
let pendingFavoritesFetchRequest: Promise<Record<string, Favorite>> | null =
|
||||
null;
|
||||
let lastFavoritesBackgroundFetchTime = 0;
|
||||
const MIN_BACKGROUND_FETCH_INTERVAL = 3000; // 3秒内不重复后台请求
|
||||
|
||||
@@ -1329,12 +1421,18 @@ export async function getAllFavorites(): Promise<Record<string, Favorite>> {
|
||||
if (cachedData) {
|
||||
// 有缓存:返回缓存,后台异步刷新(带防抖和防重复)
|
||||
const now = Date.now();
|
||||
if (now - lastFavoritesBackgroundFetchTime > MIN_BACKGROUND_FETCH_INTERVAL && !pendingFavoritesBackgroundRequest) {
|
||||
if (
|
||||
now - lastFavoritesBackgroundFetchTime >
|
||||
MIN_BACKGROUND_FETCH_INTERVAL &&
|
||||
!pendingFavoritesBackgroundRequest
|
||||
) {
|
||||
lastFavoritesBackgroundFetchTime = now;
|
||||
|
||||
pendingFavoritesBackgroundRequest = (async () => {
|
||||
try {
|
||||
const freshData = await fetchFromApi<Record<string, Favorite>>(`/api/favorites`);
|
||||
const freshData = await fetchFromApi<Record<string, Favorite>>(
|
||||
`/api/favorites`
|
||||
);
|
||||
// 只有数据真正不同时才更新缓存
|
||||
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
|
||||
cacheManager.cacheFavorites(freshData);
|
||||
@@ -1363,7 +1461,9 @@ export async function getAllFavorites(): Promise<Record<string, Favorite>> {
|
||||
|
||||
pendingFavoritesFetchRequest = (async () => {
|
||||
try {
|
||||
const freshData = await fetchFromApi<Record<string, Favorite>>(`/api/favorites`);
|
||||
const freshData = await fetchFromApi<Record<string, Favorite>>(
|
||||
`/api/favorites`
|
||||
);
|
||||
cacheManager.cacheFavorites(freshData);
|
||||
return freshData;
|
||||
} catch (err) {
|
||||
@@ -1627,10 +1727,11 @@ export async function clearAllFavorites(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ---------------- 漫画书架 / 历史 API ----------------
|
||||
|
||||
export async function getAllMangaShelf(): Promise<Record<string, MangaShelfItem>> {
|
||||
export async function getAllMangaShelf(): Promise<
|
||||
Record<string, MangaShelfItem>
|
||||
> {
|
||||
if (typeof window === 'undefined') return {};
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
@@ -1640,7 +1741,9 @@ export async function getAllMangaShelf(): Promise<Record<string, MangaShelfItem>
|
||||
.then((freshData) => {
|
||||
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
|
||||
cacheManager.cacheMangaShelf(freshData);
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: freshData }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaShelfUpdated', { detail: freshData })
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -1650,7 +1753,9 @@ export async function getAllMangaShelf(): Promise<Record<string, MangaShelfItem>
|
||||
}
|
||||
|
||||
try {
|
||||
const freshData = await fetchFromApi<Record<string, MangaShelfItem>>('/api/manga/shelf');
|
||||
const freshData = await fetchFromApi<Record<string, MangaShelfItem>>(
|
||||
'/api/manga/shelf'
|
||||
);
|
||||
cacheManager.cacheMangaShelf(freshData);
|
||||
return freshData;
|
||||
} catch (err) {
|
||||
@@ -1671,14 +1776,20 @@ export async function getAllMangaShelf(): Promise<Record<string, MangaShelfItem>
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveMangaShelf(sourceId: string, mangaId: string, item: MangaShelfItem): Promise<void> {
|
||||
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 }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaShelfUpdated', { detail: cached })
|
||||
);
|
||||
|
||||
try {
|
||||
await fetchWithAuth('/api/manga/shelf', {
|
||||
@@ -1696,20 +1807,29 @@ export async function saveMangaShelf(sourceId: string, mangaId: string, item: Ma
|
||||
const allItems = await getAllMangaShelf();
|
||||
allItems[key] = item;
|
||||
localStorage.setItem(MANGA_SHELF_KEY, JSON.stringify(allItems));
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: allItems }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaShelfUpdated', { detail: allItems })
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteMangaShelf(sourceId: string, mangaId: string): Promise<void> {
|
||||
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 }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaShelfUpdated', { detail: cached })
|
||||
);
|
||||
|
||||
try {
|
||||
await fetchWithAuth(`/api/manga/shelf?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
|
||||
await fetchWithAuth(`/api/manga/shelf?key=${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaShelf', err);
|
||||
throw err;
|
||||
@@ -1720,7 +1840,9 @@ export async function deleteMangaShelf(sourceId: string, mangaId: string): Promi
|
||||
const allItems = await getAllMangaShelf();
|
||||
delete allItems[key];
|
||||
localStorage.setItem(MANGA_SHELF_KEY, JSON.stringify(allItems));
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: allItems }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaShelfUpdated', { detail: allItems })
|
||||
);
|
||||
}
|
||||
|
||||
export async function clearAllMangaShelf(): Promise<void> {
|
||||
@@ -1740,7 +1862,9 @@ export async function clearAllMangaShelf(): Promise<void> {
|
||||
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: {} }));
|
||||
}
|
||||
|
||||
function trimMangaReadRecords(records: Record<string, MangaReadRecord>): Record<string, MangaReadRecord> {
|
||||
function trimMangaReadRecords(
|
||||
records: Record<string, MangaReadRecord>
|
||||
): Record<string, MangaReadRecord> {
|
||||
const entries = Object.entries(records);
|
||||
if (entries.length <= DEFAULT_MAX_MANGA_HISTORY_THRESHOLD) return records;
|
||||
|
||||
@@ -1751,7 +1875,9 @@ function trimMangaReadRecords(records: Record<string, MangaReadRecord>): Record<
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAllMangaReadRecords(): Promise<Record<string, MangaReadRecord>> {
|
||||
export async function getAllMangaReadRecords(): Promise<
|
||||
Record<string, MangaReadRecord>
|
||||
> {
|
||||
if (typeof window === 'undefined') return {};
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
@@ -1761,7 +1887,9 @@ export async function getAllMangaReadRecords(): Promise<Record<string, MangaRead
|
||||
.then((freshData) => {
|
||||
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
|
||||
cacheManager.cacheMangaReadRecords(freshData);
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: freshData }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaHistoryUpdated', { detail: freshData })
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -1771,7 +1899,9 @@ export async function getAllMangaReadRecords(): Promise<Record<string, MangaRead
|
||||
}
|
||||
|
||||
try {
|
||||
const freshData = await fetchFromApi<Record<string, MangaReadRecord>>('/api/manga/history');
|
||||
const freshData = await fetchFromApi<Record<string, MangaReadRecord>>(
|
||||
'/api/manga/history'
|
||||
);
|
||||
cacheManager.cacheMangaReadRecords(freshData);
|
||||
return freshData;
|
||||
} catch (err) {
|
||||
@@ -1792,7 +1922,11 @@ export async function getAllMangaReadRecords(): Promise<Record<string, MangaRead
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveMangaReadRecord(sourceId: string, mangaId: string, record: MangaReadRecord): Promise<void> {
|
||||
export async function saveMangaReadRecord(
|
||||
sourceId: string,
|
||||
mangaId: string,
|
||||
record: MangaReadRecord
|
||||
): Promise<void> {
|
||||
const key = generateStorageKey(sourceId, mangaId);
|
||||
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
@@ -1800,7 +1934,9 @@ export async function saveMangaReadRecord(sourceId: string, mangaId: string, rec
|
||||
cached[key] = record;
|
||||
const trimmedRecords = trimMangaReadRecords(cached);
|
||||
cacheManager.cacheMangaReadRecords(trimmedRecords);
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: trimmedRecords }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaHistoryUpdated', { detail: trimmedRecords })
|
||||
);
|
||||
|
||||
try {
|
||||
await fetchWithAuth('/api/manga/history', {
|
||||
@@ -1819,20 +1955,29 @@ export async function saveMangaReadRecord(sourceId: string, mangaId: string, rec
|
||||
allRecords[key] = record;
|
||||
const trimmedRecords = trimMangaReadRecords(allRecords);
|
||||
localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(trimmedRecords));
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: trimmedRecords }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaHistoryUpdated', { detail: trimmedRecords })
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteMangaReadRecord(sourceId: string, mangaId: string): Promise<void> {
|
||||
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 }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaHistoryUpdated', { detail: cached })
|
||||
);
|
||||
|
||||
try {
|
||||
await fetchWithAuth(`/api/manga/history?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
|
||||
await fetchWithAuth(`/api/manga/history?key=${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
await handleDatabaseOperationFailure('mangaHistory', err);
|
||||
throw err;
|
||||
@@ -1843,13 +1988,17 @@ export async function deleteMangaReadRecord(sourceId: string, mangaId: string):
|
||||
const allRecords = await getAllMangaReadRecords();
|
||||
delete allRecords[key];
|
||||
localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(allRecords));
|
||||
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: 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: {} }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mangaHistoryUpdated', { detail: {} })
|
||||
);
|
||||
try {
|
||||
await fetchWithAuth('/api/manga/history', { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
@@ -1886,15 +2035,21 @@ export async function refreshAllCache(): Promise<void> {
|
||||
// 使用 Promise 缓存防止并发重复刷新
|
||||
await cacheManager.getOrCreateRequest('refresh-all-cache', async () => {
|
||||
// 并行刷新所有数据
|
||||
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`),
|
||||
]);
|
||||
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`),
|
||||
]);
|
||||
|
||||
if (playRecords.status === 'fulfilled') {
|
||||
cacheManager.cachePlayRecords(playRecords.value);
|
||||
@@ -2020,7 +2175,7 @@ export function subscribeToDataUpdates<T>(
|
||||
callback: (data: T) => void
|
||||
): () => void {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => { };
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const handleUpdate = (event: CustomEvent) => {
|
||||
@@ -2425,7 +2580,10 @@ export async function saveDanmakuFilterConfig(
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem('moontv_danmaku_filter_config', JSON.stringify(config));
|
||||
localStorage.setItem(
|
||||
'moontv_danmaku_filter_config',
|
||||
JSON.stringify(config)
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('danmakuFilterConfigUpdated', {
|
||||
detail: config,
|
||||
@@ -2444,7 +2602,9 @@ export async function saveDanmakuFilterConfig(
|
||||
* 获取全部音乐播放记录。
|
||||
* 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。
|
||||
*/
|
||||
export async function getAllMusicPlayRecords(): Promise<Record<string, MusicPlayRecord>> {
|
||||
export async function getAllMusicPlayRecords(): Promise<
|
||||
Record<string, MusicPlayRecord>
|
||||
> {
|
||||
// 服务器端渲染阶段直接返回空
|
||||
if (typeof window === 'undefined') {
|
||||
return {};
|
||||
@@ -2594,9 +2754,12 @@ export async function deleteMusicPlayRecord(
|
||||
|
||||
// 异步同步到数据库
|
||||
try {
|
||||
await fetchWithAuth(`/api/music/playrecords?key=${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
await fetchWithAuth(
|
||||
`/api/music/playrecords?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('删除音乐播放记录失败:', err);
|
||||
triggerGlobalError('删除音乐播放记录失败');
|
||||
@@ -2701,7 +2864,10 @@ export async function saveEpisodeFilterConfig(
|
||||
|
||||
try {
|
||||
const normalizedConfig = normalizeEpisodeFilterConfig(config);
|
||||
localStorage.setItem('moontv_episode_filter_config', JSON.stringify(normalizedConfig));
|
||||
localStorage.setItem(
|
||||
'moontv_episode_filter_config',
|
||||
JSON.stringify(normalizedConfig)
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('episodeFilterConfigUpdated', {
|
||||
detail: normalizedConfig,
|
||||
|
||||
Reference in New Issue
Block a user