mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Add community addons feature in AddonsScreen; implement loading and error handling for community addons, enhance UI with configuration options, and integrate axios for fetching addon data. Update package.json and package-lock.json to include react-native-tab-view dependency.
This commit is contained in:
parent
c55e01802b
commit
be5331ad0c
4 changed files with 466 additions and 25 deletions
26
package-lock.json
generated
26
package-lock.json
generated
|
|
@ -54,6 +54,7 @@
|
|||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-tab-view": "^4.0.10",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"subsrt": "^1.1.1"
|
||||
|
|
@ -10787,6 +10788,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-pager-view": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.7.0.tgz",
|
||||
"integrity": "sha512-sutxKiMqBuQrEyt4mLaLNzy8taIC7IuYpxfcwQBXfSYBSSpAa0qE9G1FXlP/iXqTSlFgBXyK7BESsl9umOjECQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper": {
|
||||
"version": "5.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.13.1.tgz",
|
||||
|
|
@ -10918,6 +10930,20 @@
|
|||
"react-native-svg": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-tab-view": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-4.0.10.tgz",
|
||||
"integrity": "sha512-KU1ovavUURfKffqNn7F2jwgQ0tUSa2WosnHSztVYArCr22HP2nR7xHrd8DddFL4uenaT9KGXlNgx1IUPGUdZSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-latest-callback": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-pager-view": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-tab-view": "^4.0.10",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"subsrt": "^1.1.1"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import {
|
|||
Dimensions,
|
||||
ScrollView,
|
||||
useColorScheme,
|
||||
Switch
|
||||
Switch,
|
||||
Linking
|
||||
} from 'react-native';
|
||||
import { stremioService, Manifest } from '../services/stremioService';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
|
@ -30,10 +31,23 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { logger } from '../utils/logger';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import axios from 'axios';
|
||||
|
||||
// Extend Manifest type to include logo only (remove disabled status)
|
||||
interface ExtendedManifest extends Manifest {
|
||||
logo?: string;
|
||||
transport?: string;
|
||||
behaviorHints?: {
|
||||
configurable?: boolean;
|
||||
configurationRequired?: boolean;
|
||||
configurationURL?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Interface for Community Addon structure from the JSON URL
|
||||
interface CommunityAddon {
|
||||
transportUrl: string;
|
||||
manifest: ExtendedManifest;
|
||||
}
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
|
@ -54,8 +68,14 @@ const AddonsScreen = () => {
|
|||
// Force dark mode
|
||||
const isDarkMode = true;
|
||||
|
||||
// State for community addons
|
||||
const [communityAddons, setCommunityAddons] = useState<CommunityAddon[]>([]);
|
||||
const [communityLoading, setCommunityLoading] = useState(true);
|
||||
const [communityError, setCommunityError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
}, []);
|
||||
|
||||
const loadAddons = async () => {
|
||||
|
|
@ -92,28 +112,46 @@ const AddonsScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleAddAddon = async () => {
|
||||
if (!addonUrl) {
|
||||
Alert.alert('Error', 'Please enter an addon URL');
|
||||
// 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)
|
||||
const validAddons = response.data.filter(addon => addon.manifest && addon.transportUrl);
|
||||
setCommunityAddons(validAddons);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load community addons:', error);
|
||||
setCommunityError('Failed to load community addons. Please try again later.');
|
||||
} finally {
|
||||
setCommunityLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAddon = async (url?: string) => {
|
||||
const urlToInstall = url || addonUrl;
|
||||
if (!urlToInstall) {
|
||||
Alert.alert('Error', 'Please enter an addon URL or select a community addon');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setInstalling(true);
|
||||
// First fetch the addon manifest
|
||||
const manifest = await stremioService.getManifest(addonUrl);
|
||||
const manifest = await stremioService.getManifest(urlToInstall);
|
||||
setAddonDetails(manifest);
|
||||
setAddonUrl(urlToInstall);
|
||||
setShowConfirmModal(true);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch addon details:', error);
|
||||
Alert.alert('Error', 'Failed to fetch addon details');
|
||||
Alert.alert('Error', `Failed to fetch addon details from ${urlToInstall}`);
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmInstallAddon = async () => {
|
||||
if (!addonDetails) return;
|
||||
if (!addonDetails || !addonUrl) return;
|
||||
|
||||
try {
|
||||
setInstalling(true);
|
||||
|
|
@ -133,6 +171,7 @@ const AddonsScreen = () => {
|
|||
|
||||
const refreshAddons = async () => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
};
|
||||
|
||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
||||
|
|
@ -169,6 +208,130 @@ const AddonsScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// Add function to handle configuration
|
||||
const handleConfigureAddon = (addon: ExtendedManifest, transportUrl?: string) => {
|
||||
// Try different ways to get the configuration URL
|
||||
let configUrl = '';
|
||||
|
||||
// Debug log the addon data to help troubleshoot
|
||||
logger.info(`Configure addon: ${addon.name}, ID: ${addon.id}`);
|
||||
if (transportUrl) {
|
||||
logger.info(`TransportUrl provided: ${transportUrl}`);
|
||||
}
|
||||
|
||||
// First check if the addon has a configurationURL directly
|
||||
if (addon.behaviorHints?.configurationURL) {
|
||||
configUrl = addon.behaviorHints.configurationURL;
|
||||
logger.info(`Using configurationURL from behaviorHints: ${configUrl}`);
|
||||
}
|
||||
// If a transport URL was provided directly (for community addons)
|
||||
else if (transportUrl) {
|
||||
// Remove any trailing filename like manifest.json
|
||||
const baseUrl = transportUrl.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using transportUrl to create config URL: ${configUrl}`);
|
||||
}
|
||||
// If the addon has a url property (this is set during installation)
|
||||
else if (addon.url) {
|
||||
configUrl = `${addon.url}configure`;
|
||||
logger.info(`Using addon.url property: ${configUrl}`);
|
||||
}
|
||||
// For com.stremio.*.addon format (common format for installed addons)
|
||||
else if (addon.id && addon.id.match(/^com\.stremio\.(.*?)\.addon$/)) {
|
||||
// Extract the domain part
|
||||
const match = addon.id.match(/^com\.stremio\.(.*?)\.addon$/);
|
||||
if (match && match[1]) {
|
||||
// Construct URL from the domain part of the ID
|
||||
const addonName = match[1];
|
||||
// For torrentio specifically, use known URL
|
||||
if (addonName === 'torrentio') {
|
||||
configUrl = 'https://torrentio.strem.fun/configure';
|
||||
logger.info(`Special case for torrentio: ${configUrl}`);
|
||||
} else {
|
||||
// Try to construct a reasonable URL for other addons
|
||||
configUrl = `https://${addonName}.strem.fun/configure`;
|
||||
logger.info(`Constructed URL from addon name: ${configUrl}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the ID is a URL, use that as the base (common for installed addons)
|
||||
else if (addon.id && addon.id.startsWith('http')) {
|
||||
// Get base URL from addon id (remove manifest.json or any trailing file)
|
||||
const baseUrl = addon.id.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using addon.id as HTTP URL: ${configUrl}`);
|
||||
}
|
||||
// If the ID uses stremio:// protocol but contains http URL (common format)
|
||||
else if (addon.id && (addon.id.includes('https://') || addon.id.includes('http://'))) {
|
||||
// Extract the HTTP URL using a more flexible regex
|
||||
const match = addon.id.match(/(https?:\/\/[^\/]+)(\/[^\s]*)?/);
|
||||
if (match) {
|
||||
// Use the domain and path if available, otherwise just domain with /configure
|
||||
const domain = match[1];
|
||||
const path = match[2] ? match[2].replace(/\/[^\/]+\.json$/, '/') : '/';
|
||||
configUrl = `${domain}${path}configure`;
|
||||
logger.info(`Extracted HTTP URL from stremio:// format: ${configUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for common addon format like stremio://addon.stremio.com/...
|
||||
if (!configUrl && addon.id && addon.id.startsWith('stremio://')) {
|
||||
// Try to convert stremio://domain.com/... to https://domain.com/...
|
||||
const domainMatch = addon.id.match(/stremio:\/\/([^\/]+)(\/[^\s]*)?/);
|
||||
if (domainMatch) {
|
||||
const domain = domainMatch[1];
|
||||
const path = domainMatch[2] ? domainMatch[2].replace(/\/[^\/]+\.json$/, '/') : '/';
|
||||
configUrl = `https://${domain}${path}configure`;
|
||||
logger.info(`Converted stremio:// protocol to https:// for config URL: ${configUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Use transport property if available (some addons include this)
|
||||
if (!configUrl && addon.transport && typeof addon.transport === 'string' && addon.transport.includes('http')) {
|
||||
const baseUrl = addon.transport.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using addon.transport for config URL: ${configUrl}`);
|
||||
}
|
||||
|
||||
// Get the URL from manifest's originalUrl if available
|
||||
if (!configUrl && (addon as any).originalUrl) {
|
||||
const baseUrl = (addon as any).originalUrl.replace(/\/[^\/]+\.json$/, '/');
|
||||
configUrl = `${baseUrl}configure`;
|
||||
logger.info(`Using originalUrl property: ${configUrl}`);
|
||||
}
|
||||
|
||||
// If we couldn't determine a config URL, show an error
|
||||
if (!configUrl) {
|
||||
logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`);
|
||||
Alert.alert(
|
||||
'Configuration Unavailable',
|
||||
'Could not determine configuration URL for this addon.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the URL being opened
|
||||
logger.info(`Opening configuration for addon: ${addon.name} at URL: ${configUrl}`);
|
||||
|
||||
// Check if the URL can be opened
|
||||
Linking.canOpenURL(configUrl).then(supported => {
|
||||
if (supported) {
|
||||
Linking.openURL(configUrl);
|
||||
} else {
|
||||
logger.error(`URL cannot be opened: ${configUrl}`);
|
||||
Alert.alert(
|
||||
'Cannot Open Configuration',
|
||||
`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}
|
||||
}).catch(err => {
|
||||
logger.error(`Error checking if URL can be opened: ${configUrl}`, err);
|
||||
Alert.alert('Error', 'Could not open configuration page.');
|
||||
});
|
||||
};
|
||||
|
||||
const toggleReorderMode = () => {
|
||||
setReorderMode(!reorderMode);
|
||||
};
|
||||
|
|
@ -178,6 +341,8 @@ const AddonsScreen = () => {
|
|||
const description = item.description || '';
|
||||
// @ts-ignore - some addons might have logo property even though it's not in the type
|
||||
const logo = item.logo || null;
|
||||
// Check if addon is configurable
|
||||
const isConfigurable = item.behaviorHints?.configurable === true;
|
||||
|
||||
// Format the types into a simple category text
|
||||
const categoryText = types.length > 0
|
||||
|
|
@ -238,12 +403,22 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{!reorderMode ? (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleRemoveAddon(item)}
|
||||
>
|
||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
<>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item, item.transport)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleRemoveAddon(item)}
|
||||
>
|
||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.priorityBadge}>
|
||||
<Text style={styles.priorityText}>#{index + 1}</Text>
|
||||
|
|
@ -259,6 +434,66 @@ 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 ? (
|
||||
<ExpoImage
|
||||
source={{ uri: logo }}
|
||||
style={styles.communityAddonIcon}
|
||||
contentFit="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 }) => (
|
||||
<View style={styles.statsCard}>
|
||||
<Text style={styles.statsValue}>{value}</Text>
|
||||
|
|
@ -360,7 +595,7 @@ const AddonsScreen = () => {
|
|||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
||||
onPress={handleAddAddon}
|
||||
onPress={() => handleAddAddon()}
|
||||
disabled={installing || !addonUrl}
|
||||
>
|
||||
<Text style={styles.addButtonText}>
|
||||
|
|
@ -394,6 +629,95 @@ const AddonsScreen = () => {
|
|||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Separator */}
|
||||
<View style={styles.sectionSeparator} />
|
||||
|
||||
{/* 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 ? (
|
||||
<ExpoImage
|
||||
source={{ uri: item.manifest.logo }}
|
||||
style={styles.addonIcon}
|
||||
contentFit="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>
|
||||
)}
|
||||
|
||||
|
|
@ -896,7 +1220,11 @@ const styles = StyleSheet.create({
|
|||
marginRight: 8,
|
||||
},
|
||||
installButton: {
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: colors.success,
|
||||
borderRadius: 6,
|
||||
padding: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalButtonText: {
|
||||
color: colors.white,
|
||||
|
|
@ -909,8 +1237,101 @@ const styles = StyleSheet.create({
|
|||
deleteButton: {
|
||||
padding: 6,
|
||||
},
|
||||
refreshButton: {
|
||||
padding: 8,
|
||||
configButton: {
|
||||
padding: 6,
|
||||
marginRight: 8,
|
||||
},
|
||||
communityAddonsList: {
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
communityAddonItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 8,
|
||||
padding: 15,
|
||||
marginBottom: 10,
|
||||
},
|
||||
communityAddonIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 6,
|
||||
marginRight: 15,
|
||||
},
|
||||
communityAddonIconPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 6,
|
||||
marginRight: 15,
|
||||
backgroundColor: colors.darkGray,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
communityAddonDetails: {
|
||||
flex: 1,
|
||||
marginRight: 10,
|
||||
},
|
||||
communityAddonName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginBottom: 3,
|
||||
},
|
||||
communityAddonDesc: {
|
||||
fontSize: 13,
|
||||
color: colors.lightGray,
|
||||
marginBottom: 5,
|
||||
opacity: 0.9,
|
||||
},
|
||||
communityAddonMetaContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
opacity: 0.8,
|
||||
},
|
||||
communityAddonVersion: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
},
|
||||
communityAddonDot: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
marginHorizontal: 5,
|
||||
},
|
||||
communityAddonCategory: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
flexShrink: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 10,
|
||||
},
|
||||
sectionSeparator: {
|
||||
height: 1,
|
||||
backgroundColor: colors.border,
|
||||
marginHorizontal: 20,
|
||||
marginVertical: 20,
|
||||
},
|
||||
emptyMessage: {
|
||||
textAlign: 'center',
|
||||
color: colors.mediumGray,
|
||||
marginTop: 20,
|
||||
fontSize: 16,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
errorMessage: {
|
||||
textAlign: 'center',
|
||||
color: colors.error,
|
||||
marginTop: 20,
|
||||
fontSize: 16,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
loader: {
|
||||
marginTop: 30,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
addonActionButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -323,13 +323,6 @@ const SettingsScreen: React.FC = () => {
|
|||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('PlayerSettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Auto-Filtering"
|
||||
description="Disabled"
|
||||
icon="tune"
|
||||
isDarkMode={isDarkMode}
|
||||
renderControl={ChevronRight}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
|
|
|||
Loading…
Reference in a new issue