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();
|
await aiService.initialize();
|
||||||
console.log('AI service initialized');
|
console.log('AI service initialized');
|
||||||
|
|
||||||
// Check if announcement should be shown (version 1.0.0)
|
// What's New announcement disabled
|
||||||
const announcementShown = await mmkvStorage.getItem('announcement_v1.0.0_shown');
|
// const announcementShown = await mmkvStorage.getItem('announcement_v1.0.0_shown');
|
||||||
if (!announcementShown && onboardingCompleted === 'true') {
|
// if (!announcementShown && onboardingCompleted === 'true') {
|
||||||
// Show announcement only after app is ready
|
// // Show announcement only after app is ready
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
setShowAnnouncement(true);
|
// setShowAnnouncement(true);
|
||||||
}, 1000);
|
// }, 1000);
|
||||||
}
|
// }
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing app:', error);
|
console.error('Error initializing app:', error);
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,12 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { height } = Dimensions.get('window');
|
||||||
|
|
||||||
const FirstTimeWelcome = () => {
|
const FirstTimeWelcome = () => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
@ -21,31 +19,22 @@ const FirstTimeWelcome = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.delay(200).duration(600)}
|
entering={FadeIn.duration(300)}
|
||||||
style={[styles.container, { backgroundColor: currentTheme.colors.elevation1 }]}
|
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 }]}>
|
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
Welcome to Nuvio!
|
Welcome to Nuvio
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
|
<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>
|
</Text>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={() => navigation.navigate('Addons')}
|
onPress={() => navigation.navigate('Addons')}
|
||||||
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="extension" size={20} color="white" />
|
|
||||||
<Text style={styles.buttonText}>Install Addons</Text>
|
<Text style={styles.buttonText}>Install Addons</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -53,52 +42,30 @@ const FirstTimeWelcome = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
wrapper: {
|
||||||
margin: 16,
|
width: '100%',
|
||||||
padding: 24,
|
|
||||||
borderRadius: 16,
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
shadowColor: '#000',
|
paddingHorizontal: 32,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: '600',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
fontSize: 14,
|
fontSize: 15,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
lineHeight: 20,
|
marginBottom: 32,
|
||||||
marginBottom: 20,
|
|
||||||
maxWidth: width * 0.7,
|
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
flexDirection: 'row',
|
paddingVertical: 14,
|
||||||
alignItems: 'center',
|
paddingHorizontal: 32,
|
||||||
paddingVertical: 12,
|
borderRadius: 10,
|
||||||
paddingHorizontal: 20,
|
|
||||||
borderRadius: 25,
|
|
||||||
gap: 8,
|
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: 14,
|
fontSize: 15,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,9 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Permanently hide the trailers section
|
||||||
|
return null;
|
||||||
|
|
||||||
if (!tmdbId) {
|
if (!tmdbId) {
|
||||||
return null; // Don't show if no TMDB ID
|
return null; // Don't show if no TMDB ID
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
showPosterTitles: true,
|
showPosterTitles: true,
|
||||||
enableHomeHeroBackground: true,
|
enableHomeHeroBackground: true,
|
||||||
// Trailer settings
|
// Trailer settings
|
||||||
showTrailers: true, // Enable trailers by default
|
showTrailers: false, // Hide trailers by default
|
||||||
trailerMuted: true, // Default to muted for better user experience
|
trailerMuted: true, // Default to muted for better user experience
|
||||||
// AI
|
// AI
|
||||||
aiChatEnabled: false,
|
aiChatEnabled: false,
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,15 @@ const AISettingsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
{/* OpenRouter branding */}
|
{/* OpenRouter branding */}
|
||||||
<View style={{ alignItems: 'center', marginTop: 16, marginBottom: 32 }}>
|
<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} />
|
<SvgXml xml={OPENROUTER_SVG.replace(/CURRENTCOLOR/g, currentTheme.colors.mediumEmphasis)} width={180} height={60} />
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</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(() => {
|
useEffect(() => {
|
||||||
loadAddons();
|
loadAddons();
|
||||||
|
|
@ -1171,63 +1151,7 @@ const AddonsScreen = () => {
|
||||||
<View style={styles.sectionSeparator} />
|
<View style={styles.sectionSeparator} />
|
||||||
|
|
||||||
{/* Promotional Addon Section (hidden if installed) */}
|
{/* 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>
|
</ScrollView>
|
||||||
|
|
|
||||||
|
|
@ -685,10 +685,10 @@ const HomeScreen = () => {
|
||||||
const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);
|
const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);
|
||||||
const memoizedHeader = useMemo(() => (
|
const memoizedHeader = useMemo(() => (
|
||||||
<>
|
<>
|
||||||
{showHeroSection ? memoizedFeaturedContent : null}
|
{showHeroSection && hasAddons ? memoizedFeaturedContent : null}
|
||||||
{memoizedContinueWatchingSection}
|
{memoizedContinueWatchingSection}
|
||||||
</>
|
</>
|
||||||
), [showHeroSection, memoizedFeaturedContent, memoizedContinueWatchingSection]);
|
), [showHeroSection, hasAddons, memoizedFeaturedContent, memoizedContinueWatchingSection]);
|
||||||
// Track scroll direction manually for reliable behavior across platforms
|
// Track scroll direction manually for reliable behavior across platforms
|
||||||
const lastScrollYRef = useRef(0);
|
const lastScrollYRef = useRef(0);
|
||||||
const lastToggleRef = useRef(0);
|
const lastToggleRef = useRef(0);
|
||||||
|
|
@ -832,7 +832,19 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
// Memoize the main content section
|
// Memoize the main content section
|
||||||
const renderMainContent = useMemo(() => {
|
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 (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
description: 'Open streams in VidHub player',
|
description: 'Open streams in VidHub player',
|
||||||
icon: 'ondemand-video',
|
icon: 'ondemand-video',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'infuse_livecontainer',
|
|
||||||
title: 'Infuse Livecontainer',
|
|
||||||
description: 'Open streams in Infuse player LiveContainer',
|
|
||||||
icon: 'smart-display',
|
|
||||||
},
|
|
||||||
] : [
|
] : [
|
||||||
{
|
{
|
||||||
id: 'external',
|
id: 'external',
|
||||||
|
|
|
||||||
|
|
@ -1406,7 +1406,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Enable Plugins</Text>
|
<Text style={styles.settingTitle}>Enable Plugins</Text>
|
||||||
<Text style={styles.settingDescription}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -1427,7 +1427,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
styles={styles}
|
styles={styles}
|
||||||
>
|
>
|
||||||
<Text style={styles.sectionDescription}>
|
<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>
|
</Text>
|
||||||
|
|
||||||
{/* Current Repository */}
|
{/* Current Repository */}
|
||||||
|
|
@ -1752,7 +1752,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Enable URL Validation</Text>
|
<Text style={styles.settingTitle}>Enable URL Validation</Text>
|
||||||
<Text style={styles.settingDescription}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -1768,7 +1768,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Group Plugin Streams</Text>
|
<Text style={styles.settingTitle}>Group Plugin Streams</Text>
|
||||||
<Text style={styles.settingDescription}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -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 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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -1806,7 +1806,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Show Plugin Logos</Text>
|
<Text style={styles.settingTitle}>Show Plugin Logos</Text>
|
||||||
<Text style={styles.settingDescription}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -1917,13 +1917,11 @@ const PluginsScreen: React.FC = () => {
|
||||||
<View style={[styles.section, styles.lastSection]}>
|
<View style={[styles.section, styles.lastSection]}>
|
||||||
<Text style={styles.sectionTitle}>About Plugins</Text>
|
<Text style={styles.sectionTitle}>About Plugins</Text>
|
||||||
<Text style={styles.infoText}>
|
<Text style={styles.infoText}>
|
||||||
Plugins are JavaScript modules that can search for streaming links from various sources.
|
Plugins extend app functionality by connecting to additional content providers.
|
||||||
They run locally on your device and can be installed from trusted repositories.
|
Add repositories to discover and enable plugins.
|
||||||
</Text>
|
</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>
|
</View>
|
||||||
</ScrollView>
|
</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
|
1. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the main switch to allow plugins
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.modalText}>
|
<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>
|
||||||
<Text style={styles.modalText}>
|
<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>
|
||||||
<Text style={styles.modalText}>
|
<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>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.modalButton}
|
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
|
// Create a stable debounced search function using useMemo
|
||||||
const debouncedSearch = useMemo(() => {
|
const debouncedSearch = useMemo(() => {
|
||||||
return debounce(async (searchQuery: string) => {
|
return debounce(async (searchQuery: string) => {
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
// Cancel any in-flight live search
|
// Cancel any, in-flight live search
|
||||||
liveSearchHandle.current?.cancel();
|
liveSearchHandle.current?.cancel();
|
||||||
liveSearchHandle.current = null;
|
liveSearchHandle.current = null;
|
||||||
setResults({ byAddon: [], allResults: [] });
|
setResults({ byAddon: [], allResults: [] });
|
||||||
|
|
@ -389,6 +409,12 @@ const SearchScreen = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block search if no addons
|
||||||
|
if (hasAddons === false) {
|
||||||
|
setSearching(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel prior live search
|
// Cancel prior live search
|
||||||
liveSearchHandle.current?.cancel();
|
liveSearchHandle.current?.cancel();
|
||||||
setResults({ byAddon: [], allResults: [] });
|
setResults({ byAddon: [], allResults: [] });
|
||||||
|
|
@ -449,7 +475,7 @@ const SearchScreen = () => {
|
||||||
});
|
});
|
||||||
liveSearchHandle.current = handle;
|
liveSearchHandle.current = handle;
|
||||||
}, 800);
|
}, 800);
|
||||||
}, []); // Empty dependency array - create once and never recreate
|
}, [hasAddons]); // Re-create if hasAddons changes
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip initial mount to prevent unnecessary operations
|
// Skip initial mount to prevent unnecessary operations
|
||||||
|
|
@ -460,9 +486,12 @@ const SearchScreen = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.trim() && query.trim().length >= 2) {
|
if (query.trim() && query.trim().length >= 2) {
|
||||||
|
// Don't set searching state if no addons, to avoid flicker
|
||||||
|
if (hasAddons !== false) {
|
||||||
setSearching(true);
|
setSearching(true);
|
||||||
setSearched(true);
|
setSearched(true);
|
||||||
setShowRecent(false);
|
setShowRecent(false);
|
||||||
|
}
|
||||||
debouncedSearch(query);
|
debouncedSearch(query);
|
||||||
} else if (query.trim().length < 2 && query.trim().length > 0) {
|
} else if (query.trim().length < 2 && query.trim().length > 0) {
|
||||||
// Show that we're waiting for more characters
|
// Show that we're waiting for more characters
|
||||||
|
|
@ -486,7 +515,7 @@ const SearchScreen = () => {
|
||||||
return () => {
|
return () => {
|
||||||
debouncedSearch.cancel();
|
debouncedSearch.cancel();
|
||||||
};
|
};
|
||||||
}, [query]); // Removed debouncedSearch since it's now stable with useMemo
|
}, [query, hasAddons]); // Added hasAddons dependency
|
||||||
|
|
||||||
const handleClearSearch = () => {
|
const handleClearSearch = () => {
|
||||||
setQuery('');
|
setQuery('');
|
||||||
|
|
@ -883,6 +912,23 @@ const SearchScreen = () => {
|
||||||
offsetY={-60}
|
offsetY={-60}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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 ? (
|
) : query.trim().length === 1 ? (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={styles.emptyContainer}
|
style={styles.emptyContainer}
|
||||||
|
|
|
||||||
|
|
@ -548,6 +548,8 @@ const SettingsScreen: React.FC = () => {
|
||||||
onPress={() => navigation.navigate('Addons')}
|
onPress={() => navigation.navigate('Addons')}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Debrid Integration"
|
title="Debrid Integration"
|
||||||
description="Connect Torbox for premium streams"
|
description="Connect Torbox for premium streams"
|
||||||
|
|
@ -556,6 +558,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
onPress={() => navigation.navigate('DebridIntegration')}
|
onPress={() => navigation.navigate('DebridIntegration')}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
|
*/}
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Plugins"
|
title="Plugins"
|
||||||
description="Manage plugins and repositories"
|
description="Manage plugins and repositories"
|
||||||
|
|
@ -686,6 +689,9 @@ const SettingsScreen: React.FC = () => {
|
||||||
onPress={() => navigation.navigate('PlayerSettings')}
|
onPress={() => navigation.navigate('PlayerSettings')}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
{/*
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Show Trailers"
|
title="Show Trailers"
|
||||||
description="Display trailers in hero section"
|
description="Display trailers in hero section"
|
||||||
|
|
@ -700,6 +706,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
|
*/}
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Enable Downloads (Beta)"
|
title="Enable Downloads (Beta)"
|
||||||
description="Show Downloads tab and enable saving streams"
|
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
|
// Preinstalled addons disabled
|
||||||
const cinemetaId = 'com.linvo.cinemeta';
|
// const cinemetaId = 'com.linvo.cinemeta';
|
||||||
const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId);
|
// 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) {
|
// OpenSubtitles preinstall disabled
|
||||||
try {
|
// const opensubsId = 'org.stremio.opensubtitlesv3';
|
||||||
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
// const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
||||||
this.installedAddons.set(cinemetaId, cinemetaManifest);
|
//
|
||||||
} catch (error) {
|
// if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) {
|
||||||
// Fallback to minimal manifest if fetch fails
|
// try {
|
||||||
const fallbackManifest: Manifest = {
|
// const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||||
id: cinemetaId,
|
// this.installedAddons.set(opensubsId, opensubsManifest);
|
||||||
name: 'Cinemeta',
|
// } catch (error) {
|
||||||
version: '3.0.13',
|
// // Fallback omitted for brevity
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load addon order if exists (scoped first, then legacy, then @user:local for migration safety)
|
// 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}`);
|
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));
|
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
|
// Preinstalled addon order disabled
|
||||||
const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId);
|
// const hasUserRemovedCinemetaOrder = await this.hasUserRemovedAddon(cinemetaId);
|
||||||
if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) {
|
// if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemetaOrder) {
|
||||||
this.addonOrder.push(opensubsId);
|
// 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
|
// Add any missing addons to the order
|
||||||
const installedIds = Array.from(this.installedAddons.keys());
|
const installedIds = Array.from(this.installedAddons.keys());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue