Merge pull request #90 from vvwn/main

利用 vercel 的Upstash Redis增加收藏和播放同步功能。
This commit is contained in:
Kuek Hao Yang
2026-02-25 17:31:18 +08:00
committed by GitHub
7 changed files with 6240 additions and 1245 deletions
+46
View File
@@ -0,0 +1,46 @@
import { Redis } from '@upstash/redis';
import { NextRequest, NextResponse } from 'next/server';
// 确保这行代码在整个文件中只出现一次
export const runtime = 'edge';
const redis = Redis.fromEnv();
export async function GET(request: NextRequest) {
const profileId = request.headers.get('x-profile-id');
if (!profileId) {
return NextResponse.json({ error: 'Missing profileId' }, { status: 400 });
}
try {
const data = await redis.get(`user:sync:${profileId}`);
return NextResponse.json({
success: true,
data: data || { history: [], favorites: [] }
});
} catch (error) {
console.error('Redis Get Error:', error);
return NextResponse.json({ error: 'Failed to fetch sync data' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const profileId = request.headers.get('x-profile-id');
if (!profileId) {
return NextResponse.json({ error: 'Missing profileId' }, { status: 400 });
}
try {
const body = await request.json();
const { history, favorites } = body;
await redis.set(`user:sync:${profileId}`, { history, favorites });
return NextResponse.json({ success: true });
} catch (error) {
console.error('Redis Set Error:', error);
return NextResponse.json({ error: 'Failed to save sync data' }, { status: 500 });
}
}
+5
View File
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/ThemeProvider";
import { AutoSync } from '@/components/AutoSync'; // <-- 引入了自动同步组件
import { TVProvider } from "@/lib/contexts/TVContext";
import { TVNavigationInitializer } from "@/components/TVNavigationInitializer";
import { Analytics } from "@vercel/analytics/react";
@@ -15,6 +16,7 @@ import { ScrollPositionManager } from "@/components/ScrollPositionManager";
import fs from 'fs';
import path from 'path';
// Server Component specifically for reading env/file (async for best practices)
async function AdKeywordsWrapper() {
let keywords: string[] = [];
@@ -97,6 +99,9 @@ export default function RootLayout({
suppressHydrationWarning
>
<ThemeProvider>
{/* 加入自动同步组件,它会在后台默默工作,我们放在 ThemeProvider 内部的最前面 */}
<AutoSync />
<TVProvider>
<TVNavigationInitializer />
<PasswordGate hasAuth={!!(process.env.ADMIN_PASSWORD || process.env.ACCOUNTS || process.env.ACCESS_PASSWORD)}>
+47
View File
@@ -0,0 +1,47 @@
'use client';
import { useEffect } from 'react';
import { useHistoryStore } from '@/lib/store/history-store';
import { useFavoritesStore } from '@/lib/store/favorites-store';
import { useCloudSync } from '@/lib/hooks/useCloudSync';
import { getSession } from '@/lib/store/auth-store';
// 防抖函数,防止频繁请求
function debounce(fn: Function, delay: number) {
let timeoutId: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
export function AutoSync() {
const { pushToCloud, pullFromCloud } = useCloudSync();
useEffect(() => {
const session = getSession();
if (!session) return; // 未登录不进行同步
// 1. 刚打开网页时,主动从云端拉取一次最新数据
pullFromCloud();
// 2. 监听本地数据的变化,如果数据变了,延迟 5 秒后推送到云端
const debouncedPush = debounce(pushToCloud, 5000);
// 修改点在这里:Zustand v4/v5 默认 subscribe 只接受一个参数
const unsubHistory = useHistoryStore.subscribe(() => {
debouncedPush();
});
const unsubFavorites = useFavoritesStore.subscribe(() => {
debouncedPush();
});
return () => {
unsubHistory();
unsubFavorites();
};
}, [pushToCloud, pullFromCloud]);
return null; // 这是一个静默组件,不需要渲染任何UI
}
+7 -1
View File
@@ -77,7 +77,13 @@ export const MovieCard = memo(function MovieCard({ movie, onMovieClick }: MovieC
)}
{movie.rate && parseFloat(movie.rate) > 0 && (
<div
className="absolute top-2 right-2 bg-black/80 px-2.5 py-1.5 flex items-center gap-1.5 rounded-[var(--radius-full)]"
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // 阻止事件冒泡,防止触发外层卡片的搜索点击
window.open(movie.url, '_blank', 'noopener,noreferrer');
}}
title="在豆瓣中查看"
className="absolute top-2 right-2 bg-black/80 hover:bg-black/90 px-2.5 py-1.5 flex items-center gap-1.5 rounded-[var(--radius-full)] z-20 hover:scale-105 transition-all shadow-md"
>
<Icons.Star size={12} className="text-yellow-400 fill-yellow-400" />
<span className="text-xs font-bold text-white">
+66
View File
@@ -0,0 +1,66 @@
import { useState, useCallback } from 'react';
import { useHistoryStore, usePremiumHistoryStore } from '@/lib/store/history-store';
import { useFavoritesStore, usePremiumFavoritesStore } from '@/lib/store/favorites-store';
import { getProfileId } from '@/lib/store/auth-store';
export function useCloudSync(isPremium = false) {
const [isSyncing, setIsSyncing] = useState(false);
const historyStore = isPremium ? usePremiumHistoryStore : useHistoryStore;
const favoritesStore = isPremium ? usePremiumFavoritesStore : useFavoritesStore;
const pullFromCloud = useCallback(async () => {
const profileId = getProfileId();
if (!profileId) return;
setIsSyncing(true);
try {
const response = await fetch('/api/user/sync', {
headers: { 'x-profile-id': profileId }
});
const result = await response.json();
if (result.success && result.data) {
if (result.data.history?.length > 0) {
historyStore.getState().importHistory(result.data.history);
}
if (result.data.favorites?.length > 0) {
favoritesStore.getState().importFavorites(result.data.favorites);
}
}
} catch (error) {
console.error('Failed to pull from cloud:', error);
} finally {
setIsSyncing(false);
}
}, [historyStore, favoritesStore]);
const pushToCloud = useCallback(async () => {
const profileId = getProfileId();
if (!profileId) return;
setIsSyncing(true);
try {
const currentHistory = historyStore.getState().viewingHistory;
const currentFavorites = favoritesStore.getState().favorites;
await fetch('/api/user/sync', {
method: 'POST',
headers: {
'x-profile-id': profileId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
history: currentHistory,
favorites: currentFavorites
})
});
} catch (error) {
console.error('Failed to push to cloud:', error);
} finally {
setIsSyncing(false);
}
}, [historyStore, favoritesStore]);
return { pushToCloud, pullFromCloud, isSyncing };
}
+6068 -1244
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -13,6 +13,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@upstash/redis": "^1.34.4",
"@vercel/analytics": "^1.6.1",
"hls.js": "^1.6.15",
"lucide-react": "^0.575.0",