mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-03 08:49:07 +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 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 = {
|
||||||
|
|
@ -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>;
|
||||||
|
|
@ -432,7 +434,7 @@ 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(() => {
|
||||||
|
|
@ -462,11 +464,11 @@ const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) =
|
||||||
// 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();
|
||||||
|
|
@ -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 />
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,11 +660,21 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
@ -1208,7 +1218,7 @@ const AddonsScreen = () => {
|
||||||
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}
|
||||||
>
|
>
|
||||||
|
|
@ -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}>
|
||||||
|
|
@ -1381,8 +1391,8 @@ const AddonsScreen = () => {
|
||||||
<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>
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
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;
|
||||||
|
|
@ -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; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue