mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
refactor
This commit is contained in:
parent
076f33d6b7
commit
271aac9ae6
12 changed files with 425 additions and 522 deletions
16
App.tsx
16
App.tsx
|
|
@ -132,14 +132,14 @@ const ThemedApp = () => {
|
|||
await aiService.initialize();
|
||||
console.log('AI service initialized');
|
||||
|
||||
// Check if announcement should be shown (version 1.0.0)
|
||||
const announcementShown = await mmkvStorage.getItem('announcement_v1.0.0_shown');
|
||||
if (!announcementShown && onboardingCompleted === 'true') {
|
||||
// Show announcement only after app is ready
|
||||
setTimeout(() => {
|
||||
setShowAnnouncement(true);
|
||||
}, 1000);
|
||||
}
|
||||
// What's New announcement disabled
|
||||
// const announcementShown = await mmkvStorage.getItem('announcement_v1.0.0_shown');
|
||||
// if (!announcementShown && onboardingCompleted === 'true') {
|
||||
// // Show announcement only after app is ready
|
||||
// setTimeout(() => {
|
||||
// setShowAnnouncement(true);
|
||||
// }, 1000);
|
||||
// }
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing app:', error);
|
||||
|
|
|
|||
|
|
@ -6,14 +6,12 @@ import {
|
|||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const { height } = Dimensions.get('window');
|
||||
|
||||
const FirstTimeWelcome = () => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
|
@ -21,31 +19,22 @@ const FirstTimeWelcome = () => {
|
|||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(200).duration(600)}
|
||||
style={[styles.container, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||
entering={FadeIn.duration(300)}
|
||||
style={styles.wrapper}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[currentTheme.colors.primary, currentTheme.colors.secondary]}
|
||||
style={styles.iconContainer}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<MaterialIcons name="explore" size={40} color="white" />
|
||||
</LinearGradient>
|
||||
|
||||
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Welcome to Nuvio!
|
||||
Welcome to Nuvio
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
To get started, install some addons to access content from various sources.
|
||||
Install addons to start browsing content
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => navigation.navigate('Addons')}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialIcons name="extension" size={20} color="white" />
|
||||
<Text style={styles.buttonText}>Install Addons</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
|
@ -53,52 +42,30 @@ const FirstTimeWelcome = () => {
|
|||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 16,
|
||||
padding: 24,
|
||||
borderRadius: 16,
|
||||
wrapper: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
description: {
|
||||
fontSize: 14,
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
marginBottom: 20,
|
||||
maxWidth: width * 0.7,
|
||||
marginBottom: 32,
|
||||
},
|
||||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 25,
|
||||
gap: 8,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 32,
|
||||
borderRadius: 10,
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -449,6 +449,9 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
}
|
||||
};
|
||||
|
||||
// Permanently hide the trailers section
|
||||
return null;
|
||||
|
||||
if (!tmdbId) {
|
||||
return null; // Don't show if no TMDB ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
showPosterTitles: true,
|
||||
enableHomeHeroBackground: true,
|
||||
// Trailer settings
|
||||
showTrailers: true, // Enable trailers by default
|
||||
showTrailers: false, // Hide trailers by default
|
||||
trailerMuted: true, // Default to muted for better user experience
|
||||
// AI
|
||||
aiChatEnabled: false,
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ const AISettingsScreen: React.FC = () => {
|
|||
'Remove API Key',
|
||||
'Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.',
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Remove',
|
||||
onPress: async () => {
|
||||
|
|
@ -368,6 +368,15 @@ const AISettingsScreen: React.FC = () => {
|
|||
</View>
|
||||
{/* OpenRouter branding */}
|
||||
<View style={{ alignItems: 'center', marginTop: 16, marginBottom: 32 }}>
|
||||
<Text style={{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: 12,
|
||||
marginBottom: 8,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.5
|
||||
}}>
|
||||
Powered by
|
||||
</Text>
|
||||
<SvgXml xml={OPENROUTER_SVG.replace(/CURRENTCOLOR/g, currentTheme.colors.mediumEmphasis)} width={180} height={60} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -621,27 +621,7 @@ const AddonsScreen = () => {
|
|||
|
||||
|
||||
|
||||
// Promotional addon: Nuvio Streams
|
||||
const PROMO_ADDON_URL = 'https://nuviostreams.hayd.uk/manifest.json';
|
||||
const promoAddon: ExtendedManifest = {
|
||||
id: 'org.nuvio.streams',
|
||||
name: 'Nuvio Streams | Elfhosted',
|
||||
version: '0.5.0',
|
||||
description: 'Stremio addon for high-quality streaming links.',
|
||||
// @ts-ignore - logo not in base manifest type
|
||||
logo: 'https://raw.githubusercontent.com/tapframe/NuvioStreaming/refs/heads/appstore/assets/titlelogo.png',
|
||||
types: ['movie', 'series'],
|
||||
catalogs: [],
|
||||
behaviorHints: { configurable: true },
|
||||
// help handleConfigureAddon derive configure URL from the transport
|
||||
transport: PROMO_ADDON_URL,
|
||||
} as ExtendedManifest;
|
||||
const isPromoInstalled = addons.some(a =>
|
||||
a.id === 'org.nuvio.streams' ||
|
||||
(typeof a.id === 'string' && a.id.includes('nuviostreams.hayd.uk')) ||
|
||||
(typeof a.transport === 'string' && a.transport.includes('nuviostreams.hayd.uk')) ||
|
||||
(typeof (a as any).url === 'string' && (a as any).url.includes('nuviostreams.hayd.uk'))
|
||||
);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadAddons();
|
||||
|
|
@ -1171,63 +1151,7 @@ const AddonsScreen = () => {
|
|||
<View style={styles.sectionSeparator} />
|
||||
|
||||
{/* Promotional Addon Section (hidden if installed) */}
|
||||
{!isPromoInstalled && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>OFFICIAL ADDON</Text>
|
||||
<View style={styles.addonList}>
|
||||
<View style={styles.addonItem}>
|
||||
<View style={styles.addonHeader}>
|
||||
{promoAddon.logo ? (
|
||||
<FastImage
|
||||
source={{ uri: promoAddon.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}>{promoAddon.name}</Text>
|
||||
<View style={styles.addonMetaContainer}>
|
||||
<Text style={styles.addonVersion}>v{promoAddon.version}</Text>
|
||||
<Text style={styles.addonDot}>•</Text>
|
||||
<Text style={styles.addonCategory}>{promoAddon.types?.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{promoAddon.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.installButton}
|
||||
onPress={() => handleAddAddon(PROMO_ADDON_URL)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.addonDescription}>
|
||||
{promoAddon.description}
|
||||
</Text>
|
||||
<Text style={[styles.addonDescription, { marginTop: 4, opacity: 0.9 }]}>
|
||||
Configure and install for full functionality.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ const HomeScreen = () => {
|
|||
// Also show a global toast for consistency across screens
|
||||
// showInfo('Sign In Available', 'You can sign in anytime from Settings → Account');
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
})();
|
||||
return () => {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
|
|
@ -413,7 +413,7 @@ const HomeScreen = () => {
|
|||
statusBarConfig();
|
||||
|
||||
// Unlock orientation to allow free rotation
|
||||
ScreenOrientation.unlockAsync().catch(() => {});
|
||||
ScreenOrientation.unlockAsync().catch(() => { });
|
||||
|
||||
return () => {
|
||||
// Stop trailer when screen loses focus (navigating to other screens)
|
||||
|
|
@ -685,10 +685,10 @@ const HomeScreen = () => {
|
|||
const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);
|
||||
const memoizedHeader = useMemo(() => (
|
||||
<>
|
||||
{showHeroSection ? memoizedFeaturedContent : null}
|
||||
{showHeroSection && hasAddons ? memoizedFeaturedContent : null}
|
||||
{memoizedContinueWatchingSection}
|
||||
</>
|
||||
), [showHeroSection, memoizedFeaturedContent, memoizedContinueWatchingSection]);
|
||||
), [showHeroSection, hasAddons, memoizedFeaturedContent, memoizedContinueWatchingSection]);
|
||||
// Track scroll direction manually for reliable behavior across platforms
|
||||
const lastScrollYRef = useRef(0);
|
||||
const lastToggleRef = useRef(0);
|
||||
|
|
@ -832,7 +832,19 @@ const HomeScreen = () => {
|
|||
|
||||
// Memoize the main content section
|
||||
const renderMainContent = useMemo(() => {
|
||||
if (isLoading) return null;
|
||||
// If no addons, render welcome screen directly centered
|
||||
if (hasAddons === false) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground, justifyContent: 'center' }]}>
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
backgroundColor="transparent"
|
||||
translucent
|
||||
/>
|
||||
<FirstTimeWelcome />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
|
|
@ -966,7 +978,7 @@ const styles = StyleSheet.create<any>({
|
|||
},
|
||||
placeholderPoster: {
|
||||
width: POSTER_WIDTH,
|
||||
aspectRatio: 2/3,
|
||||
aspectRatio: 2 / 3,
|
||||
borderRadius: 12,
|
||||
marginRight: 2,
|
||||
},
|
||||
|
|
@ -1203,7 +1215,7 @@ const styles = StyleSheet.create<any>({
|
|||
},
|
||||
contentItem: {
|
||||
width: POSTER_WIDTH,
|
||||
aspectRatio: 2/3,
|
||||
aspectRatio: 2 / 3,
|
||||
margin: 0,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
|
|
|
|||
|
|
@ -127,12 +127,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
description: 'Open streams in VidHub player',
|
||||
icon: 'ondemand-video',
|
||||
},
|
||||
{
|
||||
id: 'infuse_livecontainer',
|
||||
title: 'Infuse Livecontainer',
|
||||
description: 'Open streams in Infuse player LiveContainer',
|
||||
icon: 'smart-display',
|
||||
},
|
||||
|
||||
] : [
|
||||
{
|
||||
id: 'external',
|
||||
|
|
|
|||
|
|
@ -1406,7 +1406,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Enable Plugins</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
Allow the app to use installed plugins for finding streams
|
||||
Allow the app to use installed plugins for enhanced content integration
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -1427,7 +1427,7 @@ const PluginsScreen: React.FC = () => {
|
|||
styles={styles}
|
||||
>
|
||||
<Text style={styles.sectionDescription}>
|
||||
Manage multiple plugin repositories. Switch between repositories to access different sets of plugins.
|
||||
Manage multiple plugin repositories. Switch between repositories to access different community extensions.
|
||||
</Text>
|
||||
|
||||
{/* Current Repository */}
|
||||
|
|
@ -1752,7 +1752,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Enable URL Validation</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
Validate streaming URLs before returning them (may slow down results but improves reliability)
|
||||
Validate source URLs before returning them (may slow down results but improves reliability)
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -1768,7 +1768,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Group Plugin Streams</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
When enabled, all plugin streams are grouped under "{pluginService.getRepositoryName()}". When disabled, each plugin shows as a separate provider.
|
||||
When enabled, all plugin sources are grouped under "{pluginService.getRepositoryName()}". When disabled, each plugin shows as a separate provider.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -1790,7 +1790,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Sort by Quality First</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
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.
|
||||
When enabled, sources 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>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -1806,7 +1806,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Show Plugin Logos</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
Display plugin logos next to streaming links on the streams screen.
|
||||
Display plugin logos next to source links on the sources screen.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -1917,13 +1917,11 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={[styles.section, styles.lastSection]}>
|
||||
<Text style={styles.sectionTitle}>About Plugins</Text>
|
||||
<Text style={styles.infoText}>
|
||||
Plugins are JavaScript modules that can search for streaming links from various sources.
|
||||
They run locally on your device and can be installed from trusted repositories.
|
||||
Plugins extend app functionality by connecting to additional content providers.
|
||||
Add repositories to discover and enable plugins.
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
|
||||
<Text style={{ fontWeight: '600' }}>Note:</Text> Providers marked as "Limited" depend on external APIs that may stop working without notice.
|
||||
</Text>
|
||||
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
|
|
@ -1941,13 +1939,13 @@ const PluginsScreen: React.FC = () => {
|
|||
1. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the main switch to allow plugins
|
||||
</Text>
|
||||
<Text style={styles.modalText}>
|
||||
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 repository URL to discover plugins
|
||||
</Text>
|
||||
<Text style={styles.modalText}>
|
||||
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Download available plugins from the repository
|
||||
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Update plugins from the repository
|
||||
</Text>
|
||||
<Text style={styles.modalText}>
|
||||
4. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the plugins you want to use for streaming
|
||||
4. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the plugins you want to use
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.modalButton}
|
||||
|
|
|
|||
|
|
@ -377,11 +377,31 @@ const SearchScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const [hasAddons, setHasAddons] = useState<boolean | null>(null);
|
||||
|
||||
// Check for search-capable addons on focus
|
||||
useEffect(() => {
|
||||
const checkAddons = async () => {
|
||||
try {
|
||||
const addons = await catalogService.getAllAddons();
|
||||
// Check if any addon supports search (catalog resource with extra search or just any addon)
|
||||
// For now, simpler consistent check: just if any addon is installed
|
||||
setHasAddons(addons.length > 0);
|
||||
} catch (error) {
|
||||
setHasAddons(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAddons();
|
||||
const unsubscribe = navigation.addListener('focus', checkAddons);
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
|
||||
// Create a stable debounced search function using useMemo
|
||||
const debouncedSearch = useMemo(() => {
|
||||
return debounce(async (searchQuery: string) => {
|
||||
if (!searchQuery.trim()) {
|
||||
// Cancel any in-flight live search
|
||||
// Cancel any, in-flight live search
|
||||
liveSearchHandle.current?.cancel();
|
||||
liveSearchHandle.current = null;
|
||||
setResults({ byAddon: [], allResults: [] });
|
||||
|
|
@ -389,6 +409,12 @@ const SearchScreen = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Block search if no addons
|
||||
if (hasAddons === false) {
|
||||
setSearching(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel prior live search
|
||||
liveSearchHandle.current?.cancel();
|
||||
setResults({ byAddon: [], allResults: [] });
|
||||
|
|
@ -449,7 +475,7 @@ const SearchScreen = () => {
|
|||
});
|
||||
liveSearchHandle.current = handle;
|
||||
}, 800);
|
||||
}, []); // Empty dependency array - create once and never recreate
|
||||
}, [hasAddons]); // Re-create if hasAddons changes
|
||||
|
||||
useEffect(() => {
|
||||
// Skip initial mount to prevent unnecessary operations
|
||||
|
|
@ -460,9 +486,12 @@ const SearchScreen = () => {
|
|||
}
|
||||
|
||||
if (query.trim() && query.trim().length >= 2) {
|
||||
// Don't set searching state if no addons, to avoid flicker
|
||||
if (hasAddons !== false) {
|
||||
setSearching(true);
|
||||
setSearched(true);
|
||||
setShowRecent(false);
|
||||
}
|
||||
debouncedSearch(query);
|
||||
} else if (query.trim().length < 2 && query.trim().length > 0) {
|
||||
// Show that we're waiting for more characters
|
||||
|
|
@ -486,7 +515,7 @@ const SearchScreen = () => {
|
|||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, [query]); // Removed debouncedSearch since it's now stable with useMemo
|
||||
}, [query, hasAddons]); // Added hasAddons dependency
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setQuery('');
|
||||
|
|
@ -883,6 +912,23 @@ const SearchScreen = () => {
|
|||
offsetY={-60}
|
||||
/>
|
||||
</View>
|
||||
) : hasAddons === false ? (
|
||||
<Animated.View
|
||||
style={styles.emptyContainer}
|
||||
entering={FadeIn.duration(300)}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="extension-off"
|
||||
size={64}
|
||||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
No Addons Installed
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray, marginBottom: 24 }]}>
|
||||
Install addons to enable search functionality
|
||||
</Text>
|
||||
</Animated.View>
|
||||
) : query.trim().length === 1 ? (
|
||||
<Animated.View
|
||||
style={styles.emptyContainer}
|
||||
|
|
|
|||
|
|
@ -548,6 +548,8 @@ const SettingsScreen: React.FC = () => {
|
|||
onPress={() => navigation.navigate('Addons')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
|
||||
{/*
|
||||
<SettingItem
|
||||
title="Debrid Integration"
|
||||
description="Connect Torbox for premium streams"
|
||||
|
|
@ -556,6 +558,7 @@ const SettingsScreen: React.FC = () => {
|
|||
onPress={() => navigation.navigate('DebridIntegration')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
*/}
|
||||
<SettingItem
|
||||
title="Plugins"
|
||||
description="Manage plugins and repositories"
|
||||
|
|
@ -686,6 +689,9 @@ const SettingsScreen: React.FC = () => {
|
|||
onPress={() => navigation.navigate('PlayerSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
|
||||
|
||||
{/*
|
||||
<SettingItem
|
||||
title="Show Trailers"
|
||||
description="Display trailers in hero section"
|
||||
|
|
@ -700,6 +706,7 @@ const SettingsScreen: React.FC = () => {
|
|||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
*/}
|
||||
<SettingItem
|
||||
title="Enable Downloads (Beta)"
|
||||
description="Show Downloads tab and enable saving streams"
|
||||
|
|
|
|||
|
|
@ -340,90 +340,31 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
// Install Cinemeta for new users, but allow existing users to uninstall it
|
||||
const cinemetaId = 'com.linvo.cinemeta';
|
||||
const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId);
|
||||
// Preinstalled addons disabled
|
||||
// const cinemetaId = 'com.linvo.cinemeta';
|
||||
// const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId);
|
||||
//
|
||||
// if (!this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemeta) {
|
||||
// try {
|
||||
// const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
||||
// this.installedAddons.set(cinemetaId, cinemetaManifest);
|
||||
// } catch (error) {
|
||||
// // Fallback omitted for brevity
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemeta) {
|
||||
try {
|
||||
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
||||
this.installedAddons.set(cinemetaId, cinemetaManifest);
|
||||
} catch (error) {
|
||||
// Fallback to minimal manifest if fetch fails
|
||||
const fallbackManifest: Manifest = {
|
||||
id: cinemetaId,
|
||||
name: 'Cinemeta',
|
||||
version: '3.0.13',
|
||||
description: 'Provides metadata for movies and series from TheTVDB, TheMovieDB, etc.',
|
||||
url: 'https://v3-cinemeta.strem.io',
|
||||
originalUrl: 'https://v3-cinemeta.strem.io/manifest.json',
|
||||
types: ['movie', 'series'],
|
||||
catalogs: [
|
||||
{
|
||||
type: 'movie',
|
||||
id: 'top',
|
||||
name: 'Popular',
|
||||
extraSupported: ['search', 'genre', 'skip']
|
||||
},
|
||||
{
|
||||
type: 'series',
|
||||
id: 'top',
|
||||
name: 'Popular',
|
||||
extraSupported: ['search', 'genre', 'skip']
|
||||
}
|
||||
],
|
||||
resources: [
|
||||
{
|
||||
name: 'catalog',
|
||||
types: ['movie', 'series'],
|
||||
idPrefixes: ['tt']
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
types: ['movie', 'series'],
|
||||
idPrefixes: ['tt']
|
||||
}
|
||||
],
|
||||
behaviorHints: {
|
||||
configurable: false
|
||||
}
|
||||
};
|
||||
this.installedAddons.set(cinemetaId, fallbackManifest);
|
||||
}
|
||||
}
|
||||
|
||||
// Install OpenSubtitles v3 by default unless user has explicitly removed it
|
||||
const opensubsId = 'org.stremio.opensubtitlesv3';
|
||||
const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
||||
|
||||
if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) {
|
||||
try {
|
||||
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||
this.installedAddons.set(opensubsId, opensubsManifest);
|
||||
} catch (error) {
|
||||
const fallbackManifest: Manifest = {
|
||||
id: opensubsId,
|
||||
name: 'OpenSubtitles v3',
|
||||
version: '1.0.0',
|
||||
description: 'OpenSubtitles v3 Addon for Stremio',
|
||||
url: 'https://opensubtitles-v3.strem.io',
|
||||
originalUrl: 'https://opensubtitles-v3.strem.io/manifest.json',
|
||||
types: ['movie', 'series'],
|
||||
catalogs: [],
|
||||
resources: [
|
||||
{
|
||||
name: 'subtitles',
|
||||
types: ['movie', 'series'],
|
||||
idPrefixes: ['tt']
|
||||
}
|
||||
],
|
||||
behaviorHints: {
|
||||
configurable: false
|
||||
}
|
||||
};
|
||||
this.installedAddons.set(opensubsId, fallbackManifest);
|
||||
}
|
||||
}
|
||||
// OpenSubtitles preinstall disabled
|
||||
// const opensubsId = 'org.stremio.opensubtitlesv3';
|
||||
// const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
||||
//
|
||||
// if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) {
|
||||
// try {
|
||||
// const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||
// this.installedAddons.set(opensubsId, opensubsManifest);
|
||||
// } catch (error) {
|
||||
// // Fallback omitted for brevity
|
||||
// }
|
||||
// }
|
||||
|
||||
// Load addon order if exists (scoped first, then legacy, then @user:local for migration safety)
|
||||
let storedOrder = await mmkvStorage.getItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`);
|
||||
|
|
@ -435,17 +376,18 @@ class StremioService {
|
|||
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
||||
}
|
||||
|
||||
// Add Cinemeta to order only if user hasn't removed it
|
||||
const hasUserRemovedCinemetaOrder = await this.hasUserRemovedAddon(cinemetaId);
|
||||
if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemetaOrder) {
|
||||
this.addonOrder.push(cinemetaId);
|
||||
}
|
||||
|
||||
// Only add OpenSubtitles to order if user hasn't removed it
|
||||
const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId);
|
||||
if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) {
|
||||
this.addonOrder.push(opensubsId);
|
||||
}
|
||||
// Preinstalled addon order disabled
|
||||
// const hasUserRemovedCinemetaOrder = await this.hasUserRemovedAddon(cinemetaId);
|
||||
// if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemetaOrder) {
|
||||
// this.addonOrder.push(cinemetaId);
|
||||
// }
|
||||
|
||||
// const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId);
|
||||
// if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) {
|
||||
// this.addonOrder.push(opensubsId);
|
||||
// }
|
||||
|
||||
|
||||
// Add any missing addons to the order
|
||||
const installedIds = Array.from(this.installedAddons.keys());
|
||||
|
|
@ -779,7 +721,7 @@ class StremioService {
|
|||
try {
|
||||
const key = `${manifest.id}|${type}|${id}`;
|
||||
if (typeof hasMore === 'boolean') this.catalogHasMore.set(key, hasMore);
|
||||
} catch {}
|
||||
} catch { }
|
||||
if (response.data.metas && Array.isArray(response.data.metas)) {
|
||||
return response.data.metas;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue