Files
OrionTV/stores/homeStore.ts

355 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { create } from "zustand";
import { api, SearchResult, PlayRecord } from "@/services/api";
import { PlayRecordManager } from "@/services/storage";
import useAuthStore from "./authStore";
import { useSettingsStore } from "./settingsStore";
export type RowItem = (SearchResult | PlayRecord) & {
id: string;
source: string;
title: string;
poster: string;
progress?: number;
play_time?: number;
lastPlayed?: number;
episodeIndex?: number;
sourceName?: string;
totalEpisodes?: number;
year?: string;
rate?: string;
};
export interface Category {
title: string;
type?: "movie" | "tv" | "record";
tag?: string;
tags?: string[];
}
const initialCategories: Category[] = [
{ title: "最近播放", type: "record" },
{ title: "热门剧集", type: "tv", tag: "热门" },
{ title: "电视剧", type: "tv", tags: ["国产剧", "美剧", "英剧", "韩剧", "日剧", "港剧", "日本动画", "动画"] },
{
title: "电影",
type: "movie",
tags: [
"热门",
"最新",
"经典",
"豆瓣高分",
"冷门佳片",
"华语",
"欧美",
"韩国",
"日本",
"动作",
"喜剧",
"爱情",
"科幻",
"悬疑",
"恐怖",
],
},
{ title: "综艺", type: "tv", tag: "综艺" },
{ title: "豆瓣 Top250", type: "movie", tag: "top250" },
];
// 添加缓存项接口
interface CacheItem {
data: RowItem[];
timestamp: number;
type: 'movie' | 'tv' | 'record';
hasMore: boolean;
}
const CACHE_EXPIRE_TIME = 5 * 60 * 1000; // 5分钟过期
const MAX_CACHE_SIZE = 10; // 最大缓存容量
const MAX_ITEMS_PER_CACHE = 40; // 每个缓存最大条目数
const getCacheKey = (category: Category) => {
return `${category.type || 'unknown'}-${category.title}-${category.tag || ''}`;
};
const isValidCache = (cacheItem: CacheItem) => {
return Date.now() - cacheItem.timestamp < CACHE_EXPIRE_TIME;
};
interface HomeState {
categories: Category[];
selectedCategory: Category;
contentData: RowItem[];
loading: boolean;
loadingMore: boolean;
pageStart: number;
hasMore: boolean;
error: string | null;
fetchInitialData: () => Promise<void>;
loadMoreData: () => Promise<void>;
selectCategory: (category: Category) => void;
refreshPlayRecords: () => Promise<void>;
clearError: () => void;
}
// 内存缓存,应用生命周期内有效
const dataCache = new Map<string, CacheItem>();
const useHomeStore = create<HomeState>((set, get) => ({
categories: initialCategories,
selectedCategory: initialCategories[0],
contentData: [],
loading: true,
loadingMore: false,
pageStart: 0,
hasMore: true,
error: null,
fetchInitialData: async () => {
const { apiBaseUrl } = useSettingsStore.getState();
await useAuthStore.getState().checkLoginStatus(apiBaseUrl);
const { selectedCategory } = get();
const cacheKey = getCacheKey(selectedCategory);
// 最近播放不缓存,始终实时获取
if (selectedCategory.type === 'record') {
set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null });
await get().loadMoreData();
return;
}
// 检查缓存
if (dataCache.has(cacheKey) && isValidCache(dataCache.get(cacheKey)!)) {
const cachedData = dataCache.get(cacheKey)!;
set({
loading: false,
contentData: cachedData.data,
pageStart: cachedData.data.length,
hasMore: cachedData.hasMore,
error: null
});
return;
}
set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null });
await get().loadMoreData();
},
loadMoreData: async () => {
const { selectedCategory, pageStart, loadingMore, hasMore } = get();
if (loadingMore || !hasMore) return;
if (pageStart > 0) {
set({ loadingMore: true });
}
try {
if (selectedCategory.type === "record") {
const { isLoggedIn } = useAuthStore.getState();
if (!isLoggedIn) {
set({ contentData: [], hasMore: false });
return;
}
const records = await PlayRecordManager.getAll();
const rowItems = Object.entries(records)
.map(([key, record]) => {
const [source, id] = key.split("+");
return {
...record,
id,
source,
progress: record.play_time / record.total_time,
poster: record.cover,
sourceName: record.source_name,
episodeIndex: record.index,
totalEpisodes: record.total_episodes,
lastPlayed: record.save_time,
play_time: record.play_time,
};
})
// .filter((record) => record.progress !== undefined && record.progress > 0 && record.progress < 1)
.sort((a, b) => (b.lastPlayed || 0) - (a.lastPlayed || 0));
set({ contentData: rowItems, hasMore: false });
} else if (selectedCategory.type && selectedCategory.tag) {
const result = await api.getDoubanData(
selectedCategory.type,
selectedCategory.tag,
20,
pageStart
);
const newItems = result.list.map((item) => ({
...item,
id: item.title,
source: "douban",
})) as RowItem[];
const cacheKey = getCacheKey(selectedCategory);
if (pageStart === 0) {
// 清理过期缓存
for (const [key, value] of dataCache.entries()) {
if (!isValidCache(value)) {
dataCache.delete(key);
}
}
// 如果缓存太大,删除最旧的项
if (dataCache.size >= MAX_CACHE_SIZE) {
const oldestKey = Array.from(dataCache.keys())[0];
dataCache.delete(oldestKey);
}
// 限制缓存的数据条目数,但不限制显示的数据
const cacheItems = newItems.slice(0, MAX_ITEMS_PER_CACHE);
// 存储新缓存
dataCache.set(cacheKey, {
data: cacheItems,
timestamp: Date.now(),
type: selectedCategory.type,
hasMore: true // 始终为 true因为我们允许继续加载
});
set({
contentData: newItems, // 使用完整的新数据
pageStart: newItems.length,
hasMore: result.list.length !== 0,
});
} else {
// 增量加载时更新缓存
const existingCache = dataCache.get(cacheKey);
if (existingCache) {
// 只有当缓存数据少于最大限制时才更新缓存
if (existingCache.data.length < MAX_ITEMS_PER_CACHE) {
const updatedData = [...existingCache.data, ...newItems];
const limitedCacheData = updatedData.slice(0, MAX_ITEMS_PER_CACHE);
dataCache.set(cacheKey, {
...existingCache,
data: limitedCacheData,
hasMore: true // 始终为 true因为我们允许继续加载
});
}
}
// 更新状态时使用所有数据
set((state) => ({
contentData: [...state.contentData, ...newItems],
pageStart: state.pageStart + newItems.length,
hasMore: result.list.length !== 0,
}));
}
} else if (selectedCategory.tags) {
// It's a container category, do not load content, but clear current content
set({ contentData: [], hasMore: false });
} else {
set({ hasMore: false });
}
} catch (err: any) {
let errorMessage = "加载失败,请重试";
if (err.message === "API_URL_NOT_SET") {
errorMessage = "请点击右上角设置按钮,配置您的服务器地址";
} else if (err.message === "UNAUTHORIZED") {
errorMessage = "认证失败,请重新登录";
} else if (err.message.includes("Network")) {
errorMessage = "网络连接失败,请检查网络连接";
} else if (err.message.includes("timeout")) {
errorMessage = "请求超时,请检查网络或服务器状态";
} else if (err.message.includes("404")) {
errorMessage = "服务器API路径不正确请检查服务器配置";
} else if (err.message.includes("500")) {
errorMessage = "服务器内部错误,请联系管理员";
} else if (err.message.includes("403")) {
errorMessage = "访问被拒绝,请检查权限设置";
}
set({ error: errorMessage });
} finally {
set({ loading: false, loadingMore: false });
}
},
selectCategory: (category: Category) => {
const currentCategory = get().selectedCategory;
const cacheKey = getCacheKey(category);
if (currentCategory.title !== category.title || currentCategory.tag !== category.tag) {
set({
selectedCategory: category,
contentData: [],
pageStart: 0,
hasMore: true,
error: null
});
if (category.type === 'record') {
get().fetchInitialData();
return;
}
const cachedData = dataCache.get(cacheKey);
if (cachedData && isValidCache(cachedData)) {
set({
contentData: cachedData.data,
pageStart: cachedData.data.length,
hasMore: cachedData.hasMore,
loading: false
});
} else {
// 删除过期缓存
if (cachedData) {
dataCache.delete(cacheKey);
}
get().fetchInitialData();
}
}
},
refreshPlayRecords: async () => {
const { apiBaseUrl } = useSettingsStore.getState();
await useAuthStore.getState().checkLoginStatus(apiBaseUrl);
const { isLoggedIn } = useAuthStore.getState();
if (!isLoggedIn) {
set((state) => {
const recordCategoryExists = state.categories.some((c) => c.type === "record");
if (recordCategoryExists) {
const newCategories = state.categories.filter((c) => c.type !== "record");
if (state.selectedCategory.type === "record") {
get().selectCategory(newCategories[0] || null);
}
return { categories: newCategories };
}
return {};
});
return;
}
const records = await PlayRecordManager.getAll();
const hasRecords = Object.keys(records).length > 0;
set((state) => {
const recordCategoryExists = state.categories.some((c) => c.type === "record");
if (hasRecords && !recordCategoryExists) {
return { categories: [initialCategories[0], ...state.categories] };
}
if (!hasRecords && recordCategoryExists) {
const newCategories = state.categories.filter((c) => c.type !== "record");
if (state.selectedCategory.type === "record") {
get().selectCategory(newCategories[0] || null);
}
return { categories: newCategories };
}
return {};
});
get().fetchInitialData();
},
clearError: () => {
set({ error: null });
},
}));
export default useHomeStore;