debrid integration. Torbox

This commit is contained in:
tapframe 2025-11-26 01:01:00 +05:30
parent bbf035ebae
commit 6d1ba14ab4
4 changed files with 1210 additions and 378 deletions

View file

@ -70,6 +70,7 @@ import BackdropGalleryScreen from '../screens/BackdropGalleryScreen';
import BackupScreen from '../screens/BackupScreen'; import BackupScreen from '../screens/BackupScreen';
import ContinueWatchingSettingsScreen from '../screens/ContinueWatchingSettingsScreen'; import ContinueWatchingSettingsScreen from '../screens/ContinueWatchingSettingsScreen';
import ContributorsScreen from '../screens/ContributorsScreen'; import ContributorsScreen from '../screens/ContributorsScreen';
import DebridIntegrationScreen from '../screens/DebridIntegrationScreen';
// Stack navigator types // Stack navigator types
export type RootStackParamList = { export type RootStackParamList = {
@ -82,27 +83,27 @@ export type RootStackParamList = {
Update: undefined; Update: undefined;
Search: undefined; Search: undefined;
Calendar: undefined; Calendar: undefined;
Metadata: { Metadata: {
id: string; id: string;
type: string; type: string;
episodeId?: string; episodeId?: string;
addonId?: string; addonId?: string;
}; };
Streams: { Streams: {
id: string; id: string;
type: string; type: string;
episodeId?: string; episodeId?: string;
episodeThumbnail?: string; episodeThumbnail?: string;
fromPlayer?: boolean; fromPlayer?: boolean;
}; };
PlayerIOS: { PlayerIOS: {
uri: string; uri: string;
title?: string; title?: string;
season?: number; season?: number;
episode?: number; episode?: number;
episodeTitle?: string; episodeTitle?: string;
quality?: string; quality?: string;
year?: number; year?: number;
streamProvider?: string; streamProvider?: string;
streamName?: string; streamName?: string;
headers?: { [key: string]: string }; headers?: { [key: string]: string };
@ -116,14 +117,14 @@ export type RootStackParamList = {
videoType?: string; videoType?: string;
groupedEpisodes?: { [seasonNumber: number]: any[] }; groupedEpisodes?: { [seasonNumber: number]: any[] };
}; };
PlayerAndroid: { PlayerAndroid: {
uri: string; uri: string;
title?: string; title?: string;
season?: number; season?: number;
episode?: number; episode?: number;
episodeTitle?: string; episodeTitle?: string;
quality?: string; quality?: string;
year?: number; year?: number;
streamProvider?: string; streamProvider?: string;
streamName?: string; streamName?: string;
headers?: { [key: string]: string }; headers?: { [key: string]: string };
@ -180,6 +181,7 @@ export type RootStackParamList = {
}; };
ContinueWatchingSettings: undefined; ContinueWatchingSettings: undefined;
Contributors: undefined; Contributors: undefined;
DebridIntegration: undefined;
}; };
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>; export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
@ -376,9 +378,9 @@ export const CustomNavigationDarkTheme: Theme = {
type IconNameType = string; type IconNameType = string;
// Add TabIcon component // Add TabIcon component
const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material' }: { const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material' }: {
focused: boolean; focused: boolean;
color: string; color: string;
iconName: IconNameType; iconName: IconNameType;
iconLibrary?: 'material' | 'feather' | 'ionicons'; iconLibrary?: 'material' | 'feather' | 'ionicons';
}) => { }) => {
@ -403,28 +405,28 @@ const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material'
})(); })();
return ( return (
<Animated.View style={{ <Animated.View style={{
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
transform: [{ scale: scaleAnim }] transform: [{ scale: scaleAnim }]
}}> }}>
{iconLibrary === 'feather' ? ( {iconLibrary === 'feather' ? (
<Feather <Feather
name={finalIconName as any} name={finalIconName as any}
size={24} size={24}
color={color} color={color}
/> />
) : iconLibrary === 'ionicons' ? ( ) : iconLibrary === 'ionicons' ? (
<Ionicons <Ionicons
name={finalIconName as any} name={finalIconName as any}
size={24} size={24}
color={color} color={color}
/> />
) : ( ) : (
<MaterialCommunityIcons <MaterialCommunityIcons
name={finalIconName as any} name={finalIconName as any}
size={24} size={24}
color={color} color={color}
/> />
)} )}
</Animated.View> </Animated.View>
@ -432,17 +434,17 @@ const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material'
}); });
// Update the TabScreenWrapper component with fixed layout dimensions // 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')); const [dimensions, setDimensions] = useState(Dimensions.get('window'));
useEffect(() => { useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => { const subscription = Dimensions.addEventListener('change', ({ window }) => {
setDimensions(window); setDimensions(window);
}); });
return () => subscription?.remove(); return () => subscription?.remove();
}, []); }, []);
const isTablet = useMemo(() => { const isTablet = useMemo(() => {
const { width, height } = dimensions; const { width, height } = dimensions;
const smallestDimension = Math.min(width, height); const smallestDimension = Math.min(width, height);
@ -456,35 +458,35 @@ const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) =
StatusBar.setTranslucent(true); StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent'); StatusBar.setBackgroundColor('transparent');
}; };
applyStatusBarConfig(); applyStatusBarConfig();
// Apply status bar config on every focus // Apply status bar config on every focus
const subscription = Platform.OS === 'android' const subscription = Platform.OS === 'android'
? AppState.addEventListener('change', (state) => { ? AppState.addEventListener('change', (state) => {
if (state === 'active') { if (state === 'active') {
applyStatusBarConfig(); applyStatusBarConfig();
} }
}) })
: { remove: () => {} }; : { remove: () => { } };
return () => { return () => {
subscription.remove(); subscription.remove();
}; };
}, []); }, []);
return ( return (
<View style={{ <View style={{
flex: 1, flex: 1,
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
// Lock the layout to prevent shifts // Lock the layout to prevent shifts
position: 'relative', position: 'relative',
overflow: 'hidden' overflow: 'hidden'
}}> }}>
{/* Reserve consistent space for the header area on all screens */} {/* Reserve consistent space for the header area on all screens */}
<View style={{ <View style={{
height: isTablet ? (insets.top + 64) : (Platform.OS === 'android' ? 80 : 60), height: isTablet ? (insets.top + 64) : (Platform.OS === 'android' ? 80 : 60),
width: '100%', width: '100%',
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@ -498,7 +500,7 @@ const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) =
}; };
// Add this component to wrap each screen in the tab navigator // Add this component to wrap each screen in the tab navigator
const WrappedScreen: React.FC<{Screen: React.ComponentType<any>}> = ({ Screen }) => { const WrappedScreen: React.FC<{ Screen: React.ComponentType<any> }> = ({ Screen }) => {
return ( return (
<TabScreenWrapper> <TabScreenWrapper>
<Screen /> <Screen />
@ -514,12 +516,12 @@ const MainTabs = () => {
const { settings: appSettings } = useSettingsHook(); const { settings: appSettings } = useSettingsHook();
const [hasUpdateBadge, setHasUpdateBadge] = React.useState(false); const [hasUpdateBadge, setHasUpdateBadge] = React.useState(false);
const [dimensions, setDimensions] = useState(Dimensions.get('window')); const [dimensions, setDimensions] = useState(Dimensions.get('window'));
useEffect(() => { useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => { const subscription = Dimensions.addEventListener('change', ({ window }) => {
setDimensions(window); setDimensions(window);
}); });
return () => subscription?.remove(); return () => subscription?.remove();
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@ -529,7 +531,7 @@ const MainTabs = () => {
try { try {
const flag = await mmkvStorage.getItem('@update_badge_pending'); const flag = await mmkvStorage.getItem('@update_badge_pending');
if (mounted) setHasUpdateBadge(flag === 'true'); if (mounted) setHasUpdateBadge(flag === 'true');
} catch {} } catch { }
}; };
load(); load();
// Fast poll initially for quick badge appearance, then slow down // Fast poll initially for quick badge appearance, then slow down
@ -575,7 +577,7 @@ const MainTabs = () => {
}, [hidden, headerAnim]); }, [hidden, headerAnim]);
const translateY = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [0, -70] }); const translateY = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [0, -70] });
const fade = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }); const fade = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0] });
const renderTabBar = (props: BottomTabBarProps) => { const renderTabBar = (props: BottomTabBarProps) => {
// Hide tab bar when home is loading // Hide tab bar when home is loading
if (isHomeLoading) { if (isHomeLoading) {
@ -590,18 +592,18 @@ const MainTabs = () => {
// Top floating, text-only pill nav for tablets // Top floating, text-only pill nav for tablets
return ( return (
<Animated.View <Animated.View
style={[{ style={[{
position: 'absolute', position: 'absolute',
top: insets.top + 12, top: insets.top + 12,
left: 0, left: 0,
right: 0, right: 0,
alignItems: 'center', alignItems: 'center',
backgroundColor: 'transparent', backgroundColor: 'transparent',
zIndex: 100, zIndex: 100,
}, shouldKeepFixed ? {} : { }, shouldKeepFixed ? {} : {
transform: [{ translateY }], transform: [{ translateY }],
opacity: fade, opacity: fade,
}]}> }]}>
<View style={{ <View style={{
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -645,8 +647,8 @@ const MainTabs = () => {
options.tabBarLabel !== undefined options.tabBarLabel !== undefined
? options.tabBarLabel ? options.tabBarLabel
: options.title !== undefined : options.title !== undefined
? options.title ? options.title
: route.name; : route.name;
const isFocused = props.state.index === index; const isFocused = props.state.index === index;
@ -692,10 +694,10 @@ const MainTabs = () => {
// Default bottom tab for phones // Default bottom tab for phones
return ( return (
<View style={{ <View style={{
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
height: Platform.OS === 'android' ? 70 + insets.bottom : 85 + insets.bottom, height: Platform.OS === 'android' ? 70 + insets.bottom : 85 + insets.bottom,
backgroundColor: 'transparent', backgroundColor: 'transparent',
@ -759,8 +761,8 @@ const MainTabs = () => {
options.tabBarLabel !== undefined options.tabBarLabel !== undefined
? options.tabBarLabel ? options.tabBarLabel
: options.title !== undefined : options.title !== undefined
? options.title ? options.title
: route.name; : route.name;
const isFocused = props.state.index === index; const isFocused = props.state.index === index;
@ -813,9 +815,9 @@ const MainTabs = () => {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}} }}
> >
<TabIcon <TabIcon
focused={isFocused} focused={isFocused}
color={isFocused ? currentTheme.colors.primary : currentTheme.colors.white} color={isFocused ? currentTheme.colors.primary : currentTheme.colors.white}
iconName={iconName} iconName={iconName}
iconLibrary={iconLibrary} iconLibrary={iconLibrary}
/> />
@ -838,7 +840,7 @@ const MainTabs = () => {
</View> </View>
); );
}; };
// iOS: Use native bottom tabs (@bottom-tabs/react-navigation) // iOS: Use native bottom tabs (@bottom-tabs/react-navigation)
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
// Dynamically require to avoid impacting Android bundle // Dynamically require to avoid impacting Android bundle
@ -923,7 +925,7 @@ const MainTabs = () => {
barStyle="light-content" barStyle="light-content"
backgroundColor="transparent" backgroundColor="transparent"
/> />
<Tab.Navigator <Tab.Navigator
tabBar={renderTabBar} tabBar={renderTabBar}
screenOptions={({ route, navigation, theme }) => ({ screenOptions={({ route, navigation, theme }) => ({
@ -1059,7 +1061,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { user, loading } = useAccount(); const { user, loading } = useAccount();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
// Handle Android-specific optimizations // Handle Android-specific optimizations
useEffect(() => { useEffect(() => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
@ -1070,13 +1072,13 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
} catch (error) { } catch (error) {
console.log('Immersive mode error:', error); console.log('Immersive mode error:', error);
} }
// Ensure consistent background color for Android // Ensure consistent background color for Android
StatusBar.setBackgroundColor('transparent', true); StatusBar.setBackgroundColor('transparent', true);
StatusBar.setTranslucent(true); StatusBar.setTranslucent(true);
} }
}, []); }, []);
return ( return (
<SafeAreaProvider> <SafeAreaProvider>
<StatusBar <StatusBar
@ -1085,8 +1087,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
barStyle="light-content" barStyle="light-content"
/> />
<PaperProvider theme={CustomDarkTheme}> <PaperProvider theme={CustomDarkTheme}>
<View style={{ <View style={{
flex: 1, flex: 1,
backgroundColor: currentTheme.colors.darkBackground, backgroundColor: currentTheme.colors.darkBackground,
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
// Prevent white flashes on Android // Prevent white flashes on Android
@ -1135,8 +1137,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
contentStyle: { backgroundColor: currentTheme.colors.darkBackground }, contentStyle: { backgroundColor: currentTheme.colors.darkBackground },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Onboarding" name="Onboarding"
component={OnboardingScreen} component={OnboardingScreen}
options={{ options={{
headerShown: false, headerShown: false,
@ -1147,9 +1149,9 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="MainTabs" name="MainTabs"
component={MainTabs as any} component={MainTabs as any}
options={{ options={{
contentStyle: { contentStyle: {
backgroundColor: currentTheme.colors.darkBackground, backgroundColor: currentTheme.colors.darkBackground,
@ -1168,11 +1170,11 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Metadata" name="Metadata"
component={MetadataScreen} component={MetadataScreen}
options={{ options={{
headerShown: false, headerShown: false,
animation: Platform.OS === 'android' ? 'none' : 'fade', animation: Platform.OS === 'android' ? 'none' : 'fade',
animationDuration: Platform.OS === 'android' ? 0 : 300, animationDuration: Platform.OS === 'android' ? 0 : 300,
...(Platform.OS === 'ios' && { ...(Platform.OS === 'ios' && {
@ -1186,9 +1188,9 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Streams" name="Streams"
component={StreamsScreen as any} component={StreamsScreen as any}
options={{ options={{
headerShown: false, headerShown: false,
animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none', animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none',
@ -1203,10 +1205,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
freezeOnBlur: true, freezeOnBlur: true,
}} }}
/> />
<Stack.Screen <Stack.Screen
name="PlayerIOS" name="PlayerIOS"
component={KSPlayerCore as any} component={KSPlayerCore as any}
options={{ options={{
animation: 'default', animation: 'default',
animationDuration: 0, animationDuration: 0,
// Force fullscreen presentation on iPad // Force fullscreen presentation on iPad
@ -1225,10 +1227,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
freezeOnBlur: true, freezeOnBlur: true,
}} }}
/> />
<Stack.Screen <Stack.Screen
name="PlayerAndroid" name="PlayerAndroid"
component={AndroidVideoPlayer as any} component={AndroidVideoPlayer as any}
options={{ options={{
animation: 'none', animation: 'none',
animationDuration: 0, animationDuration: 0,
presentation: 'card', presentation: 'card',
@ -1243,10 +1245,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
freezeOnBlur: true, freezeOnBlur: true,
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Catalog" name="Catalog"
component={CatalogScreen as any} component={CatalogScreen as any}
options={{ options={{
animation: 'slide_from_right', animation: 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 250 : 300, animationDuration: Platform.OS === 'android' ? 250 : 300,
contentStyle: { contentStyle: {
@ -1254,10 +1256,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Addons" name="Addons"
component={AddonsScreen as any} component={AddonsScreen as any}
options={{ options={{
animation: 'slide_from_right', animation: 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 250 : 300, animationDuration: Platform.OS === 'android' ? 250 : 300,
contentStyle: { contentStyle: {
@ -1265,10 +1267,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Search" name="Search"
component={SearchScreen as any} component={SearchScreen as any}
options={{ options={{
animation: Platform.OS === 'android' ? 'none' : 'fade', animation: Platform.OS === 'android' ? 'none' : 'fade',
animationDuration: Platform.OS === 'android' ? 0 : 350, animationDuration: Platform.OS === 'android' ? 0 : 350,
gestureEnabled: true, gestureEnabled: true,
@ -1278,10 +1280,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="CatalogSettings" name="CatalogSettings"
component={CatalogSettingsScreen as any} component={CatalogSettingsScreen as any}
options={{ options={{
animation: 'slide_from_right', animation: 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 250 : 300, animationDuration: Platform.OS === 'android' ? 250 : 300,
contentStyle: { contentStyle: {
@ -1289,8 +1291,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="HomeScreenSettings" name="HomeScreenSettings"
component={HomeScreenSettings} component={HomeScreenSettings}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default', animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
@ -1304,8 +1306,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="ContinueWatchingSettings" name="ContinueWatchingSettings"
component={ContinueWatchingSettingsScreen} component={ContinueWatchingSettingsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default', animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
@ -1319,8 +1321,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Contributors" name="Contributors"
component={ContributorsScreen} component={ContributorsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default', animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
@ -1334,8 +1336,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="HeroCatalogs" name="HeroCatalogs"
component={HeroCatalogsScreen} component={HeroCatalogsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default', animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
@ -1349,8 +1351,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="ShowRatings" name="ShowRatings"
component={ShowRatingsScreen} component={ShowRatingsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'fade_from_bottom' : 'fade', animation: Platform.OS === 'android' ? 'fade_from_bottom' : 'fade',
@ -1364,10 +1366,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Calendar" name="Calendar"
component={CalendarScreen as any} component={CalendarScreen as any}
options={{ options={{
animation: 'slide_from_right', animation: 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 250 : 300, animationDuration: Platform.OS === 'android' ? 250 : 300,
contentStyle: { contentStyle: {
@ -1375,10 +1377,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="NotificationSettings" name="NotificationSettings"
component={NotificationSettingsScreen as any} component={NotificationSettingsScreen as any}
options={{ options={{
animation: 'slide_from_right', animation: 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 250 : 300, animationDuration: Platform.OS === 'android' ? 250 : 300,
contentStyle: { contentStyle: {
@ -1386,8 +1388,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="MDBListSettings" name="MDBListSettings"
component={MDBListSettingsScreen} component={MDBListSettingsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
@ -1401,8 +1403,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="TMDBSettings" name="TMDBSettings"
component={TMDBSettingsScreen} component={TMDBSettingsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
@ -1416,8 +1418,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="TraktSettings" name="TraktSettings"
component={TraktSettingsScreen} component={TraktSettingsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
@ -1431,8 +1433,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="PlayerSettings" name="PlayerSettings"
component={PlayerSettingsScreen} component={PlayerSettingsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
@ -1446,8 +1448,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="ThemeSettings" name="ThemeSettings"
component={ThemeScreen} component={ThemeScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
@ -1461,8 +1463,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="ScraperSettings" name="ScraperSettings"
component={PluginsScreen} component={PluginsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
@ -1476,8 +1478,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="CastMovies" name="CastMovies"
component={CastMoviesScreen} component={CastMoviesScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
@ -1491,8 +1493,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen <Stack.Screen
name="Update" name="Update"
component={UpdateScreen} component={UpdateScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right', animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
@ -1563,6 +1565,21 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}, },
}} }}
/> />
<Stack.Screen
name="DebridIntegration"
component={DebridIntegrationScreen}
options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 250 : 300,
presentation: 'card',
gestureEnabled: true,
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
</Stack.Navigator> </Stack.Navigator>
</View> </View>
</PaperProvider> </PaperProvider>
@ -1571,8 +1588,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
}; };
const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => ( const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => (
<PostHogProvider <PostHogProvider
apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C" apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C"
options={{ options={{
host: "https://us.i.posthog.com", host: "https://us.i.posthog.com",
}} }}

View file

@ -524,8 +524,8 @@ const createStyles = (colors: any) => StyleSheet.create({
opacity: 0.8, opacity: 0.8,
}, },
communityAddonVersion: { communityAddonVersion: {
fontSize: 12, fontSize: 12,
color: colors.lightGray, color: colors.lightGray,
}, },
communityAddonDot: { communityAddonDot: {
fontSize: 12, fontSize: 12,
@ -533,18 +533,18 @@ const createStyles = (colors: any) => StyleSheet.create({
marginHorizontal: 5, marginHorizontal: 5,
}, },
communityAddonCategory: { communityAddonCategory: {
fontSize: 12, fontSize: 12,
color: colors.lightGray, color: colors.lightGray,
flexShrink: 1, flexShrink: 1,
}, },
separator: { separator: {
height: 10, height: 10,
}, },
sectionSeparator: { sectionSeparator: {
height: 1, height: 1,
backgroundColor: colors.border, backgroundColor: colors.border,
marginHorizontal: 20, marginHorizontal: 20,
marginVertical: 20, marginVertical: 20,
}, },
emptyMessage: { emptyMessage: {
textAlign: 'center', textAlign: 'center',
@ -660,16 +660,26 @@ const AddonsScreen = () => {
setLoading(true); setLoading(true);
// Use the regular method without disabled state // Use the regular method without disabled state
const installedAddons = await stremioService.getInstalledAddonsAsync(); 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 // Count catalogs
let totalCatalogs = 0; let totalCatalogs = 0;
installedAddons.forEach(addon => { filteredAddons.forEach(addon => {
if (addon.catalogs && addon.catalogs.length > 0) { if (addon.catalogs && addon.catalogs.length > 0) {
totalCatalogs += addon.catalogs.length; totalCatalogs += addon.catalogs.length;
} }
}); });
// Get catalog settings to determine enabled count // Get catalog settings to determine enabled count
const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings'); const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings');
if (catalogSettingsJson) { if (catalogSettingsJson) {
@ -682,11 +692,11 @@ const AddonsScreen = () => {
setCatalogCount(totalCatalogs); setCatalogCount(totalCatalogs);
} }
} catch (error) { } catch (error) {
logger.error('Failed to load addons:', error); logger.error('Failed to load addons:', error);
setAlertTitle('Error'); setAlertTitle('Error');
setAlertMessage('Failed to load addons'); setAlertMessage('Failed to load addons');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true); setAlertVisible(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -706,9 +716,9 @@ const AddonsScreen = () => {
setCommunityAddons(validAddons); setCommunityAddons(validAddons);
} catch (error) { } catch (error) {
logger.error('Failed to load community addons:', error); logger.error('Failed to load community addons:', error);
setCommunityError('Failed to load community addons. Please try again later.'); setCommunityError('Failed to load community addons. Please try again later.');
setCommunityAddons([]); setCommunityAddons([]);
} finally { } finally {
setCommunityLoading(false); setCommunityLoading(false);
} }
@ -756,16 +766,16 @@ const AddonsScreen = () => {
setShowConfirmModal(false); setShowConfirmModal(false);
setAddonDetails(null); setAddonDetails(null);
loadAddons(); loadAddons();
setAlertTitle('Success'); setAlertTitle('Success');
setAlertMessage('Addon installed successfully'); setAlertMessage('Addon installed successfully');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true); setAlertVisible(true);
} catch (error) { } catch (error) {
logger.error('Failed to install addon:', error); logger.error('Failed to install addon:', error);
setAlertTitle('Error'); setAlertTitle('Error');
setAlertMessage('Failed to install addon'); setAlertMessage('Failed to install addon');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true); setAlertVisible(true);
} finally { } finally {
setInstalling(false); setInstalling(false);
} }
@ -813,13 +823,13 @@ const AddonsScreen = () => {
const handleConfigureAddon = (addon: ExtendedManifest, transportUrl?: string) => { const handleConfigureAddon = (addon: ExtendedManifest, transportUrl?: string) => {
// Try different ways to get the configuration URL // Try different ways to get the configuration URL
let configUrl = ''; let configUrl = '';
// Debug log the addon data to help troubleshoot // Debug log the addon data to help troubleshoot
logger.info(`Configure addon: ${addon.name}, ID: ${addon.id}`); logger.info(`Configure addon: ${addon.name}, ID: ${addon.id}`);
if (transportUrl) { if (transportUrl) {
logger.info(`TransportUrl provided: ${transportUrl}`); logger.info(`TransportUrl provided: ${transportUrl}`);
} }
// First check if the addon has a configurationURL directly // First check if the addon has a configurationURL directly
if (addon.behaviorHints?.configurationURL) { if (addon.behaviorHints?.configurationURL) {
configUrl = addon.behaviorHints.configurationURL; configUrl = addon.behaviorHints.configurationURL;
@ -861,7 +871,7 @@ const AddonsScreen = () => {
const baseUrl = addon.id.replace(/\/[^\/]+\.json$/, '/'); const baseUrl = addon.id.replace(/\/[^\/]+\.json$/, '/');
configUrl = `${baseUrl}configure`; configUrl = `${baseUrl}configure`;
logger.info(`Using addon.id as HTTP URL: ${configUrl}`); logger.info(`Using addon.id as HTTP URL: ${configUrl}`);
} }
// If the ID uses stremio:// protocol but contains http URL (common format) // If the ID uses stremio:// protocol but contains http URL (common format)
else if (addon.id && (addon.id.includes('https://') || addon.id.includes('http://'))) { else if (addon.id && (addon.id.includes('https://') || addon.id.includes('http://'))) {
// Extract the HTTP URL using a more flexible regex // Extract the HTTP URL using a more flexible regex
@ -874,7 +884,7 @@ const AddonsScreen = () => {
logger.info(`Extracted HTTP URL from stremio:// format: ${configUrl}`); logger.info(`Extracted HTTP URL from stremio:// format: ${configUrl}`);
} }
} }
// Special case for common addon format like stremio://addon.stremio.com/... // Special case for common addon format like stremio://addon.stremio.com/...
if (!configUrl && addon.id && addon.id.startsWith('stremio://')) { if (!configUrl && addon.id && addon.id.startsWith('stremio://')) {
// Try to convert stremio://domain.com/... to https://domain.com/... // 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}`); logger.info(`Converted stremio:// protocol to https:// for config URL: ${configUrl}`);
} }
} }
// Use transport property if available (some addons include this) // Use transport property if available (some addons include this)
if (!configUrl && addon.transport && typeof addon.transport === 'string' && addon.transport.includes('http')) { if (!configUrl && addon.transport && typeof addon.transport === 'string' && addon.transport.includes('http')) {
const baseUrl = addon.transport.replace(/\/[^\/]+\.json$/, '/'); const baseUrl = addon.transport.replace(/\/[^\/]+\.json$/, '/');
configUrl = `${baseUrl}configure`; configUrl = `${baseUrl}configure`;
logger.info(`Using addon.transport for config URL: ${configUrl}`); logger.info(`Using addon.transport for config URL: ${configUrl}`);
} }
// Get the URL from manifest's originalUrl if available // Get the URL from manifest's originalUrl if available
if (!configUrl && (addon as any).originalUrl) { if (!configUrl && (addon as any).originalUrl) {
const baseUrl = (addon as any).originalUrl.replace(/\/[^\/]+\.json$/, '/'); const baseUrl = (addon as any).originalUrl.replace(/\/[^\/]+\.json$/, '/');
configUrl = `${baseUrl}configure`; configUrl = `${baseUrl}configure`;
logger.info(`Using originalUrl property: ${configUrl}`); logger.info(`Using originalUrl property: ${configUrl}`);
} }
// If we couldn't determine a config URL, show an error // If we couldn't determine a config URL, show an error
if (!configUrl) { if (!configUrl) {
logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`); logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`);
@ -910,10 +920,10 @@ const AddonsScreen = () => {
setAlertVisible(true); setAlertVisible(true);
return; return;
} }
// Log the URL being opened // Log the URL being opened
logger.info(`Opening configuration for addon: ${addon.name} at URL: ${configUrl}`); logger.info(`Opening configuration for addon: ${addon.name} at URL: ${configUrl}`);
// Check if the URL can be opened // Check if the URL can be opened
Linking.canOpenURL(configUrl).then(supported => { Linking.canOpenURL(configUrl).then(supported => {
if (supported) { if (supported) {
@ -927,10 +937,10 @@ const AddonsScreen = () => {
} }
}).catch(err => { }).catch(err => {
logger.error(`Error checking if URL can be opened: ${configUrl}`, err); logger.error(`Error checking if URL can be opened: ${configUrl}`, err);
setAlertTitle('Error'); setAlertTitle('Error');
setAlertMessage('Could not open configuration page.'); setAlertMessage('Could not open configuration page.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true); setAlertVisible(true);
}); });
}; };
@ -947,12 +957,12 @@ const AddonsScreen = () => {
const isConfigurable = item.behaviorHints?.configurable === true; const isConfigurable = item.behaviorHints?.configurable === true;
// Check if addon is pre-installed // Check if addon is pre-installed
const isPreInstalled = stremioService.isPreInstalledAddon(item.id); const isPreInstalled = stremioService.isPreInstalledAddon(item.id);
// Format the types into a simple category text // Format the types into a simple category text
const categoryText = types.length > 0 const categoryText = types.length > 0
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ') ? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
: 'No categories'; : 'No categories';
const isFirstItem = index === 0; const isFirstItem = index === 0;
const isLastItem = index === addons.length - 1; const isLastItem = index === addons.length - 1;
@ -960,35 +970,35 @@ const AddonsScreen = () => {
<View style={styles.addonItem}> <View style={styles.addonItem}>
{reorderMode && ( {reorderMode && (
<View style={styles.reorderButtons}> <View style={styles.reorderButtons}>
<TouchableOpacity <TouchableOpacity
style={[styles.reorderButton, isFirstItem && styles.disabledButton]} style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
onPress={() => moveAddonUp(item)} onPress={() => moveAddonUp(item)}
disabled={isFirstItem} disabled={isFirstItem}
> >
<MaterialIcons <MaterialIcons
name="arrow-upward" name="arrow-upward"
size={20} size={20}
color={isFirstItem ? colors.mediumGray : colors.white} color={isFirstItem ? colors.mediumGray : colors.white}
/> />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.reorderButton, isLastItem && styles.disabledButton]} style={[styles.reorderButton, isLastItem && styles.disabledButton]}
onPress={() => moveAddonDown(item)} onPress={() => moveAddonDown(item)}
disabled={isLastItem} disabled={isLastItem}
> >
<MaterialIcons <MaterialIcons
name="arrow-downward" name="arrow-downward"
size={20} size={20}
color={isLastItem ? colors.mediumGray : colors.white} color={isLastItem ? colors.mediumGray : colors.white}
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
<View style={styles.addonHeader}> <View style={styles.addonHeader}>
{logo ? ( {logo ? (
<FastImage <FastImage
source={{ uri: logo }} source={{ uri: logo }}
style={styles.addonIcon} style={styles.addonIcon}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
@ -1016,7 +1026,7 @@ const AddonsScreen = () => {
{!reorderMode ? ( {!reorderMode ? (
<> <>
{isConfigurable && ( {isConfigurable && (
<TouchableOpacity <TouchableOpacity
style={styles.configButton} style={styles.configButton}
onPress={() => handleConfigureAddon(item, item.transport)} onPress={() => handleConfigureAddon(item, item.transport)}
> >
@ -1024,7 +1034,7 @@ const AddonsScreen = () => {
</TouchableOpacity> </TouchableOpacity>
)} )}
{!stremioService.isPreInstalledAddon(item.id) && ( {!stremioService.isPreInstalledAddon(item.id) && (
<TouchableOpacity <TouchableOpacity
style={styles.deleteButton} style={styles.deleteButton}
onPress={() => handleRemoveAddon(item)} onPress={() => handleRemoveAddon(item)}
> >
@ -1039,7 +1049,7 @@ const AddonsScreen = () => {
)} )}
</View> </View>
</View> </View>
<Text style={styles.addonDescription}> <Text style={styles.addonDescription}>
{description.length > 100 ? description.substring(0, 100) + '...' : description} {description.length > 100 ? description.substring(0, 100) + '...' : description}
</Text> </Text>
@ -1077,9 +1087,9 @@ const AddonsScreen = () => {
<Text style={styles.communityAddonName}>{manifest.name}</Text> <Text style={styles.communityAddonName}>{manifest.name}</Text>
<Text style={styles.communityAddonDesc} numberOfLines={2}>{description}</Text> <Text style={styles.communityAddonDesc} numberOfLines={2}>{description}</Text>
<View style={styles.communityAddonMetaContainer}> <View style={styles.communityAddonMetaContainer}>
<Text style={styles.communityAddonVersion}>v{manifest.version || 'N/A'}</Text> <Text style={styles.communityAddonVersion}>v{manifest.version || 'N/A'}</Text>
<Text style={styles.communityAddonDot}></Text> <Text style={styles.communityAddonDot}></Text>
<Text style={styles.communityAddonCategory}>{categoryText}</Text> <Text style={styles.communityAddonCategory}>{categoryText}</Text>
</View> </View>
</View> </View>
<View style={styles.addonActionButtons}> <View style={styles.addonActionButtons}>
@ -1117,50 +1127,50 @@ const AddonsScreen = () => {
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <TouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<MaterialIcons name="chevron-left" size={28} color={colors.white} /> <MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Settings</Text> <Text style={styles.backText}>Settings</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Reorder Mode Toggle Button */} {/* Reorder Mode Toggle Button */}
<TouchableOpacity <TouchableOpacity
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]} style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
onPress={toggleReorderMode} onPress={toggleReorderMode}
> >
<MaterialIcons <MaterialIcons
name="swap-vert" name="swap-vert"
size={24} size={24}
color={reorderMode ? colors.primary : colors.white} color={reorderMode ? colors.primary : colors.white}
/> />
</TouchableOpacity> </TouchableOpacity>
{/* Refresh Button */} {/* Refresh Button */}
<TouchableOpacity <TouchableOpacity
style={styles.headerButton} style={styles.headerButton}
onPress={refreshAddons} onPress={refreshAddons}
disabled={loading} disabled={loading}
> >
<MaterialIcons <MaterialIcons
name="refresh" name="refresh"
size={24} size={24}
color={loading ? colors.mediumGray : colors.white} color={loading ? colors.mediumGray : colors.white}
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<Text style={styles.headerTitle}> <Text style={styles.headerTitle}>
Addons Addons
{reorderMode && <Text style={styles.reorderModeText}> (Reorder Mode)</Text>} {reorderMode && <Text style={styles.reorderModeText}> (Reorder Mode)</Text>}
</Text> </Text>
{reorderMode && ( {reorderMode && (
<View style={styles.reorderInfoBanner}> <View style={styles.reorderInfoBanner}>
<MaterialIcons name="info-outline" size={18} color={colors.primary} /> <MaterialIcons name="info-outline" size={18} color={colors.primary} />
@ -1169,18 +1179,18 @@ const AddonsScreen = () => {
</Text> </Text>
</View> </View>
)} )}
{loading ? ( {loading ? (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={colors.primary} />
</View> </View>
) : ( ) : (
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentInsetAdjustmentBehavior="automatic" contentInsetAdjustmentBehavior="automatic"
> >
{/* Overview Section */} {/* Overview Section */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>OVERVIEW</Text> <Text style={styles.sectionTitle}>OVERVIEW</Text>
@ -1192,7 +1202,7 @@ const AddonsScreen = () => {
<StatsCard value={catalogCount} label="Catalogs" /> <StatsCard value={catalogCount} label="Catalogs" />
</View> </View>
</View> </View>
{/* Hide Add Addon Section in reorder mode */} {/* Hide Add Addon Section in reorder mode */}
{!reorderMode && ( {!reorderMode && (
<View style={styles.section}> <View style={styles.section}>
@ -1207,8 +1217,8 @@ const AddonsScreen = () => {
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
/> />
<TouchableOpacity <TouchableOpacity
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]} style={[styles.addButton, { opacity: installing || !addonUrl ? 0.6 : 1 }]}
onPress={() => handleAddAddon()} onPress={() => handleAddAddon()}
disabled={installing || !addonUrl} disabled={installing || !addonUrl}
> >
@ -1219,7 +1229,7 @@ const AddonsScreen = () => {
</View> </View>
</View> </View>
)} )}
{/* Installed Addons Section */} {/* Installed Addons Section */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}> <Text style={styles.sectionTitle}>
@ -1233,8 +1243,8 @@ const AddonsScreen = () => {
</View> </View>
) : ( ) : (
addons.map((addon, index) => ( addons.map((addon, index) => (
<View <View
key={addon.id} key={addon.id}
style={{ marginBottom: index === addons.length - 1 ? 32 : 0 }} style={{ marginBottom: index === addons.length - 1 ? 32 : 0 }}
> >
{renderAddonItem({ item: addon, index })} {renderAddonItem({ item: addon, index })}
@ -1245,68 +1255,68 @@ const AddonsScreen = () => {
</View> </View>
{/* Separator */} {/* Separator */}
<View style={styles.sectionSeparator} /> <View style={styles.sectionSeparator} />
{/* Promotional Addon Section (hidden if installed) */} {/* Promotional Addon Section (hidden if installed) */}
{!isPromoInstalled && ( {!isPromoInstalled && (
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>OFFICIAL ADDON</Text> <Text style={styles.sectionTitle}>OFFICIAL ADDON</Text>
<View style={styles.addonList}> <View style={styles.addonList}>
<View style={styles.addonItem}> <View style={styles.addonItem}>
<View style={styles.addonHeader}> <View style={styles.addonHeader}>
{promoAddon.logo ? ( {promoAddon.logo ? (
<FastImage <FastImage
source={{ uri: promoAddon.logo }} source={{ uri: promoAddon.logo }}
style={styles.addonIcon} style={styles.addonIcon}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
) : ( ) : (
<View style={styles.addonIconPlaceholder}> <View style={styles.addonIconPlaceholder}>
<MaterialIcons name="extension" size={22} color={colors.mediumGray} /> <MaterialIcons name="extension" size={22} color={colors.mediumGray} />
</View> </View>
)} )}
<View style={styles.addonTitleContainer}> <View style={styles.addonTitleContainer}>
<Text style={styles.addonName}>{promoAddon.name}</Text> <Text style={styles.addonName}>{promoAddon.name}</Text>
<View style={styles.addonMetaContainer}> <View style={styles.addonMetaContainer}>
<Text style={styles.addonVersion}>v{promoAddon.version}</Text> <Text style={styles.addonVersion}>v{promoAddon.version}</Text>
<Text style={styles.addonDot}></Text> <Text style={styles.addonDot}></Text>
<Text style={styles.addonCategory}>{promoAddon.types?.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')}</Text> <Text style={styles.addonCategory}>{promoAddon.types?.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')}</Text>
</View> </View>
</View> </View>
<View style={styles.addonActions}> <View style={styles.addonActions}>
{promoAddon.behaviorHints?.configurable && ( {promoAddon.behaviorHints?.configurable && (
<TouchableOpacity <TouchableOpacity
style={styles.configButton} style={styles.configButton}
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)} onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
> >
<MaterialIcons name="settings" size={20} color={colors.primary} /> <MaterialIcons name="settings" size={20} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
)} )}
<TouchableOpacity <TouchableOpacity
style={styles.installButton} style={styles.installButton}
onPress={() => handleAddAddon(PROMO_ADDON_URL)} onPress={() => handleAddAddon(PROMO_ADDON_URL)}
disabled={installing} disabled={installing}
> >
{installing ? ( {installing ? (
<ActivityIndicator size="small" color={colors.white} /> <ActivityIndicator size="small" color={colors.white} />
) : ( ) : (
<MaterialIcons name="add" size={20} color={colors.white} /> <MaterialIcons name="add" size={20} color={colors.white} />
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<Text style={styles.addonDescription}> <Text style={styles.addonDescription}>
{promoAddon.description} {promoAddon.description}
</Text> </Text>
<Text style={[styles.addonDescription, { marginTop: 4, opacity: 0.9 }]}> <Text style={[styles.addonDescription, { marginTop: 4, opacity: 0.9 }]}>
Configure and install for full functionality. Configure and install for full functionality.
</Text> </Text>
</View> </View>
</View> </View>
</View> </View>
)} )}
{/* Community Addons Section */} {/* Community Addons Section */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>COMMUNITY ADDONS</Text> <Text style={styles.sectionTitle}>COMMUNITY ADDONS</Text>
<View style={styles.addonList}> <View style={styles.addonList}>
@ -1326,15 +1336,15 @@ const AddonsScreen = () => {
</View> </View>
) : ( ) : (
communityAddons.map((item, index) => ( communityAddons.map((item, index) => (
<View <View
key={item.transportUrl} key={item.transportUrl}
style={{ marginBottom: index === communityAddons.length - 1 ? 32 : 16 }} style={{ marginBottom: index === communityAddons.length - 1 ? 32 : 16 }}
> >
<View style={styles.addonItem}> <View style={styles.addonItem}>
<View style={styles.addonHeader}> <View style={styles.addonHeader}>
{item.manifest.logo ? ( {item.manifest.logo ? (
<FastImage <FastImage
source={{ uri: item.manifest.logo }} source={{ uri: item.manifest.logo }}
style={styles.addonIcon} style={styles.addonIcon}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
@ -1357,14 +1367,14 @@ const AddonsScreen = () => {
</View> </View>
<View style={styles.addonActions}> <View style={styles.addonActions}>
{item.manifest.behaviorHints?.configurable && ( {item.manifest.behaviorHints?.configurable && (
<TouchableOpacity <TouchableOpacity
style={styles.configButton} style={styles.configButton}
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)} onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
> >
<MaterialIcons name="settings" size={20} color={colors.primary} /> <MaterialIcons name="settings" size={20} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
)} )}
<TouchableOpacity <TouchableOpacity
style={[styles.installButton, installing && { opacity: 0.6 }]} style={[styles.installButton, installing && { opacity: 0.6 }]}
onPress={() => handleAddAddon(item.transportUrl)} onPress={() => handleAddAddon(item.transportUrl)}
disabled={installing} disabled={installing}
@ -1377,12 +1387,12 @@ const AddonsScreen = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<Text style={styles.addonDescription}> <Text style={styles.addonDescription}>
{item.manifest.description {item.manifest.description
? (item.manifest.description.length > 100 ? (item.manifest.description.length > 100
? item.manifest.description.substring(0, 100) + '...' ? item.manifest.description.substring(0, 100) + '...'
: item.manifest.description) : item.manifest.description)
: 'No description provided.'} : 'No description provided.'}
</Text> </Text>
</View> </View>
@ -1429,8 +1439,8 @@ const AddonsScreen = () => {
<MaterialIcons name="close" size={24} color={colors.white} /> <MaterialIcons name="close" size={24} color={colors.white} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<ScrollView <ScrollView
style={styles.modalScrollContent} style={styles.modalScrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
bounces={true} bounces={true}
@ -1451,14 +1461,14 @@ const AddonsScreen = () => {
<Text style={styles.addonDetailName}>{addonDetails.name}</Text> <Text style={styles.addonDetailName}>{addonDetails.name}</Text>
<Text style={styles.addonDetailVersion}>v{addonDetails.version || '1.0.0'}</Text> <Text style={styles.addonDetailVersion}>v{addonDetails.version || '1.0.0'}</Text>
</View> </View>
<View style={styles.addonDetailSection}> <View style={styles.addonDetailSection}>
<Text style={styles.addonDetailSectionTitle}>Description</Text> <Text style={styles.addonDetailSectionTitle}>Description</Text>
<Text style={styles.addonDetailDescription}> <Text style={styles.addonDetailDescription}>
{addonDetails.description || 'No description available'} {addonDetails.description || 'No description available'}
</Text> </Text>
</View> </View>
{addonDetails.types && addonDetails.types.length > 0 && ( {addonDetails.types && addonDetails.types.length > 0 && (
<View style={styles.addonDetailSection}> <View style={styles.addonDetailSection}>
<Text style={styles.addonDetailSectionTitle}>Supported Types</Text> <Text style={styles.addonDetailSectionTitle}>Supported Types</Text>
@ -1471,7 +1481,7 @@ const AddonsScreen = () => {
</View> </View>
</View> </View>
)} )}
{addonDetails.catalogs && addonDetails.catalogs.length > 0 && ( {addonDetails.catalogs && addonDetails.catalogs.length > 0 && (
<View style={styles.addonDetailSection}> <View style={styles.addonDetailSection}>
<Text style={styles.addonDetailSectionTitle}>Catalogs</Text> <Text style={styles.addonDetailSectionTitle}>Catalogs</Text>
@ -1487,7 +1497,7 @@ const AddonsScreen = () => {
</View> </View>
)} )}
</ScrollView> </ScrollView>
<View style={styles.modalActions}> <View style={styles.modalActions}>
<TouchableOpacity <TouchableOpacity
style={[styles.modalButton, styles.cancelButton]} style={[styles.modalButton, styles.cancelButton]}
@ -1515,15 +1525,15 @@ const AddonsScreen = () => {
</View> </View>
</View> </View>
</Modal> </Modal>
{/* Custom Alert Modal */} {/* Custom Alert Modal */}
<CustomAlert <CustomAlert
visible={alertVisible} visible={alertVisible}
title={alertTitle} title={alertTitle}
message={alertMessage} message={alertMessage}
onClose={() => setAlertVisible(false)} onClose={() => setAlertVisible(false)}
actions={alertActions} actions={alertActions}
/> />
</SafeAreaView> </SafeAreaView>
); );
}; };

View file

@ -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<NavigationProp<RootStackParamList>>();
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<TorboxConfig | null>(null);
const [userData, setUserData] = useState<TorboxUserData | null>(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<any[]>([]);
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 (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
<View style={styles.header}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backButton}
>
<Feather name="arrow-left" size={24} color={colors.white} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Debrid Integration</Text>
</View>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<ScrollView
style={styles.content}
contentContainerStyle={{ paddingBottom: 40 }}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.primary}
colors={[colors.primary]}
/>
}
>
{config?.isConnected ? (
// Connected state
<>
<View style={styles.statusCard}>
<View style={styles.statusRow}>
<Text style={styles.statusLabel}>Status</Text>
<Text style={[styles.statusValue, styles.statusConnected]}>Connected</Text>
</View>
<View style={styles.divider} />
<View style={styles.statusRow}>
<Text style={styles.statusLabel}>Enable Addon</Text>
<Switch
value={config.isEnabled}
onValueChange={handleToggleEnabled}
trackColor={{ false: colors.elevation2, true: colors.primary }}
thumbColor={config.isEnabled ? colors.white : colors.mediumEmphasis}
ios_backgroundColor={colors.elevation2}
/>
</View>
</View>
<TouchableOpacity
style={[styles.actionButton, styles.dangerButton, loading && styles.disabledButton]}
onPress={handleDisconnect}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Disconnecting...' : 'Disconnect & Remove'}
</Text>
</TouchableOpacity>
{/* User Data Card */}
{userData && (
<View style={styles.userDataCard}>
<View style={styles.userDataHeader}>
<Text style={styles.userDataTitle}>Account Information</Text>
{userDataLoading && (
<ActivityIndicator size="small" color={colors.primary} />
)}
</View>
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Email</Text>
<Text style={styles.userDataValue} numberOfLines={1}>
{userData.base_email || userData.email}
</Text>
</View>
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Plan</Text>
<View style={[
styles.planBadge,
userData.plan === 0 ? styles.planBadgeFree : styles.planBadgePaid
]}>
<Text style={[
styles.planBadgeText,
userData.plan === 0 ? styles.planBadgeTextFree : styles.planBadgeTextPaid
]}>
{getPlanName(userData.plan)}
</Text>
</View>
</View>
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Status</Text>
<Text style={[
styles.userDataValue,
{ color: userData.is_subscribed ? (colors.success || '#4CAF50') : colors.mediumEmphasis }
]}>
{userData.is_subscribed ? 'Active' : 'Free'}
</Text>
</View>
{userData.premium_expires_at && (
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Expires</Text>
<Text style={styles.userDataValue}>
{new Date(userData.premium_expires_at).toLocaleDateString()}
</Text>
</View>
)}
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Downloaded</Text>
<Text style={styles.userDataValue}>
{(userData.total_downloaded / (1024 * 1024 * 1024)).toFixed(2)} GB
</Text>
</View>
</View>
)}
<View style={styles.section}>
<Text style={styles.sectionTitle}> Connected to TorBox</Text>
<Text style={styles.sectionText}>
Your TorBox addon is active and providing premium streams.{config.isEnabled ? '' : ' (Currently disabled)'}
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Configure Addon</Text>
<Text style={styles.sectionText}>
Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
</Text>
<TouchableOpacity
style={styles.subscribeButton}
onPress={() => Linking.openURL('https://torbox.app/settings?section=integration-settings')}
>
<Text style={styles.subscribeButtonText}>Open Settings</Text>
</TouchableOpacity>
</View>
</>
) : (
// Not connected state
<>
<Text style={styles.description}>
Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.
</Text>
<TouchableOpacity onPress={() => Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')} style={styles.guideLink}>
<Text style={styles.guideLinkText}>What is a Debrid Service?</Text>
</TouchableOpacity>
<View style={styles.inputContainer}>
<Text style={styles.label}>Torbox API Key</Text>
<TextInput
style={styles.input}
placeholder="Enter your API Key"
placeholderTextColor={colors.mediumGray}
value={apiKey}
onChangeText={setApiKey}
autoCapitalize="none"
autoCorrect={false}
secureTextEntry
/>
</View>
<TouchableOpacity
style={[styles.connectButton, loading && styles.disabledButton]}
onPress={handleConnect}
disabled={loading}
>
<Text style={styles.connectButtonText}>
{loading ? 'Connecting...' : 'Connect & Install'}
</Text>
</TouchableOpacity>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Unlock Premium Speeds</Text>
<Text style={styles.sectionText}>
Get a Torbox subscription to access cached high-quality streams with zero buffering.
</Text>
<TouchableOpacity style={styles.subscribeButton} onPress={openSubscription}>
<Text style={styles.subscribeButtonText}>Get Subscription</Text>
</TouchableOpacity>
</View>
</>
)}
<View style={[styles.logoContainer, { marginTop: 60 }]}>
<Text style={styles.poweredBy}>Powered by</Text>
<View style={styles.logoRow}>
<Image
source={{ uri: 'https://torbox.app/assets/logo-57adbf99.svg' }}
style={styles.logo}
resizeMode="contain"
/>
<Text style={styles.logoText}>TorBox</Text>
</View>
<Text style={styles.disclaimer}>Nuvio is not affiliated with Torbox in any way.</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
);
};
export default DebridIntegrationScreen;

View file

@ -67,9 +67,9 @@ interface SettingsCardProps {
const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, isTablet = false }) => { const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, isTablet = false }) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
return ( return (
<View <View
style={[ style={[
styles.cardContainer, styles.cardContainer,
isTablet && styles.tabletCardContainer isTablet && styles.tabletCardContainer
@ -119,13 +119,13 @@ const SettingItem: React.FC<SettingItemProps> = ({
isTablet = false isTablet = false
}) => { }) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
return ( return (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.6} activeOpacity={0.6}
onPress={onPress} onPress={onPress}
style={[ style={[
styles.settingItem, styles.settingItem,
!isLast && styles.settingItemBorder, !isLast && styles.settingItemBorder,
{ borderBottomColor: currentTheme.colors.elevation2 }, { borderBottomColor: currentTheme.colors.elevation2 },
isTablet && styles.tabletSettingItem isTablet && styles.tabletSettingItem
@ -133,7 +133,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
> >
<View style={[ <View style={[
styles.settingIconContainer, styles.settingIconContainer,
{ {
backgroundColor: currentTheme.colors.darkGray, backgroundColor: currentTheme.colors.darkGray,
borderWidth: 1, borderWidth: 1,
borderColor: currentTheme.colors.primary + '20' borderColor: currentTheme.colors.primary + '20'
@ -143,17 +143,17 @@ const SettingItem: React.FC<SettingItemProps> = ({
{customIcon ? ( {customIcon ? (
customIcon customIcon
) : ( ) : (
<Feather <Feather
name={icon! as any} name={icon! as any}
size={isTablet ? 24 : 20} size={isTablet ? 24 : 20}
color={currentTheme.colors.primary} color={currentTheme.colors.primary}
/> />
)} )}
</View> </View>
<View style={styles.settingContent}> <View style={styles.settingContent}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>
<Text style={[ <Text style={[
styles.settingTitle, styles.settingTitle,
{ color: currentTheme.colors.highEmphasis }, { color: currentTheme.colors.highEmphasis },
isTablet && styles.tabletSettingTitle isTablet && styles.tabletSettingTitle
]}> ]}>
@ -161,7 +161,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
</Text> </Text>
{description && ( {description && (
<Text style={[ <Text style={[
styles.settingDescription, styles.settingDescription,
{ color: currentTheme.colors.mediumEmphasis }, { color: currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletSettingDescription isTablet && styles.tabletSettingDescription
]} numberOfLines={1}> ]} numberOfLines={1}>
@ -224,16 +224,16 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
name={category.icon as any} name={category.icon as any}
size={22} size={22}
color={ color={
selectedCategory === category.id selectedCategory === category.id
? currentTheme.colors.primary ? currentTheme.colors.primary
: currentTheme.colors.mediumEmphasis : currentTheme.colors.mediumEmphasis
} }
/> />
<Text style={[ <Text style={[
styles.sidebarItemText, styles.sidebarItemText,
{ {
color: selectedCategory === category.id color: selectedCategory === category.id
? currentTheme.colors.primary ? currentTheme.colors.primary
: currentTheme.colors.mediumEmphasis : currentTheme.colors.mediumEmphasis
} }
]}> ]}>
@ -263,7 +263,7 @@ const SettingsScreen: React.FC = () => {
) => { ) => {
setAlertTitle(title); setAlertTitle(title);
setAlertMessage(message); setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
setAlertVisible(true); setAlertVisible(true);
}; };
@ -274,7 +274,7 @@ const SettingsScreen: React.FC = () => {
try { try {
const flag = await mmkvStorage.getItem('@update_badge_pending'); const flag = await mmkvStorage.getItem('@update_badge_pending');
if (mounted) setHasUpdateBadge(flag === 'true'); if (mounted) setHasUpdateBadge(flag === 'true');
} catch {} } catch { }
})(); })();
return () => { mounted = false; }; return () => { mounted = false; };
}, []); }, []);
@ -283,7 +283,7 @@ const SettingsScreen: React.FC = () => {
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
// Tablet-specific state // Tablet-specific state
const [selectedCategory, setSelectedCategory] = useState('account'); const [selectedCategory, setSelectedCategory] = useState('account');
@ -310,7 +310,7 @@ const SettingsScreen: React.FC = () => {
} }
refreshAuthStatus(); refreshAuthStatus();
}); });
return unsubscribe; return unsubscribe;
}, [navigation, isAuthenticated, userProfile, refreshAuthStatus]); }, [navigation, isAuthenticated, userProfile, refreshAuthStatus]);
@ -320,7 +320,7 @@ const SettingsScreen: React.FC = () => {
const addons = await stremioService.getInstalledAddonsAsync(); const addons = await stremioService.getInstalledAddonsAsync();
setAddonCount(addons.length); setAddonCount(addons.length);
setInitialLoadComplete(true); setInitialLoadComplete(true);
// Count total available catalogs // Count total available catalogs
let totalCatalogs = 0; let totalCatalogs = 0;
addons.forEach(addon => { addons.forEach(addon => {
@ -328,7 +328,7 @@ const SettingsScreen: React.FC = () => {
totalCatalogs += addon.catalogs.length; totalCatalogs += addon.catalogs.length;
} }
}); });
// Load saved catalog settings // Load saved catalog settings
const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings'); const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings');
if (catalogSettingsJson) { if (catalogSettingsJson) {
@ -358,7 +358,7 @@ const SettingsScreen: React.FC = () => {
setTotalDownloads(downloads); setTotalDownloads(downloads);
setDisplayDownloads(downloads); setDisplayDownloads(downloads);
} }
} catch (error) { } catch (error) {
if (__DEV__) console.error('Error loading settings data:', error); if (__DEV__) console.error('Error loading settings data:', error);
} }
@ -382,7 +382,7 @@ const SettingsScreen: React.FC = () => {
useEffect(() => { useEffect(() => {
// Only poll when viewing the About section (where downloads counter is shown) // Only poll when viewing the About section (where downloads counter is shown)
const shouldPoll = isTablet ? selectedCategory === 'about' : true; const shouldPoll = isTablet ? selectedCategory === 'about' : true;
if (!shouldPoll) return; if (!shouldPoll) return;
const pollInterval = setInterval(async () => { const pollInterval = setInterval(async () => {
@ -414,11 +414,11 @@ const SettingsScreen: React.FC = () => {
const now = Date.now(); const now = Date.now();
const elapsed = now - startTime; const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1); const progress = Math.min(elapsed / duration, 1);
// Ease out quad for smooth deceleration // Ease out quad for smooth deceleration
const easeProgress = 1 - Math.pow(1 - progress, 2); const easeProgress = 1 - Math.pow(1 - progress, 2);
const current = Math.floor(start + (end - start) * easeProgress); const current = Math.floor(start + (end - start) * easeProgress);
setDisplayDownloads(current); setDisplayDownloads(current);
if (progress < 1) { if (progress < 1) {
@ -437,7 +437,7 @@ const SettingsScreen: React.FC = () => {
'Reset Settings', 'Reset Settings',
'Are you sure you want to reset all settings to default values?', 'Are you sure you want to reset all settings to default values?',
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Reset', label: 'Reset',
onPress: () => { onPress: () => {
@ -455,7 +455,7 @@ const SettingsScreen: React.FC = () => {
'Clear MDBList Cache', 'Clear MDBList Cache',
'Are you sure you want to clear all cached MDBList data? This cannot be undone.', 'Are you sure you want to clear all cached MDBList data? This cannot be undone.',
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Clear', label: 'Clear',
onPress: async () => { onPress: async () => {
@ -483,9 +483,9 @@ const SettingsScreen: React.FC = () => {
); );
const ChevronRight = () => ( const ChevronRight = () => (
<Feather <Feather
name="chevron-right" name="chevron-right"
size={isTablet ? 24 : 20} size={isTablet ? 24 : 20}
color={currentTheme.colors.mediumEmphasis} color={currentTheme.colors.mediumEmphasis}
/> />
); );
@ -527,6 +527,14 @@ const SettingsScreen: React.FC = () => {
onPress={() => navigation.navigate('Addons')} onPress={() => navigation.navigate('Addons')}
isTablet={isTablet} isTablet={isTablet}
/> />
<SettingItem
title="Debrid Integration"
description="Connect Torbox for premium streams"
icon="link"
renderControl={ChevronRight}
onPress={() => navigation.navigate('DebridIntegration')}
isTablet={isTablet}
/>
<SettingItem <SettingItem
title="Plugins" title="Plugins"
description="Manage plugins and repositories" description="Manage plugins and repositories"
@ -648,7 +656,7 @@ const SettingsScreen: React.FC = () => {
<SettingsCard title="PLAYBACK" isTablet={isTablet}> <SettingsCard title="PLAYBACK" isTablet={isTablet}>
<SettingItem <SettingItem
title="Video Player" title="Video Player"
description={Platform.OS === 'ios' description={Platform.OS === 'ios'
? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in') ? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in')
: (settings?.useExternalPlayer ? 'External' : 'Built-in') : (settings?.useExternalPlayer ? 'External' : 'Built-in')
} }
@ -764,7 +772,7 @@ const SettingsScreen: React.FC = () => {
'Clear All Data', 'Clear All Data',
'This will reset all settings and clear all cached data. Are you sure?', 'This will reset all settings and clear all cached data. Are you sure?',
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Clear', label: 'Clear',
onPress: async () => { onPress: async () => {
@ -824,7 +832,7 @@ const SettingsScreen: React.FC = () => {
badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined} badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined}
onPress={async () => { onPress={async () => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
try { await mmkvStorage.removeItem('@update_badge_pending'); } catch {} try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { }
setHasUpdateBadge(false); setHasUpdateBadge(false);
} }
navigation.navigate('Update'); navigation.navigate('Update');
@ -861,20 +869,20 @@ const SettingsScreen: React.FC = () => {
categories={visibleCategories} categories={visibleCategories}
extraTopPadding={tabletNavOffset} extraTopPadding={tabletNavOffset}
/> />
<View style={[ <View style={[
styles.tabletContent, styles.tabletContent,
{ {
paddingTop: (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 24 : 48) + tabletNavOffset, paddingTop: (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 24 : 48) + tabletNavOffset,
} }
]}> ]}>
<ScrollView <ScrollView
style={styles.tabletScrollView} style={styles.tabletScrollView}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.tabletScrollContent} contentContainerStyle={styles.tabletScrollContent}
> >
{renderCategoryContent(selectedCategory)} {renderCategoryContent(selectedCategory)}
{selectedCategory === 'about' && ( {selectedCategory === 'about' && (
<> <>
{displayDownloads !== null && ( {displayDownloads !== null && (
@ -887,9 +895,9 @@ const SettingsScreen: React.FC = () => {
</Text> </Text>
</View> </View>
)} )}
<View style={styles.footer}> <View style={styles.footer}>
<Text style={[styles.footerText, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.footerText, { color: currentTheme.colors.mediumEmphasis }]}>
Made with by Tapframe and Friends Made with by Tapframe and Friends
</Text> </Text>
</View> </View>
@ -906,7 +914,7 @@ const SettingsScreen: React.FC = () => {
style={styles.discordLogo} style={styles.discordLogo}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
<Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}>
Join Discord Join Discord
</Text> </Text>
</View> </View>
@ -958,7 +966,7 @@ const SettingsScreen: React.FC = () => {
</View> </View>
<View style={styles.contentContainer}> <View style={styles.contentContainer}>
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
@ -1006,7 +1014,7 @@ const SettingsScreen: React.FC = () => {
style={styles.discordLogo} style={styles.discordLogo}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
<Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}>
Join Discord Join Discord
</Text> </Text>
</View> </View>
@ -1074,7 +1082,7 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
paddingBottom: 90, paddingBottom: 90,
}, },
// Tablet-specific styles // Tablet-specific styles
tabletContainer: { tabletContainer: {
flex: 1, flex: 1,
@ -1128,7 +1136,7 @@ const styles = StyleSheet.create({
tabletScrollContent: { tabletScrollContent: {
paddingBottom: 32, paddingBottom: 32,
}, },
// Common card styles // Common card styles
cardContainer: { cardContainer: {
width: '100%', width: '100%',