mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Add event emitter for addon changes; integrate addon order management in StremioService, allowing reordering of addons in the UI. Update CatalogContext and useHomeCatalogs hooks to listen for addon events and refresh catalogs accordingly. Enhance AddonsScreen with reorder functionality and UI improvements for better user experience.
This commit is contained in:
parent
869bedba72
commit
094bc00ea3
7 changed files with 375 additions and 76 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -25,6 +25,7 @@
|
|||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.8.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.43",
|
||||
"expo-auth-session": "^6.0.3",
|
||||
"expo-blur": "^14.0.3",
|
||||
|
|
@ -6485,6 +6486,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exec-async": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.8.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.43",
|
||||
"expo-auth-session": "^6.0.3",
|
||||
"expo-blur": "^14.0.3",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
import { StreamingContent } from '../services/catalogService';
|
||||
import { addonEmitter, ADDON_EVENTS } from '../services/stremioService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
interface CatalogContextType {
|
||||
lastUpdate: number;
|
||||
|
|
@ -25,8 +27,29 @@ export const CatalogProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
|||
|
||||
const refreshCatalogs = useCallback(() => {
|
||||
setLastUpdate(Date.now());
|
||||
logger.info('Refreshing catalogs, timestamp:', Date.now());
|
||||
}, []);
|
||||
|
||||
// Listen for addon changes to update catalog data
|
||||
useEffect(() => {
|
||||
const handleAddonChange = () => {
|
||||
logger.info('Addon changed, triggering catalog refresh');
|
||||
refreshCatalogs();
|
||||
};
|
||||
|
||||
// Subscribe to all addon events to refresh catalogs
|
||||
addonEmitter.on(ADDON_EVENTS.ORDER_CHANGED, handleAddonChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
|
||||
return () => {
|
||||
// Clean up event listeners
|
||||
addonEmitter.off(ADDON_EVENTS.ORDER_CHANGED, handleAddonChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
};
|
||||
}, [refreshCatalogs]);
|
||||
|
||||
const addToLibrary = useCallback((content: StreamingContent) => {
|
||||
setLibraryItems(prev => [...prev, content]);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect } from 'react';
|
|||
import { CatalogContent, catalogService } from '../services/catalogService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { addonEmitter, ADDON_EVENTS } from '../services/stremioService';
|
||||
|
||||
export function useHomeCatalogs() {
|
||||
const [catalogs, setCatalogs] = useState<CatalogContent[]>([]);
|
||||
|
|
@ -72,6 +73,33 @@ export function useHomeCatalogs() {
|
|||
loadCatalogs();
|
||||
}, [loadCatalogs, lastUpdate]);
|
||||
|
||||
// Subscribe to addon events to refresh catalogs when addons change
|
||||
useEffect(() => {
|
||||
// Handler for addon order changes
|
||||
const handleAddonOrderChange = () => {
|
||||
logger.info('Addon order changed, refreshing catalogs');
|
||||
loadCatalogs();
|
||||
};
|
||||
|
||||
// Handler for addon added/removed
|
||||
const handleAddonChange = () => {
|
||||
logger.info('Addon added or removed, refreshing catalogs');
|
||||
loadCatalogs();
|
||||
};
|
||||
|
||||
// Subscribe to addon events
|
||||
addonEmitter.on(ADDON_EVENTS.ORDER_CHANGED, handleAddonOrderChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.on(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
addonEmitter.off(ADDON_EVENTS.ORDER_CHANGED, handleAddonOrderChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_ADDED, handleAddonChange);
|
||||
addonEmitter.off(ADDON_EVENTS.ADDON_REMOVED, handleAddonChange);
|
||||
};
|
||||
}, [loadCatalogs]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { logger } from '../utils/logger';
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { BlurView } from 'expo-blur';
|
||||
|
||||
// Extend Manifest type to include logo
|
||||
// Extend Manifest type to include logo only (remove disabled status)
|
||||
interface ExtendedManifest extends Manifest {
|
||||
logo?: string;
|
||||
}
|
||||
|
|
@ -49,7 +49,8 @@ const AddonsScreen = () => {
|
|||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [catalogCount, setCatalogCount] = useState(0);
|
||||
const [activeAddons, setActiveAddons] = useState(0);
|
||||
// Add state for reorder mode
|
||||
const [reorderMode, setReorderMode] = useState(false);
|
||||
// Force dark mode
|
||||
const isDarkMode = true;
|
||||
|
||||
|
|
@ -60,9 +61,9 @@ const AddonsScreen = () => {
|
|||
const loadAddons = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Use the regular method without disabled state
|
||||
const installedAddons = await stremioService.getInstalledAddonsAsync();
|
||||
setAddons(installedAddons);
|
||||
setActiveAddons(installedAddons.length);
|
||||
setAddons(installedAddons as ExtendedManifest[]);
|
||||
|
||||
// Count catalogs
|
||||
let totalCatalogs = 0;
|
||||
|
|
@ -130,28 +131,27 @@ const AddonsScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleToggleAddon = (addon: ExtendedManifest, enabled: boolean) => {
|
||||
// Logic to enable/disable an addon
|
||||
Alert.alert(
|
||||
enabled ? 'Disable Addon' : 'Enable Addon',
|
||||
`Are you sure you want to ${enabled ? 'disable' : 'enable'} ${addon.name}?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: enabled ? 'Disable' : 'Enable',
|
||||
style: enabled ? 'destructive' : 'default',
|
||||
onPress: () => {
|
||||
// TODO: Implement actual toggle functionality
|
||||
Alert.alert('Success', `${addon.name} ${enabled ? 'disabled' : 'enabled'}`);
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
const refreshAddons = async () => {
|
||||
loadAddons();
|
||||
};
|
||||
|
||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
||||
if (stremioService.moveAddonUp(addon.id)) {
|
||||
// Refresh the list to reflect the new order
|
||||
loadAddons();
|
||||
}
|
||||
};
|
||||
|
||||
const moveAddonDown = (addon: ExtendedManifest) => {
|
||||
if (stremioService.moveAddonDown(addon.id)) {
|
||||
// Refresh the list to reflect the new order
|
||||
loadAddons();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
||||
Alert.alert(
|
||||
'Uninstall',
|
||||
'Uninstall Addon',
|
||||
`Are you sure you want to uninstall ${addon.name}?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
|
|
@ -160,14 +160,20 @@ const AddonsScreen = () => {
|
|||
style: 'destructive',
|
||||
onPress: () => {
|
||||
stremioService.removeAddon(addon.id);
|
||||
loadAddons();
|
||||
|
||||
// Remove from addons list
|
||||
setAddons(prev => prev.filter(a => a.id !== addon.id));
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const renderAddonItem = ({ item }: { item: ExtendedManifest }) => {
|
||||
const toggleReorderMode = () => {
|
||||
setReorderMode(!reorderMode);
|
||||
};
|
||||
|
||||
const renderAddonItem = ({ item, index }: { item: ExtendedManifest, index: number }) => {
|
||||
const types = item.types || [];
|
||||
const description = item.description || '';
|
||||
// @ts-ignore - some addons might have logo property even though it's not in the type
|
||||
|
|
@ -177,9 +183,39 @@ const AddonsScreen = () => {
|
|||
const categoryText = types.length > 0
|
||||
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'No categories';
|
||||
|
||||
const isFirstItem = index === 0;
|
||||
const isLastItem = index === addons.length - 1;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.addonItem}>
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderButtons}>
|
||||
<TouchableOpacity
|
||||
style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonUp(item)}
|
||||
disabled={isFirstItem}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-upward"
|
||||
size={20}
|
||||
color={isFirstItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.reorderButton, isLastItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonDown(item)}
|
||||
disabled={isLastItem}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-downward"
|
||||
size={20}
|
||||
color={isLastItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.addonHeader}>
|
||||
{logo ? (
|
||||
<ExpoImage
|
||||
|
|
@ -200,13 +236,20 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.addonCategory}>{categoryText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={true} // Default to enabled
|
||||
onValueChange={(value) => handleToggleAddon(item, !value)}
|
||||
trackColor={{ false: colors.elevation1, true: colors.primary }}
|
||||
thumbColor={colors.white}
|
||||
ios_backgroundColor={colors.elevation1}
|
||||
/>
|
||||
<View style={styles.addonActions}>
|
||||
{!reorderMode ? (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleRemoveAddon(item)}
|
||||
>
|
||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.priorityBadge}>
|
||||
<Text style={styles.priorityText}>#{index + 1}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.addonDescription}>
|
||||
|
|
@ -236,9 +279,48 @@ const AddonsScreen = () => {
|
|||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Reorder Mode Toggle Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
|
||||
onPress={toggleReorderMode}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="swap-vert"
|
||||
size={24}
|
||||
color={reorderMode ? colors.primary : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Refresh Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={refreshAddons}
|
||||
disabled={loading}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="refresh"
|
||||
size={24}
|
||||
color={loading ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.headerTitle}>Addons</Text>
|
||||
<Text style={styles.headerTitle}>
|
||||
Addons
|
||||
{reorderMode && <Text style={styles.reorderModeText}> (Reorder Mode)</Text>}
|
||||
</Text>
|
||||
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderInfoBanner}>
|
||||
<MaterialIcons name="info-outline" size={18} color={colors.primary} />
|
||||
<Text style={styles.reorderInfoText}>
|
||||
Addons at the top have higher priority when loading content
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
|
|
@ -256,40 +338,44 @@ const AddonsScreen = () => {
|
|||
<View style={styles.statsContainer}>
|
||||
<StatsCard value={addons.length} label="Addons" />
|
||||
<View style={styles.statsDivider} />
|
||||
<StatsCard value={activeAddons} label="Active" />
|
||||
<StatsCard value={addons.length} label="Active" />
|
||||
<View style={styles.statsDivider} />
|
||||
<StatsCard value={catalogCount} label="Catalogs" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Add Addon Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>ADD NEW ADDON</Text>
|
||||
<View style={styles.addAddonContainer}>
|
||||
<TextInput
|
||||
style={styles.addonInput}
|
||||
placeholder="Addon URL"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
value={addonUrl}
|
||||
onChangeText={setAddonUrl}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
||||
onPress={handleAddAddon}
|
||||
disabled={installing || !addonUrl}
|
||||
>
|
||||
<Text style={styles.addButtonText}>
|
||||
{installing ? 'Loading...' : 'Add Addon'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{/* Hide Add Addon Section in reorder mode */}
|
||||
{!reorderMode && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>ADD NEW ADDON</Text>
|
||||
<View style={styles.addAddonContainer}>
|
||||
<TextInput
|
||||
style={styles.addonInput}
|
||||
placeholder="Addon URL"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
value={addonUrl}
|
||||
onChangeText={setAddonUrl}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, {opacity: installing || !addonUrl ? 0.6 : 1}]}
|
||||
onPress={handleAddAddon}
|
||||
disabled={installing || !addonUrl}
|
||||
>
|
||||
<Text style={styles.addButtonText}>
|
||||
{installing ? 'Loading...' : 'Add Addon'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Installed Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>INSTALLED ADDONS</Text>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{reorderMode ? "DRAG ADDONS TO REORDER" : "INSTALLED ADDONS"}
|
||||
</Text>
|
||||
<View style={styles.addonList}>
|
||||
{addons.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
|
|
@ -297,20 +383,14 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.emptyText}>No addons installed</Text>
|
||||
</View>
|
||||
) : (
|
||||
addons.map((addon, index) => {
|
||||
const isLast = index === addons.length - 1;
|
||||
return (
|
||||
<View
|
||||
key={addon.id}
|
||||
style={[
|
||||
styles.addonItem,
|
||||
{ marginBottom: isLast ? 32 : 16 }
|
||||
]}
|
||||
>
|
||||
{renderAddonItem({ item: addon })}
|
||||
</View>
|
||||
);
|
||||
})
|
||||
addons.map((addon, index) => (
|
||||
<View
|
||||
key={addon.id}
|
||||
style={{ marginBottom: index === addons.length - 1 ? 32 : 0 }}
|
||||
>
|
||||
{renderAddonItem({ item: addon, index })}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -440,9 +520,76 @@ const styles = StyleSheet.create({
|
|||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerButton: {
|
||||
padding: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
activeHeaderButton: {
|
||||
backgroundColor: 'rgba(45, 156, 219, 0.2)',
|
||||
borderRadius: 6,
|
||||
},
|
||||
reorderModeText: {
|
||||
color: colors.primary,
|
||||
fontSize: 18,
|
||||
fontWeight: '400',
|
||||
},
|
||||
reorderInfoBanner: {
|
||||
backgroundColor: 'rgba(45, 156, 219, 0.15)',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
reorderInfoText: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
marginLeft: 8,
|
||||
},
|
||||
reorderButtons: {
|
||||
position: 'absolute',
|
||||
left: -12,
|
||||
top: '50%',
|
||||
marginTop: -40,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
reorderButton: {
|
||||
backgroundColor: colors.elevation3,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginVertical: 4,
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.5,
|
||||
backgroundColor: colors.elevation2,
|
||||
},
|
||||
priorityBadge: {
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
},
|
||||
priorityText: {
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
backButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
@ -569,6 +716,7 @@ const styles = StyleSheet.create({
|
|||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
marginBottom: 16,
|
||||
},
|
||||
addonHeader: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -754,6 +902,16 @@ const styles = StyleSheet.create({
|
|||
color: colors.white,
|
||||
fontWeight: '600',
|
||||
},
|
||||
addonActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
deleteButton: {
|
||||
padding: 6,
|
||||
},
|
||||
refreshButton: {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default AddonsScreen;
|
||||
|
|
@ -153,6 +153,7 @@ class CatalogService {
|
|||
const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
||||
|
||||
// Process addons in order (they're already returned in order from getAllAddons)
|
||||
for (const addon of addons) {
|
||||
if (addon.catalogs) {
|
||||
for (const catalog of addon.catalogs) {
|
||||
|
|
@ -200,7 +201,7 @@ class CatalogService {
|
|||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error);
|
||||
logger.error(`Failed to load ${catalog.name} from ${addon.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import axios from 'axios';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { logger } from '../utils/logger';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
// Create an event emitter for addon changes
|
||||
export const addonEmitter = new EventEmitter();
|
||||
export const ADDON_EVENTS = {
|
||||
ORDER_CHANGED: 'order_changed',
|
||||
ADDON_ADDED: 'addon_added',
|
||||
ADDON_REMOVED: 'addon_removed'
|
||||
};
|
||||
|
||||
// Basic types for Stremio
|
||||
export interface Meta {
|
||||
|
|
@ -137,7 +146,9 @@ export interface AddonCapabilities {
|
|||
class StremioService {
|
||||
private static instance: StremioService;
|
||||
private installedAddons: Map<string, Manifest> = new Map();
|
||||
private addonOrder: string[] = [];
|
||||
private readonly STORAGE_KEY = 'stremio-addons';
|
||||
private readonly ADDON_ORDER_KEY = 'stremio-addon-order';
|
||||
private readonly DEFAULT_ADDONS = [
|
||||
'https://v3-cinemeta.strem.io/manifest.json',
|
||||
'https://opensubtitles-v3.strem.io/manifest.json'
|
||||
|
|
@ -177,11 +188,27 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
// Load addon order if exists
|
||||
const storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY);
|
||||
if (storedOrder) {
|
||||
this.addonOrder = JSON.parse(storedOrder);
|
||||
// Filter out any ids that aren't in installedAddons
|
||||
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
// If no addons, install defaults
|
||||
if (this.installedAddons.size === 0) {
|
||||
await this.installDefaultAddons();
|
||||
}
|
||||
|
||||
// Ensure order is saved
|
||||
await this.saveAddonOrder();
|
||||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize addons:', error);
|
||||
|
|
@ -245,6 +272,14 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
private async saveAddonOrder(): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(this.ADDON_ORDER_KEY, JSON.stringify(this.addonOrder));
|
||||
} catch (error) {
|
||||
logger.error('Failed to save addon order:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getManifest(url: string): Promise<Manifest> {
|
||||
try {
|
||||
// Clean up URL - ensure it ends with manifest.json
|
||||
|
|
@ -278,7 +313,16 @@ class StremioService {
|
|||
const manifest = await this.getManifest(url);
|
||||
if (manifest && manifest.id) {
|
||||
this.installedAddons.set(manifest.id, manifest);
|
||||
|
||||
// Add to order if not already present (new addons go to the end)
|
||||
if (!this.addonOrder.includes(manifest.id)) {
|
||||
this.addonOrder.push(manifest.id);
|
||||
}
|
||||
|
||||
await this.saveInstalledAddons();
|
||||
await this.saveAddonOrder();
|
||||
// Emit an event that an addon was added
|
||||
addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id);
|
||||
} else {
|
||||
throw new Error('Invalid addon manifest');
|
||||
}
|
||||
|
|
@ -287,12 +331,20 @@ class StremioService {
|
|||
removeAddon(id: string): void {
|
||||
if (this.installedAddons.has(id)) {
|
||||
this.installedAddons.delete(id);
|
||||
// Remove from order
|
||||
this.addonOrder = this.addonOrder.filter(addonId => addonId !== id);
|
||||
this.saveInstalledAddons();
|
||||
this.saveAddonOrder();
|
||||
// Emit an event that an addon was removed
|
||||
addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id);
|
||||
}
|
||||
}
|
||||
|
||||
getInstalledAddons(): Manifest[] {
|
||||
return Array.from(this.installedAddons.values());
|
||||
// Return addons in the specified order
|
||||
return this.addonOrder
|
||||
.filter(id => this.installedAddons.has(id))
|
||||
.map(id => this.installedAddons.get(id)!);
|
||||
}
|
||||
|
||||
async getInstalledAddonsAsync(): Promise<Manifest[]> {
|
||||
|
|
@ -476,7 +528,7 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
// Modify getStreams to use the new callback signature and rely on callbacks for results
|
||||
// Modify getStreams to use this.getInstalledAddons() instead of getEnabledAddons
|
||||
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
|
|
@ -793,6 +845,35 @@ class StremioService {
|
|||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Add methods to move addons in the order
|
||||
moveAddonUp(id: string): boolean {
|
||||
const index = this.addonOrder.indexOf(id);
|
||||
if (index > 0) {
|
||||
// Swap with the previous item
|
||||
[this.addonOrder[index - 1], this.addonOrder[index]] =
|
||||
[this.addonOrder[index], this.addonOrder[index - 1]];
|
||||
this.saveAddonOrder();
|
||||
// Emit an event that the order has changed
|
||||
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
moveAddonDown(id: string): boolean {
|
||||
const index = this.addonOrder.indexOf(id);
|
||||
if (index >= 0 && index < this.addonOrder.length - 1) {
|
||||
// Swap with the next item
|
||||
[this.addonOrder[index], this.addonOrder[index + 1]] =
|
||||
[this.addonOrder[index + 1], this.addonOrder[index]];
|
||||
this.saveAddonOrder();
|
||||
// Emit an event that the order has changed
|
||||
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const stremioService = StremioService.getInstance();
|
||||
|
|
|
|||
Loading…
Reference in a new issue