somechanges

This commit is contained in:
tapframe 2025-08-14 11:32:41 +05:30
parent 71a9042dc4
commit a32fb39743
8 changed files with 276 additions and 97 deletions

@ -1 +1 @@
Subproject commit 0a040cc12da805fa8b38411d128e607e86e0f919 Subproject commit c176aabb4edd73a709ebdc097688e780b65b651a

View file

@ -650,6 +650,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
(settings?.episodeLayoutStyle === 'horizontal') ? ( (settings?.episodeLayoutStyle === 'horizontal') ? (
// Horizontal Layout (Netflix-style) // Horizontal Layout (Netflix-style)
<FlashList <FlashList
key={`episodes-${settings?.episodeLayoutStyle}-${selectedSeason}`}
ref={episodeScrollViewRef} ref={episodeScrollViewRef}
data={currentSeasonEpisodes} data={currentSeasonEpisodes}
renderItem={({ item: episode, index }) => ( renderItem={({ item: episode, index }) => (
@ -679,6 +680,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
) : ( ) : (
// Vertical Layout (Traditional) // Vertical Layout (Traditional)
<FlashList <FlashList
key={`episodes-${settings?.episodeLayoutStyle}-${selectedSeason}`}
ref={episodeScrollViewRef} ref={episodeScrollViewRef}
data={currentSeasonEpisodes} data={currentSeasonEpisodes}
renderItem={({ item: episode, index }) => ( renderItem={({ item: episode, index }) => (

View file

@ -784,16 +784,34 @@ const AndroidVideoPlayer: React.FC = () => {
} }
disableImmersiveMode(); disableImmersiveMode();
// For series, hard reset to a single Streams route to avoid stacking multiple modals/pages if (Platform.OS === 'ios') {
if (type === 'series' && id && episodeId) { // iOS: rebuild stack so Streams is presented as modal above Metadata
(navigation as any).reset({ if (type === 'series' && id && episodeId) {
index: 0, (navigation as any).reset({
routes: [ index: 2,
{ name: 'Streams', params: { id, type: 'series', episodeId } } routes: [
] { name: 'MainTabs' },
}); { name: 'Metadata', params: { id, type } },
{ name: 'Streams', params: { id, type: 'series', episodeId, fromPlayer: true } }
]
});
} else if ((navigation as any).canGoBack()) {
(navigation as any).goBack();
} else {
(navigation as any).navigate('MainTabs');
}
} else { } else {
navigation.goBack(); // Android: hard reset to avoid stacking multiple pages/modals
if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 0,
routes: [
{ name: 'Streams', params: { id, type: 'series', episodeId, fromPlayer: true } }
]
});
} else {
(navigation as any).goBack();
}
} }
}).catch(() => { }).catch(() => {
// Fallback: still try to restore portrait then navigate // Fallback: still try to restore portrait then navigate
@ -804,16 +822,34 @@ const AndroidVideoPlayer: React.FC = () => {
} }
disableImmersiveMode(); disableImmersiveMode();
// For series, hard reset to a single Streams route to avoid stacking multiple modals/pages if (Platform.OS === 'ios') {
if (type === 'series' && id && episodeId) { // iOS: rebuild stack so Streams is presented as modal above Metadata
(navigation as any).reset({ if (type === 'series' && id && episodeId) {
index: 0, (navigation as any).reset({
routes: [ index: 2,
{ name: 'Streams', params: { id, type: 'series', episodeId } } routes: [
] { name: 'MainTabs' },
}); { name: 'Metadata', params: { id, type } },
{ name: 'Streams', params: { id, type: 'series', episodeId, fromPlayer: true } }
]
});
} else if ((navigation as any).canGoBack()) {
(navigation as any).goBack();
} else {
(navigation as any).navigate('MainTabs');
}
} else { } else {
navigation.goBack(); // Android: hard reset to avoid stacking multiple pages/modals
if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 0,
routes: [
{ name: 'Streams', params: { id, type: 'series', episodeId, fromPlayer: true } }
]
});
} else {
(navigation as any).goBack();
}
} }
}); });

