mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
272 lines
8.2 KiB
TypeScript
272 lines
8.2 KiB
TypeScript
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 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, RowItem[]>();
|
|
|
|
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 = `${selectedCategory.title}-${selectedCategory.tag || ''}`;
|
|
|
|
// 最近播放不缓存,始终实时获取
|
|
if (selectedCategory.type === 'record') {
|
|
set({ loading: true, contentData: [], pageStart: 0, hasMore: true, error: null });
|
|
await get().loadMoreData();
|
|
return;
|
|
}
|
|
|
|
// 检查缓存
|
|
if (dataCache.has(cacheKey)) {
|
|
set({
|
|
loading: false,
|
|
contentData: dataCache.get(cacheKey)!,
|
|
pageStart: dataCache.get(cacheKey)!.length,
|
|
hasMore: false,
|
|
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);
|
|
if (result.list.length === 0) {
|
|
set({ hasMore: false });
|
|
} else {
|
|
const newItems = result.list.map((item) => ({
|
|
...item,
|
|
id: item.title,
|
|
source: "douban",
|
|
})) as RowItem[];
|
|
|
|
const cacheKey = `${selectedCategory.title}-${selectedCategory.tag || ''}`;
|
|
|
|
if (pageStart === 0) {
|
|
// 缓存新数据
|
|
dataCache.set(cacheKey, newItems);
|
|
set((state) => ({
|
|
contentData: newItems,
|
|
pageStart: result.list.length,
|
|
hasMore: true,
|
|
}));
|
|
} else {
|
|
// 增量加载时不缓存,直接追加
|
|
set((state) => ({
|
|
contentData: [...state.contentData, ...newItems],
|
|
pageStart: state.pageStart + result.list.length,
|
|
hasMore: true,
|
|
}));
|
|
}
|
|
}
|
|
} 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) {
|
|
if (err.message === "API_URL_NOT_SET") {
|
|
set({ error: "请点击右上角设置按钮,配置您的服务器地址" });
|
|
} else {
|
|
set({ error: "加载失败,请重试" });
|
|
}
|
|
} finally {
|
|
set({ loading: false, loadingMore: false });
|
|
}
|
|
},
|
|
|
|
selectCategory: (category: Category) => {
|
|
const currentCategory = get().selectedCategory;
|
|
const cacheKey = `${category.title}-${category.tag || ''}`;
|
|
|
|
// 只有当分类或标签真正变化时才处理
|
|
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;
|
|
}
|
|
|
|
// 检查缓存,有则直接使用,无则请求
|
|
if (dataCache.has(cacheKey)) {
|
|
set({
|
|
contentData: dataCache.get(cacheKey)!,
|
|
pageStart: dataCache.get(cacheKey)!.length,
|
|
hasMore: false,
|
|
loading: false
|
|
});
|
|
} else {
|
|
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;
|