diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 99fdd3a..7fa89f1 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -124,8 +124,9 @@ const ContinueWatchingSection = React.forwardRef((props, re } try { + const shouldFetchMeta = stremioService.isValidContentId(type, id); const [metadata, basicContent] = await Promise.all([ - stremioService.getMetaDetails(type, id), + shouldFetchMeta ? stremioService.getMetaDetails(type, id) : Promise.resolve(null), catalogService.getBasicContentDetails(type, id) ]); diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 796c482..891b105 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -656,7 +656,7 @@ const WatchProgressDisplay = memo(({ - {progressData.episodeInfo} • {progressData.formattedTime} + {progressData.episodeInfo} {/* Trakt sync status with enhanced styling */} diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 9e5d468..0c19ef5 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -521,6 +521,9 @@ const AndroidVideoPlayer: React.FC = () => { // Volume and brightness controls const [volume, setVolume] = useState(1.0); const [brightness, setBrightness] = useState(1.0); + // Store Android system brightness state to restore on exit/unmount + const originalSystemBrightnessRef = useRef(null); + const originalSystemBrightnessModeRef = useRef(null); const [showVolumeOverlay, setShowVolumeOverlay] = useState(false); const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false); const [subtitleSettingsLoaded, setSubtitleSettingsLoaded] = useState(false); @@ -848,6 +851,22 @@ const AndroidVideoPlayer: React.FC = () => { } try { + // Capture Android system brightness and mode to restore later + if (Platform.OS === 'android') { + try { + const [sysBright, sysMode] = await Promise.all([ + (Brightness as any).getSystemBrightnessAsync?.(), + (Brightness as any).getSystemBrightnessModeAsync?.() + ]); + originalSystemBrightnessRef.current = typeof sysBright === 'number' ? sysBright : null; + originalSystemBrightnessModeRef.current = typeof sysMode === 'number' ? sysMode : null; + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Captured system brightness=${originalSystemBrightnessRef.current}, mode=${originalSystemBrightnessModeRef.current}`); + } + } catch (e) { + if (__DEV__) logger.warn('[AndroidVideoPlayer] Failed to capture system brightness state:', e); + } + } const currentBrightness = await Brightness.getBrightnessAsync(); setBrightness(currentBrightness); if (DEBUG_MODE) { @@ -1694,6 +1713,27 @@ const AndroidVideoPlayer: React.FC = () => { logger.log(`[AndroidVideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`); + // Restore Android system brightness state so app does not lock brightness + const restoreSystemBrightness = async () => { + if (Platform.OS !== 'android') return; + try { + // Restore mode first (if available), then brightness value + if (originalSystemBrightnessModeRef.current !== null && typeof (Brightness as any).setSystemBrightnessModeAsync === 'function') { + await (Brightness as any).setSystemBrightnessModeAsync(originalSystemBrightnessModeRef.current); + } + if (originalSystemBrightnessRef.current !== null && typeof (Brightness as any).setSystemBrightnessAsync === 'function') { + await (Brightness as any).setSystemBrightnessAsync(originalSystemBrightnessRef.current); + } + if (DEBUG_MODE) { + logger.log('[AndroidVideoPlayer] Restored Android system brightness and mode'); + } + } catch (e) { + logger.warn('[AndroidVideoPlayer] Failed to restore system brightness state:', e); + } + }; + + await restoreSystemBrightness(); + // Navigate immediately without delay ScreenOrientation.unlockAsync().then(() => { // On tablets keep rotation unlocked; on phones, return to portrait @@ -2226,7 +2266,17 @@ const AndroidVideoPlayer: React.FC = () => { } } }, [useVLC, selectVlcSubtitleTrack]); - + + const disableCustomSubtitles = useCallback(() => { + setUseCustomSubtitles(false); + setCustomSubtitles([]); + // Reset to first available built-in track or disable all tracks + if (useVLC) { + selectVlcSubtitleTrack(ksTextTracks.length > 0 ? 0 : null); + } + setSelectedTextTrack(ksTextTracks.length > 0 ? 0 : -1); + }, [useVLC, selectVlcSubtitleTrack, ksTextTracks.length]); + const loadSubtitleSize = async () => { try { // Prefer scoped subtitle settings @@ -2760,6 +2810,19 @@ const AndroidVideoPlayer: React.FC = () => { clearInterval(progressSaveInterval); setProgressSaveInterval(null); } + // Best-effort restore of Android system brightness state on unmount + if (Platform.OS === 'android') { + try { + if (originalSystemBrightnessModeRef.current !== null && typeof (Brightness as any).setSystemBrightnessModeAsync === 'function') { + (Brightness as any).setSystemBrightnessModeAsync(originalSystemBrightnessModeRef.current); + } + if (originalSystemBrightnessRef.current !== null && typeof (Brightness as any).setSystemBrightnessAsync === 'function') { + (Brightness as any).setSystemBrightnessAsync(originalSystemBrightnessRef.current); + } + } catch (e) { + logger.warn('[AndroidVideoPlayer] Failed to restore system brightness on unmount:', e); + } + } }; }, []); @@ -4018,6 +4081,7 @@ const AndroidVideoPlayer: React.FC = () => { fetchAvailableSubtitles={fetchAvailableSubtitles} loadWyzieSubtitle={loadWyzieSubtitle} selectTextTrack={selectTextTrack} + disableCustomSubtitles={disableCustomSubtitles} increaseSubtitleSize={increaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize} toggleSubtitleBackground={toggleSubtitleBackground} diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 2f5179e..0d03340 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -178,6 +178,14 @@ const KSPlayerCore: React.FC = () => { const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState(false); const [showSourcesModal, setShowSourcesModal] = useState(false); const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {}); + // Playback speed controls required by PlayerControls + const speedOptions = [0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0]; + const [playbackSpeed, setPlaybackSpeed] = useState(1.0); + const cyclePlaybackSpeed = useCallback(() => { + const idx = speedOptions.indexOf(playbackSpeed); + const nextIdx = (idx + 1) % speedOptions.length; + setPlaybackSpeed(speedOptions[nextIdx]); + }, [playbackSpeed, speedOptions]); // Smart URL processing for KSPlayer compatibility const processUrlForKsPlayer = (url: string): string => { try { @@ -1640,6 +1648,13 @@ const KSPlayerCore: React.FC = () => { } }; + const disableCustomSubtitles = () => { + setUseCustomSubtitles(false); + setCustomSubtitles([]); + // Reset to first available built-in track or disable all tracks + setSelectedTextTrack(ksTextTracks.length > 0 ? 0 : -1); + }; + // Ensure native KSPlayer text tracks are disabled when using custom (addon) subtitles // and re-applied when switching back to built-in tracks. This prevents double-rendering. useEffect(() => { @@ -2687,6 +2702,8 @@ const KSPlayerCore: React.FC = () => { onSlidingComplete={handleSlidingComplete} buffered={buffered} formatTime={formatTime} + cyclePlaybackSpeed={cyclePlaybackSpeed} + currentPlaybackSpeed={playbackSpeed} /> {showPauseOverlay && ( @@ -3290,6 +3307,7 @@ const KSPlayerCore: React.FC = () => { fetchAvailableSubtitles={fetchAvailableSubtitles} loadWyzieSubtitle={loadWyzieSubtitle} selectTextTrack={selectTextTrack} + disableCustomSubtitles={disableCustomSubtitles} increaseSubtitleSize={increaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize} toggleSubtitleBackground={toggleSubtitleBackground} diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index b23a87e..8019096 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -31,6 +31,7 @@ interface SubtitleModalsProps { fetchAvailableSubtitles: () => void; loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void; selectTextTrack: (trackId: number) => void; + disableCustomSubtitles: () => void; increaseSubtitleSize: () => void; decreaseSubtitleSize: () => void; toggleSubtitleBackground: () => void; @@ -79,6 +80,7 @@ export const SubtitleModals: React.FC = ({ fetchAvailableSubtitles, loadWyzieSubtitle, selectTextTrack, + disableCustomSubtitles, increaseSubtitleSize, decreaseSubtitleSize, toggleSubtitleBackground, @@ -162,6 +164,22 @@ export const SubtitleModals: React.FC = ({ loadWyzieSubtitle(subtitle); }; + const getFileNameFromUrl = (url?: string): string | null => { + if (!url || typeof url !== 'string') return null; + try { + // Prefer URL parsing to safely strip query/hash + const u = new URL(url); + const raw = u.pathname.split('/').pop() || ''; + const decoded = decodeURIComponent(raw); + return decoded || null; + } catch { + // Fallback for non-standard URLs + const path = url.split('?')[0].split('#')[0]; + const raw = path.split('/').pop() || ''; + try { return decodeURIComponent(raw) || null; } catch { return raw || null; } + } + }; + // Main subtitle menu const renderSubtitleMenu = () => { if (!showSubtitleModal) return null; @@ -383,32 +401,61 @@ export const SubtitleModals: React.FC = ({ }}> Addon Subtitles - fetchAvailableSubtitles()} - disabled={isLoadingSubtitleList} - > - {isLoadingSubtitleList ? ( - - ) : ( - + + {useCustomSubtitles && ( + { + disableCustomSubtitles(); + setSelectedOnlineSubtitleId(null); + }} + activeOpacity={0.7} + > + + + Disable + + )} - - {isLoadingSubtitleList ? 'Searching' : 'Refresh'} - - + fetchAvailableSubtitles()} + disabled={isLoadingSubtitleList} + > + {isLoadingSubtitleList ? ( + + ) : ( + + )} + + {isLoadingSubtitleList ? 'Searching' : 'Refresh'} + + + {(availableSubtitles.length === 0) && !isLoadingSubtitleList ? ( @@ -473,10 +520,19 @@ export const SubtitleModals: React.FC = ({ > - + {sub.display} - + {(() => { + const filename = getFileNameFromUrl(sub.url); + if (!filename) return null; + return ( + + {filename} + + ); + })()} + {formatLanguage(sub.language)}{sub.source ? ` · ${sub.source}` : ''} diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index 3988513..cb8ce85 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -489,6 +489,10 @@ class CatalogService { for (let i = 0; i < 2; i++) { try { + // Skip meta requests for non-content ids (e.g., provider slugs) + if (!stremioService.isValidContentId(type, id)) { + break; + } meta = await stremioService.getMetaDetails(type, id, preferredAddonId); if (meta) break; await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i))); @@ -536,6 +540,10 @@ class CatalogService { for (let i = 0; i < 3; i++) { try { + // Skip meta requests for non-content ids (e.g., provider slugs) + if (!stremioService.isValidContentId(type, id)) { + break; + } meta = await stremioService.getMetaDetails(type, id, preferredAddonId); if (meta) break; await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i))); diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 9be32c5..a068d1b 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -188,6 +188,21 @@ class StremioService { this.initializationPromise = this.initialize(); } + // Shared validator for content IDs eligible for metadata requests + public isValidContentId(type: string, id: string | null | undefined): boolean { + const isValidType = type === 'movie' || type === 'series'; + const lowerId = (id || '').toLowerCase(); + const looksLikeImdb = /^tt\d+/.test(lowerId); + const looksLikeKitsu = lowerId.startsWith('kitsu:') || lowerId === 'kitsu'; + const looksLikeSeriesId = lowerId.startsWith('series:'); + const isNullishId = !id || lowerId === 'null' || lowerId === 'undefined'; + const providerLikeIds = new Set(['moviebox', 'torbox']); + const isProviderSlug = providerLikeIds.has(lowerId); + + if (!isValidType || isNullishId || isProviderSlug) return false; + return looksLikeImdb || looksLikeKitsu || looksLikeSeriesId; + } + static getInstance(): StremioService { if (!StremioService.instance) { StremioService.instance = new StremioService();