Merge pull request #152 from senshinya/d1_cache

feat: d1 local cache
This commit is contained in:
senshinya
2025-07-14 22:16:37 +08:00
committed by GitHub
6 changed files with 992 additions and 121 deletions

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps */
'use client';
import { ChevronRight } from 'lucide-react';
@@ -9,6 +11,7 @@ import {
clearAllFavorites,
getAllFavorites,
getAllPlayRecords,
subscribeToDataUpdates,
} from '@/lib/db.client';
import { DoubanItem, DoubanResult } from '@/lib/types';
@@ -82,42 +85,57 @@ function HomeClient() {
fetchDoubanData();
}, []);
// 处理收藏数据更新的函数
const updateFavoriteItems = async (allFavorites: Record<string, any>) => {
const allPlayRecords = await getAllPlayRecords();
// 根据保存时间排序(从近到远)
const sorted = Object.entries(allFavorites)
.sort(([, a], [, b]) => b.save_time - a.save_time)
.map(([key, fav]) => {
const plusIndex = key.indexOf('+');
const source = key.slice(0, plusIndex);
const id = key.slice(plusIndex + 1);
// 查找对应的播放记录,获取当前集数
const playRecord = allPlayRecords[key];
const currentEpisode = playRecord?.index;
return {
id,
source,
title: fav.title,
year: fav.year,
poster: fav.cover,
episodes: fav.total_episodes,
source_name: fav.source_name,
currentEpisode,
search_title: fav?.search_title,
} as FavoriteItem;
});
setFavoriteItems(sorted);
};
// 当切换到收藏夹时加载收藏数据
useEffect(() => {
if (activeTab !== 'favorites') return;
(async () => {
const [allFavorites, allPlayRecords] = await Promise.all([
getAllFavorites(),
getAllPlayRecords(),
]);
const loadFavorites = async () => {
const allFavorites = await getAllFavorites();
await updateFavoriteItems(allFavorites);
};
// 根据保存时间排序(从近到远)
const sorted = Object.entries(allFavorites)
.sort(([, a], [, b]) => b.save_time - a.save_time)
.map(([key, fav]) => {
const plusIndex = key.indexOf('+');
const source = key.slice(0, plusIndex);
const id = key.slice(plusIndex + 1);
loadFavorites();
// 查找对应的播放记录,获取当前集数
const playRecord = allPlayRecords[key];
const currentEpisode = playRecord?.index;
// 监听收藏更新事件
const unsubscribe = subscribeToDataUpdates(
'favoritesUpdated',
(newFavorites: Record<string, any>) => {
updateFavoriteItems(newFavorites);
}
);
return {
id,
source,
title: fav.title,
year: fav.year,
poster: fav.cover,
episodes: fav.total_episodes,
source_name: fav.source_name,
currentEpisode,
search_title: fav?.search_title,
} as FavoriteItem;
});
setFavoriteItems(sorted);
})();
return unsubscribe;
}, [activeTab]);
const handleCloseAnnouncement = (announcement: string) => {

View File

@@ -14,6 +14,7 @@ import {
getAllPlayRecords,
isFavorited,
savePlayRecord,
subscribeToDataUpdates,
toggleFavorite,
} from '@/lib/db.client';
import { SearchResult } from '@/lib/types';
@@ -916,6 +917,22 @@ function PlayPageClient() {
})();
}, [currentSource, currentId]);
// 监听收藏数据更新事件
useEffect(() => {
if (!currentSource || !currentId) return;
const unsubscribe = subscribeToDataUpdates(
'favoritesUpdated',
(favorites: Record<string, any>) => {
const key = generateStorageKey(currentSource, currentId);
const isFav = !!favorites[key];
setFavorited(isFav);
}
);
return unsubscribe;
}, [currentSource, currentId]);
// 切换收藏
const handleToggleFavorite = async () => {
if (

View File

@@ -10,6 +10,7 @@ import {
clearSearchHistory,
deleteSearchHistory,
getSearchHistory,
subscribeToDataUpdates,
} from '@/lib/db.client';
import { SearchResult } from '@/lib/types';
@@ -85,7 +86,19 @@ function SearchPageClient() {
useEffect(() => {
// 无搜索参数时聚焦搜索框
!searchParams.get('q') && document.getElementById('searchInput')?.focus();
// 初始加载搜索历史
getSearchHistory().then(setSearchHistory);
// 监听搜索历史更新事件
const unsubscribe = subscribeToDataUpdates(
'searchHistoryUpdated',
(newHistory: string[]) => {
setSearchHistory(newHistory);
}
);
return unsubscribe;
}, []);
useEffect(() => {
@@ -95,11 +108,8 @@ function SearchPageClient() {
setSearchQuery(query);
fetchSearchResults(query);
// 保存到搜索历史
addSearchHistory(query).then(async () => {
const history = await getSearchHistory();
setSearchHistory(history);
});
// 保存到搜索历史 (事件监听会自动更新界面)
addSearchHistory(query);
} else {
setShowResults(false);
}
@@ -161,11 +171,8 @@ function SearchPageClient() {
// 直接发请求
fetchSearchResults(trimmed);
// 保存到搜索历史
addSearchHistory(trimmed).then(async () => {
const history = await getSearchHistory();
setSearchHistory(history);
});
// 保存到搜索历史 (事件监听会自动更新界面)
addSearchHistory(trimmed);
};
return (
@@ -276,9 +283,8 @@ function SearchPageClient() {
{searchHistory.length > 0 && (
<button
onClick={async () => {
await clearSearchHistory();
setSearchHistory([]);
onClick={() => {
clearSearchHistory(); // 事件监听会自动更新界面
}}
className='ml-3 text-sm text-gray-500 hover:text-red-500 transition-colors dark:text-gray-400 dark:hover:text-red-500'
>
@@ -303,12 +309,10 @@ function SearchPageClient() {
{/* 删除按钮 */}
<button
aria-label='删除搜索历史'
onClick={async (e) => {
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
await deleteSearchHistory(item);
const history = await getSearchHistory();
setSearchHistory(history);
deleteSearchHistory(item); // 事件监听会自动更新界面
}}
className='absolute -top-1 -right-1 w-4 h-4 opacity-0 group-hover:opacity-100 bg-gray-400 hover:bg-red-500 text-white rounded-full flex items-center justify-center text-[10px] transition-colors'
>

View File

@@ -4,7 +4,11 @@
import { useEffect, useState } from 'react';
import type { PlayRecord } from '@/lib/db.client';
import { clearAllPlayRecords, getAllPlayRecords } from '@/lib/db.client';
import {
clearAllPlayRecords,
getAllPlayRecords,
subscribeToDataUpdates,
} from '@/lib/db.client';
import ScrollableRow from '@/components/ScrollableRow';
import VideoCard from '@/components/VideoCard';
@@ -19,28 +23,30 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
>([]);
const [loading, setLoading] = useState(true);
// 处理播放记录数据更新的函数
const updatePlayRecords = (allRecords: Record<string, PlayRecord>) => {
// 将记录转换为数组并根据 save_time 由近到远排序
const recordsArray = Object.entries(allRecords).map(([key, record]) => ({
...record,
key,
}));
// 按 save_time 降序排序(最新的在前面)
const sortedRecords = recordsArray.sort(
(a, b) => b.save_time - a.save_time
);
setPlayRecords(sortedRecords);
};
useEffect(() => {
const fetchPlayRecords = async () => {
try {
setLoading(true);
// 从 localStorage 获取所有播放记录
// 从缓存或API获取所有播放记录
const allRecords = await getAllPlayRecords();
// 将记录转换为数组并根据 save_time 由近到远排序
const recordsArray = Object.entries(allRecords).map(
([key, record]) => ({
...record,
key,
})
);
// 按 save_time 降序排序(最新的在前面)
const sortedRecords = recordsArray.sort(
(a, b) => b.save_time - a.save_time
);
setPlayRecords(sortedRecords);
updatePlayRecords(allRecords);
} catch (error) {
console.error('获取播放记录失败:', error);
setPlayRecords([]);
@@ -50,6 +56,16 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
};
fetchPlayRecords();
// 监听播放记录更新事件
const unsubscribe = subscribeToDataUpdates(
'playRecordsUpdated',
(newRecords: Record<string, PlayRecord>) => {
updatePlayRecords(newRecords);
}
);
return unsubscribe;
}, []);
// 如果没有播放记录,则不渲染组件

View File

@@ -1,9 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CheckCircle, Heart, Link, PlayCircleIcon } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { deletePlayRecord, isFavorited, toggleFavorite } from '@/lib/db.client';
import {
deletePlayRecord,
generateStorageKey,
isFavorited,
subscribeToDataUpdates,
toggleFavorite,
} from '@/lib/db.client';
import { SearchResult } from '@/lib/types';
import { ImagePlaceholder } from '@/components/ImagePlaceholder';
@@ -103,6 +111,7 @@ export default function VideoCard({
// 获取收藏状态
useEffect(() => {
if (from === 'douban' || !actualSource || !actualId) return;
const fetchFavoriteStatus = async () => {
try {
const fav = await isFavorited(actualSource, actualId);
@@ -111,7 +120,21 @@ export default function VideoCard({
throw new Error('检查收藏状态失败');
}
};
fetchFavoriteStatus();
// 监听收藏状态更新事件
const storageKey = generateStorageKey(actualSource, actualId);
const unsubscribe = subscribeToDataUpdates(
'favoritesUpdated',
(newFavorites: Record<string, any>) => {
// 检查当前项目是否在新的收藏列表中
const isNowFavorited = !!newFavorites[storageKey];
setFavorited(isNowFavorited);
}
);
return unsubscribe;
}, [from, actualSource, actualId]);
const handleToggleFavorite = useCallback(

File diff suppressed because it is too large Load Diff