mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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.
This commit is contained in:
parent
937930540f
commit
2dfb8da36c
11 changed files with 444 additions and 102 deletions
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
57
src/hooks/useCustomCatalogNames.ts
Normal file
57
src/hooks/useCustomCatalogNames.ts
Normal file
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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<RootStackParamList, 'Catalog'>;
|
||||
|
|
@ -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<CatalogScreenProps> = ({ route, navigation }) => {
|
||||
const { addonId, type, id, name, genreFilter } = route.params;
|
||||
const { addonId, type, id, name: originalName, genreFilter } = route.params;
|
||||
const [items, setItems] = useState<Meta[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
</View>
|
||||
);
|
||||
|
||||
if (loading && items.length === 0) {
|
||||
const isScreenLoading = loading || isLoadingCustomNames;
|
||||
|
||||
if (isScreenLoading && items.length === 0) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
|
@ -259,7 +264,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
<Text style={styles.headerTitle}>{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
{renderLoadingState()}
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
@ -278,7 +283,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
{renderErrorState()}
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
@ -296,7 +301,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
|
||||
{items.length > 0 ? (
|
||||
<FlatList
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ import {
|
|||
SafeAreaView,
|
||||
StatusBar,
|
||||
Platform,
|
||||
Modal,
|
||||
TextInput,
|
||||
Pressable,
|
||||
Button,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
|
@ -18,6 +23,7 @@ import { stremioService } from '../services/stremioService';
|
|||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { logger } from '../utils/logger';
|
||||
import { BlurView } from 'expo-blur';
|
||||
|
||||
interface CatalogSetting {
|
||||
addonId: string;
|
||||
|
|
@ -25,6 +31,7 @@ interface CatalogSetting {
|
|||
type: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
customName?: string;
|
||||
}
|
||||
|
||||
interface CatalogSettingsStorage {
|
||||
|
|
@ -42,6 +49,7 @@ interface GroupedCatalogs {
|
|||
}
|
||||
|
||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
const CatalogSettingsScreen = () => {
|
||||
|
|
@ -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<CatalogSetting | null>(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<string, CatalogSetting>();
|
||||
|
||||
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 = () => {
|
|||
</TouchableOpacity>
|
||||
|
||||
{group.expanded && group.catalogs.map((setting, index) => (
|
||||
<View key={`${setting.addonId}:${setting.type}:${setting.catalogId}`} style={styles.catalogItem}>
|
||||
<Pressable
|
||||
key={`${setting.addonId}:${setting.type}:${setting.catalogId}`}
|
||||
onLongPress={() => handleLongPress(setting)} // Added long press handler
|
||||
style={({ pressed }) => [
|
||||
styles.catalogItem,
|
||||
pressed && styles.catalogItemPressed, // Optional pressed style
|
||||
]}
|
||||
>
|
||||
<View style={styles.catalogInfo}>
|
||||
<Text style={styles.catalogName}>
|
||||
{setting.name}
|
||||
{setting.customName || setting.name} {/* Display custom or default name */}
|
||||
</Text>
|
||||
<Text style={styles.catalogType}>
|
||||
{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"
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={styles.addonSection}>
|
||||
<Text style={styles.addonTitle}>ORGANIZATION</Text>
|
||||
<View style={styles.card}>
|
||||
<TouchableOpacity style={styles.organizationItem}>
|
||||
<Text style={styles.organizationItemText}>Reorder Sections</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={colors.mediumGray} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.organizationItem}>
|
||||
<Text style={styles.organizationItemText}>Customize Names</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={colors.mediumGray} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Rename Modal */}
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={isRenameModalVisible}
|
||||
onRequestClose={() => {
|
||||
setIsRenameModalVisible(false);
|
||||
setCatalogToRename(null);
|
||||
}}
|
||||
>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Pressable style={styles.modalOverlay} onPress={() => setIsRenameModalVisible(false)}>
|
||||
<BlurView
|
||||
style={styles.modalContent}
|
||||
intensity={90}
|
||||
tint="default"
|
||||
>
|
||||
<Pressable onPress={(e) => e.stopPropagation()}>
|
||||
<Text style={styles.modalTitle}>Rename Catalog</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
value={currentRenameValue}
|
||||
onChangeText={setCurrentRenameValue}
|
||||
placeholder="Enter new catalog name"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<View style={styles.modalButtons}>
|
||||
<Button title="Cancel" onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
|
||||
<Button title="Save" onPress={handleSaveRename} color={colors.primary} />
|
||||
</View>
|
||||
</Pressable>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable style={styles.modalOverlay} onPress={() => setIsRenameModalVisible(false)}>
|
||||
<Pressable style={styles.modalContent} onPress={(e) => e.stopPropagation()}>
|
||||
<Text style={styles.modalTitle}>Rename Catalog</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
value={currentRenameValue}
|
||||
onChangeText={setCurrentRenameValue}
|
||||
placeholder="Enter new catalog name"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<View style={styles.modalButtons}>
|
||||
<Button title="Cancel" onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
|
||||
<Button title="Save" onPress={handleSaveRename} color={colors.primary} />
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
|
@ -385,9 +483,14 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
||||
// Ensure last item doesn't have border if needed (check logic)
|
||||
},
|
||||
catalogItemPressed: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)', // Subtle feedback for press
|
||||
},
|
||||
catalogInfo: {
|
||||
flex: 1,
|
||||
marginRight: 8, // Add space before switch
|
||||
},
|
||||
catalogName: {
|
||||
fontSize: 15,
|
||||
|
|
@ -398,18 +501,47 @@ const styles = StyleSheet.create({
|
|||
fontSize: 13,
|
||||
color: colors.mediumGray,
|
||||
},
|
||||
organizationItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
// Modal Styles
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
},
|
||||
organizationItemText: {
|
||||
fontSize: 17,
|
||||
modalContent: {
|
||||
backgroundColor: Platform.OS === 'ios' ? undefined : colors.elevation3,
|
||||
borderRadius: 14,
|
||||
padding: 20,
|
||||
width: '85%',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 10,
|
||||
elevation: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginBottom: 15,
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalInput: {
|
||||
backgroundColor: colors.elevation1, // Darker input background
|
||||
color: colors.white,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
marginBottom: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around', // Adjust as needed (e.g., 'flex-end')
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -189,7 +189,6 @@ const CatalogSection = React.memo(({
|
|||
const { width } = Dimensions.get('window');
|
||||
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
|
||||
|
||||
// Only display the first 3 items in the row
|
||||
const displayItems = useMemo(() =>
|
||||
catalog.items.slice(0, 3),
|
||||
[catalog.items]
|
||||
|
|
@ -207,13 +206,34 @@ const CatalogSection = React.memo(({
|
|||
), [handleContentPress]);
|
||||
|
||||
const handleSeeMorePress = useCallback(() => {
|
||||
// Get addon/catalog info from the first item (assuming homogeneity)
|
||||
const firstItem = catalog.items[0];
|
||||
if (!firstItem) return; // Should not happen if section exists
|
||||
|
||||
// We need addonId and catalogId. These aren't directly on StreamingContent.
|
||||
// We might need to fetch this or adjust the GenreCatalog structure.
|
||||
// FOR NOW: Assuming CatalogScreen can handle potentially missing addonId/catalogId
|
||||
// OR: We could pass the *genre* as the name and let CatalogScreen figure it out?
|
||||
// Let's pass the necessary info if available, assuming StreamingContent might have it
|
||||
// (Requires checking StreamingContent interface or how it's populated)
|
||||
|
||||
// --- TEMPORARY/PLACEHOLDER ---
|
||||
// Ideally, GenreCatalog should contain addonId/catalogId for the group.
|
||||
// If not, CatalogScreen needs modification or we fetch IDs here.
|
||||
// Let's stick to passing genre and type for now, CatalogScreen logic might suffice?
|
||||
navigation.navigate('Catalog', {
|
||||
id: 'discover',
|
||||
// We don't have a single catalog ID or Addon ID for a genre section.
|
||||
// Pass the genre as the 'id' and 'name' for CatalogScreen to potentially filter.
|
||||
// This might require CatalogScreen to be adapted to handle genre-based views.
|
||||
addonId: 'genre-based', // Placeholder or identifier
|
||||
id: catalog.genre,
|
||||
type: selectedCategory.type,
|
||||
name: `${catalog.genre} ${selectedCategory.name}`,
|
||||
genreFilter: catalog.genre
|
||||
name: `${catalog.genre} ${selectedCategory.name}`, // Pass constructed name for now
|
||||
genreFilter: catalog.genre // Keep the genre filter
|
||||
});
|
||||
}, [navigation, selectedCategory, catalog.genre]);
|
||||
// --- END TEMPORARY ---
|
||||
|
||||
}, [navigation, selectedCategory, catalog.genre, catalog.items]);
|
||||
|
||||
const keyExtractor = useCallback((item: StreamingContent) => item.id, []);
|
||||
const ItemSeparator = useCallback(() => <View style={{ width: 16 }} />, []);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useNavigation } from '@react-navigation/native';
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../styles/colors';
|
||||
import { catalogService, StreamingAddon } from '../services/catalogService';
|
||||
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
|
||||
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
|
||||
const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames();
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigation.goBack();
|
||||
|
|
@ -125,7 +127,7 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
</Text>
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
{loading || isLoadingCustomNames ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||
|
|
@ -175,30 +177,35 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
styles.catalogsContainer,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation1 : colors.white }
|
||||
]}>
|
||||
{addonCatalogs.map(catalog => (
|
||||
<TouchableOpacity
|
||||
key={catalog.id}
|
||||
style={[
|
||||
styles.catalogItem,
|
||||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
|
||||
]}
|
||||
onPress={() => toggleCatalog(catalog.id)}
|
||||
>
|
||||
<View style={styles.catalogInfo}>
|
||||
<Text style={[styles.catalogName, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
{catalog.name}
|
||||
</Text>
|
||||
<Text style={[styles.catalogType, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||
{catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialIcons
|
||||
name={selectedCatalogs.includes(catalog.id) ? "check-box" : "check-box-outline-blank"}
|
||||
size={24}
|
||||
color={selectedCatalogs.includes(catalog.id) ? colors.primary : isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{addonCatalogs.map(catalog => {
|
||||
const [addonId, type, catalogId] = catalog.id.split(':');
|
||||
const displayName = getCustomName(addonId, type, catalogId, catalog.name);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={catalog.id}
|
||||
style={[
|
||||
styles.catalogItem,
|
||||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
|
||||
]}
|
||||
onPress={() => toggleCatalog(catalog.id)}
|
||||
>
|
||||
<View style={styles.catalogInfo}>
|
||||
<Text style={[styles.catalogName, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text style={[styles.catalogType, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||
{catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialIcons
|
||||
name={selectedCatalogs.includes(catalog.id) ? "check-box" : "check-box-outline-blank"}
|
||||
size={24}
|
||||
color={selectedCatalogs.includes(catalog.id) ? colors.primary : isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ const MetadataScreen = () => {
|
|||
|
||||
// Fetch logo immediately for TMDB content
|
||||
useEffect(() => {
|
||||
if (metadata && id.startsWith('tmdb:')) {
|
||||
if (metadata && id.startsWith('tmdb:') && !metadata.logo) {
|
||||
const fetchLogo = async () => {
|
||||
try {
|
||||
const tmdbId = id.split(':')[1];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||
import axios from 'axios';
|
||||
import { TMDBService } from './tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { getCatalogDisplayName } from '../utils/catalogNameUtils';
|
||||
|
||||
export interface StreamingAddon {
|
||||
id: string;
|
||||
|
|
@ -55,6 +56,8 @@ export interface CatalogContent {
|
|||
items: StreamingContent[];
|
||||
}
|
||||
|
||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||
|
||||
class CatalogService {
|
||||
private static instance: CatalogService;
|
||||
private readonly LIBRARY_KEY = 'stremio-library';
|
||||
|
|
@ -137,43 +140,37 @@ class CatalogService {
|
|||
const addons = await this.getAllAddons();
|
||||
const catalogs: CatalogContent[] = [];
|
||||
|
||||
// Get saved catalog settings
|
||||
const savedSettings = await AsyncStorage.getItem('catalog_settings');
|
||||
const catalogSettings: { [key: string]: boolean } = savedSettings ? JSON.parse(savedSettings) : {};
|
||||
// Load enabled/disabled settings
|
||||
const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
||||
|
||||
// Get featured catalogs
|
||||
for (const addon of addons) {
|
||||
if (addon.catalogs && addon.catalogs.length > 0) {
|
||||
// For each catalog, check if it's enabled in settings
|
||||
if (addon.catalogs) {
|
||||
for (const catalog of addon.catalogs) {
|
||||
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
||||
// If setting doesn't exist, default to true for backward compatibility
|
||||
const isEnabled = catalogSettings[settingKey] ?? true;
|
||||
|
||||
if (isEnabled) {
|
||||
try {
|
||||
// Get the items for this catalog
|
||||
const addonManifest = await stremioService.getInstalledAddonsAsync();
|
||||
const manifest = addonManifest.find(a => a.id === addon.id);
|
||||
if (!manifest) continue;
|
||||
|
||||
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
|
||||
if (metas && metas.length > 0) {
|
||||
// Convert Meta to StreamingContent
|
||||
const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
|
||||
|
||||
// Format the catalog name
|
||||
let displayName = catalog.name;
|
||||
// Get potentially custom display name
|
||||
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name);
|
||||
|
||||
// Remove duplicate words and clean up the name (case-insensitive)
|
||||
const words = displayName.split(' ');
|
||||
const uniqueWords = [];
|
||||
const seenWords = new Set();
|
||||
|
||||
for (const word of words) {
|
||||
const lowerWord = word.toLowerCase();
|
||||
if (!seenWords.has(lowerWord)) {
|
||||
uniqueWords.push(word); // Keep original case
|
||||
uniqueWords.push(word);
|
||||
seenWords.add(lowerWord);
|
||||
}
|
||||
}
|
||||
|
|
@ -208,7 +205,6 @@ class CatalogService {
|
|||
const addons = await this.getAllAddons();
|
||||
const catalogs: CatalogContent[] = [];
|
||||
|
||||
// Filter addons with catalogs of the specified type
|
||||
const typeAddons = addons.filter(addon =>
|
||||
addon.catalogs && addon.catalogs.some(catalog => catalog.type === type)
|
||||
);
|
||||
|
|
@ -222,18 +218,20 @@ class CatalogService {
|
|||
const manifest = addonManifest.find(a => a.id === addon.id);
|
||||
if (!manifest) continue;
|
||||
|
||||
// Apply genre filter if provided
|
||||
const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : [];
|
||||
const metas = await stremioService.getCatalog(manifest, type, catalog.id, 1, filters);
|
||||
|
||||
if (metas && metas.length > 0) {
|
||||
const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
|
||||
|
||||
// Get potentially custom display name
|
||||
const displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name);
|
||||
|
||||
catalogs.push({
|
||||
addon: addon.id,
|
||||
type,
|
||||
id: catalog.id,
|
||||
name: catalog.name,
|
||||
name: displayName,
|
||||
genre: genreFilter,
|
||||
items
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ export class MDBListService {
|
|||
private static instance: MDBListService;
|
||||
private apiKey: string | null = null;
|
||||
private enabled: boolean = true;
|
||||
private apiKeyErrorCount: number = 0; // Add counter for API key errors
|
||||
private lastApiKeyErrorTime: number = 0; // To track when last error occurred
|
||||
private ratingsCache: Map<string, MDBListRatings | null> = new Map(); // Cache for ratings - null values represent known "not found" results
|
||||
|
||||
private constructor() {
|
||||
logger.log('[MDBListService] Service initialized');
|
||||
|
|
@ -36,16 +39,32 @@ export class MDBListService {
|
|||
try {
|
||||
// First check if MDBList is enabled
|
||||
const enabledSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
|
||||
const wasEnabled = this.enabled;
|
||||
this.enabled = enabledSetting === null || enabledSetting === 'true';
|
||||
logger.log('[MDBListService] MDBList enabled:', this.enabled);
|
||||
|
||||
// Clear cache if enabled state changed
|
||||
if (wasEnabled !== this.enabled) {
|
||||
this.clearCache();
|
||||
logger.log('[MDBListService] Cache cleared due to enabled state change');
|
||||
}
|
||||
|
||||
if (!this.enabled) {
|
||||
logger.log('[MDBListService] MDBList is disabled, skipping API key loading');
|
||||
this.apiKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiKey = await AsyncStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
|
||||
const newApiKey = await AsyncStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
|
||||
// Reset error counter when API key changes
|
||||
if (newApiKey !== this.apiKey) {
|
||||
this.apiKeyErrorCount = 0;
|
||||
this.lastApiKeyErrorTime = 0;
|
||||
// Clear the cache when API key changes
|
||||
this.clearCache();
|
||||
logger.log('[MDBListService] Cache cleared due to API key change');
|
||||
}
|
||||
this.apiKey = newApiKey;
|
||||
logger.log('[MDBListService] Initialized with API key:', this.apiKey ? 'Present' : 'Not found');
|
||||
} catch (error) {
|
||||
logger.error('[MDBListService] Failed to load settings:', error);
|
||||
|
|
@ -57,6 +76,17 @@ export class MDBListService {
|
|||
async getRatings(imdbId: string, mediaType: 'movie' | 'show'): Promise<MDBListRatings | null> {
|
||||
logger.log(`[MDBListService] Fetching ratings for ${mediaType} with IMDB ID:`, imdbId);
|
||||
|
||||
// Create cache key
|
||||
const cacheKey = `${mediaType}:${imdbId}`;
|
||||
|
||||
// Check cache first - including null values which mean "no ratings available"
|
||||
if (this.ratingsCache.has(cacheKey)) {
|
||||
const cachedRatings = this.ratingsCache.get(cacheKey);
|
||||
logger.log(`[MDBListService] Retrieved ${cachedRatings ? 'ratings' : 'negative result'} from cache for ${mediaType}:`, imdbId);
|
||||
// TypeScript knows cachedRatings can't be undefined here since we checked with .has()
|
||||
return cachedRatings as MDBListRatings | null;
|
||||
}
|
||||
|
||||
// Check if MDBList is enabled before doing anything else
|
||||
if (!this.enabled) {
|
||||
// Try to refresh enabled status in case it was changed
|
||||
|
|
@ -139,7 +169,13 @@ export class MDBListService {
|
|||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
if (errorJson.error === "Invalid API key") {
|
||||
logger.error('[MDBListService] API Key rejected by server:', this.apiKey);
|
||||
// Only log the error every 5 requests or if more than 10 minutes have passed
|
||||
const now = Date.now();
|
||||
this.apiKeyErrorCount++;
|
||||
if (this.apiKeyErrorCount === 1 || this.apiKeyErrorCount % 5 === 0 || now - this.lastApiKeyErrorTime > 600000) {
|
||||
logger.error('[MDBListService] API Key rejected by server:', this.apiKey);
|
||||
this.lastApiKeyErrorTime = now;
|
||||
}
|
||||
} else {
|
||||
logger.warn(`[MDBListService] 403 Forbidden, but not invalid key error:`, errorJson);
|
||||
}
|
||||
|
|
@ -171,12 +207,37 @@ export class MDBListService {
|
|||
|
||||
const ratingCount = Object.keys(ratings).length;
|
||||
logger.log(`[MDBListService] Fetched ${ratingCount} ratings successfully:`, ratings);
|
||||
return ratingCount > 0 ? ratings : null;
|
||||
|
||||
// Store in cache even if we got no ratings - this prevents repeated API calls for content with no ratings
|
||||
const result = ratingCount > 0 ? ratings : null;
|
||||
this.ratingsCache.set(cacheKey, result);
|
||||
logger.log(`[MDBListService] Stored ${result ? 'ratings' : 'negative result'} in cache for ${mediaType}:`, imdbId);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[MDBListService] Error fetching MDBList ratings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to clear the cache
|
||||
clearCache(): void {
|
||||
this.ratingsCache.clear();
|
||||
logger.log('[MDBListService] Cache cleared');
|
||||
}
|
||||
|
||||
// Method to invalidate a specific cache entry
|
||||
invalidateCache(imdbId: string, mediaType: 'movie' | 'show'): void {
|
||||
const cacheKey = `${mediaType}:${imdbId}`;
|
||||
const hadEntry = this.ratingsCache.delete(cacheKey);
|
||||
logger.log(`[MDBListService] Cache entry ${hadEntry ? 'invalidated' : 'not found'} for ${mediaType}:`, imdbId);
|
||||
}
|
||||
|
||||
// Method to check if a rating is in cache
|
||||
isCached(imdbId: string, mediaType: 'movie' | 'show'): boolean {
|
||||
const cacheKey = `${mediaType}:${imdbId}`;
|
||||
return this.ratingsCache.has(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
export const mdblistService = MDBListService.getInstance();
|
||||
|
|
|
|||
46
src/utils/catalogNameUtils.ts
Normal file
46
src/utils/catalogNameUtils.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { logger } from './logger';
|
||||
|
||||
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
||||
|
||||
// Initialize cache as an empty object
|
||||
let customNamesCache: { [key: string]: string } = {};
|
||||
let cacheTimestamp: number = 0; // 0 indicates cache is invalid/empty initially
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
async function loadCustomNamesIfNeeded(): Promise<{ [key: string]: string }> {
|
||||
const now = Date.now();
|
||||
// Check if cache is valid based on timestamp
|
||||
if (cacheTimestamp > 0 && (now - cacheTimestamp < CACHE_DURATION)) {
|
||||
return customNamesCache; // Cache is valid and guaranteed to be an object
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Loading custom catalog names from storage...');
|
||||
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
|
||||
// Assign parsed object or empty object if null/error
|
||||
customNamesCache = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
|
||||
cacheTimestamp = now; // Set timestamp only on successful load
|
||||
return customNamesCache;
|
||||
} catch (error) {
|
||||
logger.error('Failed to load custom catalog names in utility:', error);
|
||||
// Invalidate cache timestamp on error
|
||||
cacheTimestamp = 0;
|
||||
// Return the last known cache (which might be empty {}), or a fresh empty object
|
||||
return customNamesCache || {}; // Return cache (could be outdated but non-null) or empty {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCatalogDisplayName(addonId: string, type: string, catalogId: string, originalName: string): Promise<string> {
|
||||
// Ensure cache is loaded/refreshed before getting name
|
||||
const customNames = await loadCustomNamesIfNeeded();
|
||||
const key = `${addonId}:${type}:${catalogId}`;
|
||||
return customNames[key] || originalName;
|
||||
}
|
||||
|
||||
// Function to clear the cache if settings are updated elsewhere
|
||||
export function clearCustomNameCache() {
|
||||
customNamesCache = {}; // Reset to empty object
|
||||
cacheTimestamp = 0; // Invalidate timestamp
|
||||
logger.info('Custom catalog name cache cleared.');
|
||||
}
|
||||
Loading…
Reference in a new issue