This commit is contained in:
tapframe 2025-07-28 17:40:37 +05:30
parent c21f279aa3
commit d605383720
4 changed files with 172 additions and 63 deletions

View file

@ -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 navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -671,16 +657,15 @@ const AddonsScreen = () => {
// Filter out addons without a manifest or transportUrl (basic validation)
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');
// Add Cinemeta to the beginning of the list
setCommunityAddons([cinemetaAddon, ...validAddons]);
setCommunityAddons(validAddons);
} catch (error) {
logger.error('Failed to load community addons:', error);
setCommunityError('Failed to load community addons. Please try again later.');
// Still show Cinemeta if the community list fails to load
setCommunityAddons([cinemetaAddon]);
// Set empty array on error since Cinemeta is pre-installed
setCommunityAddons([]);
} finally {
setCommunityLoading(false);
}
@ -746,6 +731,16 @@ const AddonsScreen = () => {
};
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(
'Uninstall Addon',
`Are you sure you want to uninstall ${addon.name}?`,
@ -900,6 +895,8 @@ const AddonsScreen = () => {
const logo = item.logo || null;
// Check if addon is configurable
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
const categoryText = types.length > 0
@ -951,7 +948,14 @@ const AddonsScreen = () => {
</View>
)}
<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}>
<Text style={styles.addonVersion}>v{item.version || '1.0.0'}</Text>
<Text style={styles.addonDot}></Text>
@ -969,12 +973,14 @@ const AddonsScreen = () => {
<MaterialIcons name="settings" size={20} color={colors.primary} />
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleRemoveAddon(item)}
>
<MaterialIcons name="delete" size={20} color={colors.error} />
</TouchableOpacity>
{!stremioService.isPreInstalledAddon(item.id) && (
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleRemoveAddon(item)}
>
<MaterialIcons name="delete" size={20} color={colors.error} />
</TouchableOpacity>
)}
</>
) : (
<View style={styles.priorityBadge}>
@ -1410,4 +1416,4 @@ const AddonsScreen = () => {
);
};
export default AddonsScreen;
export default AddonsScreen;

View file

@ -187,12 +187,12 @@ const PulsingChip = memo(({ text, delay }: { text: string; delay: number }) => {
const { currentTheme } = useTheme();
const styles = React.useMemo(() => createStyles(currentTheme.colors), [currentTheme.colors]);
const pulseValue = useSharedValue(0.7);
const pulseValue = useSharedValue(0.6);
useEffect(() => {
const startPulse = () => {
pulseValue.value = withTiming(1, { duration: 800 }, () => {
pulseValue.value = withTiming(0.7, { duration: 800 }, () => {
pulseValue.value = withTiming(1, { duration: 1200 }, () => {
pulseValue.value = withTiming(0.6, { duration: 1200 }, () => {
runOnJS(startPulse)();
});
});
@ -207,8 +207,7 @@ const PulsingChip = memo(({ text, delay }: { text: string; delay: number }) => {
const animatedStyle = useAnimatedStyle(() => {
return {
opacity: pulseValue.value,
transform: [{ scale: interpolate(pulseValue.value, [0.7, 1], [0.95, 1], Extrapolate.CLAMP) }]
opacity: pulseValue.value
};
});
@ -1722,36 +1721,34 @@ const createStyles = (colors: any) => StyleSheet.create({
},
activeScrapersContainer: {
paddingHorizontal: 16,
paddingBottom: 12,
backgroundColor: colors.elevation1,
paddingVertical: 8,
backgroundColor: 'transparent',
marginHorizontal: 16,
marginBottom: 8,
borderRadius: 8,
paddingVertical: 12,
marginBottom: 4,
},
activeScrapersTitle: {
color: colors.primary,
fontSize: 13,
fontWeight: '600',
marginBottom: 8,
color: colors.mediumEmphasis,
fontSize: 12,
fontWeight: '500',
marginBottom: 6,
opacity: 0.8,
},
activeScrapersRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
gap: 4,
},
activeScraperChip: {
backgroundColor: colors.surfaceVariant,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.primary + '40',
backgroundColor: colors.elevation2,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
borderWidth: 0,
},
activeScraperText: {
color: colors.highEmphasis,
fontSize: 12,
fontWeight: '500',
color: colors.mediumEmphasis,
fontSize: 11,
fontWeight: '400',
},
});

View file

@ -53,6 +53,8 @@ class NotificationService {
private backgroundSyncInterval: NodeJS.Timeout | null = null;
private librarySubscription: (() => void) | null = null;
private appStateSubscription: any = null;
private lastSyncTime: number = 0;
private readonly MIN_SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes minimum between syncs
private constructor() {
// Initialize notifications
@ -149,6 +151,16 @@ class NotificationService {
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 now = new Date();
@ -162,9 +174,9 @@ class NotificationService {
const notificationTime = new Date(releaseDate);
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) {
notificationTime.setTime(now.getTime() + 60000);
return null;
}
// Schedule the notification
@ -254,9 +266,17 @@ class NotificationService {
this.librarySubscription = catalogService.subscribeToLibraryUpdates(async (libraryItems) => {
if (!this.settings.enabled) return;
// Reduced logging verbosity
// logger.log('[NotificationService] Library updated, syncing notifications for', libraryItems.length, 'items');
await this.syncNotificationsForLibrary(libraryItems);
const now = Date.now();
const timeSinceLastSync = now - this.lastSyncTime;
// 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) {
logger.error('[NotificationService] Error setting up library integration:', error);
@ -284,10 +304,18 @@ class NotificationService {
private handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (nextAppState === 'active' && this.settings.enabled) {
// App came to foreground, sync notifications
// Reduced logging verbosity
// logger.log('[NotificationService] App became active, syncing notifications');
await this.performBackgroundSync();
const now = Date.now();
const timeSinceLastSync = now - this.lastSyncTime;
// 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
private async performBackgroundSync(): Promise<void> {
try {
// Update last sync time at the start
this.lastSyncTime = Date.now();
// Reduced logging verbosity
// logger.log('[NotificationService] Starting comprehensive background sync');
@ -467,7 +498,13 @@ class NotificationService {
if (!video.released) return false;
const releaseDate = parseISO(video.released);
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

View file

@ -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
const storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY);
if (storedOrder) {
@ -221,13 +266,26 @@ class StremioService {
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
const installedIds = Array.from(this.installedAddons.keys());
const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
this.addonOrder = [...this.addonOrder, ...missingIds];
// Ensure order is saved
// Ensure order and addons are saved
await this.saveAddonOrder();
await this.saveInstalledAddons();
this.initialized = true;
} catch (error) {
@ -336,6 +394,12 @@ class StremioService {
}
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)) {
this.installedAddons.delete(id);
// Remove from order
@ -359,6 +423,11 @@ class StremioService {
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 {
return id.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
}