mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-04 16:59:42 +00:00
debrid integration. Torbox
This commit is contained in:
parent
bbf035ebae
commit
6d1ba14ab4
4 changed files with 1210 additions and 378 deletions
|
|
@ -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<RootStackParamList>;
|
||||
|
|
@ -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 (
|
||||
<Animated.View style={{
|
||||
alignItems: 'center',
|
||||
<Animated.View style={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transform: [{ scale: scaleAnim }]
|
||||
}}>
|
||||
{iconLibrary === 'feather' ? (
|
||||
<Feather
|
||||
<Feather
|
||||
name={finalIconName as any}
|
||||
size={24}
|
||||
color={color}
|
||||
size={24}
|
||||
color={color}
|
||||
/>
|
||||
) : iconLibrary === 'ionicons' ? (
|
||||
<Ionicons
|
||||
<Ionicons
|
||||
name={finalIconName as any}
|
||||
size={24}
|
||||
color={color}
|
||||
/>
|
||||
) : (
|
||||
<MaterialCommunityIcons
|
||||
<MaterialCommunityIcons
|
||||
name={finalIconName as any}
|
||||
size={24}
|
||||
color={color}
|
||||
size={24}
|
||||
color={color}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
|
@ -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 (
|
||||
<View style={{
|
||||
flex: 1,
|
||||
<View style={{
|
||||
flex: 1,
|
||||
backgroundColor: colors.darkBackground,
|
||||
// Lock the layout to prevent shifts
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Reserve consistent space for the header area on all screens */}
|
||||
<View style={{
|
||||
height: isTablet ? (insets.top + 64) : (Platform.OS === 'android' ? 80 : 60),
|
||||
width: '100%',
|
||||
<View style={{
|
||||
height: isTablet ? (insets.top + 64) : (Platform.OS === 'android' ? 80 : 60),
|
||||
width: '100%',
|
||||
backgroundColor: colors.darkBackground,
|
||||
position: 'absolute',
|
||||
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
|
||||
const WrappedScreen: React.FC<{Screen: React.ComponentType<any>}> = ({ Screen }) => {
|
||||
const WrappedScreen: React.FC<{ Screen: React.ComponentType<any> }> = ({ Screen }) => {
|
||||
return (
|
||||
<TabScreenWrapper>
|
||||
<Screen />
|
||||
|
|
@ -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 (
|
||||
<Animated.View
|
||||
style={[{
|
||||
position: 'absolute',
|
||||
top: insets.top + 12,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
zIndex: 100,
|
||||
}, shouldKeepFixed ? {} : {
|
||||
transform: [{ translateY }],
|
||||
opacity: fade,
|
||||
}]}>
|
||||
style={[{
|
||||
position: 'absolute',
|
||||
top: insets.top + 12,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
zIndex: 100,
|
||||
}, shouldKeepFixed ? {} : {
|
||||
transform: [{ translateY }],
|
||||
opacity: fade,
|
||||
}]}>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
@ -645,8 +647,8 @@ const MainTabs = () => {
|
|||
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 (
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: Platform.OS === 'android' ? 70 + insets.bottom : 85 + insets.bottom,
|
||||
backgroundColor: 'transparent',
|
||||
|
|
@ -759,8 +761,8 @@ const MainTabs = () => {
|
|||
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',
|
||||
}}
|
||||
>
|
||||
<TabIcon
|
||||
focused={isFocused}
|
||||
color={isFocused ? currentTheme.colors.primary : currentTheme.colors.white}
|
||||
<TabIcon
|
||||
focused={isFocused}
|
||||
color={isFocused ? currentTheme.colors.primary : currentTheme.colors.white}
|
||||
iconName={iconName}
|
||||
iconLibrary={iconLibrary}
|
||||
/>
|
||||
|
|
@ -838,7 +840,7 @@ const MainTabs = () => {
|
|||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// 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"
|
||||
/>
|
||||
|
||||
|
||||
<Tab.Navigator
|
||||
tabBar={renderTabBar}
|
||||
screenOptions={({ route, navigation, theme }) => ({
|
||||
|
|
@ -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 (
|
||||
<SafeAreaProvider>
|
||||
<StatusBar
|
||||
|
|
@ -1085,8 +1087,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
barStyle="light-content"
|
||||
/>
|
||||
<PaperProvider theme={CustomDarkTheme}>
|
||||
<View style={{
|
||||
flex: 1,
|
||||
<View style={{
|
||||
flex: 1,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
...(Platform.OS === 'android' && {
|
||||
// Prevent white flashes on Android
|
||||
|
|
@ -1135,8 +1137,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
contentStyle: { backgroundColor: currentTheme.colors.darkBackground },
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Onboarding"
|
||||
<Stack.Screen
|
||||
name="Onboarding"
|
||||
component={OnboardingScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
|
|
@ -1147,9 +1149,9 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MainTabs"
|
||||
component={MainTabs as any}
|
||||
<Stack.Screen
|
||||
name="MainTabs"
|
||||
component={MainTabs as any}
|
||||
options={{
|
||||
contentStyle: {
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
|
|
@ -1168,11 +1170,11 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Metadata"
|
||||
<Stack.Screen
|
||||
name="Metadata"
|
||||
component={MetadataScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
options={{
|
||||
headerShown: false,
|
||||
animation: Platform.OS === 'android' ? 'none' : 'fade',
|
||||
animationDuration: Platform.OS === 'android' ? 0 : 300,
|
||||
...(Platform.OS === 'ios' && {
|
||||
|
|
@ -1186,9 +1188,9 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Streams"
|
||||
component={StreamsScreen as any}
|
||||
<Stack.Screen
|
||||
name="Streams"
|
||||
component={StreamsScreen as any}
|
||||
options={{
|
||||
headerShown: false,
|
||||
animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none',
|
||||
|
|
@ -1203,10 +1205,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
freezeOnBlur: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PlayerIOS"
|
||||
component={KSPlayerCore as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="PlayerIOS"
|
||||
component={KSPlayerCore as any}
|
||||
options={{
|
||||
animation: 'default',
|
||||
animationDuration: 0,
|
||||
// Force fullscreen presentation on iPad
|
||||
|
|
@ -1225,10 +1227,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
freezeOnBlur: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PlayerAndroid"
|
||||
component={AndroidVideoPlayer as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="PlayerAndroid"
|
||||
component={AndroidVideoPlayer as any}
|
||||
options={{
|
||||
animation: 'none',
|
||||
animationDuration: 0,
|
||||
presentation: 'card',
|
||||
|
|
@ -1243,10 +1245,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
freezeOnBlur: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Catalog"
|
||||
component={CatalogScreen as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="Catalog"
|
||||
component={CatalogScreen as any}
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
||||
contentStyle: {
|
||||
|
|
@ -1254,10 +1256,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Addons"
|
||||
component={AddonsScreen as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="Addons"
|
||||
component={AddonsScreen as any}
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
||||
contentStyle: {
|
||||
|
|
@ -1265,10 +1267,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Search"
|
||||
component={SearchScreen as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="Search"
|
||||
component={SearchScreen as any}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'none' : 'fade',
|
||||
animationDuration: Platform.OS === 'android' ? 0 : 350,
|
||||
gestureEnabled: true,
|
||||
|
|
@ -1278,10 +1280,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="CatalogSettings"
|
||||
component={CatalogSettingsScreen as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="CatalogSettings"
|
||||
component={CatalogSettingsScreen as any}
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
||||
contentStyle: {
|
||||
|
|
@ -1289,8 +1291,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="HomeScreenSettings"
|
||||
<Stack.Screen
|
||||
name="HomeScreenSettings"
|
||||
component={HomeScreenSettings}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
||||
|
|
@ -1304,8 +1306,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ContinueWatchingSettings"
|
||||
<Stack.Screen
|
||||
name="ContinueWatchingSettings"
|
||||
component={ContinueWatchingSettingsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
||||
|
|
@ -1319,8 +1321,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Contributors"
|
||||
<Stack.Screen
|
||||
name="Contributors"
|
||||
component={ContributorsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
||||
|
|
@ -1334,8 +1336,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="HeroCatalogs"
|
||||
<Stack.Screen
|
||||
name="HeroCatalogs"
|
||||
component={HeroCatalogsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
||||
|
|
@ -1349,8 +1351,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ShowRatings"
|
||||
<Stack.Screen
|
||||
name="ShowRatings"
|
||||
component={ShowRatingsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'fade_from_bottom' : 'fade',
|
||||
|
|
@ -1364,10 +1366,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Calendar"
|
||||
component={CalendarScreen as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="Calendar"
|
||||
component={CalendarScreen as any}
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
||||
contentStyle: {
|
||||
|
|
@ -1375,10 +1377,10 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationSettings"
|
||||
component={NotificationSettingsScreen as any}
|
||||
options={{
|
||||
<Stack.Screen
|
||||
name="NotificationSettings"
|
||||
component={NotificationSettingsScreen as any}
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
||||
contentStyle: {
|
||||
|
|
@ -1386,8 +1388,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MDBListSettings"
|
||||
<Stack.Screen
|
||||
name="MDBListSettings"
|
||||
component={MDBListSettingsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
@ -1401,8 +1403,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TMDBSettings"
|
||||
<Stack.Screen
|
||||
name="TMDBSettings"
|
||||
component={TMDBSettingsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
@ -1416,8 +1418,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TraktSettings"
|
||||
<Stack.Screen
|
||||
name="TraktSettings"
|
||||
component={TraktSettingsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
@ -1431,8 +1433,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PlayerSettings"
|
||||
<Stack.Screen
|
||||
name="PlayerSettings"
|
||||
component={PlayerSettingsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
@ -1446,8 +1448,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ThemeSettings"
|
||||
<Stack.Screen
|
||||
name="ThemeSettings"
|
||||
component={ThemeScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
@ -1461,8 +1463,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ScraperSettings"
|
||||
<Stack.Screen
|
||||
name="ScraperSettings"
|
||||
component={PluginsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
@ -1476,8 +1478,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="CastMovies"
|
||||
<Stack.Screen
|
||||
name="CastMovies"
|
||||
component={CastMoviesScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
@ -1491,8 +1493,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Update"
|
||||
<Stack.Screen
|
||||
name="Update"
|
||||
component={UpdateScreen}
|
||||
options={{
|
||||
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>
|
||||
</View>
|
||||
</PaperProvider>
|
||||
|
|
@ -1571,8 +1588,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
};
|
||||
|
||||
const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => (
|
||||
<PostHogProvider
|
||||
apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C"
|
||||
<PostHogProvider
|
||||
apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C"
|
||||
options={{
|
||||
host: "https://us.i.posthog.com",
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -524,8 +524,8 @@ const createStyles = (colors: any) => 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 = () => {
|
|||
<View style={styles.addonItem}>
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderButtons}>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonUp(item)}
|
||||
disabled={isFirstItem}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-upward"
|
||||
size={20}
|
||||
color={isFirstItem ? colors.mediumGray : colors.white}
|
||||
<MaterialIcons
|
||||
name="arrow-upward"
|
||||
size={20}
|
||||
color={isFirstItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.reorderButton, isLastItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonDown(item)}
|
||||
disabled={isLastItem}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-downward"
|
||||
size={20}
|
||||
<MaterialIcons
|
||||
name="arrow-downward"
|
||||
size={20}
|
||||
color={isLastItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
<View style={styles.addonHeader}>
|
||||
{logo ? (
|
||||
<FastImage
|
||||
source={{ uri: logo }}
|
||||
<FastImage
|
||||
source={{ uri: logo }}
|
||||
style={styles.addonIcon}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
|
|
@ -1016,7 +1026,7 @@ const AddonsScreen = () => {
|
|||
{!reorderMode ? (
|
||||
<>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item, item.transport)}
|
||||
>
|
||||
|
|
@ -1024,7 +1034,7 @@ const AddonsScreen = () => {
|
|||
</TouchableOpacity>
|
||||
)}
|
||||
{!stremioService.isPreInstalledAddon(item.id) && (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleRemoveAddon(item)}
|
||||
>
|
||||
|
|
@ -1039,7 +1049,7 @@ const AddonsScreen = () => {
|
|||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<Text style={styles.addonDescription}>
|
||||
{description.length > 100 ? description.substring(0, 100) + '...' : description}
|
||||
</Text>
|
||||
|
|
@ -1077,9 +1087,9 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.communityAddonName}>{manifest.name}</Text>
|
||||
<Text style={styles.communityAddonDesc} numberOfLines={2}>{description}</Text>
|
||||
<View style={styles.communityAddonMetaContainer}>
|
||||
<Text style={styles.communityAddonVersion}>v{manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.communityAddonDot}>•</Text>
|
||||
<Text style={styles.communityAddonCategory}>{categoryText}</Text>
|
||||
<Text style={styles.communityAddonVersion}>v{manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.communityAddonDot}>•</Text>
|
||||
<Text style={styles.communityAddonCategory}>{categoryText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActionButtons}>
|
||||
|
|
@ -1117,50 +1127,50 @@ const AddonsScreen = () => {
|
|||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Reorder Mode Toggle Button */}
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
|
||||
onPress={toggleReorderMode}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="swap-vert"
|
||||
size={24}
|
||||
color={reorderMode ? colors.primary : colors.white}
|
||||
<MaterialIcons
|
||||
name="swap-vert"
|
||||
size={24}
|
||||
color={reorderMode ? colors.primary : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
{/* Refresh Button */}
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={refreshAddons}
|
||||
disabled={loading}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="refresh"
|
||||
size={24}
|
||||
color={loading ? colors.mediumGray : colors.white}
|
||||
<MaterialIcons
|
||||
name="refresh"
|
||||
size={24}
|
||||
color={loading ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<Text style={styles.headerTitle}>
|
||||
Addons
|
||||
{reorderMode && <Text style={styles.reorderModeText}> (Reorder Mode)</Text>}
|
||||
</Text>
|
||||
|
||||
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderInfoBanner}>
|
||||
<MaterialIcons name="info-outline" size={18} color={colors.primary} />
|
||||
|
|
@ -1169,18 +1179,18 @@ const AddonsScreen = () => {
|
|||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
>
|
||||
|
||||
|
||||
{/* Overview Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>OVERVIEW</Text>
|
||||
|
|
@ -1192,7 +1202,7 @@ const AddonsScreen = () => {
|
|||
<StatsCard value={catalogCount} label="Catalogs" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
{/* Hide Add Addon Section in reorder mode */}
|
||||
{!reorderMode && (
|
||||
<View style={styles.section}>
|
||||
|
|
@ -1207,8 +1217,8 @@ const AddonsScreen = () => {
|
|||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, { opacity: installing || !addonUrl ? 0.6 : 1 }]}
|
||||
onPress={() => handleAddAddon()}
|
||||
disabled={installing || !addonUrl}
|
||||
>
|
||||
|
|
@ -1219,7 +1229,7 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
{/* Installed Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
|
|
@ -1233,8 +1243,8 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
) : (
|
||||
addons.map((addon, index) => (
|
||||
<View
|
||||
key={addon.id}
|
||||
<View
|
||||
key={addon.id}
|
||||
style={{ marginBottom: index === addons.length - 1 ? 32 : 0 }}
|
||||
>
|
||||
{renderAddonItem({ item: addon, index })}
|
||||
|
|
@ -1245,68 +1255,68 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
|
||||
{/* Separator */}
|
||||
<View style={styles.sectionSeparator} />
|
||||
<View style={styles.sectionSeparator} />
|
||||
|
||||
{/* Promotional Addon Section (hidden if installed) */}
|
||||
{!isPromoInstalled && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>OFFICIAL ADDON</Text>
|
||||
<View style={styles.addonList}>
|
||||
<View style={styles.addonItem}>
|
||||
<View style={styles.addonHeader}>
|
||||
{promoAddon.logo ? (
|
||||
<FastImage
|
||||
source={{ uri: promoAddon.logo }}
|
||||
style={styles.addonIcon}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.addonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.mediumGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.addonTitleContainer}>
|
||||
<Text style={styles.addonName}>{promoAddon.name}</Text>
|
||||
<View style={styles.addonMetaContainer}>
|
||||
<Text style={styles.addonVersion}>v{promoAddon.version}</Text>
|
||||
<Text style={styles.addonDot}>•</Text>
|
||||
<Text style={styles.addonCategory}>{promoAddon.types?.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{promoAddon.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.installButton}
|
||||
onPress={() => handleAddAddon(PROMO_ADDON_URL)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.addonDescription}>
|
||||
{promoAddon.description}
|
||||
</Text>
|
||||
<Text style={[styles.addonDescription, { marginTop: 4, opacity: 0.9 }]}>
|
||||
Configure and install for full functionality.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{/* Promotional Addon Section (hidden if installed) */}
|
||||
{!isPromoInstalled && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>OFFICIAL ADDON</Text>
|
||||
<View style={styles.addonList}>
|
||||
<View style={styles.addonItem}>
|
||||
<View style={styles.addonHeader}>
|
||||
{promoAddon.logo ? (
|
||||
<FastImage
|
||||
source={{ uri: promoAddon.logo }}
|
||||
style={styles.addonIcon}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.addonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.mediumGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.addonTitleContainer}>
|
||||
<Text style={styles.addonName}>{promoAddon.name}</Text>
|
||||
<View style={styles.addonMetaContainer}>
|
||||
<Text style={styles.addonVersion}>v{promoAddon.version}</Text>
|
||||
<Text style={styles.addonDot}>•</Text>
|
||||
<Text style={styles.addonCategory}>{promoAddon.types?.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{promoAddon.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.installButton}
|
||||
onPress={() => handleAddAddon(PROMO_ADDON_URL)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.addonDescription}>
|
||||
{promoAddon.description}
|
||||
</Text>
|
||||
<Text style={[styles.addonDescription, { marginTop: 4, opacity: 0.9 }]}>
|
||||
Configure and install for full functionality.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Community Addons Section */}
|
||||
{/* Community Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>COMMUNITY ADDONS</Text>
|
||||
<View style={styles.addonList}>
|
||||
|
|
@ -1326,15 +1336,15 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
) : (
|
||||
communityAddons.map((item, index) => (
|
||||
<View
|
||||
key={item.transportUrl}
|
||||
<View
|
||||
key={item.transportUrl}
|
||||
style={{ marginBottom: index === communityAddons.length - 1 ? 32 : 16 }}
|
||||
>
|
||||
<View style={styles.addonItem}>
|
||||
<View style={styles.addonHeader}>
|
||||
{item.manifest.logo ? (
|
||||
<FastImage
|
||||
source={{ uri: item.manifest.logo }}
|
||||
<FastImage
|
||||
source={{ uri: item.manifest.logo }}
|
||||
style={styles.addonIcon}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
|
|
@ -1357,14 +1367,14 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{item.manifest.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(item.transportUrl)}
|
||||
disabled={installing}
|
||||
|
|
@ -1377,12 +1387,12 @@ const AddonsScreen = () => {
|
|||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<Text style={styles.addonDescription}>
|
||||
{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.'}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -1429,8 +1439,8 @@ const AddonsScreen = () => {
|
|||
<MaterialIcons name="close" size={24} color={colors.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
|
||||
<ScrollView
|
||||
style={styles.modalScrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={true}
|
||||
|
|
@ -1451,14 +1461,14 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.addonDetailName}>{addonDetails.name}</Text>
|
||||
<Text style={styles.addonDetailVersion}>v{addonDetails.version || '1.0.0'}</Text>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={styles.addonDetailSection}>
|
||||
<Text style={styles.addonDetailSectionTitle}>Description</Text>
|
||||
<Text style={styles.addonDetailDescription}>
|
||||
{addonDetails.description || 'No description available'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
{addonDetails.types && addonDetails.types.length > 0 && (
|
||||
<View style={styles.addonDetailSection}>
|
||||
<Text style={styles.addonDetailSectionTitle}>Supported Types</Text>
|
||||
|
|
@ -1471,7 +1481,7 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
{addonDetails.catalogs && addonDetails.catalogs.length > 0 && (
|
||||
<View style={styles.addonDetailSection}>
|
||||
<Text style={styles.addonDetailSectionTitle}>Catalogs</Text>
|
||||
|
|
@ -1487,7 +1497,7 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalButton, styles.cancelButton]}
|
||||
|
|
@ -1515,15 +1525,15 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
{/* Custom Alert Modal */}
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
title={alertTitle}
|
||||
message={alertMessage}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
{/* Custom Alert Modal */}
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
title={alertTitle}
|
||||
message={alertMessage}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
797
src/screens/DebridIntegrationScreen.tsx
Normal file
797
src/screens/DebridIntegrationScreen.tsx
Normal 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;
|
||||
|
|
@ -67,9 +67,9 @@ interface SettingsCardProps {
|
|||
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, isTablet = false }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
<View
|
||||
<View
|
||||
style={[
|
||||
styles.cardContainer,
|
||||
isTablet && styles.tabletCardContainer
|
||||
|
|
@ -119,13 +119,13 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
isTablet = false
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.6}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
styles.settingItem,
|
||||
styles.settingItem,
|
||||
!isLast && styles.settingItemBorder,
|
||||
{ borderBottomColor: currentTheme.colors.elevation2 },
|
||||
isTablet && styles.tabletSettingItem
|
||||
|
|
@ -133,7 +133,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
>
|
||||
<View style={[
|
||||
styles.settingIconContainer,
|
||||
{
|
||||
{
|
||||
backgroundColor: currentTheme.colors.darkGray,
|
||||
borderWidth: 1,
|
||||
borderColor: currentTheme.colors.primary + '20'
|
||||
|
|
@ -143,17 +143,17 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
{customIcon ? (
|
||||
customIcon
|
||||
) : (
|
||||
<Feather
|
||||
name={icon! as any}
|
||||
size={isTablet ? 24 : 20}
|
||||
color={currentTheme.colors.primary}
|
||||
<Feather
|
||||
name={icon! as any}
|
||||
size={isTablet ? 24 : 20}
|
||||
color={currentTheme.colors.primary}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[
|
||||
styles.settingTitle,
|
||||
styles.settingTitle,
|
||||
{ color: currentTheme.colors.highEmphasis },
|
||||
isTablet && styles.tabletSettingTitle
|
||||
]}>
|
||||
|
|
@ -161,7 +161,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
</Text>
|
||||
{description && (
|
||||
<Text style={[
|
||||
styles.settingDescription,
|
||||
styles.settingDescription,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
isTablet && styles.tabletSettingDescription
|
||||
]} numberOfLines={1}>
|
||||
|
|
@ -224,16 +224,16 @@ const Sidebar: React.FC<SidebarProps> = ({ 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
|
||||
}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.sidebarItemText,
|
||||
{
|
||||
color: selectedCategory === category.id
|
||||
? currentTheme.colors.primary
|
||||
color: 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 = () => (
|
||||
<Feather
|
||||
name="chevron-right"
|
||||
size={isTablet ? 24 : 20}
|
||||
<Feather
|
||||
name="chevron-right"
|
||||
size={isTablet ? 24 : 20}
|
||||
color={currentTheme.colors.mediumEmphasis}
|
||||
/>
|
||||
);
|
||||
|
|
@ -527,6 +527,14 @@ const SettingsScreen: React.FC = () => {
|
|||
onPress={() => navigation.navigate('Addons')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Debrid Integration"
|
||||
description="Connect Torbox for premium streams"
|
||||
icon="link"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('DebridIntegration')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Plugins"
|
||||
description="Manage plugins and repositories"
|
||||
|
|
@ -648,7 +656,7 @@ const SettingsScreen: React.FC = () => {
|
|||
<SettingsCard title="PLAYBACK" isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title="Video Player"
|
||||
description={Platform.OS === 'ios'
|
||||
description={Platform.OS === 'ios'
|
||||
? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in')
|
||||
: (settings?.useExternalPlayer ? 'External' : 'Built-in')
|
||||
}
|
||||
|
|
@ -764,7 +772,7 @@ const SettingsScreen: React.FC = () => {
|
|||
'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}
|
||||
/>
|
||||
|
||||
|
||||
<View style={[
|
||||
styles.tabletContent,
|
||||
{
|
||||
paddingTop: (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 24 : 48) + tabletNavOffset,
|
||||
}
|
||||
]}>
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
style={styles.tabletScrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.tabletScrollContent}
|
||||
>
|
||||
{renderCategoryContent(selectedCategory)}
|
||||
|
||||
|
||||
{selectedCategory === 'about' && (
|
||||
<>
|
||||
{displayDownloads !== null && (
|
||||
|
|
@ -887,9 +895,9 @@ const SettingsScreen: React.FC = () => {
|
|||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
<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
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -906,7 +914,7 @@ const SettingsScreen: React.FC = () => {
|
|||
style={styles.discordLogo}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
<Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}>
|
||||
<Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Join Discord
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -958,7 +966,7 @@ const SettingsScreen: React.FC = () => {
|
|||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
|
|
@ -1006,7 +1014,7 @@ const SettingsScreen: React.FC = () => {
|
|||
style={styles.discordLogo}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
<Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}>
|
||||
<Text style={[styles.discordButtonText, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Join Discord
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -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%',
|
||||
|
|
|
|||
Loading…
Reference in a new issue