mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +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-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "^15.11.2",
|
"react-native-svg": "^15.11.2",
|
||||||
|
"react-native-tab-view": "^4.0.10",
|
||||||
"react-native-video": "^6.12.0",
|
"react-native-video": "^6.12.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"subsrt": "^1.1.1"
|
"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": {
|
"node_modules/react-native-paper": {
|
||||||
"version": "5.13.1",
|
"version": "5.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.13.1.tgz",
|
"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"
|
"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": {
|
"node_modules/react-native-vector-icons": {
|
||||||
"version": "10.2.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
|
"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-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "^15.11.2",
|
"react-native-svg": "^15.11.2",
|
||||||
|
"react-native-tab-view": "^4.0.10",
|
||||||
"react-native-video": "^6.12.0",
|
"react-native-video": "^6.12.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"subsrt": "^1.1.1"
|
"subsrt": "^1.1.1"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
Switch
|
Switch,
|
||||||
|
Linking
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { stremioService, Manifest } from '../services/stremioService';
|
import { stremioService, Manifest } from '../services/stremioService';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
|
@ -30,10 +31,23 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
// Extend Manifest type to include logo only (remove disabled status)
|
// Extend Manifest type to include logo only (remove disabled status)
|
||||||
interface ExtendedManifest extends Manifest {
|
interface ExtendedManifest extends Manifest {
|
||||||
logo?: string;
|
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');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
@ -54,8 +68,14 @@ const AddonsScreen = () => {
|
||||||
// Force dark mode
|
// Force dark mode
|
||||||
const isDarkMode = true;
|
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(() => {
|
useEffect(() => {
|
||||||
loadAddons();
|
loadAddons();
|
||||||
|
loadCommunityAddons();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadAddons = async () => {
|
const loadAddons = async () => {
|
||||||
|
|
@ -92,28 +112,46 @@ const AddonsScreen = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddAddon = async () => {
|
// Function to load community addons
|
||||||
if (!addonUrl) {
|
const loadCommunityAddons = async () => {
|
||||||
Alert.alert('Error', 'Please enter an addon URL');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setInstalling(true);
|
setInstalling(true);
|
||||||
// First fetch the addon manifest
|
const manifest = await stremioService.getManifest(urlToInstall);
|
||||||
const manifest = await stremioService.getManifest(addonUrl);
|
|
||||||
setAddonDetails(manifest);
|
setAddonDetails(manifest);
|
||||||
|
setAddonUrl(urlToInstall);
|
||||||
setShowConfirmModal(true);
|
setShowConfirmModal(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch addon details:', 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 {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmInstallAddon = async () => {
|
const confirmInstallAddon = async () => {
|
||||||
if (!addonDetails) return;
|
if (!addonDetails || !addonUrl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setInstalling(true);
|
setInstalling(true);
|
||||||
|
|
@ -133,6 +171,7 @@ const AddonsScreen = () => {
|
||||||
|
|
||||||
const refreshAddons = async () => {
|
const refreshAddons = async () => {
|
||||||
loadAddons();
|
loadAddons();
|
||||||
|
loadCommunityAddons();
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
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 = () => {
|
const toggleReorderMode = () => {
|
||||||
setReorderMode(!reorderMode);
|
setReorderMode(!reorderMode);
|
||||||
};
|
};
|
||||||
|
|
@ -178,6 +341,8 @@ const AddonsScreen = () => {
|
||||||
const description = item.description || '';
|
const description = item.description || '';
|
||||||
// @ts-ignore - some addons might have logo property even though it's not in the type
|
// @ts-ignore - some addons might have logo property even though it's not in the type
|
||||||
const logo = item.logo || null;
|
const logo = item.logo || null;
|
||||||
|
// Check if addon is configurable
|
||||||
|
const isConfigurable = item.behaviorHints?.configurable === true;
|
||||||
|
|
||||||
// Format the types into a simple category text
|
// Format the types into a simple category text
|
||||||
const categoryText = types.length > 0
|
const categoryText = types.length > 0
|
||||||
|
|
@ -238,12 +403,22 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.addonActions}>
|
<View style={styles.addonActions}>
|
||||||
{!reorderMode ? (
|
{!reorderMode ? (
|
||||||
<TouchableOpacity
|
<>
|
||||||
style={styles.deleteButton}
|
{isConfigurable && (
|
||||||
onPress={() => handleRemoveAddon(item)}
|
<TouchableOpacity
|
||||||
>
|
style={styles.configButton}
|
||||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
onPress={() => handleConfigureAddon(item, item.transport)}
|
||||||
</TouchableOpacity>
|
>
|
||||||
|
<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}>
|
<View style={styles.priorityBadge}>
|
||||||
<Text style={styles.priorityText}>#{index + 1}</Text>
|
<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 }) => (
|
const StatsCard = ({ value, label }: { value: number; label: string }) => (
|
||||||
<View style={styles.statsCard}>
|
<View style={styles.statsCard}>
|
||||||
<Text style={styles.statsValue}>{value}</Text>
|
<Text style={styles.statsValue}>{value}</Text>
|
||||||
|
|
@ -360,7 +595,7 @@ const AddonsScreen = () => {
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
||||||
onPress={handleAddAddon}
|
onPress={() => handleAddAddon()}
|
||||||
disabled={installing || !addonUrl}
|
disabled={installing || !addonUrl}
|
||||||
>
|
>
|
||||||
<Text style={styles.addButtonText}>
|
<Text style={styles.addButtonText}>
|
||||||
|
|
@ -394,6 +629,95 @@ const AddonsScreen = () => {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</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>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -896,7 +1220,11 @@ const styles = StyleSheet.create({
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
},
|
},
|
||||||
installButton: {
|
installButton: {
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.success,
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: 8,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
modalButtonText: {
|
modalButtonText: {
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
|
|
@ -909,8 +1237,101 @@ const styles = StyleSheet.create({
|
||||||
deleteButton: {
|
deleteButton: {
|
||||||
padding: 6,
|
padding: 6,
|
||||||
},
|
},
|
||||||
refreshButton: {
|
configButton: {
|
||||||
padding: 8,
|
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}
|
isDarkMode={isDarkMode}
|
||||||
renderControl={ChevronRight}
|
renderControl={ChevronRight}
|
||||||
onPress={() => navigation.navigate('PlayerSettings')}
|
onPress={() => navigation.navigate('PlayerSettings')}
|
||||||
/>
|
|
||||||
<SettingItem
|
|
||||||
title="Auto-Filtering"
|
|
||||||
description="Disabled"
|
|
||||||
icon="tune"
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
renderControl={ChevronRight}
|
|
||||||
isLast={true}
|
isLast={true}
|
||||||
/>
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue