mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +00:00
test
This commit is contained in:
parent
c21f279aa3
commit
d605383720
4 changed files with 172 additions and 63 deletions
|
|
@ -586,21 +586,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cinemeta addon details
|
|
||||||
const cinemetaAddon: CommunityAddon = {
|
|
||||||
transportUrl: 'https://v3-cinemeta.strem.io/manifest.json',
|
|
||||||
manifest: {
|
|
||||||
id: 'com.linvo.cinemeta',
|
|
||||||
version: '3.0.13',
|
|
||||||
name: 'Cinemeta',
|
|
||||||
description: 'Provides metadata for movies and series from TheTVDB, TheMovieDB, etc.',
|
|
||||||
logo: 'https://static.strem.io/addons/cinemeta.png',
|
|
||||||
types: ['movie', 'series'],
|
|
||||||
behaviorHints: {
|
|
||||||
configurable: false
|
|
||||||
}
|
|
||||||
} as ExtendedManifest,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddonsScreen = () => {
|
const AddonsScreen = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
|
@ -671,16 +657,15 @@ const AddonsScreen = () => {
|
||||||
// Filter out addons without a manifest or transportUrl (basic validation)
|
// Filter out addons without a manifest or transportUrl (basic validation)
|
||||||
let validAddons = response.data.filter(addon => addon.manifest && addon.transportUrl);
|
let validAddons = response.data.filter(addon => addon.manifest && addon.transportUrl);
|
||||||
|
|
||||||
// Filter out Cinemeta if it's already in the community list to avoid duplication
|
// Filter out Cinemeta since it's now pre-installed
|
||||||
validAddons = validAddons.filter(addon => addon.manifest.id !== 'com.linvo.cinemeta');
|
validAddons = validAddons.filter(addon => addon.manifest.id !== 'com.linvo.cinemeta');
|
||||||
|
|
||||||
// Add Cinemeta to the beginning of the list
|
setCommunityAddons(validAddons);
|
||||||
setCommunityAddons([cinemetaAddon, ...validAddons]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load community addons:', error);
|
logger.error('Failed to load community addons:', error);
|
||||||
setCommunityError('Failed to load community addons. Please try again later.');
|
setCommunityError('Failed to load community addons. Please try again later.');
|
||||||
// Still show Cinemeta if the community list fails to load
|
// Set empty array on error since Cinemeta is pre-installed
|
||||||
setCommunityAddons([cinemetaAddon]);
|
setCommunityAddons([]);
|
||||||
} finally {
|
} finally {
|
||||||
setCommunityLoading(false);
|
setCommunityLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -746,6 +731,16 @@ const AddonsScreen = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
||||||
|
// Check if this is a pre-installed addon
|
||||||
|
if (stremioService.isPreInstalledAddon(addon.id)) {
|
||||||
|
Alert.alert(
|
||||||
|
'Cannot Remove Addon',
|
||||||
|
`${addon.name} is a pre-installed addon and cannot be removed.`,
|
||||||
|
[{ text: 'OK', style: 'default' }]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Uninstall Addon',
|
'Uninstall Addon',
|
||||||
`Are you sure you want to uninstall ${addon.name}?`,
|
`Are you sure you want to uninstall ${addon.name}?`,
|
||||||
|
|
@ -900,6 +895,8 @@ const AddonsScreen = () => {
|
||||||
const logo = item.logo || null;
|
const logo = item.logo || null;
|
||||||
// Check if addon is configurable
|
// Check if addon is configurable
|
||||||
const isConfigurable = item.behaviorHints?.configurable === true;
|
const isConfigurable = item.behaviorHints?.configurable === true;
|
||||||
|
// Check if addon is pre-installed
|
||||||
|
const isPreInstalled = stremioService.isPreInstalledAddon(item.id);
|
||||||
|
|
||||||
// Format the types into a simple category text
|
// Format the types into a simple category text
|
||||||
const categoryText = types.length > 0
|
const categoryText = types.length > 0
|
||||||
|
|
@ -951,7 +948,14 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={styles.addonTitleContainer}>
|
<View style={styles.addonTitleContainer}>
|
||||||
<Text style={styles.addonName}>{item.name}</Text>
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 2 }}>
|
||||||
|
<Text style={styles.addonName}>{item.name}</Text>
|
||||||
|
{isPreInstalled && (
|
||||||
|
<View style={[styles.priorityBadge, { marginLeft: 8, backgroundColor: colors.success }]}>
|
||||||
|
<Text style={[styles.priorityText, { fontSize: 10 }]}>PRE-INSTALLED</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
<View style={styles.addonMetaContainer}>
|
<View style={styles.addonMetaContainer}>
|
||||||
<Text style={styles.addonVersion}>v{item.version || '1.0.0'}</Text>
|
<Text style={styles.addonVersion}>v{item.version || '1.0.0'}</Text>
|
||||||
<Text style={styles.addonDot}>•</Text>
|
<Text style={styles.addonDot}>•</Text>
|
||||||
|
|
@ -969,12 +973,14 @@ const AddonsScreen = () => {
|
||||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
{!stremioService.isPreInstalledAddon(item.id) && (
|
||||||
style={styles.deleteButton}
|
<TouchableOpacity
|
||||||
onPress={() => handleRemoveAddon(item)}
|
style={styles.deleteButton}
|
||||||
>
|
onPress={() => handleRemoveAddon(item)}
|
||||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
>
|
||||||
</TouchableOpacity>
|
<MaterialIcons name="delete" size={20} color={colors.error} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.priorityBadge}>
|
<View style={styles.priorityBadge}>
|
||||||
|
|
@ -1410,4 +1416,4 @@ const AddonsScreen = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddonsScreen;
|
export default AddonsScreen;
|
||||||
|
|
@ -187,12 +187,12 @@ const PulsingChip = memo(({ text, delay }: { text: string; delay: number }) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const styles = React.useMemo(() => createStyles(currentTheme.colors), [currentTheme.colors]);
|
const styles = React.useMemo(() => createStyles(currentTheme.colors), [currentTheme.colors]);
|
||||||
|
|
||||||
const pulseValue = useSharedValue(0.7);
|
const pulseValue = useSharedValue(0.6);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const startPulse = () => {
|
const startPulse = () => {
|
||||||
pulseValue.value = withTiming(1, { duration: 800 }, () => {
|
pulseValue.value = withTiming(1, { duration: 1200 }, () => {
|
||||||
pulseValue.value = withTiming(0.7, { duration: 800 }, () => {
|
pulseValue.value = withTiming(0.6, { duration: 1200 }, () => {
|
||||||
runOnJS(startPulse)();
|
runOnJS(startPulse)();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -207,8 +207,7 @@ const PulsingChip = memo(({ text, delay }: { text: string; delay: number }) => {
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
opacity: pulseValue.value,
|
opacity: pulseValue.value
|
||||||
transform: [{ scale: interpolate(pulseValue.value, [0.7, 1], [0.95, 1], Extrapolate.CLAMP) }]
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1722,36 +1721,34 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
},
|
},
|
||||||
activeScrapersContainer: {
|
activeScrapersContainer: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 12,
|
paddingVertical: 8,
|
||||||
backgroundColor: colors.elevation1,
|
backgroundColor: 'transparent',
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
marginBottom: 8,
|
marginBottom: 4,
|
||||||
borderRadius: 8,
|
|
||||||
paddingVertical: 12,
|
|
||||||
},
|
},
|
||||||
activeScrapersTitle: {
|
activeScrapersTitle: {
|
||||||
color: colors.primary,
|
color: colors.mediumEmphasis,
|
||||||
fontSize: 13,
|
fontSize: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '500',
|
||||||
marginBottom: 8,
|
marginBottom: 6,
|
||||||
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
activeScrapersRow: {
|
activeScrapersRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 6,
|
gap: 4,
|
||||||
},
|
},
|
||||||
activeScraperChip: {
|
activeScraperChip: {
|
||||||
backgroundColor: colors.surfaceVariant,
|
backgroundColor: colors.elevation2,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 4,
|
paddingVertical: 3,
|
||||||
borderRadius: 12,
|
borderRadius: 6,
|
||||||
borderWidth: 1,
|
borderWidth: 0,
|
||||||
borderColor: colors.primary + '40',
|
|
||||||
},
|
},
|
||||||
activeScraperText: {
|
activeScraperText: {
|
||||||
color: colors.highEmphasis,
|
color: colors.mediumEmphasis,
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontWeight: '500',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ class NotificationService {
|
||||||
private backgroundSyncInterval: NodeJS.Timeout | null = null;
|
private backgroundSyncInterval: NodeJS.Timeout | null = null;
|
||||||
private librarySubscription: (() => void) | null = null;
|
private librarySubscription: (() => void) | null = null;
|
||||||
private appStateSubscription: any = null;
|
private appStateSubscription: any = null;
|
||||||
|
private lastSyncTime: number = 0;
|
||||||
|
private readonly MIN_SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes minimum between syncs
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
// Initialize notifications
|
// Initialize notifications
|
||||||
|
|
@ -149,6 +151,16 @@ class NotificationService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if notification already exists for this episode
|
||||||
|
const existingNotification = this.scheduledNotifications.find(
|
||||||
|
notification => notification.seriesId === item.seriesId &&
|
||||||
|
notification.season === item.season &&
|
||||||
|
notification.episode === item.episode
|
||||||
|
);
|
||||||
|
if (existingNotification) {
|
||||||
|
return null; // Don't schedule duplicate notifications
|
||||||
|
}
|
||||||
|
|
||||||
const releaseDate = parseISO(item.releaseDate);
|
const releaseDate = parseISO(item.releaseDate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
|
@ -162,9 +174,9 @@ class NotificationService {
|
||||||
const notificationTime = new Date(releaseDate);
|
const notificationTime = new Date(releaseDate);
|
||||||
notificationTime.setHours(notificationTime.getHours() - this.settings.timeBeforeAiring);
|
notificationTime.setHours(notificationTime.getHours() - this.settings.timeBeforeAiring);
|
||||||
|
|
||||||
// If notification time has already passed, set to now + 1 minute
|
// If notification time has already passed, don't schedule the notification
|
||||||
if (notificationTime < now) {
|
if (notificationTime < now) {
|
||||||
notificationTime.setTime(now.getTime() + 60000);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the notification
|
// Schedule the notification
|
||||||
|
|
@ -254,9 +266,17 @@ class NotificationService {
|
||||||
this.librarySubscription = catalogService.subscribeToLibraryUpdates(async (libraryItems) => {
|
this.librarySubscription = catalogService.subscribeToLibraryUpdates(async (libraryItems) => {
|
||||||
if (!this.settings.enabled) return;
|
if (!this.settings.enabled) return;
|
||||||
|
|
||||||
// Reduced logging verbosity
|
const now = Date.now();
|
||||||
// logger.log('[NotificationService] Library updated, syncing notifications for', libraryItems.length, 'items');
|
const timeSinceLastSync = now - this.lastSyncTime;
|
||||||
await this.syncNotificationsForLibrary(libraryItems);
|
|
||||||
|
// Only sync if enough time has passed since last sync
|
||||||
|
if (timeSinceLastSync >= this.MIN_SYNC_INTERVAL) {
|
||||||
|
// Reduced logging verbosity
|
||||||
|
// logger.log('[NotificationService] Library updated, syncing notifications for', libraryItems.length, 'items');
|
||||||
|
await this.syncNotificationsForLibrary(libraryItems);
|
||||||
|
} else {
|
||||||
|
// logger.log(`[NotificationService] Library updated, but skipping sync (last sync ${Math.round(timeSinceLastSync / 1000)}s ago)`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[NotificationService] Error setting up library integration:', error);
|
logger.error('[NotificationService] Error setting up library integration:', error);
|
||||||
|
|
@ -284,10 +304,18 @@ class NotificationService {
|
||||||
|
|
||||||
private handleAppStateChange = async (nextAppState: AppStateStatus) => {
|
private handleAppStateChange = async (nextAppState: AppStateStatus) => {
|
||||||
if (nextAppState === 'active' && this.settings.enabled) {
|
if (nextAppState === 'active' && this.settings.enabled) {
|
||||||
// App came to foreground, sync notifications
|
const now = Date.now();
|
||||||
// Reduced logging verbosity
|
const timeSinceLastSync = now - this.lastSyncTime;
|
||||||
// logger.log('[NotificationService] App became active, syncing notifications');
|
|
||||||
await this.performBackgroundSync();
|
// Only sync if enough time has passed since last sync
|
||||||
|
if (timeSinceLastSync >= this.MIN_SYNC_INTERVAL) {
|
||||||
|
// App came to foreground, sync notifications
|
||||||
|
// Reduced logging verbosity
|
||||||
|
// logger.log('[NotificationService] App became active, syncing notifications');
|
||||||
|
await this.performBackgroundSync();
|
||||||
|
} else {
|
||||||
|
// logger.log(`[NotificationService] App became active, but skipping sync (last sync ${Math.round(timeSinceLastSync / 1000)}s ago)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -312,6 +340,9 @@ class NotificationService {
|
||||||
// Perform comprehensive background sync including Trakt integration
|
// Perform comprehensive background sync including Trakt integration
|
||||||
private async performBackgroundSync(): Promise<void> {
|
private async performBackgroundSync(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// Update last sync time at the start
|
||||||
|
this.lastSyncTime = Date.now();
|
||||||
|
|
||||||
// Reduced logging verbosity
|
// Reduced logging verbosity
|
||||||
// logger.log('[NotificationService] Starting comprehensive background sync');
|
// logger.log('[NotificationService] Starting comprehensive background sync');
|
||||||
|
|
||||||
|
|
@ -467,7 +498,13 @@ class NotificationService {
|
||||||
if (!video.released) return false;
|
if (!video.released) return false;
|
||||||
const releaseDate = parseISO(video.released);
|
const releaseDate = parseISO(video.released);
|
||||||
return releaseDate > now && releaseDate < fourWeeksLater;
|
return releaseDate > now && releaseDate < fourWeeksLater;
|
||||||
});
|
}).map(video => ({
|
||||||
|
id: video.id,
|
||||||
|
title: (video as any).title || (video as any).name || `Episode ${video.episode}`,
|
||||||
|
season: video.season || 0,
|
||||||
|
episode: video.episode || 0,
|
||||||
|
released: video.released,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no upcoming episodes from Stremio, try TMDB
|
// If no upcoming episodes from Stremio, try TMDB
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,51 @@ class StremioService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure Cinemeta is always installed as a pre-installed addon
|
||||||
|
const cinemetaId = 'com.linvo.cinemeta';
|
||||||
|
if (!this.installedAddons.has(cinemetaId)) {
|
||||||
|
const cinemetaManifest: 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: 'Top Movies',
|
||||||
|
extraSupported: ['search', 'genre', 'skip']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'series',
|
||||||
|
id: 'top',
|
||||||
|
name: 'Top Series',
|
||||||
|
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, cinemetaManifest);
|
||||||
|
logger.log('✅ Cinemeta pre-installed as default addon');
|
||||||
|
}
|
||||||
|
|
||||||
// Load addon order if exists
|
// Load addon order if exists
|
||||||
const storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY);
|
const storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY);
|
||||||
if (storedOrder) {
|
if (storedOrder) {
|
||||||
|
|
@ -221,13 +266,26 @@ class StremioService {
|
||||||
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure Cinemeta is first in the order
|
||||||
|
if (!this.addonOrder.includes(cinemetaId)) {
|
||||||
|
this.addonOrder.unshift(cinemetaId);
|
||||||
|
} else {
|
||||||
|
// Move Cinemeta to the front if it's not already there
|
||||||
|
const cinemetaIndex = this.addonOrder.indexOf(cinemetaId);
|
||||||
|
if (cinemetaIndex > 0) {
|
||||||
|
this.addonOrder.splice(cinemetaIndex, 1);
|
||||||
|
this.addonOrder.unshift(cinemetaId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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());
|
||||||
const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
|
const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
|
||||||
this.addonOrder = [...this.addonOrder, ...missingIds];
|
this.addonOrder = [...this.addonOrder, ...missingIds];
|
||||||
|
|
||||||
// Ensure order is saved
|
// Ensure order and addons are saved
|
||||||
await this.saveAddonOrder();
|
await this.saveAddonOrder();
|
||||||
|
await this.saveInstalledAddons();
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -336,6 +394,12 @@ class StremioService {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAddon(id: string): void {
|
removeAddon(id: string): void {
|
||||||
|
// Prevent removal of Cinemeta as it's a pre-installed addon
|
||||||
|
if (id === 'com.linvo.cinemeta') {
|
||||||
|
logger.warn('❌ Cannot remove Cinemeta - it is a pre-installed addon');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.installedAddons.has(id)) {
|
if (this.installedAddons.has(id)) {
|
||||||
this.installedAddons.delete(id);
|
this.installedAddons.delete(id);
|
||||||
// Remove from order
|
// Remove from order
|
||||||
|
|
@ -359,6 +423,11 @@ class StremioService {
|
||||||
return this.getInstalledAddons();
|
return this.getInstalledAddons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if an addon is pre-installed and cannot be removed
|
||||||
|
isPreInstalledAddon(id: string): boolean {
|
||||||
|
return id === 'com.linvo.cinemeta';
|
||||||
|
}
|
||||||
|
|
||||||
private formatId(id: string): string {
|
private formatId(id: string): string {
|
||||||
return id.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
|
return id.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue