mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-22 09:11:56 +00:00
refactor for tf
This commit is contained in:
parent
8a34bf6678
commit
9cc8b2ea67
6 changed files with 431 additions and 602 deletions
|
|
@ -155,7 +155,7 @@ export type RootStackParamList = {
|
||||||
TraktSettings: undefined;
|
TraktSettings: undefined;
|
||||||
PlayerSettings: undefined;
|
PlayerSettings: undefined;
|
||||||
ThemeSettings: undefined;
|
ThemeSettings: undefined;
|
||||||
ScraperSettings: undefined;
|
PluginSettings: undefined;
|
||||||
CastMovies: {
|
CastMovies: {
|
||||||
castMember: {
|
castMember: {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -1463,7 +1463,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ScraperSettings"
|
name="PluginSettings"
|
||||||
component={PluginsScreen}
|
component={PluginsScreen}
|
||||||
options={{
|
options={{
|
||||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ if (Platform.OS === 'ios') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Removed community blur and expo-constants for Android overlay
|
// Removed community blur and expo-constants for Android overlay
|
||||||
import axios from 'axios';
|
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
// Extend Manifest type to include logo only (remove disabled status)
|
// Extend Manifest type to include logo only (remove disabled status)
|
||||||
|
|
@ -60,11 +60,7 @@ interface ExtendedManifest extends Manifest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for Community Addon structure from the JSON URL
|
|
||||||
interface CommunityAddon {
|
|
||||||
transportUrl: string;
|
|
||||||
manifest: ExtendedManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -623,10 +619,7 @@ const AddonsScreen = () => {
|
||||||
const colors = currentTheme.colors;
|
const colors = currentTheme.colors;
|
||||||
const styles = createStyles(colors);
|
const styles = createStyles(colors);
|
||||||
|
|
||||||
// State for community addons
|
|
||||||
const [communityAddons, setCommunityAddons] = useState<CommunityAddon[]>([]);
|
|
||||||
const [communityLoading, setCommunityLoading] = useState(true);
|
|
||||||
const [communityError, setCommunityError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Promotional addon: Nuvio Streams
|
// Promotional addon: Nuvio Streams
|
||||||
const PROMO_ADDON_URL = 'https://nuviostreams.hayd.uk/manifest.json';
|
const PROMO_ADDON_URL = 'https://nuviostreams.hayd.uk/manifest.json';
|
||||||
|
|
@ -652,7 +645,6 @@ const AddonsScreen = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAddons();
|
loadAddons();
|
||||||
loadCommunityAddons();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadAddons = async () => {
|
const loadAddons = async () => {
|
||||||
|
|
@ -702,27 +694,7 @@ const AddonsScreen = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to load community addons
|
|
||||||
const loadCommunityAddons = async () => {
|
|
||||||
setCommunityLoading(true);
|
|
||||||
setCommunityError(null);
|
|
||||||
try {
|
|
||||||
const response = await axios.get<CommunityAddon[]>('https://stremio-addons.com/catalog.json');
|
|
||||||
// Filter out addons without a manifest or transportUrl (basic validation)
|
|
||||||
let validAddons = response.data.filter(addon => addon.manifest && addon.transportUrl);
|
|
||||||
|
|
||||||
// Filter out Cinemeta since it's now pre-installed
|
|
||||||
validAddons = validAddons.filter(addon => addon.manifest.id !== 'com.linvo.cinemeta');
|
|
||||||
|
|
||||||
setCommunityAddons(validAddons);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to load community addons:', error);
|
|
||||||
setCommunityError('Failed to load community addons. Please try again later.');
|
|
||||||
setCommunityAddons([]);
|
|
||||||
} finally {
|
|
||||||
setCommunityLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddAddon = async (url?: string) => {
|
const handleAddAddon = async (url?: string) => {
|
||||||
let urlToInstall = url || addonUrl;
|
let urlToInstall = url || addonUrl;
|
||||||
|
|
@ -783,7 +755,6 @@ const AddonsScreen = () => {
|
||||||
|
|
||||||
const refreshAddons = async () => {
|
const refreshAddons = async () => {
|
||||||
loadAddons();
|
loadAddons();
|
||||||
loadCommunityAddons();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
const moveAddonUp = (addon: ExtendedManifest) => {
|
||||||
|
|
@ -835,7 +806,7 @@ const AddonsScreen = () => {
|
||||||
configUrl = addon.behaviorHints.configurationURL;
|
configUrl = addon.behaviorHints.configurationURL;
|
||||||
logger.info(`Using configurationURL from behaviorHints: ${configUrl}`);
|
logger.info(`Using configurationURL from behaviorHints: ${configUrl}`);
|
||||||
}
|
}
|
||||||
// If a transport URL was provided directly (for community addons)
|
// If a transport URL was provided directly
|
||||||
else if (transportUrl) {
|
else if (transportUrl) {
|
||||||
// Remove any trailing filename like manifest.json
|
// Remove any trailing filename like manifest.json
|
||||||
const baseUrl = transportUrl.replace(/\/[^\/]+\.json$/, '/');
|
const baseUrl = transportUrl.replace(/\/[^\/]+\.json$/, '/');
|
||||||
|
|
@ -1057,65 +1028,7 @@ const AddonsScreen = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to render community addon items
|
|
||||||
const renderCommunityAddonItem = ({ item }: { item: CommunityAddon }) => {
|
|
||||||
const { manifest, transportUrl } = item;
|
|
||||||
const types = manifest.types || [];
|
|
||||||
const description = manifest.description || 'No description provided.';
|
|
||||||
// @ts-ignore - logo might exist
|
|
||||||
const logo = manifest.logo || null;
|
|
||||||
const categoryText = types.length > 0
|
|
||||||
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
|
||||||
: 'General';
|
|
||||||
// Check if addon is configurable
|
|
||||||
const isConfigurable = manifest.behaviorHints?.configurable === true;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.communityAddonItem}>
|
|
||||||
{logo ? (
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: logo }}
|
|
||||||
style={styles.communityAddonIcon}
|
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View style={styles.communityAddonIconPlaceholder}>
|
|
||||||
<MaterialIcons name="extension" size={22} color={colors.darkGray} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={styles.communityAddonDetails}>
|
|
||||||
<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>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.addonActionButtons}>
|
|
||||||
{isConfigurable && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.configButton}
|
|
||||||
onPress={() => handleConfigureAddon(manifest, transportUrl)}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
|
||||||
onPress={() => handleAddAddon(transportUrl)}
|
|
||||||
disabled={installing}
|
|
||||||
>
|
|
||||||
{installing ? (
|
|
||||||
<ActivityIndicator size="small" color={colors.white} />
|
|
||||||
) : (
|
|
||||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatsCard = ({ value, label }: { value: number; label: string }) => (
|
const StatsCard = ({ value, label }: { value: number; label: string }) => (
|
||||||
<View style={styles.statsCard}>
|
<View style={styles.statsCard}>
|
||||||
|
|
@ -1316,91 +1229,7 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Community Addons Section */}
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Text style={styles.sectionTitle}>COMMUNITY ADDONS</Text>
|
|
||||||
<View style={styles.addonList}>
|
|
||||||
{communityLoading ? (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
|
||||||
</View>
|
|
||||||
) : communityError ? (
|
|
||||||
<View style={styles.emptyContainer}>
|
|
||||||
<MaterialIcons name="error-outline" size={32} color={colors.error} />
|
|
||||||
<Text style={styles.emptyText}>{communityError}</Text>
|
|
||||||
</View>
|
|
||||||
) : communityAddons.length === 0 ? (
|
|
||||||
<View style={styles.emptyContainer}>
|
|
||||||
<MaterialIcons name="extension-off" size={32} color={colors.mediumGray} />
|
|
||||||
<Text style={styles.emptyText}>No community addons available</Text>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
communityAddons.map((item, index) => (
|
|
||||||
<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 }}
|
|
||||||
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}>{item.manifest.name}</Text>
|
|
||||||
<View style={styles.addonMetaContainer}>
|
|
||||||
<Text style={styles.addonVersion}>v{item.manifest.version || 'N/A'}</Text>
|
|
||||||
<Text style={styles.addonDot}>•</Text>
|
|
||||||
<Text style={styles.addonCategory}>
|
|
||||||
{item.manifest.types && item.manifest.types.length > 0
|
|
||||||
? item.manifest.types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
|
||||||
: 'General'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.addonActions}>
|
|
||||||
{item.manifest.behaviorHints?.configurable && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.configButton}
|
|
||||||
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
|
||||||
onPress={() => handleAddAddon(item.transportUrl)}
|
|
||||||
disabled={installing}
|
|
||||||
>
|
|
||||||
{installing ? (
|
|
||||||
<ActivityIndicator size="small" color={colors.white} />
|
|
||||||
) : (
|
|
||||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.addonDescription}>
|
|
||||||
{item.manifest.description
|
|
||||||
? (item.manifest.description.length > 100
|
|
||||||
? item.manifest.description.substring(0, 100) + '...'
|
|
||||||
: item.manifest.description)
|
|
||||||
: 'No description provided.'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,17 +78,17 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
sectionHeader: {
|
sectionHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
sectionDescription: {
|
sectionDescription: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: colors.mediumGray,
|
color: colors.mediumGray,
|
||||||
|
|
@ -283,59 +283,59 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
infoText: {
|
infoText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: colors.mediumEmphasis,
|
color: colors.mediumEmphasis,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 32,
|
paddingVertical: 32,
|
||||||
},
|
},
|
||||||
emptyStateTitle: {
|
emptyStateTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
emptyStateDescription: {
|
emptyStateDescription: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: colors.mediumGray,
|
color: colors.mediumGray,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
scrapersList: {
|
scrapersList: {
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
scrapersContainer: {
|
scrapersContainer: {
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
lastSection: {
|
lastSection: {
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
},
|
},
|
||||||
disabledSection: {
|
disabledSection: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledText: {
|
disabledText: {
|
||||||
color: colors.elevation3,
|
color: colors.elevation3,
|
||||||
},
|
},
|
||||||
disabledContainer: {
|
disabledContainer: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledInput: {
|
disabledInput: {
|
||||||
backgroundColor: colors.elevation1,
|
backgroundColor: colors.elevation1,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledButton: {
|
disabledButton: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledImage: {
|
disabledImage: {
|
||||||
opacity: 0.3,
|
opacity: 0.3,
|
||||||
},
|
},
|
||||||
availableIndicator: {
|
availableIndicator: {
|
||||||
|
|
@ -842,7 +842,7 @@ const PluginsScreen: 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -940,7 +940,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
openAlert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredScrapers.length} scrapers`);
|
openAlert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredScrapers.length} scrapers`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to bulk toggle:', error);
|
logger.error('[PluginsScreen] Failed to bulk toggle:', error);
|
||||||
openAlert('Error', 'Failed to update scrapers');
|
openAlert('Error', 'Failed to update scrapers');
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
|
|
@ -1021,7 +1021,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
openAlert('Success', 'Repository switched successfully');
|
openAlert('Success', 'Repository switched successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to switch repository:', error);
|
logger.error('[PluginsScreen] Failed to switch repository:', error);
|
||||||
openAlert('Error', 'Failed to switch repository');
|
openAlert('Error', 'Failed to switch repository');
|
||||||
} finally {
|
} finally {
|
||||||
setSwitchingRepository(null);
|
setSwitchingRepository(null);
|
||||||
|
|
@ -1044,7 +1044,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
alertTitle,
|
alertTitle,
|
||||||
alertMessage,
|
alertMessage,
|
||||||
[
|
[
|
||||||
{ label: 'Cancel', onPress: () => {} },
|
{ label: 'Cancel', onPress: () => { } },
|
||||||
{
|
{
|
||||||
label: 'Remove',
|
label: 'Remove',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
|
|
@ -1057,7 +1057,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
: 'Repository removed successfully';
|
: 'Repository removed successfully';
|
||||||
openAlert('Success', successMessage);
|
openAlert('Success', successMessage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to remove repository:', error);
|
logger.error('[PluginsScreen] Failed to remove repository:', error);
|
||||||
openAlert('Error', error instanceof Error ? error.message : 'Failed to remove repository');
|
openAlert('Error', error instanceof Error ? error.message : 'Failed to remove repository');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1097,7 +1097,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
setShowboxTokenVisible(false);
|
setShowboxTokenVisible(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to load scrapers:', error);
|
logger.error('[PluginsScreen] Failed to load scrapers:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1118,7 +1118,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
setRepositoryUrl(currentRepo.url);
|
setRepositoryUrl(currentRepo.url);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to load repositories:', error);
|
logger.error('[PluginsScreen] Failed to load repositories:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1130,7 +1130,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
setRepositoryUrl(repoUrl);
|
setRepositoryUrl(repoUrl);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to check repository:', error);
|
logger.error('[PluginsScreen] Failed to check repository:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1157,7 +1157,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
setHasRepository(true);
|
setHasRepository(true);
|
||||||
openAlert('Success', 'Repository URL saved successfully');
|
openAlert('Success', 'Repository URL saved successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to save repository:', error);
|
logger.error('[PluginsScreen] Failed to save repository:', error);
|
||||||
openAlert('Error', 'Failed to save repository URL');
|
openAlert('Error', 'Failed to save repository URL');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
@ -1211,28 +1211,28 @@ const PluginsScreen: React.FC = () => {
|
||||||
await pluginService.setScraperEnabled(scraperId, enabled);
|
await pluginService.setScraperEnabled(scraperId, enabled);
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to toggle scraper:', error);
|
logger.error('[PluginsScreen] Failed to toggle plugin:', error);
|
||||||
openAlert('Error', 'Failed to update scraper status');
|
openAlert('Error', 'Failed to update plugin status');
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearScrapers = () => {
|
const handleClearScrapers = () => {
|
||||||
openAlert(
|
openAlert(
|
||||||
'Clear All Scrapers',
|
'Clear All Plugins',
|
||||||
'Are you sure you want to remove all installed scrapers? This action cannot be undone.',
|
'Are you sure you want to remove all installed plugins? This action cannot be undone.',
|
||||||
[
|
[
|
||||||
{ label: 'Cancel', onPress: () => {} },
|
{ label: 'Cancel', onPress: () => { } },
|
||||||
{
|
{
|
||||||
label: 'Clear',
|
label: 'Clear',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
try {
|
try {
|
||||||
await pluginService.clearScrapers();
|
await pluginService.clearScrapers();
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
openAlert('Success', 'All scrapers have been removed');
|
openAlert('Success', 'All plugins have been removed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to clear scrapers:', error);
|
logger.error('[PluginsScreen] Failed to clear plugins:', error);
|
||||||
openAlert('Error', 'Failed to clear scrapers');
|
openAlert('Error', 'Failed to clear plugins');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -1243,9 +1243,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
const handleClearCache = () => {
|
const handleClearCache = () => {
|
||||||
openAlert(
|
openAlert(
|
||||||
'Clear Repository Cache',
|
'Clear Repository Cache',
|
||||||
'This will remove the saved repository URL and clear all cached scraper data. You will need to re-enter your repository URL.',
|
'This will remove the saved repository URL and clear all cached plugin data. You will need to re-enter your repository URL.',
|
||||||
[
|
[
|
||||||
{ label: 'Cancel', onPress: () => {} },
|
{ label: 'Cancel', onPress: () => { } },
|
||||||
{
|
{
|
||||||
label: 'Clear Cache',
|
label: 'Clear Cache',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
|
|
@ -1258,7 +1258,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
openAlert('Success', 'Repository cache cleared successfully');
|
openAlert('Success', 'Repository cache cleared successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[ScraperSettings] Failed to clear cache:', error);
|
logger.error('[PluginsScreen] Failed to clear cache:', error);
|
||||||
openAlert('Error', 'Failed to clear repository cache');
|
openAlert('Error', 'Failed to clear repository cache');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1427,7 +1427,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
styles={styles}
|
styles={styles}
|
||||||
>
|
>
|
||||||
<Text style={styles.sectionDescription}>
|
<Text style={styles.sectionDescription}>
|
||||||
Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers.
|
Manage multiple plugin repositories. Switch between repositories to access different sets of plugins.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Current Repository */}
|
{/* Current Repository */}
|
||||||
|
|
@ -1466,9 +1466,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
<Text style={styles.repositoryUrl}>{repo.url}</Text>
|
<Text style={styles.repositoryUrl}>{repo.url}</Text>
|
||||||
<Text style={styles.repositoryMeta}>
|
<Text style={styles.repositoryMeta}>
|
||||||
{repo.scraperCount || 0} scrapers • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
|
{repo.scraperCount || 0} plugins • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.repositoryActions}>
|
<View style={styles.repositoryActions}>
|
||||||
{repo.id !== currentRepositoryId && (
|
{repo.id !== currentRepositoryId && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -1502,7 +1502,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1535,15 +1535,15 @@ const PluginsScreen: React.FC = () => {
|
||||||
style={styles.searchInput}
|
style={styles.searchInput}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChangeText={setSearchQuery}
|
onChangeText={setSearchQuery}
|
||||||
placeholder="Search scrapers..."
|
placeholder="Search plugins..."
|
||||||
placeholderTextColor={colors.mediumGray}
|
placeholderTextColor={colors.mediumGray}
|
||||||
/>
|
/>
|
||||||
{searchQuery.length > 0 && (
|
{searchQuery.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
||||||
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Filter Chips */}
|
{/* Filter Chips */}
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
|
|
@ -1561,7 +1561,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
selectedFilter === filter && styles.filterChipTextSelected
|
selectedFilter === filter && styles.filterChipTextSelected
|
||||||
]}>
|
]}>
|
||||||
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1597,12 +1597,12 @@ const PluginsScreen: React.FC = () => {
|
||||||
style={styles.emptyStateIcon}
|
style={styles.emptyStateIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.emptyStateTitle}>
|
<Text style={styles.emptyStateTitle}>
|
||||||
{searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'}
|
{searchQuery ? 'No Plugins Found' : 'No Plugins Available'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.emptyStateDescription}>
|
<Text style={styles.emptyStateDescription}>
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? `No scrapers match "${searchQuery}". Try a different search term.`
|
? `No plugins match "${searchQuery}". Try a different search term.`
|
||||||
: 'Configure a repository above to view available scrapers.'
|
: 'Configure a repository above to view available plugins.'
|
||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
|
|
@ -1613,44 +1613,44 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.scrapersContainer}>
|
<View style={styles.scrapersContainer}>
|
||||||
{filteredScrapers.map((scraper) => (
|
{filteredScrapers.map((scraper) => (
|
||||||
<View key={scraper.id} style={styles.scraperCard}>
|
<View key={scraper.id} style={styles.scraperCard}>
|
||||||
<View style={styles.scraperCardHeader}>
|
<View style={styles.scraperCardHeader}>
|
||||||
{scraper.logo ? (
|
{scraper.logo ? (
|
||||||
(scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? (
|
(scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: scraper.logo }}
|
source={{ uri: scraper.logo }}
|
||||||
style={styles.scraperLogo}
|
style={styles.scraperLogo}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: scraper.logo }}
|
source={{ uri: scraper.logo }}
|
||||||
style={styles.scraperLogo}
|
style={styles.scraperLogo}
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.scraperLogo} />
|
<View style={styles.scraperLogo} />
|
||||||
)}
|
)}
|
||||||
<View style={styles.scraperCardInfo}>
|
<View style={styles.scraperCardInfo}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}>
|
||||||
<Text style={styles.scraperName}>{scraper.name}</Text>
|
<Text style={styles.scraperName}>{scraper.name}</Text>
|
||||||
<StatusBadge status={getScraperStatus(scraper)} colors={colors} />
|
<StatusBadge status={getScraperStatus(scraper)} colors={colors} />
|
||||||
</View>
|
|
||||||
<Text style={styles.scraperDescription}>{scraper.description}</Text>
|
|
||||||
</View>
|
|
||||||
<Switch
|
|
||||||
value={scraper.enabled && settings.enableLocalScrapers}
|
|
||||||
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
|
||||||
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
|
||||||
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
<Text style={styles.scraperDescription}>{scraper.description}</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={scraper.enabled && settings.enableLocalScrapers}
|
||||||
|
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
||||||
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
|
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||||
|
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.scraperCardMeta}>
|
<View style={styles.scraperCardMeta}>
|
||||||
<View style={styles.scraperCardMetaItem}>
|
<View style={styles.scraperCardMetaItem}>
|
||||||
|
|
@ -1682,62 +1682,62 @@ const PluginsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ShowBox Settings - only visible when ShowBox scraper is available */}
|
{/* ShowBox Settings - only visible when ShowBox scraper is available */}
|
||||||
{showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && (
|
{showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && (
|
||||||
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
|
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
|
||||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
|
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
|
style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
|
||||||
value={showboxUiToken}
|
value={showboxUiToken}
|
||||||
onChangeText={setShowboxUiToken}
|
onChangeText={setShowboxUiToken}
|
||||||
placeholder="Paste your ShowBox UI token"
|
placeholder="Paste your ShowBox UI token"
|
||||||
placeholderTextColor={colors.mediumGray}
|
placeholderTextColor={colors.mediumGray}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible}
|
secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible}
|
||||||
multiline={false}
|
multiline={false}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
/>
|
/>
|
||||||
{showboxSavedToken.length > 0 && (
|
{showboxSavedToken.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
||||||
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
{showboxUiToken !== showboxSavedToken && (
|
{showboxUiToken !== showboxSavedToken && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.primaryButton]}
|
style={[styles.button, styles.primaryButton]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (showboxScraperId) {
|
if (showboxScraperId) {
|
||||||
await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken });
|
await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken });
|
||||||
}
|
}
|
||||||
setShowboxSavedToken(showboxUiToken);
|
setShowboxSavedToken(showboxUiToken);
|
||||||
openAlert('Saved', 'ShowBox settings updated');
|
openAlert('Saved', 'ShowBox settings updated');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Save</Text>
|
<Text style={styles.buttonText}>Save</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.secondaryButton]}
|
style={[styles.button, styles.secondaryButton]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setShowboxUiToken('');
|
setShowboxUiToken('');
|
||||||
setShowboxSavedToken('');
|
setShowboxSavedToken('');
|
||||||
if (showboxScraperId) {
|
if (showboxScraperId) {
|
||||||
await pluginService.setScraperSettings(showboxScraperId, {});
|
await pluginService.setScraperSettings(showboxScraperId, {});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Additional Settings */}
|
{/* Additional Settings */}
|
||||||
|
|
@ -1772,50 +1772,50 @@ const PluginsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.streamDisplayMode === 'grouped'}
|
value={settings.streamDisplayMode === 'grouped'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
|
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
|
||||||
// Auto-disable quality sorting when grouping is disabled
|
// Auto-disable quality sorting when grouping is disabled
|
||||||
if (!value && settings.streamSortMode === 'quality-then-scraper') {
|
if (!value && settings.streamSortMode === 'quality-then-scraper') {
|
||||||
updateSetting('streamSortMode', 'scraper-then-quality');
|
updateSetting('streamSortMode', 'scraper-then-quality');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
|
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
|
||||||
disabled={!settings.enableLocalScrapers}
|
disabled={!settings.enableLocalScrapers}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.settingRow}>
|
<View style={styles.settingRow}>
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Sort by Quality First</Text>
|
<Text style={styles.settingTitle}>Sort by Quality First</Text>
|
||||||
<Text style={styles.settingDescription}>
|
<Text style={styles.settingDescription}>
|
||||||
When enabled, streams are sorted by quality first, then by scraper. When disabled, streams are sorted by scraper first, then by quality. Only available when grouping is enabled.
|
When enabled, streams are sorted by quality first, then by plugin. When disabled, streams are sorted by plugin first, then by quality. Only available when grouping is enabled.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.streamSortMode === 'quality-then-scraper'}
|
value={settings.streamSortMode === 'quality-then-scraper'}
|
||||||
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
|
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
|
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
|
||||||
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
|
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.settingRow}>
|
<View style={styles.settingRow}>
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Show Scraper Logos</Text>
|
<Text style={styles.settingTitle}>Show Plugin Logos</Text>
|
||||||
<Text style={styles.settingDescription}>
|
<Text style={styles.settingDescription}>
|
||||||
Display scraper logos next to streaming links on the streams screen.
|
Display plugin logos next to streaming links on the streams screen.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.showScraperLogos && settings.enableLocalScrapers}
|
value={settings.showScraperLogos && settings.enableLocalScrapers}
|
||||||
onValueChange={(value) => updateSetting('showScraperLogos', value)}
|
onValueChange={(value) => updateSetting('showScraperLogos', value)}
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||||
disabled={!settings.enableLocalScrapers}
|
disabled={!settings.enableLocalScrapers}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
|
@ -1944,10 +1944,10 @@ const PluginsScreen: React.FC = () => {
|
||||||
2. <Text style={{ fontWeight: '600' }}>Add Repository</Text> - Add a GitHub raw URL or use the default repository
|
2. <Text style={{ fontWeight: '600' }}>Add Repository</Text> - Add a GitHub raw URL or use the default repository
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.modalText}>
|
<Text style={styles.modalText}>
|
||||||
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Download available scrapers from the repository
|
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Download available plugins from the repository
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.modalText}>
|
<Text style={styles.modalText}>
|
||||||
4. <Text style={{ fontWeight: '600' }}>Enable Scrapers</Text> - Turn on the scrapers you want to use for streaming
|
4. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the plugins you want to use for streaming
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.modalButton}
|
style={styles.modalButton}
|
||||||
|
|
@ -1988,36 +1988,36 @@ const PluginsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* Format Hint */}
|
{/* Format Hint */}
|
||||||
<Text style={styles.formatHint}>
|
<Text style={styles.formatHint}>
|
||||||
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
|
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={styles.compactActions}>
|
<View style={styles.compactActions}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.compactButton, styles.cancelButton]}
|
style={[styles.compactButton, styles.cancelButton]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowAddRepositoryModal(false);
|
setShowAddRepositoryModal(false);
|
||||||
setNewRepositoryUrl('');
|
setNewRepositoryUrl('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
||||||
onPress={handleAddRepository}
|
onPress={handleAddRepository}
|
||||||
disabled={!newRepositoryUrl.trim() || isLoading}
|
disabled={!newRepositoryUrl.trim() || isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator size="small" color={colors.white} />
|
<ActivityIndicator size="small" color={colors.white} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.addButtonText}>Add</Text>
|
<Text style={styles.addButtonText}>Add</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -561,7 +561,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
description="Manage plugins and repositories"
|
description="Manage plugins and repositories"
|
||||||
customIcon={<PluginIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
|
customIcon={<PluginIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||||
renderControl={ChevronRight}
|
renderControl={ChevronRight}
|
||||||
onPress={() => navigation.navigate('ScraperSettings')}
|
onPress={() => navigation.navigate('PluginSettings')}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
|
|
|
||||||
|
|
@ -913,13 +913,13 @@ export const StreamsScreen = () => {
|
||||||
const handleStreamPress = useCallback(async (stream: Stream) => {
|
const handleStreamPress = useCallback(async (stream: Stream) => {
|
||||||
try {
|
try {
|
||||||
if (stream.url) {
|
if (stream.url) {
|
||||||
// Block magnet links - not supported yet
|
|
||||||
|
// Block magnet links with sanitized message
|
||||||
if (typeof stream.url === 'string' && stream.url.startsWith('magnet:')) {
|
if (typeof stream.url === 'string' && stream.url.startsWith('magnet:')) {
|
||||||
try {
|
openAlert('Stream Not Supported', 'This stream format is not supported.');
|
||||||
openAlert('Not supported', 'Torrent streaming is not supported yet.');
|
|
||||||
} catch (_e) { }
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If stream is actually MKV format, force the in-app VLC-based player on iOS
|
// If stream is actually MKV format, force the in-app VLC-based player on iOS
|
||||||
try {
|
try {
|
||||||
if (Platform.OS === 'ios' && settings.preferredPlayer === 'internal') {
|
if (Platform.OS === 'ios' && settings.preferredPlayer === 'internal') {
|
||||||
|
|
@ -1078,7 +1078,7 @@ export const StreamsScreen = () => {
|
||||||
const isMagnet = typeof stream.url === 'string' && stream.url.startsWith('magnet:');
|
const isMagnet = typeof stream.url === 'string' && stream.url.startsWith('magnet:');
|
||||||
|
|
||||||
if (isMagnet) {
|
if (isMagnet) {
|
||||||
// For magnet links, open directly which will trigger the torrent app chooser
|
// For magnet links, open directly
|
||||||
if (__DEV__) console.log('Opening magnet link directly');
|
if (__DEV__) console.log('Opening magnet link directly');
|
||||||
Linking.openURL(stream.url)
|
Linking.openURL(stream.url)
|
||||||
.then(() => { if (__DEV__) console.log('Successfully opened magnet link'); })
|
.then(() => { if (__DEV__) console.log('Successfully opened magnet link'); })
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ class LocalScraperService {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: this.extractRepositoryName(storedRepoUrl),
|
name: this.extractRepositoryName(storedRepoUrl),
|
||||||
url: storedRepoUrl,
|
url: storedRepoUrl,
|
||||||
description: 'Default repository',
|
description: 'Default Plugins Repository',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
lastUpdated: Date.now()
|
lastUpdated: Date.now()
|
||||||
|
|
@ -516,27 +516,27 @@ class LocalScraperService {
|
||||||
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`;
|
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`;
|
||||||
|
|
||||||
const response = await axios.get(manifestUrl, {
|
const response = await axios.get(manifestUrl, {
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'Pragma': 'no-cache',
|
'Pragma': 'no-cache',
|
||||||
'Expires': '0'
|
'Expires': '0'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const manifest: ScraperManifest = response.data;
|
const manifest: ScraperManifest = response.data;
|
||||||
|
|
||||||
// Store repository name from manifest
|
// Store repository name from manifest
|
||||||
if (manifest.name) {
|
if (manifest.name) {
|
||||||
this.repositoryName = manifest.name;
|
this.repositoryName = manifest.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('[LocalScraperService] getAvailableScrapers - Raw manifest data:', JSON.stringify(manifest, null, 2));
|
logger.log('[LocalScraperService] getAvailableScrapers - Raw manifest data:', JSON.stringify(manifest, null, 2));
|
||||||
logger.log('[LocalScraperService] getAvailableScrapers - Manifest scrapers count:', manifest.scrapers?.length || 0);
|
logger.log('[LocalScraperService] getAvailableScrapers - Manifest scrapers count:', manifest.scrapers?.length || 0);
|
||||||
|
|
||||||
// Log each scraper's enabled status from manifest
|
// Log each scraper's enabled status from manifest
|
||||||
manifest.scrapers?.forEach(scraper => {
|
manifest.scrapers?.forEach(scraper => {
|
||||||
logger.log(`[LocalScraperService] getAvailableScrapers - Scraper ${scraper.name}: enabled=${scraper.enabled}`);
|
logger.log(`[LocalScraperService] getAvailableScrapers - Scraper ${scraper.name}: enabled=${scraper.enabled}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log('[LocalScraperService] Found', manifest.scrapers.length, 'scrapers in repository');
|
logger.log('[LocalScraperService] Found', manifest.scrapers.length, 'scrapers in repository');
|
||||||
|
|
||||||
|
|
@ -760,13 +760,13 @@ class LocalScraperService {
|
||||||
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`;
|
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`;
|
||||||
|
|
||||||
const response = await axios.get(manifestUrl, {
|
const response = await axios.get(manifestUrl, {
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'Pragma': 'no-cache',
|
'Pragma': 'no-cache',
|
||||||
'Expires': '0'
|
'Expires': '0'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const manifest: ScraperManifest = response.data;
|
const manifest: ScraperManifest = response.data;
|
||||||
|
|
||||||
// Store repository name from manifest
|
// Store repository name from manifest
|
||||||
|
|
@ -808,12 +808,12 @@ class LocalScraperService {
|
||||||
|
|
||||||
logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository');
|
logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository');
|
||||||
|
|
||||||
// Log final scraper states being returned to UI
|
// Log final scraper states being returned to UI
|
||||||
availableScrapers.forEach(scraper => {
|
availableScrapers.forEach(scraper => {
|
||||||
logger.log(`[LocalScraperService] Final scraper ${scraper.name}: manifestEnabled=${scraper.manifestEnabled}, enabled=${scraper.enabled}`);
|
logger.log(`[LocalScraperService] Final scraper ${scraper.name}: manifestEnabled=${scraper.manifestEnabled}, enabled=${scraper.enabled}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return availableScrapers;
|
return availableScrapers;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[LocalScraperService] Failed to fetch available scrapers from manifest:', error);
|
logger.error('[LocalScraperService] Failed to fetch available scrapers from manifest:', error);
|
||||||
|
|
@ -920,7 +920,7 @@ class LocalScraperService {
|
||||||
if (enabledScrapers.length > 0) {
|
if (enabledScrapers.length > 0) {
|
||||||
try {
|
try {
|
||||||
logger.log('[LocalScraperService] Enabled scrapers:', enabledScrapers.map(s => s.name).join(', '));
|
logger.log('[LocalScraperService] Enabled scrapers:', enabledScrapers.map(s => s.name).join(', '));
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enabledScrapers.length === 0) {
|
if (enabledScrapers.length === 0) {
|
||||||
|
|
@ -983,7 +983,7 @@ class LocalScraperService {
|
||||||
promise.finally(() => {
|
promise.finally(() => {
|
||||||
const current = this.inFlightByKey.get(flightKey);
|
const current = this.inFlightByKey.get(flightKey);
|
||||||
if (current === promise) this.inFlightByKey.delete(flightKey);
|
if (current === promise) this.inFlightByKey.delete(flightKey);
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await promise;
|
const results = await promise;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue