From bf99aee5f2f7e2780c82d588cc0bbc5d137586fc Mon Sep 17 00:00:00 2001 From: zimplexing Date: Tue, 15 Jul 2025 15:03:58 +0800 Subject: [PATCH] refactor: update play time property and enhance player settings management - Changed the play time property from 'play_time' to 'time' in HomeScreen. - Removed unused player control functions from PlayScreen. - Added PlayerSettings interface and implemented PlayerSettingsManager for local storage of player settings. - Refactored PlayRecordManager to merge API records with local player settings. - Updated authentication logic in authStore to handle optional username parameter in login function. - Cleaned up and optimized imports across various components. --- app/index.tsx | 2 +- app/play.tsx | 4 -- services/api.ts | 3 +- services/storage.ts | 80 +++++++++++++++++++++++++++++++--- stores/authStore.ts | 5 +++ stores/homeStore.ts | 99 ++++++++++++++++++++++++++++--------------- stores/playerStore.ts | 15 ++----- 7 files changed, 152 insertions(+), 56 deletions(-) diff --git a/app/index.tsx b/app/index.tsx index c2f8b13..e449b55 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -96,7 +96,7 @@ export default function HomeScreen() { year={item.year} rate={item.rate} progress={item.progress} - playTime={item.play_time} + playTime={item.time} episodeIndex={item.episodeIndex} sourceName={item.sourceName} totalEpisodes={item.totalEpisodes} diff --git a/app/play.tsx b/app/play.tsx index 9ec5610..9ff61c9 100644 --- a/app/play.tsx +++ b/app/play.tsx @@ -26,7 +26,6 @@ export default function PlayScreen() { const { detail } = useDetailStore(); const { - status, isLoading, showControls, showNextEpisodeOverlay, @@ -37,9 +36,6 @@ export default function PlayScreen() { setShowControls, setShowNextEpisodeOverlay, reset, - playEpisode, - togglePlayPause, - seek, } = usePlayerStore(); useEffect(() => { diff --git a/services/api.ts b/services/api.ts index 461602b..561ab8a 100644 --- a/services/api.ts +++ b/services/api.ts @@ -57,6 +57,7 @@ export interface PlayRecord { play_time: number; total_time: number; save_time: number; + year: string; } export interface ApiSite { @@ -108,7 +109,7 @@ export class API { return response.json(); } - async login(username: string | undefined, password: string): Promise<{ ok: boolean }> { + async login(username?: string | undefined, password?: string): Promise<{ ok: boolean }> { const response = await this._fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/services/storage.ts b/services/storage.ts index 2ea35ff..ff70db6 100644 --- a/services/storage.ts +++ b/services/storage.ts @@ -4,6 +4,7 @@ import { api, PlayRecord as ApiPlayRecord, Favorite as ApiFavorite } from "./api // --- Storage Keys --- const STORAGE_KEYS = { SETTINGS: "mytv_settings", + PLAYER_SETTINGS: "mytv_player_settings", } as const; // --- Type Definitions (aligned with api.ts) --- @@ -14,6 +15,11 @@ export type PlayRecord = ApiPlayRecord & { }; export type Favorite = ApiFavorite; +export interface PlayerSettings { + introEndTime?: number; + outroStartTime?: number; +} + export interface AppSettings { apiBaseUrl: string; remoteInputEnabled: boolean; @@ -29,6 +35,47 @@ export interface AppSettings { // --- Helper --- const generateKey = (source: string, id: string) => `${source}+${id}`; +// --- PlayerSettingsManager (Uses AsyncStorage) --- +export class PlayerSettingsManager { + static async getAll(): Promise> { + try { + const data = await AsyncStorage.getItem(STORAGE_KEYS.PLAYER_SETTINGS); + return data ? JSON.parse(data) : {}; + } catch (error) { + console.error("Failed to get all player settings:", error); + return {}; + } + } + + static async get(source: string, id: string): Promise { + const allSettings = await this.getAll(); + return allSettings[generateKey(source, id)] || null; + } + + static async save(source: string, id: string, settings: PlayerSettings): Promise { + const allSettings = await this.getAll(); + const key = generateKey(source, id); + // Only save if there are actual values to save + if (settings.introEndTime !== undefined || settings.outroStartTime !== undefined) { + allSettings[key] = { ...allSettings[key], ...settings }; + } else { + // If both are undefined, remove the key + delete allSettings[key]; + } + await AsyncStorage.setItem(STORAGE_KEYS.PLAYER_SETTINGS, JSON.stringify(allSettings)); + } + + static async remove(source: string, id: string): Promise { + const allSettings = await this.getAll(); + delete allSettings[generateKey(source, id)]; + await AsyncStorage.setItem(STORAGE_KEYS.PLAYER_SETTINGS, JSON.stringify(allSettings)); + } + + static async clearAll(): Promise { + await AsyncStorage.removeItem(STORAGE_KEYS.PLAYER_SETTINGS); + } +} + // --- FavoriteManager (Refactored to use API) --- export class FavoriteManager { static async getAll(): Promise> { @@ -67,30 +114,53 @@ export class FavoriteManager { } } -// --- PlayRecordManager (Refactored to use API) --- +// --- PlayRecordManager (Refactored to use API and local settings) --- export class PlayRecordManager { static async getAll(): Promise> { - return (await api.getPlayRecords()) as Record; + const apiRecords = await api.getPlayRecords(); + const localSettings = await PlayerSettingsManager.getAll(); + + const mergedRecords: Record = {}; + for (const key in apiRecords) { + mergedRecords[key] = { + ...apiRecords[key], + ...localSettings[key], + }; + } + return mergedRecords; } static async save(source: string, id: string, record: Omit): Promise { const key = generateKey(source, id); - // The API will handle setting the save_time - await api.savePlayRecord(key, record); + const { introEndTime, outroStartTime, ...apiRecord } = record; + + // Save player settings locally + await PlayerSettingsManager.save(source, id, { introEndTime, outroStartTime }); + + // Save core record to API + await api.savePlayRecord(key, apiRecord); } static async get(source: string, id: string): Promise { + const key = generateKey(source, id); + // This can be optimized, but for consistency, we call getAll const records = await this.getAll(); - return records[generateKey(source, id)] || null; + return records[key] || null; } static async remove(source: string, id: string): Promise { const key = generateKey(source, id); + // Remove from API first await api.deletePlayRecord(key); + // Then remove from local settings + await PlayerSettingsManager.remove(source, id); } static async clearAll(): Promise { + // Clear from API first await api.deletePlayRecord(); + // Then clear from local settings + await PlayerSettingsManager.clearAll(); } } diff --git a/stores/authStore.ts b/stores/authStore.ts index 959df2d..af2b1d3 100644 --- a/stores/authStore.ts +++ b/stores/authStore.ts @@ -18,6 +18,11 @@ const useAuthStore = create((set) => ({ hideLoginModal: () => set({ isLoginModalVisible: false }), checkLoginStatus: async () => { try { + const { ok } = await api.login(); + if (ok) { + set({ isLoggedIn: true }); + return; + } const cookies = await Cookies.get(api.baseURL); const isLoggedIn = cookies && !!cookies.auth; set({ isLoggedIn }); diff --git a/stores/homeStore.ts b/stores/homeStore.ts index e423eba..d3b7012 100644 --- a/stores/homeStore.ts +++ b/stores/homeStore.ts @@ -1,7 +1,7 @@ -import { create } from 'zustand'; -import { api, SearchResult, PlayRecord } from '@/services/api'; -import { PlayRecordManager } from '@/services/storage'; -import useAuthStore from './authStore'; +import { create } from "zustand"; +import { api, SearchResult, PlayRecord } from "@/services/api"; +import { PlayRecordManager } from "@/services/storage"; +import useAuthStore from "./authStore"; export type RowItem = (SearchResult | PlayRecord) & { id: string; @@ -20,18 +20,38 @@ export type RowItem = (SearchResult | PlayRecord) & { export interface Category { title: string; - type?: 'movie' | 'tv' | 'record'; + 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' }, + { 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 { @@ -73,7 +93,7 @@ const useHomeStore = create((set, get) => ({ } try { - if (selectedCategory.type === 'record') { + if (selectedCategory.type === "record") { const { isLoggedIn } = useAuthStore.getState(); if (!isLoggedIn) { set({ contentData: [], hasMore: false }); @@ -82,24 +102,35 @@ const useHomeStore = create((set, get) => ({ 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 }; + 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) + .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 => ({ + const newItems = result.list.map((item) => ({ ...item, id: item.title, - source: 'douban', + source: "douban", })) as RowItem[]; - set(state => ({ + set((state) => ({ contentData: pageStart === 0 ? newItems : [...state.contentData, ...newItems], pageStart: state.pageStart + result.list.length, hasMore: true, @@ -112,10 +143,10 @@ const useHomeStore = create((set, get) => ({ set({ hasMore: false }); } } catch (err: any) { - if (err.message === 'API_URL_NOT_SET') { - set({ error: '请点击右上角设置按钮,配置您的 API 地址' }); + if (err.message === "API_URL_NOT_SET") { + set({ error: "请点击右上角设置按钮,配置您的 API 地址" }); } else { - set({ error: '加载失败,请重试' }); + set({ error: "加载失败,请重试" }); } } finally { set({ loading: false, loadingMore: false }); @@ -134,12 +165,12 @@ const useHomeStore = create((set, get) => ({ refreshPlayRecords: async () => { const { isLoggedIn } = useAuthStore.getState(); if (!isLoggedIn) { - set(state => { - const recordCategoryExists = state.categories.some(c => c.type === 'record'); + 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); + const newCategories = state.categories.filter((c) => c.type !== "record"); + if (state.selectedCategory.type === "record") { + get().selectCategory(newCategories[0] || null); } return { categories: newCategories }; } @@ -149,24 +180,24 @@ const useHomeStore = create((set, get) => ({ } const records = await PlayRecordManager.getAll(); const hasRecords = Object.keys(records).length > 0; - set(state => { - const recordCategoryExists = state.categories.some(c => c.type === 'record'); + 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); + const newCategories = state.categories.filter((c) => c.type !== "record"); + if (state.selectedCategory.type === "record") { + get().selectCategory(newCategories[0] || null); } return { categories: newCategories }; } return {}; }); - if (get().selectedCategory.type === 'record') { + if (get().selectedCategory.type === "record") { get().fetchInitialData(); } }, })); -export default useHomeStore; \ No newline at end of file +export default useHomeStore; diff --git a/stores/playerStore.ts b/stores/playerStore.ts index f3f4f07..01fb59b 100644 --- a/stores/playerStore.ts +++ b/stores/playerStore.ts @@ -27,11 +27,7 @@ interface PlayerState { introEndTime?: number; outroStartTime?: number; setVideoRef: (ref: RefObject