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

View file

@ -784,16 +784,34 @@ const AndroidVideoPlayer: React.FC = () => {
}
disableImmersiveMode();
// For series, hard reset to a single Streams route to avoid stacking multiple modals/pages
if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 0,
routes: [
{ name: 'Streams', params: { id, type: 'series', episodeId } }
]
});
if (Platform.OS === 'ios') {
// iOS: rebuild stack so Streams is presented as modal above Metadata
if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 2,
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 {
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(() => {
// Fallback: still try to restore portrait then navigate
@ -804,16 +822,34 @@ const AndroidVideoPlayer: React.FC = () => {
}
disableImmersiveMode();
// For series, hard reset to a single Streams route to avoid stacking multiple modals/pages
if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 0,
routes: [
{ name: 'Streams', params: { id, type: 'series', episodeId } }
]
});
if (Platform.OS === 'ios') {
// iOS: rebuild stack so Streams is presented as modal above Metadata
if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 2,
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 {
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
try {
// For series, hard reset to a single Streams route to avoid stacking multiple modals/pages
if (type === 'series' && id && episodeId) {
(navigation as any).reset({
index: 0,
routes: [
{ name: 'Streams', params: { id, type: 'series', episodeId } }
]
});
} else if (navigation.canGoBack()) {
navigation.goBack();
// On iOS, ensure Streams shows the CURRENT episode as modal by navigating directly
if (Platform.OS === 'ios') {
if (type === 'series' && id && episodeId) {
// Ensure modal by restoring MainTabs -> Metadata -> Streams
(navigation as any).reset({
index: 2,
routes: [
{ name: 'MainTabs' },
{ name: 'Metadata', params: { id, type } },
{ name: 'Streams', params: { id, type: 'series', episodeId, fromPlayer: true } }
]
});
} else if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate('MainTabs');
}
} else {
// Fallback: navigate to main tabs if can't go back
navigation.navigate('MainTabs');
// 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 if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate('MainTabs');
}
}
logger.log('[VideoPlayer] Navigation completed');
} catch (navError) {

View file

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

View file

@ -368,6 +368,21 @@ const PluginsScreen: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = 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(() => {
loadScrapers();
@ -378,6 +393,13 @@ const PluginsScreen: React.FC = () => {
try {
const scrapers = await localScraperService.getAvailableScrapers();
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) {
logger.error('[ScraperSettings] Failed to load scrapers:', error);
}
@ -706,66 +728,117 @@ const PluginsScreen: React.FC = () => {
</View>
) : (
<View style={styles.scrapersContainer}>
{installedScrapers.map((scraper) => {
// Check if scraper is actually installed (has cached code)
const isInstalled = localScraperService.getInstalledScrapers().then(installed =>
installed.some(s => s.id === scraper.id)
);
return (
<View key={scraper.id} style={[styles.scraperItem, !settings.enableLocalScrapers && styles.disabledContainer]}>
{scraper.logo ? (
<Image
source={{ uri: scraper.logo }}
style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledImage]}
resizeMode="contain"
/>
) : (
<View style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledContainer]} />
)}
<View style={styles.scraperInfo}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={[styles.scraperName, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.name}</Text>
{scraper.manifestEnabled === false ? (
<View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}>
<Text style={styles.availableIndicatorText}>Disabled</Text>
</View>
) : scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android') ? (
<View style={[styles.availableIndicator, { backgroundColor: '#ff9500' }]}>
<Text style={styles.availableIndicatorText}>Platform Disabled</Text>
</View>
) : !scraper.enabled && (
<View style={styles.availableIndicator}>
<Text style={styles.availableIndicatorText}>Available</Text>
</View>
)}
{installedScrapers.map((scraper) => {
return (
<View key={scraper.id} style={[styles.scraperItem, !settings.enableLocalScrapers && styles.disabledContainer]}>
<View style={{ flexDirection: 'row', alignItems: 'center', width: '100%' }}>
{scraper.logo ? (
<Image
source={{ uri: scraper.logo }}
style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledImage]}
resizeMode="contain"
/>
) : (
<View style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledContainer]} />
)}
<View style={styles.scraperInfo}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={[styles.scraperName, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.name}</Text>
{scraper.manifestEnabled === false ? (
<View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}>
<Text style={styles.availableIndicatorText}>Disabled</Text>
</View>
) : scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android') ? (
<View style={[styles.availableIndicator, { backgroundColor: '#ff9500' }]}>
<Text style={styles.availableIndicatorText}>Platform Disabled</Text>
</View>
) : !scraper.enabled && (
<View style={styles.availableIndicator}>
<Text style={styles.availableIndicatorText}>Available</Text>
</View>
)}
</View>
<Text style={[styles.scraperDescription, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.description}</Text>
<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>
<Text style={[styles.scraperDescription, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.description}</Text>
<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>
<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>
{scraper.id === 'showboxog' && settings.enableLocalScrapers && (
<View style={{ marginTop: 16, width: '100%', paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox Cookie</Text>
<TextInput
style={[styles.textInput, { marginBottom: 12 }]}
value={showboxCookie}
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>
<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 file

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

View file

@ -61,6 +61,7 @@ class LocalScraperService {
private repositoryUrl: string = '';
private repositoryName: string = '';
private initialized: boolean = false;
private scraperSettingsCache: Record<string, any> | null = null;
private constructor() {
this.initialize();
@ -409,6 +410,38 @@ class LocalScraperService {
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)
async getAvailableScrapers(): Promise<ScraperInfo[]> {
if (!this.repositoryUrl) {
@ -568,12 +601,17 @@ class LocalScraperService {
logger.log('[LocalScraperService] Executing scraper:', scraper.name);
// Load per-scraper settings
const scraperSettings = await this.getScraperSettings(scraper.id);
// Create a sandboxed execution environment
const results = await this.executeSandboxed(code, {
tmdbId,
mediaType: type,
season,
episode
episode,
scraperId: scraper.id,
settings: scraperSettings
});
// Convert results to Nuvio Stream format
@ -602,6 +640,11 @@ class LocalScraperService {
const settings = settingsData ? JSON.parse(settingsData) : {};
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
const moduleExports = {};
const moduleObj = { exports: moduleExports };
@ -683,7 +726,10 @@ class LocalScraperService {
exports: moduleExports,
global: {}, // Empty global object
// 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