From 2dfb8da36ccd9e44ff8d1d9136c128bc0cdad1eb Mon Sep 17 00:00:00 2001 From: Nayif Noushad Date: Sun, 20 Apr 2025 11:43:11 +0530 Subject: [PATCH 01/13] Add react-native-draggable-flatlist dependency; enhance CatalogScreen and CatalogSettingsScreen with custom catalog name functionality and modal for renaming; improve loading states across screens for better user experience. --- package-lock.json | 15 ++ package.json | 1 + src/hooks/useCustomCatalogNames.ts | 57 +++++++ src/screens/CatalogScreen.tsx | 17 +- src/screens/CatalogSettingsScreen.tsx | 224 ++++++++++++++++++++------ src/screens/DiscoverScreen.tsx | 30 +++- src/screens/HeroCatalogsScreen.tsx | 57 ++++--- src/screens/MetadataScreen.tsx | 2 +- src/services/catalogService.ts | 30 ++-- src/services/mdblistService.ts | 67 +++++++- src/utils/catalogNameUtils.ts | 46 ++++++ 11 files changed, 444 insertions(+), 102 deletions(-) create mode 100644 src/hooks/useCustomCatalogNames.ts create mode 100644 src/utils/catalogNameUtils.ts diff --git a/package-lock.json b/package-lock.json index f019a51..43c76f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "react": "18.3.1", "react-native": "0.76.9", "react-native-awesome-slider": "^2.9.0", + "react-native-draggable-flatlist": "^4.0.2", "react-native-gesture-handler": "~2.20.2", "react-native-immersive-mode": "^2.0.2", "react-native-modal": "^14.0.0-rc.1", @@ -10617,6 +10618,20 @@ "react-native-reanimated": ">=3.0.0" } }, + "node_modules/react-native-draggable-flatlist": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.2.tgz", + "integrity": "sha512-6WN/o3WIRIJdcrQtwGju5vtXjfK8KTFtXds10QIT00MP9SxEFt5VoX7QW+JC22Rpk651cNScOVm+WKs7vDV0iw==", + "license": "MIT", + "dependencies": { + "@babel/preset-typescript": "^7.17.12" + }, + "peerDependencies": { + "react-native": ">=0.64.0", + "react-native-gesture-handler": ">=2.0.0", + "react-native-reanimated": ">=2.8.0" + } + }, "node_modules/react-native-gesture-handler": { "version": "2.20.2", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz", diff --git a/package.json b/package.json index 61ad751..46b699a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react": "18.3.1", "react-native": "0.76.9", "react-native-awesome-slider": "^2.9.0", + "react-native-draggable-flatlist": "^4.0.2", "react-native-gesture-handler": "~2.20.2", "react-native-immersive-mode": "^2.0.2", "react-native-modal": "^14.0.0-rc.1", diff --git a/src/hooks/useCustomCatalogNames.ts b/src/hooks/useCustomCatalogNames.ts new file mode 100644 index 0000000..adb1d27 --- /dev/null +++ b/src/hooks/useCustomCatalogNames.ts @@ -0,0 +1,57 @@ +import { useState, useEffect, useCallback } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { logger } from '../utils/logger'; + +const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names'; + +interface CustomNamesCache { + names: { [key: string]: string }; + lastUpdate: number; +} + +// Simple in-memory cache to avoid repeated AsyncStorage reads within the same session +let cache: CustomNamesCache | null = null; + +export function useCustomCatalogNames() { + const [customNames, setCustomNames] = useState<{ [key: string]: string } | null>(cache?.names || null); + const [isLoading, setIsLoading] = useState(!cache); // Only loading if cache is empty + + const loadCustomNames = useCallback(async () => { + // Check if cache is recent enough (e.g., within last 5 minutes) - adjust as needed + const now = Date.now(); + if (cache && (now - cache.lastUpdate < 5 * 60 * 1000)) { + if (!customNames) setCustomNames(cache.names); // Ensure state is updated if cache existed + setIsLoading(false); + return; + } + + setIsLoading(true); + try { + const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY); + const loadedNames = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {}; + setCustomNames(loadedNames); + // Update cache + cache = { names: loadedNames, lastUpdate: now }; + } catch (error) { + logger.error('Failed to load custom catalog names:', error); + setCustomNames({}); // Set to empty object on error to avoid breaking lookups + } finally { + setIsLoading(false); + } + }, []); // Removed customNames dependency to prevent re-running loop + + useEffect(() => { + loadCustomNames(); + }, [loadCustomNames]); // Load on mount and if load function changes + + const getCustomName = useCallback((addonId: string, type: string, catalogId: string, originalName: string): string => { + if (isLoading || !customNames) { + // Return original name while loading or if loading failed + return originalName; + } + const key = `${addonId}:${type}:${catalogId}`; + return customNames[key] || originalName; + }, [customNames, isLoading]); + + return { getCustomName, isLoadingCustomNames: isLoading, refreshCustomNames: loadCustomNames }; +} \ No newline at end of file diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx index cefcbfa..fc9f60b 100644 --- a/src/screens/CatalogScreen.tsx +++ b/src/screens/CatalogScreen.tsx @@ -20,6 +20,7 @@ import { colors } from '../styles'; import { Image } from 'expo-image'; import { MaterialIcons } from '@expo/vector-icons'; import { logger } from '../utils/logger'; +import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames'; type CatalogScreenProps = { route: RouteProp; @@ -44,16 +45,18 @@ const ITEM_MARGIN = SPACING.sm; const ITEM_WIDTH = (width - (SPACING.lg * 2) - (ITEM_MARGIN * 2 * NUM_COLUMNS)) / NUM_COLUMNS; const CatalogScreen: React.FC = ({ route, navigation }) => { - const { addonId, type, id, name, genreFilter } = route.params; + const { addonId, type, id, name: originalName, genreFilter } = route.params; const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [error, setError] = useState(null); - // Force dark mode const isDarkMode = true; + const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames(); + const displayName = getCustomName(addonId || '', type || '', id || '', originalName || ''); + const loadItems = useCallback(async (pageNum: number, shouldRefresh: boolean = false) => { try { if (shouldRefresh) { @@ -246,7 +249,9 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { ); - if (loading && items.length === 0) { + const isScreenLoading = loading || isLoadingCustomNames; + + if (isScreenLoading && items.length === 0) { return ( @@ -259,7 +264,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { Back - {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} + {displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} {renderLoadingState()} ); @@ -278,7 +283,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { Back - {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} + {displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} {renderErrorState()} ); @@ -296,7 +301,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { Back - {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} + {displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} {items.length > 0 ? ( { @@ -52,6 +60,11 @@ const CatalogSettingsScreen = () => { const { refreshCatalogs } = useCatalogContext(); const isDarkMode = true; // Force dark mode + // Modal State + const [isRenameModalVisible, setIsRenameModalVisible] = useState(false); + const [catalogToRename, setCatalogToRename] = useState(null); + const [currentRenameValue, setCurrentRenameValue] = useState(''); + // Load saved settings and available catalogs const loadSettings = useCallback(async () => { try { @@ -61,24 +74,22 @@ const CatalogSettingsScreen = () => { const addons = await stremioService.getInstalledAddonsAsync(); const availableCatalogs: CatalogSetting[] = []; - // Get saved settings - const savedSettings = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY); - const savedCatalogs: { [key: string]: boolean } = savedSettings ? JSON.parse(savedSettings) : {}; + // Get saved enable/disable settings + const savedSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY); + const savedEnabledSettings: { [key: string]: boolean } = savedSettingsJson ? JSON.parse(savedSettingsJson) : {}; + + // Get saved custom names + const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY); + const savedCustomNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {}; // Process each addon's catalogs addons.forEach(addon => { if (addon.catalogs && addon.catalogs.length > 0) { - // Create a map to store unique catalogs by their type and id const uniqueCatalogs = new Map(); addon.catalogs.forEach(catalog => { - // Create a unique key that includes addon id, type, and catalog id const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`; - - // Format catalog name let displayName = catalog.name || catalog.id; - - // If catalog is a movie or series catalog, make that clear const catalogType = catalog.type === 'movie' ? 'Movies' : catalog.type === 'series' ? 'TV Shows' : catalog.type.charAt(0).toUpperCase() + catalog.type.slice(1); uniqueCatalogs.set(settingKey, { @@ -86,18 +97,17 @@ const CatalogSettingsScreen = () => { catalogId: catalog.id, type: catalog.type, name: displayName, - enabled: savedCatalogs[settingKey] !== undefined ? savedCatalogs[settingKey] : true // Enable by default + enabled: savedEnabledSettings[settingKey] !== undefined ? savedEnabledSettings[settingKey] : true, + customName: savedCustomNames[settingKey] }); }); - // Add unique catalogs to the available catalogs array availableCatalogs.push(...uniqueCatalogs.values()); } }); // Group settings by addon name const grouped: GroupedCatalogs = {}; - availableCatalogs.forEach(setting => { const addon = addons.find(a => a.id === setting.addonId); if (!addon) return; @@ -106,7 +116,7 @@ const CatalogSettingsScreen = () => { grouped[setting.addonId] = { name: addon.name, catalogs: [], - expanded: true, // Start expanded + expanded: true, enabledCount: 0 }; } @@ -126,8 +136,8 @@ const CatalogSettingsScreen = () => { } }, []); - // Save settings when they change - const saveSettings = async (newSettings: CatalogSetting[]) => { + // Save settings when they change (ENABLE/DISABLE ONLY) + const saveEnabledSettings = async (newSettings: CatalogSetting[]) => { try { const settingsObj: CatalogSettingsStorage = { _lastUpdate: Date.now() @@ -139,11 +149,11 @@ const CatalogSettingsScreen = () => { await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj)); refreshCatalogs(); // Trigger catalog refresh after saving settings } catch (error) { - logger.error('Failed to save catalog settings:', error); + logger.error('Failed to save catalog enabled settings:', error); } }; - // Toggle individual catalog + // Toggle individual catalog enabled state const toggleCatalog = (addonId: string, index: number) => { const newSettings = [...settings]; const catalogsForAddon = groupedSettings[addonId].catalogs; @@ -154,7 +164,6 @@ const CatalogSettingsScreen = () => { enabled: !setting.enabled }; - // Update the setting in the flat list const flatIndex = newSettings.findIndex(s => s.addonId === setting.addonId && s.type === setting.type && @@ -165,14 +174,13 @@ const CatalogSettingsScreen = () => { newSettings[flatIndex] = updatedSetting; } - // Update the grouped settings const newGroupedSettings = { ...groupedSettings }; newGroupedSettings[addonId].catalogs[index] = updatedSetting; newGroupedSettings[addonId].enabledCount += updatedSetting.enabled ? 1 : -1; setSettings(newSettings); setGroupedSettings(newGroupedSettings); - saveSettings(newSettings); + saveEnabledSettings(newSettings); // Use specific save function }; // Toggle expansion of a group @@ -186,6 +194,47 @@ const CatalogSettingsScreen = () => { })); }; + // Handle long press on catalog item + const handleLongPress = (setting: CatalogSetting) => { + setCatalogToRename(setting); + setCurrentRenameValue(setting.customName || setting.name); + setIsRenameModalVisible(true); + }; + + // Handle saving the renamed catalog + const handleSaveRename = async () => { + if (!catalogToRename || !currentRenameValue) return; + + const settingKey = `${catalogToRename.addonId}:${catalogToRename.type}:${catalogToRename.catalogId}`; + + try { + const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY); + const customNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {}; + + const trimmedNewName = currentRenameValue.trim(); + + if (trimmedNewName === catalogToRename.name || trimmedNewName === '') { + delete customNames[settingKey]; + } else { + customNames[settingKey] = trimmedNewName; + } + + await AsyncStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames)); + + // --- Reload settings to reflect the change --- + await loadSettings(); + // --- No need to manually update local state anymore --- + + } catch (error) { + logger.error('Failed to save custom catalog name:', error); + Alert.alert('Error', 'Could not save the custom name.'); // Inform user + } finally { + setIsRenameModalVisible(false); + setCatalogToRename(null); + setCurrentRenameValue(''); + } + }; + useEffect(() => { loadSettings(); }, [loadSettings]); @@ -252,10 +301,17 @@ const CatalogSettingsScreen = () => { {group.expanded && group.catalogs.map((setting, index) => ( - + handleLongPress(setting)} // Added long press handler + style={({ pressed }) => [ + styles.catalogItem, + pressed && styles.catalogItemPressed, // Optional pressed style + ]} + > - {setting.name} + {setting.customName || setting.name} {/* Display custom or default name */} {setting.type.charAt(0).toUpperCase() + setting.type.slice(1)} @@ -268,26 +324,68 @@ const CatalogSettingsScreen = () => { thumbColor={Platform.OS === 'android' ? colors.white : undefined} ios_backgroundColor="#505050" /> - + ))} ))} - - - ORGANIZATION - - - Reorder Sections - - - - Customize Names - - - - + + {/* Rename Modal */} + { + setIsRenameModalVisible(false); + setCatalogToRename(null); + }} + > + {Platform.OS === 'ios' ? ( + setIsRenameModalVisible(false)}> + + e.stopPropagation()}> + Rename Catalog + + +