View file

@ -828,19 +828,37 @@ const VideoPlayer: React.FC = () => {
// Navigate back with proper handling for fullscreen modal // Navigate back with proper handling for fullscreen modal
try { try {
// For series, hard reset to a single Streams route to avoid stacking multiple modals/pages // On iOS, ensure Streams shows the CURRENT episode as modal by navigating directly
if (type === 'series' && id && episodeId) { if (Platform.OS === 'ios') {
(navigation as any).reset({ if (type === 'series' && id && episodeId) {
index: 0, // Ensure modal by restoring MainTabs -> Metadata -> Streams
routes: [ (navigation as any).reset({
{ name: 'Streams', params: { id, type: 'series', episodeId } } index: 2,
] routes: [
}); { name: 'MainTabs' },
} else if (navigation.canGoBack()) { { name: 'Metadata', params: { id, type } },
navigation.goBack(); { name: 'Streams', params: { id, type: 'series', episodeId, fromPlayer: true } }
]
});
} else if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate('MainTabs');
}
} else { } else {
// Fallback: navigate to main tabs if can't go back // Android: hard reset to avoid stacking multiple pages/modals
navigation.navigate('MainTabs'); if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 0,
routes: [
{ name: 'Streams', params: { id, type: 'series', episodeId, fromPlayer: true } }
]
});
} else if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate('MainTabs');
}
} }
logger.log('[VideoPlayer] Navigation completed'); logger.log('[VideoPlayer] Navigation completed');
} catch (navError) { } catch (navError) {

View file

@ -65,6 +65,7 @@ export type RootStackParamList = {
type: string; type: string;
episodeId?: string; episodeId?: string;
episodeThumbnail?: string; episodeThumbnail?: string;
fromPlayer?: boolean;
}; };
VideoPlayer: { VideoPlayer: {
id: string; id: string;

View file

@ -368,6 +368,21 @@ const PluginsScreen: React.FC = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [hasRepository, setHasRepository] = useState(false); const [hasRepository, setHasRepository] = useState(false);
const [showboxCookie, setShowboxCookie] = useState<string>('');
const [showboxRegion, setShowboxRegion] = useState<string>('');
const regionOptions = [
{ value: 'USA7', label: 'US East' },
{ value: 'USA6', label: 'US West' },
{ value: 'USA5', label: 'US Middle' },
{ value: 'UK3', label: 'United Kingdom' },
{ value: 'CA1', label: 'Canada' },
{ value: 'FR1', label: 'France' },
{ value: 'DE2', label: 'Germany' },
{ value: 'HK1', label: 'Hong Kong' },
{ value: 'IN1', label: 'India' },
{ value: 'AU1', label: 'Australia' },
{ value: 'SZ', label: 'China' },
];
useEffect(() => { useEffect(() => {
loadScrapers(); loadScrapers();
@ -378,6 +393,13 @@ const PluginsScreen: React.FC = () => {
try { try {
const scrapers = await localScraperService.getAvailableScrapers(); const scrapers = await localScraperService.getAvailableScrapers();
setInstalledScrapers(scrapers); setInstalledScrapers(scrapers);
// preload showbox settings if present
const sb = scrapers.find(s => s.id === 'showboxog');
if (sb) {
const s = await localScraperService.getScraperSettings('showboxog');
setShowboxCookie(s.cookie || '');
setShowboxRegion(s.region || '');
}
} catch (error) { } catch (error) {
logger.error('[ScraperSettings] Failed to load scrapers:', error); logger.error('[ScraperSettings] Failed to load scrapers:', error);
} }
@ -706,66 +728,117 @@ const PluginsScreen: React.FC = () => {
</View> </View>
) : ( ) : (
<View style={styles.scrapersContainer}> <View style={styles.scrapersContainer}>
{installedScrapers.map((scraper) => { {installedScrapers.map((scraper) => {
// Check if scraper is actually installed (has cached code) return (
const isInstalled = localScraperService.getInstalledScrapers().then(installed => <View key={scraper.id} style={[styles.scraperItem, !settings.enableLocalScrapers && styles.disabledContainer]}>
installed.some(s => s.id === scraper.id) <View style={{ flexDirection: 'row', alignItems: 'center', width: '100%' }}>
); {scraper.logo ? (
<Image
return ( source={{ uri: scraper.logo }}
<View key={scraper.id} style={[styles.scraperItem, !settings.enableLocalScrapers && styles.disabledContainer]}> style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledImage]}
{scraper.logo ? ( resizeMode="contain"
<Image />
source={{ uri: scraper.logo }} ) : (
style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledImage]} <View style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledContainer]} />
resizeMode="contain" )}
/> <View style={styles.scraperInfo}>
) : ( <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledContainer]} /> <Text style={[styles.scraperName, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.name}</Text>
)} {scraper.manifestEnabled === false ? (
<View style={styles.scraperInfo}> <View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <Text style={styles.availableIndicatorText}>Disabled</Text>
<Text style={[styles.scraperName, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.name}</Text> </View>
{scraper.manifestEnabled === false ? ( ) : scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android') ? (
<View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}> <View style={[styles.availableIndicator, { backgroundColor: '#ff9500' }]}>
<Text style={styles.availableIndicatorText}>Disabled</Text> <Text style={styles.availableIndicatorText}>Platform Disabled</Text>
</View> </View>
) : scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android') ? ( ) : !scraper.enabled && (
<View style={[styles.availableIndicator, { backgroundColor: '#ff9500' }]}> <View style={styles.availableIndicator}>
<Text style={styles.availableIndicatorText}>Platform Disabled</Text> <Text style={styles.availableIndicatorText}>Available</Text>
</View> </View>
) : !scraper.enabled && ( )}
<View style={styles.availableIndicator}> </View>
<Text style={styles.availableIndicatorText}>Available</Text> <Text style={[styles.scraperDescription, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.description}</Text>
</View> <View style={styles.scraperMeta}>
)} <Text style={[styles.scraperVersion, !settings.enableLocalScrapers && styles.disabledText]}>v{scraper.version}</Text>
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text>
<Text style={[styles.scraperTypes, !settings.enableLocalScrapers && styles.disabledText]}>
{scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'}
</Text>
{scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && (
<>
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text>
<Text style={[styles.scraperLanguage, !settings.enableLocalScrapers && styles.disabledText]}>
{scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')}
</Text>
</>
)}
</View>
</View> </View>
<Text style={[styles.scraperDescription, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.description}</Text> <Switch
<View style={styles.scraperMeta}> value={scraper.enabled && settings.enableLocalScrapers}
<Text style={[styles.scraperVersion, !settings.enableLocalScrapers && styles.disabledText]}>v{scraper.version}</Text> onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text> trackColor={{ false: colors.elevation3, true: colors.primary }}
<Text style={[styles.scraperTypes, !settings.enableLocalScrapers && styles.disabledText]}> thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
{scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'} disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
</Text> style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))) ? 0.5 : 1 }}
{scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && ( />
<> </View>
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text> {scraper.id === 'showboxog' && settings.enableLocalScrapers && (
<Text style={[styles.scraperLanguage, !settings.enableLocalScrapers && styles.disabledText]}> <View style={{ marginTop: 16, width: '100%', paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
{scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')} <Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox Cookie</Text>
</Text> <TextInput
</> style={[styles.textInput, { marginBottom: 12 }]}
)} value={showboxCookie}
</View> onChangeText={setShowboxCookie}
placeholder="Paste FebBox ui cookie value"
placeholderTextColor={colors.mediumGray}
autoCapitalize="none"
autoCorrect={false}
multiline={true}
numberOfLines={3}
/>
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>Region</Text>
<View style={[styles.qualityChipsContainer, { marginBottom: 16 }]}>
{regionOptions.map(opt => {
const selected = showboxRegion === opt.value;
return (
<TouchableOpacity
key={opt.value}
style={[styles.qualityChip, selected && styles.qualityChipSelected]}
onPress={() => setShowboxRegion(opt.value)}
>
<Text style={[styles.qualityChipText, selected && styles.qualityChipTextSelected]}>
{opt.label}
</Text>
</TouchableOpacity>
);
})}
</View>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.primaryButton]}
onPress={async () => {
await localScraperService.setScraperSettings('showboxog', { cookie: showboxCookie, region: showboxRegion });
Alert.alert('Saved', 'ShowBox settings updated');
}}
>
<Text style={styles.buttonText}>Save</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.secondaryButton]}
onPress={async () => {
setShowboxCookie('');
setShowboxRegion('');
await localScraperService.setScraperSettings('showboxog', {});
}}
>
<Text style={styles.secondaryButtonText}>Clear</Text>
</TouchableOpacity>
</View>
</View>
)}
</View> </View>
<Switch
value={scraper.enabled && settings.enableLocalScrapers}
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))) ? 0.5 : 1 }}
/>
</View>
); );
})} })}
</View> </View>

