mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-19 07:42:09 +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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +1466,7 @@ 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}>
|
||||||
|
|
@ -1535,7 +1535,7 @@ 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 && (
|
||||||
|
|
@ -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 && (
|
||||||
|
|
@ -1790,7 +1790,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<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
|
||||||
|
|
@ -1804,9 +1804,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
<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
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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