From 335012c7926c6a28a5bf5cbfd53381258d2ad3ed Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 8 Jul 2025 01:01:01 +0530 Subject: [PATCH] todo:fix moviesmod --- src/navigation/AppNavigator.tsx | 17 +++ src/screens/PluginsScreen.tsx | 253 ++++++++++++++++++++++++++++++++ src/screens/SettingsScreen.tsx | 84 ++++------- src/screens/index.ts | 14 +- src/services/PluginManager.ts | 108 +++++++++++++- 5 files changed, 404 insertions(+), 72 deletions(-) create mode 100644 src/screens/PluginsScreen.tsx diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index ba6944fd..d48384af 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -39,6 +39,7 @@ import LogoSourceSettings from '../screens/LogoSourceSettings'; import ThemeScreen from '../screens/ThemeScreen'; import ProfilesScreen from '../screens/ProfilesScreen'; import OnboardingScreen from '../screens/OnboardingScreen'; +import PluginsScreen from '../screens/PluginsScreen'; // Stack navigator types export type RootStackParamList = { @@ -104,6 +105,7 @@ export type RootStackParamList = { LogoSourceSettings: undefined; ThemeSettings: undefined; ProfilesSettings: undefined; + Plugins: undefined; }; export type RootStackNavigationProp = NativeStackNavigationProp; @@ -1013,6 +1015,21 @@ const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStack }, }} /> + diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx new file mode 100644 index 00000000..2453bc5e --- /dev/null +++ b/src/screens/PluginsScreen.tsx @@ -0,0 +1,253 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ScrollView, + SafeAreaView, + StatusBar, + Alert, + Platform, + Dimensions, + TextInput, + ActivityIndicator, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import { useTheme } from '../contexts/ThemeContext'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { pluginManager } from '../services/PluginManager'; +import { SettingItem, SettingsCard } from './SettingsScreen'; // Assuming these are exported + +const { width } = Dimensions.get('window'); + +interface LoadedPlugin { + name: string; + version: string; + sourceUrl?: string; +} + +const PluginsScreen: React.FC = () => { + const navigation = useNavigation(); + const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); + + const [pluginUrl, setPluginUrl] = useState(''); + const [loadedPlugins, setLoadedPlugins] = useState([]); + const [isLoadingPlugin, setIsLoadingPlugin] = useState(false); + + const refreshPluginsList = useCallback(() => { + const plugins = pluginManager.getScraperPlugins(); + setLoadedPlugins( + plugins.map(p => ({ + name: p.name, + version: p.version, + sourceUrl: p.sourceUrl, + })) + ); + }, []); + + useEffect(() => { + refreshPluginsList(); + }, [refreshPluginsList]); + + const handleLoadPlugin = useCallback(async () => { + if (!pluginUrl.trim() || !pluginUrl.startsWith('http')) { + Alert.alert('Invalid URL', 'Please enter a valid plugin URL.'); + return; + } + setIsLoadingPlugin(true); + const success = await pluginManager.loadPluginFromUrl(pluginUrl.trim()); + setIsLoadingPlugin(false); + if (success) { + Alert.alert('Success', 'Plugin loaded successfully.'); + setPluginUrl(''); + refreshPluginsList(); + } else { + Alert.alert( + 'Error', + 'Failed to load the plugin. Check the URL and console for errors.' + ); + } + }, [pluginUrl, refreshPluginsList]); + + const handleRemovePlugin = useCallback( + (sourceUrl: string) => { + if (!sourceUrl) return; + Alert.alert( + 'Remove Plugin', + 'Are you sure you want to remove this plugin?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Remove', + style: 'destructive', + onPress: () => { + pluginManager.removePlugin(sourceUrl); + refreshPluginsList(); + }, + }, + ] + ); + }, + [refreshPluginsList] + ); + + const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; + const topSpacing = Platform.OS === 'android' ? StatusBar.currentHeight || 0 : insets.top; + const headerHeight = headerBaseHeight + topSpacing; + + return ( + + + + + navigation.goBack()} style={styles.backButton}> + + + + Plugins + + + + + + + + + + {isLoadingPlugin ? ( + + ) : ( + Load + )} + + + + + + {loadedPlugins.length > 0 ? ( + loadedPlugins.map((plugin, index) => ( + + plugin.sourceUrl ? ( + handleRemovePlugin(plugin.sourceUrl!)} style={styles.removeButton}> + + + ) : null + } + /> + )) + ) : ( + + )} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + paddingHorizontal: Math.max(1, width * 0.05), + flexDirection: 'row', + alignItems: 'flex-end', + paddingBottom: 8, + backgroundColor: 'transparent', + zIndex: 2, + }, + backButton: { + position: 'absolute', + left: Math.max(1, width * 0.05), + bottom: 8, + zIndex: 10, + }, + headerTitle: { + flex: 1, + textAlign: 'center', + fontSize: Math.min(24, width * 0.06), + fontWeight: '700', + letterSpacing: 0.3, + }, + contentContainer: { + flex: 1, + zIndex: 1, + width: '100%', + }, + scrollView: { + flex: 1, + width: '100%', + }, + scrollContent: { + flexGrow: 1, + width: '100%', + paddingBottom: 100, + }, + pluginInputContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 8, + borderBottomWidth: 1, + }, + pluginInput: { + flex: 1, + fontSize: 16, + paddingVertical: 10, + }, + loadButton: { + marginLeft: 12, + paddingHorizontal: 16, + height: 40, + borderRadius: 20, + justifyContent: 'center', + alignItems: 'center', + minWidth: 80, + }, + loadButtonText: { + fontSize: 15, + fontWeight: '600', + }, + removeButton: { + padding: 8, + borderRadius: 20, + marginLeft: 8, + }, +}); + +export default PluginsScreen; \ No newline at end of file diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 4058e9f2..f7b9b19d 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -38,12 +38,12 @@ const { width } = Dimensions.get('window'); const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; // Card component with minimalistic style -interface SettingsCardProps { +export interface SettingsCardProps { children: React.ReactNode; title?: string; } -const SettingsCard: React.FC = ({ children, title }) => { +export const SettingsCard: React.FC = ({ children, title }) => { const { currentTheme } = useTheme(); return ( @@ -68,7 +68,7 @@ const SettingsCard: React.FC = ({ children, title }) => { ); }; -interface SettingItemProps { +export interface SettingItemProps { title: string; description?: string; icon: string; @@ -78,7 +78,7 @@ interface SettingItemProps { badge?: string | number; } -const SettingItem: React.FC = ({ +export const SettingItem: React.FC = ({ title, description, icon, @@ -131,6 +131,12 @@ const SettingItem: React.FC = ({ ); }; +interface LoadedPlugin { + name: string; + version: string; + sourceUrl?: string; +} + const SettingsScreen: React.FC = () => { const { settings, updateSetting } = useSettings(); const navigation = useNavigation>(); @@ -160,32 +166,14 @@ const SettingsScreen: React.FC = () => { const [addonCount, setAddonCount] = useState(0); const [catalogCount, setCatalogCount] = useState(0); const [mdblistKeySet, setMdblistKeySet] = useState(false); - const [pluginUrl, setPluginUrl] = useState(''); + const [loadedPlugins, setLoadedPlugins] = useState([]); - const [isLoadingPlugin, setIsLoadingPlugin] = useState(false); const refreshPluginsList = useCallback(() => { const plugins = pluginManager.getScraperPlugins(); setLoadedPlugins(plugins.map(p => `${p.name} v${p.version}`)); }, []); - const handleLoadPlugin = useCallback(async () => { - if (!pluginUrl.trim() || !pluginUrl.startsWith('http')) { - Alert.alert('Invalid URL', 'Please enter a valid plugin URL.'); - return; - } - setIsLoadingPlugin(true); - const success = await pluginManager.loadPluginFromUrl(pluginUrl.trim()); - setIsLoadingPlugin(false); - if (success) { - Alert.alert('Success', 'Plugin loaded successfully.'); - setPluginUrl(''); - refreshPluginsList(); - } else { - Alert.alert('Error', 'Failed to load the plugin. Check the URL and console for errors.'); - } - }, [pluginUrl, refreshPluginsList]); - const loadData = useCallback(async () => { try { // Load addon count and get their catalogs @@ -418,6 +406,17 @@ const SettingsScreen: React.FC = () => { isLast={true} /> + + + navigation.navigate('Plugins')} + renderControl={ChevronRight} + isLast={true} + /> + {/* Playback & Experience */} @@ -526,40 +525,6 @@ const SettingsScreen: React.FC = () => { )} - - - - - {isLoadingPlugin ? ( - - ) : ( - Load - )} - - - 0 ? loadedPlugins.join(', ') : 'No custom plugins loaded'} - isLast={true} - /> - - Made with ❤️ by the Nuvio team @@ -741,6 +706,11 @@ const styles = StyleSheet.create({ fontSize: 15, fontWeight: '600', }, + removeButton: { + padding: 8, + borderRadius: 20, + marginLeft: 8, + }, }); export default SettingsScreen; \ No newline at end of file diff --git a/src/screens/index.ts b/src/screens/index.ts index 46e6babf..1718a547 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -1,13 +1,11 @@ // Export all screens from a single file -export { default as HomeScreen } from './HomeScreen'; +export { default as PlayerSettingsScreen } from './PlayerSettingsScreen'; +export { default as ProfilesScreen } from './ProfilesScreen'; export { default as SearchScreen } from './SearchScreen'; -export { default as AddonsScreen } from './AddonsScreen'; export { default as SettingsScreen } from './SettingsScreen'; -export { default as MetadataScreen } from './MetadataScreen'; -export { default as CatalogScreen } from './CatalogScreen'; -export { default as DiscoverScreen } from './DiscoverScreen'; -export { default as LibraryScreen } from './LibraryScreen'; export { default as ShowRatingsScreen } from './ShowRatingsScreen'; -export { default as CatalogSettingsScreen } from './CatalogSettingsScreen'; export { default as StreamsScreen } from './StreamsScreen'; -export { default as OnboardingScreen } from './OnboardingScreen'; \ No newline at end of file +export { default as ThemeScreen } from './ThemeScreen'; +export { default as TMDBSettingsScreen } from './TMDBSettingsScreen'; +export { default as TraktSettingsScreen } from './TraktSettingsScreen'; +export { default as PluginsScreen } from './PluginsScreen'; \ No newline at end of file diff --git a/src/services/PluginManager.ts b/src/services/PluginManager.ts index de70f3bc..27adeb90 100644 --- a/src/services/PluginManager.ts +++ b/src/services/PluginManager.ts @@ -1,9 +1,12 @@ import { logger } from '../utils/logger'; import * as cheerio from 'cheerio'; +import AsyncStorage from '@react-native-async-storage/async-storage'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore – no types for Babel standalone const Babel = require('@babel/standalone'); +const PLUGIN_URLS_STORAGE_KEY = '@plugin_urls'; + // --- Type Definitions --- interface Plugin { @@ -13,6 +16,7 @@ interface Plugin { description: string; type: 'scraper' | 'other'; getStreams: (options: GetStreamsOptions) => Promise; + sourceUrl?: string; // To track the origin of the plugin } interface Stream { @@ -104,6 +108,9 @@ class PluginManager { private constructor() { this.loadBuiltInPlugins(); + this.loadPersistedPlugins().catch(err => { + logger.error('[PluginManager] Error during async initialization', err); + }); } public static getInstance(): PluginManager { @@ -113,20 +120,89 @@ class PluginManager { return PluginManager.instance; } - public async loadPluginFromUrl(url: string): Promise { + private async loadPersistedPlugins() { + try { + const storedUrlsJson = await AsyncStorage.getItem(PLUGIN_URLS_STORAGE_KEY); + if (storedUrlsJson) { + const urls = JSON.parse(storedUrlsJson); + if (Array.isArray(urls)) { + logger.log('[PluginManager] Loading persisted plugins...', urls); + for (const url of urls) { + await this.loadPluginFromUrl(url, false); + } + } + } + } catch (error) { + logger.error('[PluginManager] Failed to load persisted plugins:', error); + } + } + + private async persistPluginUrl(url: string) { + try { + const storedUrlsJson = await AsyncStorage.getItem(PLUGIN_URLS_STORAGE_KEY); + let urls: string[] = []; + if (storedUrlsJson) { + urls = JSON.parse(storedUrlsJson); + } + if (!urls.includes(url)) { + urls.push(url); + await AsyncStorage.setItem(PLUGIN_URLS_STORAGE_KEY, JSON.stringify(urls)); + logger.log(`[PluginManager] Persisted plugin URL: ${url}`); + } + } catch (error) { + logger.error('[PluginManager] Failed to persist plugin URL:', error); + } + } + + private async removePersistedPluginUrl(url: string) { + try { + const storedUrlsJson = await AsyncStorage.getItem(PLUGIN_URLS_STORAGE_KEY); + if (storedUrlsJson) { + let urls: string[] = JSON.parse(storedUrlsJson); + const index = urls.indexOf(url); + if (index > -1) { + urls.splice(index, 1); + await AsyncStorage.setItem(PLUGIN_URLS_STORAGE_KEY, JSON.stringify(urls)); + logger.log(`[PluginManager] Removed persisted plugin URL: ${url}`); + } + } + } catch (error) { + logger.error('[PluginManager] Failed to remove persisted plugin URL:', error); + } + } + + public async loadPluginFromUrl(url: string, persist = true): Promise { logger.log(`[PluginManager] Attempting to load plugin from URL: ${url}`); + + if (this.plugins.some(p => p.sourceUrl === url)) { + logger.log(`[PluginManager] Plugin from URL ${url} is already loaded.`); + return true; + } + try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch plugin from URL: ${response.statusText}`); } const pluginCode = await response.text(); - this.runPlugin(pluginCode); - // Assuming runPlugin is synchronous for registration purposes. - // A more robust system might have runPlugin return success/failure. - return true; + const newPlugin = this.runPlugin(pluginCode, url); + + if (newPlugin) { + if (persist) { + await this.persistPluginUrl(url); + } + return true; + } + + logger.error(`[PluginManager] Plugin from ${url} executed but failed to register.`); + return false; + } catch (error) { logger.error(`[PluginManager] Failed to load plugin from URL ${url}:`, error); + if (persist === false) { + logger.log(`[PluginManager] Removing failed persisted plugin URL: ${url}`); + await this.removePersistedPluginUrl(url); + } return false; } } @@ -144,7 +220,7 @@ class PluginManager { }; // Require and execute the built-in MoviesMod plugin module (IIFE) - require('./plugins/moviesmod.plugin.js'); + //require('./plugins/moviesmod.plugin.js'); delete (global as any).registerPlugin; } catch (error) { @@ -152,7 +228,22 @@ class PluginManager { } } - private runPlugin(pluginCode: string) { + public removePlugin(sourceUrl: string) { + const pluginIndex = this.plugins.findIndex(p => p.sourceUrl === sourceUrl); + if (pluginIndex > -1) { + const plugin = this.plugins[pluginIndex]; + this.plugins.splice(pluginIndex, 1); + logger.log(`[PluginManager] Removed plugin: ${plugin.name}`); + if (plugin.sourceUrl) { + this.removePersistedPluginUrl(plugin.sourceUrl).catch(err => { + logger.error(`[PluginManager] Failed to remove persisted URL: ${plugin.sourceUrl}`, err); + }); + } + } + } + + private runPlugin(pluginCode: string, sourceUrl?: string): Plugin | null { + let registeredPlugin: Plugin | null = null; const pluginsBefore = this.plugins.length; // Attempt to strip the JSDoc-style header comment which may cause parsing issues in some JS engines. @@ -164,7 +255,9 @@ class PluginManager { // This is simpler and more reliable than using `with` or the Function constructor's scope. (global as any).registerPlugin = (plugin: Plugin) => { if (plugin && typeof plugin.getStreams === 'function') { + if (sourceUrl) plugin.sourceUrl = sourceUrl; this.plugins.push(plugin); + registeredPlugin = plugin; logger.log(`[PluginManager] Successfully registered plugin: ${plugin.name} v${plugin.version}`); } else { logger.error('[PluginManager] An invalid plugin was passed to registerPlugin.'); @@ -192,6 +285,7 @@ class PluginManager { // Clean up the global scope to prevent pollution delete (global as any).registerPlugin; } + return registeredPlugin; } public getScraperPlugins(): Plugin[] {