View file

@ -353,7 +353,7 @@ export const StreamsScreen = () => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const route = useRoute<RouteProp<RootStackParamList, 'Streams'>>(); const route = useRoute<RouteProp<RootStackParamList, 'Streams'>>();
const navigation = useNavigation<RootStackNavigationProp>(); const navigation = useNavigation<RootStackNavigationProp>();
const { id, type, episodeId, episodeThumbnail } = route.params; const { id, type, episodeId, episodeThumbnail, fromPlayer } = route.params;
const { settings } = useSettings(); const { settings } = useSettings();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { colors } = currentTheme; const { colors } = currentTheme;
@ -565,17 +565,20 @@ export const StreamsScreen = () => {
// Reset autoplay state when content changes // Reset autoplay state when content changes
setAutoplayTriggered(false); setAutoplayTriggered(false);
if (settings.autoplayBestStream) { if (settings.autoplayBestStream && !fromPlayer) {
setIsAutoplayWaiting(true); setIsAutoplayWaiting(true);
logger.log('🔄 Autoplay enabled, waiting for best stream...'); logger.log('🔄 Autoplay enabled, waiting for best stream...');
} else { } else {
setIsAutoplayWaiting(false); setIsAutoplayWaiting(false);
if (fromPlayer) {
logger.log('🚫 Autoplay disabled: returning from player');
}
} }
} }
}; };
checkProviders(); checkProviders();
}, [type, id, episodeId, settings.autoplayBestStream]); }, [type, id, episodeId, settings.autoplayBestStream, fromPlayer]);
React.useEffect(() => { React.useEffect(() => {
// Trigger entrance animations // Trigger entrance animations
@ -1432,7 +1435,7 @@ export const StreamsScreen = () => {
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.backButton, styles.backButton,
Platform.OS === 'ios' ? { paddingTop: Math.max(insets.top, 12) + 6 } : null Platform.OS === 'ios' ? { marginTop: 20 } : null
]} ]}
onPress={handleBack} onPress={handleBack}
activeOpacity={0.7} activeOpacity={0.7}
@ -1662,7 +1665,7 @@ const createStyles = (colors: any) => StyleSheet.create({
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
zIndex: 2, zIndex: 9999,
pointerEvents: 'box-none', pointerEvents: 'box-none',
}, },
backButton: { backButton: {
@ -1670,7 +1673,7 @@ const createStyles = (colors: any) => StyleSheet.create({
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
padding: 14, padding: 14,
paddingTop: Platform.OS === 'android' ? 45 : 15, paddingTop: 0,
}, },
backButtonText: { backButtonText: {
color: colors.highEmphasis, color: colors.highEmphasis,

View file

@ -61,6 +61,7 @@ class LocalScraperService {
private repositoryUrl: string = ''; private repositoryUrl: string = '';
private repositoryName: string = ''; private repositoryName: string = '';
private initialized: boolean = false; private initialized: boolean = false;
private scraperSettingsCache: Record<string, any> | null = null;
private constructor() { private constructor() {
this.initialize(); this.initialize();
@ -409,6 +410,38 @@ class LocalScraperService {
return Array.from(this.installedScrapers.values()); return Array.from(this.installedScrapers.values());
} }
// Per-scraper settings storage
async getScraperSettings(scraperId: string): Promise<Record<string, any>> {
await this.ensureInitialized();
try {
if (!this.scraperSettingsCache) {
const raw = await AsyncStorage.getItem(this.SCRAPER_SETTINGS_KEY);
this.scraperSettingsCache = raw ? JSON.parse(raw) : {};
}
const cache = this.scraperSettingsCache || {};
return cache[scraperId] || {};
} catch (error) {
logger.warn('[LocalScraperService] Failed to get scraper settings for', scraperId, error);
return {};
}
}
async setScraperSettings(scraperId: string, settings: Record<string, any>): Promise<void> {
await this.ensureInitialized();
try {
if (!this.scraperSettingsCache) {
const raw = await AsyncStorage.getItem(this.SCRAPER_SETTINGS_KEY);
this.scraperSettingsCache = raw ? JSON.parse(raw) : {};
}
const cache = this.scraperSettingsCache || {};
cache[scraperId] = settings || {};
this.scraperSettingsCache = cache;
await AsyncStorage.setItem(this.SCRAPER_SETTINGS_KEY, JSON.stringify(cache));
} catch (error) {
logger.error('[LocalScraperService] Failed to set scraper settings for', scraperId, error);
}
}
// Get available scrapers from manifest.json (for display in settings) // Get available scrapers from manifest.json (for display in settings)
async getAvailableScrapers(): Promise<ScraperInfo[]> { async getAvailableScrapers(): Promise<ScraperInfo[]> {
if (!this.repositoryUrl) { if (!this.repositoryUrl) {
@ -568,12 +601,17 @@ class LocalScraperService {
logger.log('[LocalScraperService] Executing scraper:', scraper.name); logger.log('[LocalScraperService] Executing scraper:', scraper.name);
// Load per-scraper settings
const scraperSettings = await this.getScraperSettings(scraper.id);
// Create a sandboxed execution environment // Create a sandboxed execution environment
const results = await this.executeSandboxed(code, { const results = await this.executeSandboxed(code, {
tmdbId, tmdbId,
mediaType: type, mediaType: type,
season, season,
episode episode,
scraperId: scraper.id,
settings: scraperSettings
}); });
// Convert results to Nuvio Stream format // Convert results to Nuvio Stream format
@ -602,6 +640,11 @@ class LocalScraperService {
const settings = settingsData ? JSON.parse(settingsData) : {}; const settings = settingsData ? JSON.parse(settingsData) : {};
const urlValidationEnabled = settings.enableScraperUrlValidation ?? true; const urlValidationEnabled = settings.enableScraperUrlValidation ?? true;
// Load per-scraper settings for this run
const allScraperSettingsRaw = await AsyncStorage.getItem(this.SCRAPER_SETTINGS_KEY);
const allScraperSettings = allScraperSettingsRaw ? JSON.parse(allScraperSettingsRaw) : {};
const perScraperSettings = (params && params.scraperId && allScraperSettings[params.scraperId]) ? allScraperSettings[params.scraperId] : (params?.settings || {});
// Create a limited global context // Create a limited global context
const moduleExports = {}; const moduleExports = {};
const moduleObj = { exports: moduleExports }; const moduleObj = { exports: moduleExports };
@ -683,7 +726,10 @@ class LocalScraperService {
exports: moduleExports, exports: moduleExports,
global: {}, // Empty global object global: {}, // Empty global object
// URL validation setting // URL validation setting
URL_VALIDATION_ENABLED: urlValidationEnabled URL_VALIDATION_ENABLED: urlValidationEnabled,
// Expose per-scraper settings to the plugin code
SCRAPER_SETTINGS: perScraperSettings,
SCRAPER_ID: params?.scraperId
}; };
// Execute the scraper code without timeout // Execute the scraper code without timeout