mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
some ui changes
This commit is contained in:
parent
4f1b8103d0
commit
ca52a81141
7 changed files with 192 additions and 30 deletions
|
|
@ -124,8 +124,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((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)
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -656,7 +656,7 @@ const WatchProgressDisplay = memo(({
|
|||
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
|
||||
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
|
||||
}]}>
|
||||
{progressData.episodeInfo} • {progressData.formattedTime}
|
||||
{progressData.episodeInfo}
|
||||
</Text>
|
||||
|
||||
{/* Trakt sync status with enhanced styling */}
|
||||
|
|
|
|||
|
|
@ -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<number | null>(null);
|
||||
const originalSystemBrightnessModeRef = useRef<number | null>(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}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,14 @@ const KSPlayerCore: React.FC = () => {
|
|||
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
||||
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(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<number>(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}
|
||||
|
|
|
|||
|
|
@ -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<SubtitleModalsProps> = ({
|
|||
fetchAvailableSubtitles,
|
||||
loadWyzieSubtitle,
|
||||
selectTextTrack,
|
||||
disableCustomSubtitles,
|
||||
increaseSubtitleSize,
|
||||
decreaseSubtitleSize,
|
||||
toggleSubtitleBackground,
|
||||
|
|
@ -162,6 +164,22 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
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<SubtitleModalsProps> = ({
|
|||
}}>
|
||||
Addon Subtitles
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.15)',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: chipPadH,
|
||||
paddingVertical: chipPadV-2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onPress={() => fetchAvailableSubtitles()}
|
||||
disabled={isLoadingSubtitleList}
|
||||
>
|
||||
{isLoadingSubtitleList ? (
|
||||
<ActivityIndicator size="small" color="#22C55E" />
|
||||
) : (
|
||||
<MaterialIcons name="refresh" size={16} color="#22C55E" />
|
||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||
{useCustomSubtitles && (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: chipPadH,
|
||||
paddingVertical: chipPadV-2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onPress={() => {
|
||||
disableCustomSubtitles();
|
||||
setSelectedOnlineSubtitleId(null);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="close" size={16} color="#EF4444" />
|
||||
<Text style={{
|
||||
color: '#EF4444',
|
||||
fontSize: isCompact ? 11 : 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
}}>
|
||||
Disable
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text style={{
|
||||
color: '#22C55E',
|
||||
fontSize: isCompact ? 11 : 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
}}>
|
||||
{isLoadingSubtitleList ? 'Searching' : 'Refresh'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.15)',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: chipPadH,
|
||||
paddingVertical: chipPadV-2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onPress={() => fetchAvailableSubtitles()}
|
||||
disabled={isLoadingSubtitleList}
|
||||
>
|
||||
{isLoadingSubtitleList ? (
|
||||
<ActivityIndicator size="small" color="#22C55E" />
|
||||
) : (
|
||||
<MaterialIcons name="refresh" size={16} color="#22C55E" />
|
||||
)}
|
||||
<Text style={{
|
||||
color: '#22C55E',
|
||||
fontSize: isCompact ? 11 : 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
}}>
|
||||
{isLoadingSubtitleList ? 'Searching' : 'Refresh'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{(availableSubtitles.length === 0) && !isLoadingSubtitleList ? (
|
||||
|
|
@ -473,10 +520,19 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ color: '#FFFFFF', fontSize: 15, fontWeight: '500', marginBottom: 4 }}>
|
||||
<Text style={{ color: '#FFFFFF', fontSize: 15, fontWeight: '500' }}>
|
||||
{sub.display}
|
||||
</Text>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 13 }}>
|
||||
{(() => {
|
||||
const filename = getFileNameFromUrl(sub.url);
|
||||
if (!filename) return null;
|
||||
return (
|
||||
<Text style={{ color: 'rgba(255,255,255,0.75)', fontSize: 12, marginTop: 2 }} numberOfLines={1}>
|
||||
{filename}
|
||||
</Text>
|
||||
);
|
||||
})()}
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 13, marginTop: 2 }}>
|
||||
{formatLanguage(sub.language)}{sub.source ? ` · ${sub.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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<string>(['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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue