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.
This commit is contained in:
zimplexing
2025-07-15 15:03:58 +08:00
parent 2bed3a4d00
commit bf99aee5f2
7 changed files with 152 additions and 56 deletions

View File

@@ -96,7 +96,7 @@ export default function HomeScreen() {
year={item.year} year={item.year}
rate={item.rate} rate={item.rate}
progress={item.progress} progress={item.progress}
playTime={item.play_time} playTime={item.time}
episodeIndex={item.episodeIndex} episodeIndex={item.episodeIndex}
sourceName={item.sourceName} sourceName={item.sourceName}
totalEpisodes={item.totalEpisodes} totalEpisodes={item.totalEpisodes}

View File

@@ -26,7 +26,6 @@ export default function PlayScreen() {
const { detail } = useDetailStore(); const { detail } = useDetailStore();
const { const {
status,
isLoading, isLoading,
showControls, showControls,
showNextEpisodeOverlay, showNextEpisodeOverlay,
@@ -37,9 +36,6 @@ export default function PlayScreen() {
setShowControls, setShowControls,
setShowNextEpisodeOverlay, setShowNextEpisodeOverlay,
reset, reset,
playEpisode,
togglePlayPause,
seek,
} = usePlayerStore(); } = usePlayerStore();
useEffect(() => { useEffect(() => {

View File

@@ -57,6 +57,7 @@ export interface PlayRecord {
play_time: number; play_time: number;
total_time: number; total_time: number;
save_time: number; save_time: number;
year: string;
} }
export interface ApiSite { export interface ApiSite {
@@ -108,7 +109,7 @@ export class API {
return response.json(); 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", { const response = await this._fetch("/api/login", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@@ -4,6 +4,7 @@ import { api, PlayRecord as ApiPlayRecord, Favorite as ApiFavorite } from "./api
// --- Storage Keys --- // --- Storage Keys ---
const STORAGE_KEYS = { const STORAGE_KEYS = {
SETTINGS: "mytv_settings", SETTINGS: "mytv_settings",
PLAYER_SETTINGS: "mytv_player_settings",
} as const; } as const;
// --- Type Definitions (aligned with api.ts) --- // --- Type Definitions (aligned with api.ts) ---
@@ -14,6 +15,11 @@ export type PlayRecord = ApiPlayRecord & {
}; };
export type Favorite = ApiFavorite; export type Favorite = ApiFavorite;
export interface PlayerSettings {
introEndTime?: number;
outroStartTime?: number;
}
export interface AppSettings { export interface AppSettings {
apiBaseUrl: string; apiBaseUrl: string;
remoteInputEnabled: boolean; remoteInputEnabled: boolean;
@@ -29,6 +35,47 @@ export interface AppSettings {
// --- Helper --- // --- Helper ---
const generateKey = (source: string, id: string) => `${source}+${id}`; const generateKey = (source: string, id: string) => `${source}+${id}`;
// --- PlayerSettingsManager (Uses AsyncStorage) ---
export class PlayerSettingsManager {
static async getAll(): Promise<Record<string, PlayerSettings>> {
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<PlayerSettings | null> {
const allSettings = await this.getAll();
return allSettings[generateKey(source, id)] || null;
}
static async save(source: string, id: string, settings: PlayerSettings): Promise<void> {
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<void> {
const allSettings = await this.getAll();
delete allSettings[generateKey(source, id)];
await AsyncStorage.setItem(STORAGE_KEYS.PLAYER_SETTINGS, JSON.stringify(allSettings));
}
static async clearAll(): Promise<void> {
await AsyncStorage.removeItem(STORAGE_KEYS.PLAYER_SETTINGS);
}
}
// --- FavoriteManager (Refactored to use API) --- // --- FavoriteManager (Refactored to use API) ---
export class FavoriteManager { export class FavoriteManager {
static async getAll(): Promise<Record<string, Favorite>> { static async getAll(): Promise<Record<string, Favorite>> {
@@ -67,30 +114,53 @@ export class FavoriteManager {
} }
} }
// --- PlayRecordManager (Refactored to use API) --- // --- PlayRecordManager (Refactored to use API and local settings) ---
export class PlayRecordManager { export class PlayRecordManager {
static async getAll(): Promise<Record<string, PlayRecord>> { static async getAll(): Promise<Record<string, PlayRecord>> {
return (await api.getPlayRecords()) as Record<string, PlayRecord>; const apiRecords = await api.getPlayRecords();
const localSettings = await PlayerSettingsManager.getAll();
const mergedRecords: Record<string, PlayRecord> = {};
for (const key in apiRecords) {
mergedRecords[key] = {
...apiRecords[key],
...localSettings[key],
};
}
return mergedRecords;
} }
static async save(source: string, id: string, record: Omit<PlayRecord, "save_time">): Promise<void> { static async save(source: string, id: string, record: Omit<PlayRecord, "save_time">): Promise<void> {
const key = generateKey(source, id); const key = generateKey(source, id);
// The API will handle setting the save_time const { introEndTime, outroStartTime, ...apiRecord } = record;
await api.savePlayRecord(key, 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<PlayRecord | null> { static async get(source: string, id: string): Promise<PlayRecord | null> {
const key = generateKey(source, id);
// This can be optimized, but for consistency, we call getAll
const records = await this.getAll(); const records = await this.getAll();
return records[generateKey(source, id)] || null; return records[key] || null;
} }
static async remove(source: string, id: string): Promise<void> { static async remove(source: string, id: string): Promise<void> {
const key = generateKey(source, id); const key = generateKey(source, id);
// Remove from API first
await api.deletePlayRecord(key); await api.deletePlayRecord(key);
// Then remove from local settings
await PlayerSettingsManager.remove(source, id);
} }
static async clearAll(): Promise<void> { static async clearAll(): Promise<void> {
// Clear from API first
await api.deletePlayRecord(); await api.deletePlayRecord();
// Then clear from local settings
await PlayerSettingsManager.clearAll();
} }
} }

View File

@@ -18,6 +18,11 @@ const useAuthStore = create<AuthState>((set) => ({
hideLoginModal: () => set({ isLoginModalVisible: false }), hideLoginModal: () => set({ isLoginModalVisible: false }),
checkLoginStatus: async () => { checkLoginStatus: async () => {
try { try {
const { ok } = await api.login();
if (ok) {
set({ isLoggedIn: true });
return;
}
const cookies = await Cookies.get(api.baseURL); const cookies = await Cookies.get(api.baseURL);
const isLoggedIn = cookies && !!cookies.auth; const isLoggedIn = cookies && !!cookies.auth;
set({ isLoggedIn }); set({ isLoggedIn });

View File

@@ -1,7 +1,7 @@
import { create } from 'zustand'; import { create } from "zustand";
import { api, SearchResult, PlayRecord } from '@/services/api'; import { api, SearchResult, PlayRecord } from "@/services/api";
import { PlayRecordManager } from '@/services/storage'; import { PlayRecordManager } from "@/services/storage";
import useAuthStore from './authStore'; import useAuthStore from "./authStore";
export type RowItem = (SearchResult | PlayRecord) & { export type RowItem = (SearchResult | PlayRecord) & {
id: string; id: string;
@@ -20,18 +20,38 @@ export type RowItem = (SearchResult | PlayRecord) & {
export interface Category { export interface Category {
title: string; title: string;
type?: 'movie' | 'tv' | 'record'; type?: "movie" | "tv" | "record";
tag?: string; tag?: string;
tags?: string[]; tags?: string[];
} }
const initialCategories: Category[] = [ const initialCategories: Category[] = [
{ title: '最近播放', type: 'record' }, { title: "最近播放", type: "record" },
{ title: '热门剧集', type: 'tv', tag: '热门' }, { title: "热门剧集", type: "tv", tag: "热门" },
{ title: '电视剧', type: 'tv', tags: [ '国产剧', '美剧', '英剧', '韩剧', '日剧', '港剧', '日本动画', '动画'] }, { title: "电视剧", type: "tv", tags: ["国产剧", "美剧", "英剧", "韩剧", "日剧", "港剧", "日本动画", "动画"] },
{ title: '电影', type: 'movie', tags: ['热门', '最新', '经典', '豆瓣高分', '冷门佳片', '华语', '欧美', '韩国', '日本', '动作', '喜剧', '爱情', '科幻', '悬疑', '恐怖'] }, {
{ title: '综艺', type: 'tv', tag: '综艺' }, title: "电影",
{ title: '豆瓣 Top250', type: 'movie', tag: 'top250' }, type: "movie",
tags: [
"热门",
"最新",
"经典",
"豆瓣高分",
"冷门佳片",
"华语",
"欧美",
"韩国",
"日本",
"动作",
"喜剧",
"爱情",
"科幻",
"悬疑",
"恐怖",
],
},
{ title: "综艺", type: "tv", tag: "综艺" },
{ title: "豆瓣 Top250", type: "movie", tag: "top250" },
]; ];
interface HomeState { interface HomeState {
@@ -73,7 +93,7 @@ const useHomeStore = create<HomeState>((set, get) => ({
} }
try { try {
if (selectedCategory.type === 'record') { if (selectedCategory.type === "record") {
const { isLoggedIn } = useAuthStore.getState(); const { isLoggedIn } = useAuthStore.getState();
if (!isLoggedIn) { if (!isLoggedIn) {
set({ contentData: [], hasMore: false }); set({ contentData: [], hasMore: false });
@@ -82,24 +102,35 @@ const useHomeStore = create<HomeState>((set, get) => ({
const records = await PlayRecordManager.getAll(); const records = await PlayRecordManager.getAll();
const rowItems = Object.entries(records) const rowItems = Object.entries(records)
.map(([key, record]) => { .map(([key, record]) => {
const [source, id] = key.split('+'); 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 }; 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)); .sort((a, b) => (b.lastPlayed || 0) - (a.lastPlayed || 0));
set({ contentData: rowItems, hasMore: false }); set({ contentData: rowItems, hasMore: false });
} else if (selectedCategory.type && selectedCategory.tag) { } else if (selectedCategory.type && selectedCategory.tag) {
const result = await api.getDoubanData(selectedCategory.type, selectedCategory.tag, 20, pageStart); const result = await api.getDoubanData(selectedCategory.type, selectedCategory.tag, 20, pageStart);
if (result.list.length === 0) { if (result.list.length === 0) {
set({ hasMore: false }); set({ hasMore: false });
} else { } else {
const newItems = result.list.map(item => ({ const newItems = result.list.map((item) => ({
...item, ...item,
id: item.title, id: item.title,
source: 'douban', source: "douban",
})) as RowItem[]; })) as RowItem[];
set(state => ({ set((state) => ({
contentData: pageStart === 0 ? newItems : [...state.contentData, ...newItems], contentData: pageStart === 0 ? newItems : [...state.contentData, ...newItems],
pageStart: state.pageStart + result.list.length, pageStart: state.pageStart + result.list.length,
hasMore: true, hasMore: true,
@@ -112,10 +143,10 @@ const useHomeStore = create<HomeState>((set, get) => ({
set({ hasMore: false }); set({ hasMore: false });
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'API_URL_NOT_SET') { if (err.message === "API_URL_NOT_SET") {
set({ error: '请点击右上角设置按钮,配置您的 API 地址' }); set({ error: "请点击右上角设置按钮,配置您的 API 地址" });
} else { } else {
set({ error: '加载失败,请重试' }); set({ error: "加载失败,请重试" });
} }
} finally { } finally {
set({ loading: false, loadingMore: false }); set({ loading: false, loadingMore: false });
@@ -134,12 +165,12 @@ const useHomeStore = create<HomeState>((set, get) => ({
refreshPlayRecords: async () => { refreshPlayRecords: async () => {
const { isLoggedIn } = useAuthStore.getState(); const { isLoggedIn } = useAuthStore.getState();
if (!isLoggedIn) { if (!isLoggedIn) {
set(state => { set((state) => {
const recordCategoryExists = state.categories.some(c => c.type === 'record'); const recordCategoryExists = state.categories.some((c) => c.type === "record");
if (recordCategoryExists) { if (recordCategoryExists) {
const newCategories = state.categories.filter(c => c.type !== 'record'); const newCategories = state.categories.filter((c) => c.type !== "record");
if (state.selectedCategory.type === 'record') { if (state.selectedCategory.type === "record") {
get().selectCategory(newCategories[0] || null); get().selectCategory(newCategories[0] || null);
} }
return { categories: newCategories }; return { categories: newCategories };
} }
@@ -149,24 +180,24 @@ const useHomeStore = create<HomeState>((set, get) => ({
} }
const records = await PlayRecordManager.getAll(); const records = await PlayRecordManager.getAll();
const hasRecords = Object.keys(records).length > 0; const hasRecords = Object.keys(records).length > 0;
set(state => { set((state) => {
const recordCategoryExists = state.categories.some(c => c.type === 'record'); const recordCategoryExists = state.categories.some((c) => c.type === "record");
if (hasRecords && !recordCategoryExists) { if (hasRecords && !recordCategoryExists) {
return { categories: [initialCategories[0], ...state.categories] }; return { categories: [initialCategories[0], ...state.categories] };
} }
if (!hasRecords && recordCategoryExists) { if (!hasRecords && recordCategoryExists) {
const newCategories = state.categories.filter(c => c.type !== 'record'); const newCategories = state.categories.filter((c) => c.type !== "record");
if (state.selectedCategory.type === 'record') { if (state.selectedCategory.type === "record") {
get().selectCategory(newCategories[0] || null); get().selectCategory(newCategories[0] || null);
} }
return { categories: newCategories }; return { categories: newCategories };
} }
return {}; return {};
}); });
if (get().selectedCategory.type === 'record') { if (get().selectedCategory.type === "record") {
get().fetchInitialData(); get().fetchInitialData();
} }
}, },
})); }));
export default useHomeStore; export default useHomeStore;

View File

@@ -27,11 +27,7 @@ interface PlayerState {
introEndTime?: number; introEndTime?: number;
outroStartTime?: number; outroStartTime?: number;
setVideoRef: (ref: RefObject<Video>) => void; setVideoRef: (ref: RefObject<Video>) => void;
loadVideo: ( loadVideo: (source: string, episodeIndex: number, position?: number) => Promise<void>;
source: string,
episodeIndex: number,
position?: number
) => Promise<void>;
playEpisode: (index: number) => void; playEpisode: (index: number) => void;
togglePlayPause: () => void; togglePlayPause: () => void;
seek: (duration: number) => void; seek: (duration: number) => void;
@@ -86,10 +82,7 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
}); });
try { try {
const playRecord = await PlayRecordManager.get( const playRecord = await PlayRecordManager.get(detail.source, detail.id.toString());
detail.source,
detail.id.toString()
);
set({ set({
isLoading: false, isLoading: false,
introEndTime: playRecord?.introEndTime, introEndTime: playRecord?.introEndTime,
@@ -101,7 +94,6 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
} }
}, },
playEpisode: (index) => { playEpisode: (index) => {
const { episodes, videoRef } = get(); const { episodes, videoRef } = get();
if (index >= 0 && index < episodes.length) { if (index >= 0 && index < episodes.length) {
@@ -209,12 +201,13 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
}; };
PlayRecordManager.save(detail.source, detail.id.toString(), { PlayRecordManager.save(detail.source, detail.id.toString(), {
title: detail.title, title: detail.title,
poster: detail.poster || "", cover: detail.poster || "",
index: currentEpisodeIndex, index: currentEpisodeIndex,
total_episodes: episodes.length, total_episodes: episodes.length,
play_time: status.positionMillis, play_time: status.positionMillis,
total_time: status.durationMillis || 0, total_time: status.durationMillis || 0,
source_name: detail.source_name, source_name: detail.source_name,
year: detail.year || "",
...existingRecord, ...existingRecord,
...updates, ...updates,
}); });