This commit is contained in:
tapframe 2025-12-12 16:08:14 +05:30
parent 076f33d6b7
commit 271aac9ae6
12 changed files with 425 additions and 522 deletions

16
App.tsx
View file

@ -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);

View file

@ -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',
}, },
}); });

View file

@ -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
} }

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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 }]}>

View file

@ -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',

View file

@ -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}

View file

@ -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}

View file

@ -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"

View file

@ -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());