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

View file

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

View file

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

View file

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

View file

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

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(() => {
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>

View file

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

View file

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

View file

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

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

View file

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

View file

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