diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 0fba6733..2f350dc2 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -70,6 +70,7 @@ import BackdropGalleryScreen from '../screens/BackdropGalleryScreen'; import BackupScreen from '../screens/BackupScreen'; import ContinueWatchingSettingsScreen from '../screens/ContinueWatchingSettingsScreen'; import ContributorsScreen from '../screens/ContributorsScreen'; +import DebridIntegrationScreen from '../screens/DebridIntegrationScreen'; // Stack navigator types export type RootStackParamList = { @@ -82,27 +83,27 @@ export type RootStackParamList = { Update: undefined; Search: undefined; Calendar: undefined; - Metadata: { - id: string; + Metadata: { + id: string; type: string; episodeId?: string; addonId?: string; }; - Streams: { - id: string; + Streams: { + id: string; type: string; episodeId?: string; episodeThumbnail?: string; fromPlayer?: boolean; }; - PlayerIOS: { - uri: string; - title?: string; - season?: number; - episode?: number; - episodeTitle?: string; - quality?: string; - year?: number; + PlayerIOS: { + uri: string; + title?: string; + season?: number; + episode?: number; + episodeTitle?: string; + quality?: string; + year?: number; streamProvider?: string; streamName?: string; headers?: { [key: string]: string }; @@ -116,14 +117,14 @@ export type RootStackParamList = { videoType?: string; groupedEpisodes?: { [seasonNumber: number]: any[] }; }; - PlayerAndroid: { - uri: string; - title?: string; - season?: number; - episode?: number; - episodeTitle?: string; - quality?: string; - year?: number; + PlayerAndroid: { + uri: string; + title?: string; + season?: number; + episode?: number; + episodeTitle?: string; + quality?: string; + year?: number; streamProvider?: string; streamName?: string; headers?: { [key: string]: string }; @@ -180,6 +181,7 @@ export type RootStackParamList = { }; ContinueWatchingSettings: undefined; Contributors: undefined; + DebridIntegration: undefined; }; export type RootStackNavigationProp = NativeStackNavigationProp; @@ -376,9 +378,9 @@ export const CustomNavigationDarkTheme: Theme = { type IconNameType = string; // Add TabIcon component -const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material' }: { - focused: boolean; - color: string; +const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material' }: { + focused: boolean; + color: string; iconName: IconNameType; iconLibrary?: 'material' | 'feather' | 'ionicons'; }) => { @@ -403,28 +405,28 @@ const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material' })(); return ( - {iconLibrary === 'feather' ? ( - ) : iconLibrary === 'ionicons' ? ( - ) : ( - )} @@ -432,17 +434,17 @@ const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material' }); // Update the TabScreenWrapper component with fixed layout dimensions -const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => { +const TabScreenWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [dimensions, setDimensions] = useState(Dimensions.get('window')); - + useEffect(() => { const subscription = Dimensions.addEventListener('change', ({ window }) => { setDimensions(window); }); - + return () => subscription?.remove(); }, []); - + const isTablet = useMemo(() => { const { width, height } = dimensions; const smallestDimension = Math.min(width, height); @@ -456,35 +458,35 @@ const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) = StatusBar.setTranslucent(true); StatusBar.setBackgroundColor('transparent'); }; - + applyStatusBarConfig(); - + // Apply status bar config on every focus - const subscription = Platform.OS === 'android' + const subscription = Platform.OS === 'android' ? AppState.addEventListener('change', (state) => { - if (state === 'active') { - applyStatusBarConfig(); - } - }) - : { remove: () => {} }; - + if (state === 'active') { + applyStatusBarConfig(); + } + }) + : { remove: () => { } }; + return () => { subscription.remove(); }; }, []); return ( - {/* Reserve consistent space for the header area on all screens */} - = ({ children }) = }; // Add this component to wrap each screen in the tab navigator -const WrappedScreen: React.FC<{Screen: React.ComponentType}> = ({ Screen }) => { +const WrappedScreen: React.FC<{ Screen: React.ComponentType }> = ({ Screen }) => { return ( @@ -514,12 +516,12 @@ const MainTabs = () => { const { settings: appSettings } = useSettingsHook(); const [hasUpdateBadge, setHasUpdateBadge] = React.useState(false); const [dimensions, setDimensions] = useState(Dimensions.get('window')); - + useEffect(() => { const subscription = Dimensions.addEventListener('change', ({ window }) => { setDimensions(window); }); - + return () => subscription?.remove(); }, []); React.useEffect(() => { @@ -529,7 +531,7 @@ const MainTabs = () => { try { const flag = await mmkvStorage.getItem('@update_badge_pending'); if (mounted) setHasUpdateBadge(flag === 'true'); - } catch {} + } catch { } }; load(); // Fast poll initially for quick badge appearance, then slow down @@ -575,7 +577,7 @@ const MainTabs = () => { }, [hidden, headerAnim]); const translateY = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [0, -70] }); const fade = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }); - + const renderTabBar = (props: BottomTabBarProps) => { // Hide tab bar when home is loading if (isHomeLoading) { @@ -590,18 +592,18 @@ const MainTabs = () => { // Top floating, text-only pill nav for tablets return ( + style={[{ + position: 'absolute', + top: insets.top + 12, + left: 0, + right: 0, + alignItems: 'center', + backgroundColor: 'transparent', + zIndex: 100, + }, shouldKeepFixed ? {} : { + transform: [{ translateY }], + opacity: fade, + }]}> { options.tabBarLabel !== undefined ? options.tabBarLabel : options.title !== undefined - ? options.title - : route.name; + ? options.title + : route.name; const isFocused = props.state.index === index; @@ -692,10 +694,10 @@ const MainTabs = () => { // Default bottom tab for phones return ( - { options.tabBarLabel !== undefined ? options.tabBarLabel : options.title !== undefined - ? options.title - : route.name; + ? options.title + : route.name; const isFocused = props.state.index === index; @@ -813,9 +815,9 @@ const MainTabs = () => { backgroundColor: 'transparent', }} > - @@ -838,7 +840,7 @@ const MainTabs = () => { ); }; - + // iOS: Use native bottom tabs (@bottom-tabs/react-navigation) if (Platform.OS === 'ios') { // Dynamically require to avoid impacting Android bundle @@ -923,7 +925,7 @@ const MainTabs = () => { barStyle="light-content" backgroundColor="transparent" /> - + ({ @@ -1059,7 +1061,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta const { currentTheme } = useTheme(); const { user, loading } = useAccount(); const insets = useSafeAreaInsets(); - + // Handle Android-specific optimizations useEffect(() => { if (Platform.OS === 'android') { @@ -1070,13 +1072,13 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta } catch (error) { console.log('Immersive mode error:', error); } - + // Ensure consistent background color for Android StatusBar.setBackgroundColor('transparent', true); StatusBar.setTranslucent(true); } }, []); - + return ( - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1571,8 +1588,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta }; const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => ( - StyleSheet.create({ opacity: 0.8, }, communityAddonVersion: { - fontSize: 12, - color: colors.lightGray, + fontSize: 12, + color: colors.lightGray, }, communityAddonDot: { fontSize: 12, @@ -533,18 +533,18 @@ const createStyles = (colors: any) => StyleSheet.create({ marginHorizontal: 5, }, communityAddonCategory: { - fontSize: 12, - color: colors.lightGray, - flexShrink: 1, + fontSize: 12, + color: colors.lightGray, + flexShrink: 1, }, separator: { height: 10, }, sectionSeparator: { - height: 1, - backgroundColor: colors.border, - marginHorizontal: 20, - marginVertical: 20, + height: 1, + backgroundColor: colors.border, + marginHorizontal: 20, + marginVertical: 20, }, emptyMessage: { textAlign: 'center', @@ -660,16 +660,26 @@ const AddonsScreen = () => { setLoading(true); // Use the regular method without disabled state const installedAddons = await stremioService.getInstalledAddonsAsync(); - setAddons(installedAddons as ExtendedManifest[]); - + + // Filter out Torbox addons (managed via DebridIntegrationScreen) + const filteredAddons = installedAddons.filter(addon => { + const isTorboxAddon = + addon.id?.includes('torbox') || + addon.url?.includes('torbox') || + (addon as any).transport?.includes('torbox'); + return !isTorboxAddon; + }); + + setAddons(filteredAddons as ExtendedManifest[]); + // Count catalogs let totalCatalogs = 0; - installedAddons.forEach(addon => { + filteredAddons.forEach(addon => { if (addon.catalogs && addon.catalogs.length > 0) { totalCatalogs += addon.catalogs.length; } }); - + // Get catalog settings to determine enabled count const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings'); if (catalogSettingsJson) { @@ -682,11 +692,11 @@ const AddonsScreen = () => { setCatalogCount(totalCatalogs); } } catch (error) { - logger.error('Failed to load addons:', error); - setAlertTitle('Error'); - setAlertMessage('Failed to load addons'); - setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); - setAlertVisible(true); + logger.error('Failed to load addons:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to load addons'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setLoading(false); } @@ -706,9 +716,9 @@ const AddonsScreen = () => { setCommunityAddons(validAddons); } catch (error) { - logger.error('Failed to load community addons:', error); - setCommunityError('Failed to load community addons. Please try again later.'); - setCommunityAddons([]); + logger.error('Failed to load community addons:', error); + setCommunityError('Failed to load community addons. Please try again later.'); + setCommunityAddons([]); } finally { setCommunityLoading(false); } @@ -756,16 +766,16 @@ const AddonsScreen = () => { setShowConfirmModal(false); setAddonDetails(null); loadAddons(); - setAlertTitle('Success'); - setAlertMessage('Addon installed successfully'); - setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); - setAlertVisible(true); + setAlertTitle('Success'); + setAlertMessage('Addon installed successfully'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } catch (error) { - logger.error('Failed to install addon:', error); - setAlertTitle('Error'); - setAlertMessage('Failed to install addon'); - setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); - setAlertVisible(true); + logger.error('Failed to install addon:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to install addon'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setInstalling(false); } @@ -813,13 +823,13 @@ const AddonsScreen = () => { const handleConfigureAddon = (addon: ExtendedManifest, transportUrl?: string) => { // Try different ways to get the configuration URL let configUrl = ''; - + // Debug log the addon data to help troubleshoot logger.info(`Configure addon: ${addon.name}, ID: ${addon.id}`); if (transportUrl) { logger.info(`TransportUrl provided: ${transportUrl}`); } - + // First check if the addon has a configurationURL directly if (addon.behaviorHints?.configurationURL) { configUrl = addon.behaviorHints.configurationURL; @@ -861,7 +871,7 @@ const AddonsScreen = () => { const baseUrl = addon.id.replace(/\/[^\/]+\.json$/, '/'); configUrl = `${baseUrl}configure`; logger.info(`Using addon.id as HTTP URL: ${configUrl}`); - } + } // If the ID uses stremio:// protocol but contains http URL (common format) else if (addon.id && (addon.id.includes('https://') || addon.id.includes('http://'))) { // Extract the HTTP URL using a more flexible regex @@ -874,7 +884,7 @@ const AddonsScreen = () => { logger.info(`Extracted HTTP URL from stremio:// format: ${configUrl}`); } } - + // Special case for common addon format like stremio://addon.stremio.com/... if (!configUrl && addon.id && addon.id.startsWith('stremio://')) { // Try to convert stremio://domain.com/... to https://domain.com/... @@ -886,21 +896,21 @@ const AddonsScreen = () => { logger.info(`Converted stremio:// protocol to https:// for config URL: ${configUrl}`); } } - + // Use transport property if available (some addons include this) if (!configUrl && addon.transport && typeof addon.transport === 'string' && addon.transport.includes('http')) { const baseUrl = addon.transport.replace(/\/[^\/]+\.json$/, '/'); configUrl = `${baseUrl}configure`; logger.info(`Using addon.transport for config URL: ${configUrl}`); } - + // Get the URL from manifest's originalUrl if available if (!configUrl && (addon as any).originalUrl) { const baseUrl = (addon as any).originalUrl.replace(/\/[^\/]+\.json$/, '/'); configUrl = `${baseUrl}configure`; logger.info(`Using originalUrl property: ${configUrl}`); } - + // If we couldn't determine a config URL, show an error if (!configUrl) { logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`); @@ -910,10 +920,10 @@ const AddonsScreen = () => { setAlertVisible(true); return; } - + // Log the URL being opened logger.info(`Opening configuration for addon: ${addon.name} at URL: ${configUrl}`); - + // Check if the URL can be opened Linking.canOpenURL(configUrl).then(supported => { if (supported) { @@ -927,10 +937,10 @@ const AddonsScreen = () => { } }).catch(err => { logger.error(`Error checking if URL can be opened: ${configUrl}`, err); - setAlertTitle('Error'); - setAlertMessage('Could not open configuration page.'); - setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); - setAlertVisible(true); + setAlertTitle('Error'); + setAlertMessage('Could not open configuration page.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); }); }; @@ -947,12 +957,12 @@ const AddonsScreen = () => { const isConfigurable = item.behaviorHints?.configurable === true; // Check if addon is pre-installed const isPreInstalled = stremioService.isPreInstalledAddon(item.id); - + // Format the types into a simple category text - const categoryText = types.length > 0 - ? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ') + const categoryText = types.length > 0 + ? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ') : 'No categories'; - + const isFirstItem = index === 0; const isLastItem = index === addons.length - 1; @@ -960,35 +970,35 @@ const AddonsScreen = () => { {reorderMode && ( - moveAddonUp(item)} disabled={isFirstItem} > - - moveAddonDown(item)} disabled={isLastItem} > - )} - + {logo ? ( - @@ -1016,7 +1026,7 @@ const AddonsScreen = () => { {!reorderMode ? ( <> {isConfigurable && ( - handleConfigureAddon(item, item.transport)} > @@ -1024,7 +1034,7 @@ const AddonsScreen = () => { )} {!stremioService.isPreInstalledAddon(item.id) && ( - handleRemoveAddon(item)} > @@ -1039,7 +1049,7 @@ const AddonsScreen = () => { )} - + {description.length > 100 ? description.substring(0, 100) + '...' : description} @@ -1077,9 +1087,9 @@ const AddonsScreen = () => { {manifest.name} {description} - v{manifest.version || 'N/A'} - - {categoryText} + v{manifest.version || 'N/A'} + + {categoryText} @@ -1117,50 +1127,50 @@ const AddonsScreen = () => { return ( - + {/* Header */} - navigation.goBack()} > Settings - + {/* Reorder Mode Toggle Button */} - - - + {/* Refresh Button */} - - - + Addons {reorderMode && (Reorder Mode)} - + {reorderMode && ( @@ -1169,18 +1179,18 @@ const AddonsScreen = () => { )} - + {loading ? ( ) : ( - - + {/* Overview Section */} OVERVIEW @@ -1192,7 +1202,7 @@ const AddonsScreen = () => { - + {/* Hide Add Addon Section in reorder mode */} {!reorderMode && ( @@ -1207,8 +1217,8 @@ const AddonsScreen = () => { autoCapitalize="none" autoCorrect={false} /> - handleAddAddon()} disabled={installing || !addonUrl} > @@ -1219,7 +1229,7 @@ const AddonsScreen = () => { )} - + {/* Installed Addons Section */} @@ -1233,8 +1243,8 @@ const AddonsScreen = () => { ) : ( addons.map((addon, index) => ( - {renderAddonItem({ item: addon, index })} @@ -1245,68 +1255,68 @@ const AddonsScreen = () => { {/* Separator */} - + - {/* Promotional Addon Section (hidden if installed) */} - {!isPromoInstalled && ( - - OFFICIAL ADDON - - - - {promoAddon.logo ? ( - - ) : ( - - - - )} - - {promoAddon.name} - - v{promoAddon.version} - - {promoAddon.types?.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')} - - - - {promoAddon.behaviorHints?.configurable && ( - handleConfigureAddon(promoAddon, PROMO_ADDON_URL)} - > - - - )} - handleAddAddon(PROMO_ADDON_URL)} - disabled={installing} - > - {installing ? ( - - ) : ( - - )} - - - - - {promoAddon.description} - - - Configure and install for full functionality. - - - - - )} + {/* Promotional Addon Section (hidden if installed) */} + {!isPromoInstalled && ( + + OFFICIAL ADDON + + + + {promoAddon.logo ? ( + + ) : ( + + + + )} + + {promoAddon.name} + + v{promoAddon.version} + + {promoAddon.types?.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')} + + + + {promoAddon.behaviorHints?.configurable && ( + handleConfigureAddon(promoAddon, PROMO_ADDON_URL)} + > + + + )} + handleAddAddon(PROMO_ADDON_URL)} + disabled={installing} + > + {installing ? ( + + ) : ( + + )} + + + + + {promoAddon.description} + + + Configure and install for full functionality. + + + + + )} - {/* Community Addons Section */} + {/* Community Addons Section */} COMMUNITY ADDONS @@ -1326,15 +1336,15 @@ const AddonsScreen = () => { ) : ( communityAddons.map((item, index) => ( - {item.manifest.logo ? ( - @@ -1357,14 +1367,14 @@ const AddonsScreen = () => { {item.manifest.behaviorHints?.configurable && ( - handleConfigureAddon(item.manifest, item.transportUrl)} > )} - handleAddAddon(item.transportUrl)} disabled={installing} @@ -1377,12 +1387,12 @@ const AddonsScreen = () => { - + - {item.manifest.description - ? (item.manifest.description.length > 100 - ? item.manifest.description.substring(0, 100) + '...' - : item.manifest.description) + {item.manifest.description + ? (item.manifest.description.length > 100 + ? item.manifest.description.substring(0, 100) + '...' + : item.manifest.description) : 'No description provided.'} @@ -1429,8 +1439,8 @@ const AddonsScreen = () => { - - { {addonDetails.name} v{addonDetails.version || '1.0.0'} - + Description {addonDetails.description || 'No description available'} - + {addonDetails.types && addonDetails.types.length > 0 && ( Supported Types @@ -1471,7 +1481,7 @@ const AddonsScreen = () => { )} - + {addonDetails.catalogs && addonDetails.catalogs.length > 0 && ( Catalogs @@ -1487,7 +1497,7 @@ const AddonsScreen = () => { )} - + { - {/* Custom Alert Modal */} - setAlertVisible(false)} - actions={alertActions} - /> - + {/* Custom Alert Modal */} + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/DebridIntegrationScreen.tsx b/src/screens/DebridIntegrationScreen.tsx new file mode 100644 index 00000000..c1365f14 --- /dev/null +++ b/src/screens/DebridIntegrationScreen.tsx @@ -0,0 +1,797 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + TextInput, + TouchableOpacity, + SafeAreaView, + StatusBar, + Platform, + Linking, + ScrollView, + KeyboardAvoidingView, + Image, + Switch, + ActivityIndicator, + RefreshControl +} from 'react-native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; +import { NavigationProp } from '@react-navigation/native'; +import { RootStackParamList } from '../navigation/AppNavigator'; +import { useTheme } from '../contexts/ThemeContext'; +import { Feather, MaterialCommunityIcons } from '@expo/vector-icons'; +import { stremioService } from '../services/stremioService'; +import { logger } from '../utils/logger'; +import CustomAlert from '../components/CustomAlert'; +import { mmkvStorage } from '../services/mmkvStorage'; +import axios from 'axios'; + +const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; +const TORBOX_STORAGE_KEY = 'torbox_debrid_config'; +const TORBOX_API_BASE = 'https://api.torbox.app/v1'; + +interface TorboxConfig { + apiKey: string; + isConnected: boolean; + isEnabled: boolean; + addonId?: string; +} + +interface TorboxUserData { + id: number; + email: string; + plan: number; + total_downloaded: number; + is_subscribed: boolean; + premium_expires_at: string | null; + base_email: string; +} + +const getPlanName = (plan: number): string => { + switch (plan) { + case 0: return 'Free'; + case 1: return 'Essential ($3/mo)'; + case 2: return 'Pro ($10/mo)'; + case 3: return 'Standard ($5/mo)'; + default: return 'Unknown'; + } +}; +const createStyles = (colors: any) => StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.darkBackground, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8, + paddingBottom: 8, + }, + backButton: { + padding: 8, + marginRight: 4, + }, + headerTitle: { + fontSize: 20, + fontWeight: '700', + color: colors.white, + letterSpacing: 0.3, + }, + content: { + flex: 1, + paddingHorizontal: 16, + }, + description: { + fontSize: 14, + color: colors.mediumEmphasis, + marginBottom: 16, + lineHeight: 20, + opacity: 0.9, + }, + statusCard: { + backgroundColor: colors.elevation2, + borderRadius: 12, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + statusRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + statusLabel: { + fontSize: 12, + fontWeight: '600', + color: colors.mediumEmphasis, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + statusValue: { + fontSize: 15, + fontWeight: '700', + color: colors.white, + }, + statusConnected: { + color: colors.success || '#4CAF50', + }, + statusDisconnected: { + color: colors.error || '#F44336', + }, + divider: { + height: 1, + backgroundColor: colors.elevation3, + marginVertical: 10, + }, + actionButton: { + borderRadius: 10, + padding: 12, + alignItems: 'center', + marginBottom: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + primaryButton: { + backgroundColor: colors.primary, + }, + dangerButton: { + backgroundColor: colors.error || '#F44336', + }, + buttonText: { + color: colors.white, + fontSize: 14, + fontWeight: '700', + letterSpacing: 0.3, + }, + inputContainer: { + marginBottom: 16, + }, + label: { + fontSize: 12, + fontWeight: '600', + color: colors.white, + marginBottom: 6, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + input: { + backgroundColor: colors.elevation2, + borderRadius: 10, + padding: 12, + color: colors.white, + fontSize: 14, + borderWidth: 1, + borderColor: colors.elevation3, + }, + connectButton: { + backgroundColor: colors.primary, + borderRadius: 10, + padding: 14, + alignItems: 'center', + marginBottom: 16, + shadowColor: colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3, + }, + connectButtonText: { + color: colors.white, + fontSize: 15, + fontWeight: '700', + letterSpacing: 0.5, + }, + disabledButton: { + opacity: 0.5, + }, + section: { + marginTop: 16, + backgroundColor: colors.elevation1, + borderRadius: 12, + padding: 16, + alignItems: 'center', + }, + sectionTitle: { + fontSize: 16, + fontWeight: '700', + color: colors.white, + marginBottom: 6, + letterSpacing: 0.3, + }, + sectionText: { + fontSize: 13, + color: colors.mediumEmphasis, + textAlign: 'center', + marginBottom: 12, + lineHeight: 18, + opacity: 0.9, + }, + subscribeButton: { + backgroundColor: colors.elevation3, + paddingHorizontal: 16, + paddingVertical: 10, + borderRadius: 8, + }, + subscribeButtonText: { + color: colors.primary, + fontWeight: '700', + fontSize: 13, + letterSpacing: 0.3, + }, + logoContainer: { + alignItems: 'center', + marginTop: 'auto', + paddingBottom: 16, + paddingTop: 16, + }, + poweredBy: { + fontSize: 10, + color: colors.mediumGray, + marginBottom: 6, + textTransform: 'uppercase', + letterSpacing: 1, + opacity: 0.6, + }, + logo: { + width: 48, + height: 48, + marginBottom: 4, + }, + logoRow: { + alignItems: 'center', + justifyContent: 'center', + marginBottom: 4, + }, + logoText: { + fontSize: 20, + fontWeight: '700', + color: colors.white, + letterSpacing: 0.5, + }, + userDataCard: { + backgroundColor: colors.elevation2, + borderRadius: 12, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + userDataRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 6, + }, + userDataLabel: { + fontSize: 13, + color: colors.mediumEmphasis, + flex: 1, + letterSpacing: 0.2, + }, + userDataValue: { + fontSize: 14, + fontWeight: '600', + color: colors.white, + flex: 1, + textAlign: 'right', + letterSpacing: 0.2, + }, + planBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 6, + alignSelf: 'flex-start', + }, + planBadgeFree: { + backgroundColor: colors.elevation3, + }, + planBadgePaid: { + backgroundColor: colors.primary + '20', + }, + planBadgeText: { + fontSize: 12, + fontWeight: '700', + letterSpacing: 0.3, + }, + planBadgeTextFree: { + color: colors.mediumEmphasis, + }, + planBadgeTextPaid: { + color: colors.primary, + }, + userDataHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + paddingBottom: 8, + borderBottomWidth: 1, + borderBottomColor: colors.elevation3, + }, + userDataTitle: { + fontSize: 15, + fontWeight: '700', + color: colors.white, + letterSpacing: 0.3, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + guideLink: { + marginBottom: 16, + alignSelf: 'flex-start', + }, + guideLinkText: { + color: colors.primary, + fontSize: 13, + fontWeight: '600', + textDecorationLine: 'underline', + }, + disclaimer: { + fontSize: 10, + color: colors.mediumGray, + textAlign: 'center', + marginTop: 8, + opacity: 0.6, + } +}); + +const DebridIntegrationScreen = () => { + const navigation = useNavigation>(); + const { currentTheme } = useTheme(); + const colors = currentTheme.colors; + const styles = createStyles(colors); + + const [apiKey, setApiKey] = useState(''); + const [loading, setLoading] = useState(false); + const [initialLoading, setInitialLoading] = useState(true); + const [config, setConfig] = useState(null); + const [userData, setUserData] = useState(null); + const [userDataLoading, setUserDataLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + + // Alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); + + const loadConfig = useCallback(async () => { + try { + const storedConfig = await mmkvStorage.getItem(TORBOX_STORAGE_KEY); + if (storedConfig) { + const parsedConfig = JSON.parse(storedConfig); + setConfig(parsedConfig); + + // Check if addon is actually installed + const addons = await stremioService.getInstalledAddonsAsync(); + const torboxAddon = addons.find(addon => + addon.id?.includes('torbox') || + addon.url?.includes('torbox') || + (addon as any).transport?.includes('torbox') + ); + + if (torboxAddon && !parsedConfig.isConnected) { + // Update config if addon exists but config says not connected + const updatedConfig = { ...parsedConfig, isConnected: true, addonId: torboxAddon.id }; + setConfig(updatedConfig); + await mmkvStorage.setItem(TORBOX_STORAGE_KEY, JSON.stringify(updatedConfig)); + } else if (!torboxAddon && parsedConfig.isConnected) { + // Update config if addon doesn't exist but config says connected + const updatedConfig = { ...parsedConfig, isConnected: false, addonId: undefined }; + setConfig(updatedConfig); + await mmkvStorage.setItem(TORBOX_STORAGE_KEY, JSON.stringify(updatedConfig)); + } + } + } catch (error) { + logger.error('Failed to load Torbox config:', error); + } finally { + setInitialLoading(false); + } + }, []); + + const fetchUserData = useCallback(async () => { + if (!config?.apiKey || !config?.isConnected) return; + + setUserDataLoading(true); + try { + const response = await axios.get(`${TORBOX_API_BASE}/api/user/me`, { + headers: { + 'Authorization': `Bearer ${config.apiKey}` + }, + params: { + settings: false + } + }); + + if (response.data.success && response.data.data) { + setUserData(response.data.data); + } + } catch (error) { + logger.error('Failed to fetch Torbox user data:', error); + // Don't show error to user, just log it + } finally { + setUserDataLoading(false); + } + }, [config]); + + useFocusEffect( + useCallback(() => { + loadConfig(); + }, [loadConfig]) + ); + + useEffect(() => { + if (config?.isConnected) { + fetchUserData(); + } + }, [config?.isConnected, fetchUserData]); + + const onRefresh = useCallback(async () => { + setRefreshing(true); + await Promise.all([loadConfig(), fetchUserData()]); + setRefreshing(false); + }, [loadConfig, fetchUserData]); + + const handleConnect = async () => { + if (!apiKey.trim()) { + setAlertTitle('Error'); + setAlertMessage('Please enter a valid API Key'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + return; + } + + setLoading(true); + try { + const manifestUrl = `https://stremio.torbox.app/${apiKey.trim()}/manifest.json`; + + // Install the addon using stremioService + await stremioService.installAddon(manifestUrl); + + // Get the installed addon ID + const addons = await stremioService.getInstalledAddonsAsync(); + const torboxAddon = addons.find(addon => + addon.id?.includes('torbox') || + addon.url?.includes('torbox') || + (addon as any).transport?.includes('torbox') + ); + + // Save config + const newConfig: TorboxConfig = { + apiKey: apiKey.trim(), + isConnected: true, + isEnabled: true, + addonId: torboxAddon?.id + }; + await mmkvStorage.setItem(TORBOX_STORAGE_KEY, JSON.stringify(newConfig)); + setConfig(newConfig); + setApiKey(''); + + setAlertTitle('Success'); + setAlertMessage('Torbox addon connected successfully!'); + setAlertActions([{ + label: 'OK', + onPress: () => setAlertVisible(false) + }]); + setAlertVisible(true); + } catch (error) { + logger.error('Failed to install Torbox addon:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to connect addon. Please check your API Key and try again.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + } finally { + setLoading(false); + } + }; + + const handleToggleEnabled = async (enabled: boolean) => { + if (!config) return; + + try { + const updatedConfig = { ...config, isEnabled: enabled }; + await mmkvStorage.setItem(TORBOX_STORAGE_KEY, JSON.stringify(updatedConfig)); + setConfig(updatedConfig); + + // Note: Since we can't disable/enable addons in the current stremioService, + // we'll just track the state. The addon filtering will happen in AddonsScreen + } catch (error) { + logger.error('Failed to toggle Torbox addon:', error); + } + }; + + const handleDisconnect = async () => { + setAlertTitle('Disconnect Torbox'); + setAlertMessage('Are you sure you want to disconnect Torbox? This will remove the addon and clear your saved API key.'); + setAlertActions([ + { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } }, + { + label: 'Disconnect', + onPress: async () => { + setAlertVisible(false); + setLoading(true); + try { + // Find and remove the torbox addon + const addons = await stremioService.getInstalledAddonsAsync(); + const torboxAddon = addons.find(addon => + addon.id?.includes('torbox') || + addon.url?.includes('torbox') || + (addon as any).transport?.includes('torbox') + ); + + if (torboxAddon) { + await stremioService.removeAddon(torboxAddon.id); + } + + // Clear config + await mmkvStorage.removeItem(TORBOX_STORAGE_KEY); + setConfig(null); + + setAlertTitle('Success'); + setAlertMessage('Torbox disconnected successfully'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + } catch (error) { + logger.error('Failed to disconnect Torbox:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to disconnect Torbox'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + } finally { + setLoading(false); + } + }, + style: { color: colors.error || '#F44336' } + } + ]); + setAlertVisible(true); + }; + + const openSubscription = () => { + Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7'); + }; + + if (initialLoading) { + return ( + + + + + + + ); + } + + return ( + + + + + navigation.goBack()} + style={styles.backButton} + > + + + Debrid Integration + + + + + } + > + {config?.isConnected ? ( + // Connected state + <> + + + Status + Connected + + + + + + Enable Addon + + + + + + + {loading ? 'Disconnecting...' : 'Disconnect & Remove'} + + + + {/* User Data Card */} + {userData && ( + + + Account Information + {userDataLoading && ( + + )} + + + + Email + + {userData.base_email || userData.email} + + + + + Plan + + + {getPlanName(userData.plan)} + + + + + + Status + + {userData.is_subscribed ? 'Active' : 'Free'} + + + + {userData.premium_expires_at && ( + + Expires + + {new Date(userData.premium_expires_at).toLocaleDateString()} + + + )} + + + Downloaded + + {(userData.total_downloaded / (1024 * 1024 * 1024)).toFixed(2)} GB + + + + )} + + + ✓ Connected to TorBox + + Your TorBox addon is active and providing premium streams.{config.isEnabled ? '' : ' (Currently disabled)'} + + + + + Configure Addon + + Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings. + + Linking.openURL('https://torbox.app/settings?section=integration-settings')} + > + Open Settings + + + + ) : ( + // Not connected state + <> + + Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience. + + + Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')} style={styles.guideLink}> + What is a Debrid Service? + + + + Torbox API Key + + + + + + {loading ? 'Connecting...' : 'Connect & Install'} + + + + + Unlock Premium Speeds + + Get a Torbox subscription to access cached high-quality streams with zero buffering. + + + Get Subscription + + + + )} + + + Powered by + + + TorBox + + Nuvio is not affiliated with Torbox in any way. + + + + + setAlertVisible(false)} + /> + + ); +}; + +export default DebridIntegrationScreen; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index e068a20f..3d93e035 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -67,9 +67,9 @@ interface SettingsCardProps { const SettingsCard: React.FC = ({ children, title, isTablet = false }) => { const { currentTheme } = useTheme(); - + return ( - = ({ isTablet = false }) => { const { currentTheme } = useTheme(); - + return ( - = ({ > = ({ {customIcon ? ( customIcon ) : ( - )} @@ -161,7 +161,7 @@ const SettingItem: React.FC = ({ {description && ( @@ -224,16 +224,16 @@ const Sidebar: React.FC = ({ selectedCategory, onCategorySelect, c name={category.icon as any} size={22} color={ - selectedCategory === category.id - ? currentTheme.colors.primary + selectedCategory === category.id + ? currentTheme.colors.primary : currentTheme.colors.mediumEmphasis } /> @@ -263,7 +263,7 @@ const SettingsScreen: React.FC = () => { ) => { setAlertTitle(title); setAlertMessage(message); - setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); setAlertVisible(true); }; @@ -274,7 +274,7 @@ const SettingsScreen: React.FC = () => { try { const flag = await mmkvStorage.getItem('@update_badge_pending'); if (mounted) setHasUpdateBadge(flag === 'true'); - } catch {} + } catch { } })(); return () => { mounted = false; }; }, []); @@ -283,7 +283,7 @@ const SettingsScreen: React.FC = () => { const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - + // Tablet-specific state const [selectedCategory, setSelectedCategory] = useState('account'); @@ -310,7 +310,7 @@ const SettingsScreen: React.FC = () => { } refreshAuthStatus(); }); - + return unsubscribe; }, [navigation, isAuthenticated, userProfile, refreshAuthStatus]); @@ -320,7 +320,7 @@ const SettingsScreen: React.FC = () => { const addons = await stremioService.getInstalledAddonsAsync(); setAddonCount(addons.length); setInitialLoadComplete(true); - + // Count total available catalogs let totalCatalogs = 0; addons.forEach(addon => { @@ -328,7 +328,7 @@ const SettingsScreen: React.FC = () => { totalCatalogs += addon.catalogs.length; } }); - + // Load saved catalog settings const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings'); if (catalogSettingsJson) { @@ -358,7 +358,7 @@ const SettingsScreen: React.FC = () => { setTotalDownloads(downloads); setDisplayDownloads(downloads); } - + } catch (error) { if (__DEV__) console.error('Error loading settings data:', error); } @@ -382,7 +382,7 @@ const SettingsScreen: React.FC = () => { useEffect(() => { // Only poll when viewing the About section (where downloads counter is shown) const shouldPoll = isTablet ? selectedCategory === 'about' : true; - + if (!shouldPoll) return; const pollInterval = setInterval(async () => { @@ -414,11 +414,11 @@ const SettingsScreen: React.FC = () => { const now = Date.now(); const elapsed = now - startTime; const progress = Math.min(elapsed / duration, 1); - + // Ease out quad for smooth deceleration const easeProgress = 1 - Math.pow(1 - progress, 2); const current = Math.floor(start + (end - start) * easeProgress); - + setDisplayDownloads(current); if (progress < 1) { @@ -437,7 +437,7 @@ const SettingsScreen: React.FC = () => { 'Reset Settings', 'Are you sure you want to reset all settings to default values?', [ - { label: 'Cancel', onPress: () => {} }, + { label: 'Cancel', onPress: () => { } }, { label: 'Reset', onPress: () => { @@ -455,7 +455,7 @@ const SettingsScreen: React.FC = () => { 'Clear MDBList Cache', 'Are you sure you want to clear all cached MDBList data? This cannot be undone.', [ - { label: 'Cancel', onPress: () => {} }, + { label: 'Cancel', onPress: () => { } }, { label: 'Clear', onPress: async () => { @@ -483,9 +483,9 @@ const SettingsScreen: React.FC = () => { ); const ChevronRight = () => ( - ); @@ -527,6 +527,14 @@ const SettingsScreen: React.FC = () => { onPress={() => navigation.navigate('Addons')} isTablet={isTablet} /> + navigation.navigate('DebridIntegration')} + isTablet={isTablet} + /> { { 'Clear All Data', 'This will reset all settings and clear all cached data. Are you sure?', [ - { label: 'Cancel', onPress: () => {} }, + { label: 'Cancel', onPress: () => { } }, { label: 'Clear', onPress: async () => { @@ -824,7 +832,7 @@ const SettingsScreen: React.FC = () => { badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined} onPress={async () => { if (Platform.OS === 'android') { - try { await mmkvStorage.removeItem('@update_badge_pending'); } catch {} + try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } setHasUpdateBadge(false); } navigation.navigate('Update'); @@ -861,20 +869,20 @@ const SettingsScreen: React.FC = () => { categories={visibleCategories} extraTopPadding={tabletNavOffset} /> - + - {renderCategoryContent(selectedCategory)} - + {selectedCategory === 'about' && ( <> {displayDownloads !== null && ( @@ -887,9 +895,9 @@ const SettingsScreen: React.FC = () => { )} - + - + Made with ❤️ by Tapframe and Friends @@ -906,7 +914,7 @@ const SettingsScreen: React.FC = () => { style={styles.discordLogo} resizeMode={FastImage.resizeMode.contain} /> - + Join Discord @@ -958,7 +966,7 @@ const SettingsScreen: React.FC = () => { - { style={styles.discordLogo} resizeMode={FastImage.resizeMode.contain} /> - + Join Discord @@ -1074,7 +1082,7 @@ const styles = StyleSheet.create({ width: '100%', paddingBottom: 90, }, - + // Tablet-specific styles tabletContainer: { flex: 1, @@ -1128,7 +1136,7 @@ const styles = StyleSheet.create({ tabletScrollContent: { paddingBottom: 32, }, - + // Common card styles cardContainer: { width: '100%',