diff --git a/app/settings/hooks/useSettingsPage.ts b/app/settings/hooks/useSettingsPage.ts index 9101507..dcf15e6 100644 --- a/app/settings/hooks/useSettingsPage.ts +++ b/app/settings/hooks/useSettingsPage.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { settingsStore, getDefaultSources, type SortOption, type SearchDisplayMode } from '@/lib/store/settings-store'; +import { settingsStore, getDefaultSources, type SortOption, type SearchDisplayMode, type ProxyMode } from '@/lib/store/settings-store'; import type { VideoSource, SourceSubscription } from '@/lib/types'; import { type ImportResult, @@ -27,6 +27,7 @@ export function useSettingsPage() { const [realtimeLatency, setRealtimeLatency] = useState(false); const [searchDisplayMode, setSearchDisplayMode] = useState('normal'); const [fullscreenType, setFullscreenType] = useState<'native' | 'window'>('native'); + const [proxyMode, setProxyMode] = useState('retry'); useEffect(() => { const settings = settingsStore.getSettings(); @@ -38,6 +39,7 @@ export function useSettingsPage() { setRealtimeLatency(settings.realtimeLatency); setSearchDisplayMode(settings.searchDisplayMode); setFullscreenType(settings.fullscreenType); + setProxyMode(settings.proxyMode); // Fetch env password status fetch('/api/config') @@ -290,6 +292,15 @@ export function useSettingsPage() { }); }; + const handleProxyModeChange = (mode: ProxyMode) => { + setProxyMode(mode); + const currentSettings = settingsStore.getSettings(); + settingsStore.saveSettings({ + ...currentSettings, + proxyMode: mode, + }); + }; + const handleRestoreDefaults = () => { const defaults = getDefaultSources(); handleSourcesChange(defaults); @@ -342,5 +353,7 @@ export function useSettingsPage() { handleSearchDisplayModeChange, fullscreenType, handleFullscreenTypeChange, + proxyMode, + handleProxyModeChange, }; } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 49b9fbf..ca2165b 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -55,6 +55,8 @@ export default function SettingsPage() { handleRealtimeLatencyChange, handleSearchDisplayModeChange, handleFullscreenTypeChange, + proxyMode, + handleProxyModeChange, } = useSettingsPage(); return ( @@ -67,6 +69,8 @@ export default function SettingsPage() { {/* Password Settings */} diff --git a/components/player/VideoPlayer.tsx b/components/player/VideoPlayer.tsx index f9b7bee..8d22c01 100644 --- a/components/player/VideoPlayer.tsx +++ b/components/player/VideoPlayer.tsx @@ -42,20 +42,37 @@ export function VideoPlayer({ const SAVE_INTERVAL = 5000; // 5 seconds throttle // Get showModeIndicator setting + // Get showModeIndicator and proxyMode settings const [showModeIndicator, setShowModeIndicator] = useState(false); + const [proxyMode, setProxyMode] = useState<'retry' | 'none' | 'always'>('retry'); useEffect(() => { // Initial value - setShowModeIndicator(settingsStore.getSettings().showModeIndicator); + const settings = settingsStore.getSettings(); + setShowModeIndicator(settings.showModeIndicator); + setProxyMode(settings.proxyMode); // Subscribe to changes const unsubscribe = settingsStore.subscribe(() => { - setShowModeIndicator(settingsStore.getSettings().showModeIndicator); + const newSettings = settingsStore.getSettings(); + setShowModeIndicator(newSettings.showModeIndicator); + setProxyMode(newSettings.proxyMode); }); return () => unsubscribe(); }, []); + // Initialize useProxy based on proxyMode when the component mounts or proxyMode changes + // We use a separate effect for this to react to setting changes + useEffect(() => { + if (proxyMode === 'always') { + setUseProxy(true); + } else if (proxyMode === 'none') { + setUseProxy(false); + } + // For 'retry', we assume it starts as false (direct), which is the default state of useProxy + }, [proxyMode]); + // Use reactive hook to subscribe to history updates // This ensures the component re-renders when history is hydrated from localStorage @@ -134,8 +151,13 @@ export function VideoPlayer({ const handleVideoError = (error: string) => { console.error('Video playback error:', error); - // Auto-retry with proxy if not already using it - if (!useProxy) { + // Auto-retry with proxy if: + // 1. Not already using proxy + // 2. Proxy mode is NOT 'none' (so 'retry' or potentially 'always' if it somehow failed locally) + // 3. Proxy mode is 'retry' (specifically for the auto-switch logic) + // Note: If mode is 'always', we are already using proxy. If it fails, we show error. + + if (!useProxy && proxyMode === 'retry') { setUseProxy(true); setShouldAutoPlay(true); // Force autoplay after proxy retry setVideoError(''); @@ -164,7 +186,7 @@ export function VideoPlayer({ setUseProxy(prev => !prev); }; - const finalPlayUrl = useProxy + const finalPlayUrl = useProxy || proxyMode === 'always' ? `/api/proxy?url=${encodeURIComponent(playUrl)}&retry=${retryCount}` // Add retry param to force fresh request : playUrl; diff --git a/components/player/hooks/usePlayerSettings.ts b/components/player/hooks/usePlayerSettings.ts index 58976db..520965d 100644 --- a/components/player/hooks/usePlayerSettings.ts +++ b/components/player/hooks/usePlayerSettings.ts @@ -21,6 +21,7 @@ export function usePlayerSettings() { adFilterMode: stored.adFilterMode, adKeywords: stored.adKeywords, fullscreenType: stored.fullscreenType, + proxyMode: stored.proxyMode, }; }); @@ -39,6 +40,7 @@ export function usePlayerSettings() { adFilterMode: stored.adFilterMode, adKeywords: stored.adKeywords, fullscreenType: stored.fullscreenType, + proxyMode: stored.proxyMode, }); }); return unsubscribe; @@ -95,6 +97,10 @@ export function usePlayerSettings() { updateSetting('fullscreenType', value); }, [updateSetting]); + const setProxyMode = useCallback((value: 'retry' | 'none' | 'always') => { + updateSetting('proxyMode', value); + }, [updateSetting]); + return { ...settings, setAutoNextEpisode, @@ -107,5 +113,6 @@ export function usePlayerSettings() { setAdFilterMode, setAdKeywords, setFullscreenType, + setProxyMode, }; } diff --git a/components/settings/PlayerSettings.tsx b/components/settings/PlayerSettings.tsx index ce37288..40094cf 100644 --- a/components/settings/PlayerSettings.tsx +++ b/components/settings/PlayerSettings.tsx @@ -6,50 +6,102 @@ */ import { Icons } from '@/components/ui/Icon'; +import type { ProxyMode } from '@/lib/store/settings-store'; interface PlayerSettingsProps { fullscreenType: 'native' | 'window'; onFullscreenTypeChange: (type: 'native' | 'window') => void; + proxyMode: ProxyMode; + onProxyModeChange: (mode: ProxyMode) => void; } export function PlayerSettings({ fullscreenType, onFullscreenTypeChange, + proxyMode, + onProxyModeChange, }: PlayerSettingsProps) { return (

播放器设置

- {/* Fullscreen Mode Selection */} -
-

- - 默认全屏方式 -

-

- 选择在桌面端点击播放器全屏按钮时的行为 -

-
- - +
+ {/* Fullscreen Mode Selection */} +
+

+ + 默认全屏方式 +

+

+ 选择在桌面端点击播放器全屏按钮时的行为 +

+
+ + +
+
+ +
+ + {/* Proxy Mode Selection */} +
+

+ + 代理播放模式 +

+

+ 控制视频播放时的网络请求策略 +

+
+ + + +
diff --git a/lib/store/settings-store.ts b/lib/store/settings-store.ts index 8272613..71227cc 100644 --- a/lib/store/settings-store.ts +++ b/lib/store/settings-store.ts @@ -19,6 +19,7 @@ export type SortOption = export type SearchDisplayMode = 'normal' | 'grouped'; export type AdFilterMode = 'off' | 'keyword' | 'heuristic' | 'aggressive'; +export type ProxyMode = 'retry' | 'none' | 'always'; export interface AppSettings { sources: VideoSource[]; @@ -44,6 +45,7 @@ export interface AppSettings { searchDisplayMode: SearchDisplayMode; // 'normal' = individual cards, 'grouped' = group same-name videos episodeReverseOrder: boolean; // Persist episode list reverse state fullscreenType: 'native' | 'window'; // Fullscreen mode preference + proxyMode: ProxyMode; // Proxy behavior: 'retry' | 'none' | 'always' } import { exportSettings, importSettings, SEARCH_HISTORY_KEY, WATCH_HISTORY_KEY } from './settings-helpers'; @@ -116,6 +118,7 @@ function getDefaultAppSettings(): AppSettings { searchDisplayMode: 'normal', episodeReverseOrder: false, fullscreenType: 'native', + proxyMode: 'retry', }; } @@ -192,6 +195,7 @@ export const settingsStore = { searchDisplayMode: parsed.searchDisplayMode === 'grouped' ? 'grouped' : 'normal', episodeReverseOrder: parsed.episodeReverseOrder !== undefined ? parsed.episodeReverseOrder : false, fullscreenType: parsed.fullscreenType === 'window' ? 'window' : 'native', + proxyMode: (parsed.proxyMode === 'retry' || parsed.proxyMode === 'none' || parsed.proxyMode === 'always') ? parsed.proxyMode : 'retry', }; } catch { // Even if localStorage fails, we should return defaults + ENV subscriptions diff --git a/package-lock.json b/package-lock.json index c14e680..6dfe967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvideo", - "version": "4.0.3", + "version": "4.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvideo", - "version": "4.0.3", + "version": "4.0.4", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/package.json b/package.json index 03daebe..9bc3693 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kvideo", - "version": "4.0.3", + "version": "4.0.4", "private": true, "scripts": { "dev": "next dev",