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}
rate={item.rate}
progress={item.progress}
playTime={item.play_time}
playTime={item.time}
episodeIndex={item.episodeIndex}
sourceName={item.sourceName}
totalEpisodes={item.totalEpisodes}

View File

@@ -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(() => {

View File

@@ -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" },

View File

@@ -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<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) ---
export class FavoriteManager {
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 {
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> {
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<PlayRecord | null> {
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<void> {
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<void> {
// Clear from API first
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 }),
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 });

View File

@@ -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<HomeState>((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<HomeState>((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<HomeState>((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<HomeState>((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<HomeState>((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;
export default useHomeStore;

View File

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