mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-07 02:30: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": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
|
"react-native-draggable-flatlist": "^4.0.2",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-immersive-mode": "^2.0.2",
|
"react-native-immersive-mode": "^2.0.2",
|
||||||
"react-native-modal": "^14.0.0-rc.1",
|
"react-native-modal": "^14.0.0-rc.1",
|
||||||
|
|
@ -10617,6 +10618,20 @@
|
||||||
"react-native-reanimated": ">=3.0.0"
|
"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": {
|
"node_modules/react-native-gesture-handler": {
|
||||||
"version": "2.20.2",
|
"version": "2.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
"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": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
|
"react-native-draggable-flatlist": "^4.0.2",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-immersive-mode": "^2.0.2",
|
"react-native-immersive-mode": "^2.0.2",
|
||||||
"react-native-modal": "^14.0.0-rc.1",
|
"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 { Image } from 'expo-image';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||||
|
|
||||||
type CatalogScreenProps = {
|
type CatalogScreenProps = {
|
||||||
route: RouteProp<RootStackParamList, 'Catalog'>;
|
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 ITEM_WIDTH = (width - (SPACING.lg * 2) - (ITEM_MARGIN * 2 * NUM_COLUMNS)) / NUM_COLUMNS;
|
||||||
|
|
||||||
const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
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 [items, setItems] = useState<Meta[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// Force dark mode
|
|
||||||
const isDarkMode = true;
|
const isDarkMode = true;
|
||||||
|
|
||||||
|
const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames();
|
||||||
|
const displayName = getCustomName(addonId || '', type || '', id || '', originalName || '');
|
||||||
|
|
||||||
const loadItems = useCallback(async (pageNum: number, shouldRefresh: boolean = false) => {
|
const loadItems = useCallback(async (pageNum: number, shouldRefresh: boolean = false) => {
|
||||||
try {
|
try {
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
|
|
@ -246,7 +249,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading && items.length === 0) {
|
const isScreenLoading = loading || isLoadingCustomNames;
|
||||||
|
|
||||||
|
if (isScreenLoading && items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
@ -259,7 +264,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<Text style={styles.backText}>Back</Text>
|
<Text style={styles.backText}>Back</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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()}
|
{renderLoadingState()}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
@ -278,7 +283,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<Text style={styles.backText}>Back</Text>
|
<Text style={styles.backText}>Back</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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()}
|
{renderErrorState()}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
@ -296,7 +301,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<Text style={styles.backText}>Back</Text>
|
<Text style={styles.backText}>Back</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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 ? (
|
{items.length > 0 ? (
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import {
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
Pressable,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
@ -18,6 +23,7 @@ import { stremioService } from '../services/stremioService';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { BlurView } from 'expo-blur';
|
||||||
|
|
||||||
interface CatalogSetting {
|
interface CatalogSetting {
|
||||||
addonId: string;
|
addonId: string;
|
||||||
|
|
@ -25,6 +31,7 @@ interface CatalogSetting {
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
customName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CatalogSettingsStorage {
|
interface CatalogSettingsStorage {
|
||||||
|
|
@ -42,6 +49,7 @@ interface GroupedCatalogs {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||||
|
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
const CatalogSettingsScreen = () => {
|
const CatalogSettingsScreen = () => {
|
||||||
|
|
@ -52,6 +60,11 @@ const CatalogSettingsScreen = () => {
|
||||||
const { refreshCatalogs } = useCatalogContext();
|
const { refreshCatalogs } = useCatalogContext();
|
||||||
const isDarkMode = true; // Force dark mode
|
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
|
// Load saved settings and available catalogs
|
||||||
const loadSettings = useCallback(async () => {
|
const loadSettings = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -61,24 +74,22 @@ const CatalogSettingsScreen = () => {
|
||||||
const addons = await stremioService.getInstalledAddonsAsync();
|
const addons = await stremioService.getInstalledAddonsAsync();
|
||||||
const availableCatalogs: CatalogSetting[] = [];
|
const availableCatalogs: CatalogSetting[] = [];
|
||||||
|
|
||||||
// Get saved settings
|
// Get saved enable/disable settings
|
||||||
const savedSettings = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
const savedSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||||
const savedCatalogs: { [key: string]: boolean } = savedSettings ? JSON.parse(savedSettings) : {};
|
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
|
// Process each addon's catalogs
|
||||||
addons.forEach(addon => {
|
addons.forEach(addon => {
|
||||||
if (addon.catalogs && addon.catalogs.length > 0) {
|
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>();
|
const uniqueCatalogs = new Map<string, CatalogSetting>();
|
||||||
|
|
||||||
addon.catalogs.forEach(catalog => {
|
addon.catalogs.forEach(catalog => {
|
||||||
// Create a unique key that includes addon id, type, and catalog id
|
|
||||||
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
||||||
|
|
||||||
// Format catalog name
|
|
||||||
let displayName = catalog.name || catalog.id;
|
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);
|
const catalogType = catalog.type === 'movie' ? 'Movies' : catalog.type === 'series' ? 'TV Shows' : catalog.type.charAt(0).toUpperCase() + catalog.type.slice(1);
|
||||||
|
|
||||||
uniqueCatalogs.set(settingKey, {
|
uniqueCatalogs.set(settingKey, {
|
||||||
|
|
@ -86,18 +97,17 @@ const CatalogSettingsScreen = () => {
|
||||||
catalogId: catalog.id,
|
catalogId: catalog.id,
|
||||||
type: catalog.type,
|
type: catalog.type,
|
||||||
name: displayName,
|
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());
|
availableCatalogs.push(...uniqueCatalogs.values());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Group settings by addon name
|
// Group settings by addon name
|
||||||
const grouped: GroupedCatalogs = {};
|
const grouped: GroupedCatalogs = {};
|
||||||
|
|
||||||
availableCatalogs.forEach(setting => {
|
availableCatalogs.forEach(setting => {
|
||||||
const addon = addons.find(a => a.id === setting.addonId);
|
const addon = addons.find(a => a.id === setting.addonId);
|
||||||
if (!addon) return;
|
if (!addon) return;
|
||||||
|
|
@ -106,7 +116,7 @@ const CatalogSettingsScreen = () => {
|
||||||
grouped[setting.addonId] = {
|
grouped[setting.addonId] = {
|
||||||
name: addon.name,
|
name: addon.name,
|
||||||
catalogs: [],
|
catalogs: [],
|
||||||
expanded: true, // Start expanded
|
expanded: true,
|
||||||
enabledCount: 0
|
enabledCount: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -126,8 +136,8 @@ const CatalogSettingsScreen = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Save settings when they change
|
// Save settings when they change (ENABLE/DISABLE ONLY)
|
||||||
const saveSettings = async (newSettings: CatalogSetting[]) => {
|
const saveEnabledSettings = async (newSettings: CatalogSetting[]) => {
|
||||||
try {
|
try {
|
||||||
const settingsObj: CatalogSettingsStorage = {
|
const settingsObj: CatalogSettingsStorage = {
|
||||||
_lastUpdate: Date.now()
|
_lastUpdate: Date.now()
|
||||||
|
|
@ -139,11 +149,11 @@ const CatalogSettingsScreen = () => {
|
||||||
await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
|
await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
|
||||||
refreshCatalogs(); // Trigger catalog refresh after saving settings
|
refreshCatalogs(); // Trigger catalog refresh after saving settings
|
||||||
} catch (error) {
|
} 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 toggleCatalog = (addonId: string, index: number) => {
|
||||||
const newSettings = [...settings];
|
const newSettings = [...settings];
|
||||||
const catalogsForAddon = groupedSettings[addonId].catalogs;
|
const catalogsForAddon = groupedSettings[addonId].catalogs;
|
||||||
|
|
@ -154,7 +164,6 @@ const CatalogSettingsScreen = () => {
|
||||||
enabled: !setting.enabled
|
enabled: !setting.enabled
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the setting in the flat list
|
|
||||||
const flatIndex = newSettings.findIndex(s =>
|
const flatIndex = newSettings.findIndex(s =>
|
||||||
s.addonId === setting.addonId &&
|
s.addonId === setting.addonId &&
|
||||||
s.type === setting.type &&
|
s.type === setting.type &&
|
||||||
|
|
@ -165,14 +174,13 @@ const CatalogSettingsScreen = () => {
|
||||||
newSettings[flatIndex] = updatedSetting;
|
newSettings[flatIndex] = updatedSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the grouped settings
|
|
||||||
const newGroupedSettings = { ...groupedSettings };
|
const newGroupedSettings = { ...groupedSettings };
|
||||||
newGroupedSettings[addonId].catalogs[index] = updatedSetting;
|
newGroupedSettings[addonId].catalogs[index] = updatedSetting;
|
||||||
newGroupedSettings[addonId].enabledCount += updatedSetting.enabled ? 1 : -1;
|
newGroupedSettings[addonId].enabledCount += updatedSetting.enabled ? 1 : -1;
|
||||||
|
|
||||||
setSettings(newSettings);
|
setSettings(newSettings);
|
||||||
setGroupedSettings(newGroupedSettings);
|
setGroupedSettings(newGroupedSettings);
|
||||||
saveSettings(newSettings);
|
saveEnabledSettings(newSettings); // Use specific save function
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle expansion of a group
|
// 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(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}, [loadSettings]);
|
}, [loadSettings]);
|
||||||
|
|
@ -252,10 +301,17 @@ const CatalogSettingsScreen = () => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{group.expanded && group.catalogs.map((setting, index) => (
|
{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}>
|
<View style={styles.catalogInfo}>
|
||||||
<Text style={styles.catalogName}>
|
<Text style={styles.catalogName}>
|
||||||
{setting.name}
|
{setting.customName || setting.name} {/* Display custom or default name */}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.catalogType}>
|
<Text style={styles.catalogType}>
|
||||||
{setting.type.charAt(0).toUpperCase() + setting.type.slice(1)}
|
{setting.type.charAt(0).toUpperCase() + setting.type.slice(1)}
|
||||||
|
|
@ -268,26 +324,68 @@ const CatalogSettingsScreen = () => {
|
||||||
thumbColor={Platform.OS === 'android' ? colors.white : undefined}
|
thumbColor={Platform.OS === 'android' ? colors.white : undefined}
|
||||||
ios_backgroundColor="#505050"
|
ios_backgroundColor="#505050"
|
||||||
/>
|
/>
|
||||||
</View>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</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>
|
</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>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -385,9 +483,14 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
borderBottomWidth: 0.5,
|
borderBottomWidth: 0.5,
|
||||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
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: {
|
catalogInfo: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
marginRight: 8, // Add space before switch
|
||||||
},
|
},
|
||||||
catalogName: {
|
catalogName: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
|
@ -398,18 +501,47 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: colors.mediumGray,
|
color: colors.mediumGray,
|
||||||
},
|
},
|
||||||
organizationItem: {
|
|
||||||
flexDirection: 'row',
|
// Modal Styles
|
||||||
justifyContent: 'space-between',
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 12,
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||||
paddingHorizontal: 16,
|
|
||||||
borderBottomWidth: 0.5,
|
|
||||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
},
|
||||||
organizationItemText: {
|
modalContent: {
|
||||||
fontSize: 17,
|
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,
|
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 { width } = Dimensions.get('window');
|
||||||
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
|
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
|
||||||
|
|
||||||
// Only display the first 3 items in the row
|
|
||||||
const displayItems = useMemo(() =>
|
const displayItems = useMemo(() =>
|
||||||
catalog.items.slice(0, 3),
|
catalog.items.slice(0, 3),
|
||||||
[catalog.items]
|
[catalog.items]
|
||||||
|
|
@ -207,13 +206,34 @@ const CatalogSection = React.memo(({
|
||||||
), [handleContentPress]);
|
), [handleContentPress]);
|
||||||
|
|
||||||
const handleSeeMorePress = useCallback(() => {
|
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', {
|
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,
|
type: selectedCategory.type,
|
||||||
name: `${catalog.genre} ${selectedCategory.name}`,
|
name: `${catalog.genre} ${selectedCategory.name}`, // Pass constructed name for now
|
||||||
genreFilter: catalog.genre
|
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 keyExtractor = useCallback((item: StreamingContent) => item.id, []);
|
||||||
const ItemSeparator = useCallback(() => <View style={{ width: 16 }} />, []);
|
const ItemSeparator = useCallback(() => <View style={{ width: 16 }} />, []);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useNavigation } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../styles/colors';
|
import { colors } from '../styles/colors';
|
||||||
import { catalogService, StreamingAddon } from '../services/catalogService';
|
import { catalogService, StreamingAddon } from '../services/catalogService';
|
||||||
|
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||||
|
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
|
|
@ -36,6 +37,7 @@ const HeroCatalogsScreen: React.FC = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
|
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
|
||||||
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
|
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
|
||||||
|
const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames();
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
|
|
@ -125,7 +127,7 @@ const HeroCatalogsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{loading ? (
|
{loading || isLoadingCustomNames ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
<Text style={[styles.loadingText, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
<Text style={[styles.loadingText, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||||
|
|
@ -175,30 +177,35 @@ const HeroCatalogsScreen: React.FC = () => {
|
||||||
styles.catalogsContainer,
|
styles.catalogsContainer,
|
||||||
{ backgroundColor: isDarkMode ? colors.elevation1 : colors.white }
|
{ backgroundColor: isDarkMode ? colors.elevation1 : colors.white }
|
||||||
]}>
|
]}>
|
||||||
{addonCatalogs.map(catalog => (
|
{addonCatalogs.map(catalog => {
|
||||||
<TouchableOpacity
|
const [addonId, type, catalogId] = catalog.id.split(':');
|
||||||
key={catalog.id}
|
const displayName = getCustomName(addonId, type, catalogId, catalog.name);
|
||||||
style={[
|
|
||||||
styles.catalogItem,
|
return (
|
||||||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
|
<TouchableOpacity
|
||||||
]}
|
key={catalog.id}
|
||||||
onPress={() => toggleCatalog(catalog.id)}
|
style={[
|
||||||
>
|
styles.catalogItem,
|
||||||
<View style={styles.catalogInfo}>
|
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
|
||||||
<Text style={[styles.catalogName, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
]}
|
||||||
{catalog.name}
|
onPress={() => toggleCatalog(catalog.id)}
|
||||||
</Text>
|
>
|
||||||
<Text style={[styles.catalogType, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
<View style={styles.catalogInfo}>
|
||||||
{catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
|
<Text style={[styles.catalogName, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||||
</Text>
|
{displayName}
|
||||||
</View>
|
</Text>
|
||||||
<MaterialIcons
|
<Text style={[styles.catalogType, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||||
name={selectedCatalogs.includes(catalog.id) ? "check-box" : "check-box-outline-blank"}
|
{catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
|
||||||
size={24}
|
</Text>
|
||||||
color={selectedCatalogs.includes(catalog.id) ? colors.primary : isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}
|
</View>
|
||||||
/>
|
<MaterialIcons
|
||||||
</TouchableOpacity>
|
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>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ const MetadataScreen = () => {
|
||||||
|
|
||||||
// Fetch logo immediately for TMDB content
|
// Fetch logo immediately for TMDB content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metadata && id.startsWith('tmdb:')) {
|
if (metadata && id.startsWith('tmdb:') && !metadata.logo) {
|
||||||
const fetchLogo = async () => {
|
const fetchLogo = async () => {
|
||||||
try {
|
try {
|
||||||
const tmdbId = id.split(':')[1];
|
const tmdbId = id.split(':')[1];
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { TMDBService } from './tmdbService';
|
import { TMDBService } from './tmdbService';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { getCatalogDisplayName } from '../utils/catalogNameUtils';
|
||||||
|
|
||||||
export interface StreamingAddon {
|
export interface StreamingAddon {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -55,6 +56,8 @@ export interface CatalogContent {
|
||||||
items: StreamingContent[];
|
items: StreamingContent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||||
|
|
||||||
class CatalogService {
|
class CatalogService {
|
||||||
private static instance: CatalogService;
|
private static instance: CatalogService;
|
||||||
private readonly LIBRARY_KEY = 'stremio-library';
|
private readonly LIBRARY_KEY = 'stremio-library';
|
||||||
|
|
@ -137,43 +140,37 @@ class CatalogService {
|
||||||
const addons = await this.getAllAddons();
|
const addons = await this.getAllAddons();
|
||||||
const catalogs: CatalogContent[] = [];
|
const catalogs: CatalogContent[] = [];
|
||||||
|
|
||||||
// Get saved catalog settings
|
// Load enabled/disabled settings
|
||||||
const savedSettings = await AsyncStorage.getItem('catalog_settings');
|
const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||||
const catalogSettings: { [key: string]: boolean } = savedSettings ? JSON.parse(savedSettings) : {};
|
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
||||||
|
|
||||||
// Get featured catalogs
|
|
||||||
for (const addon of addons) {
|
for (const addon of addons) {
|
||||||
if (addon.catalogs && addon.catalogs.length > 0) {
|
if (addon.catalogs) {
|
||||||
// For each catalog, check if it's enabled in settings
|
|
||||||
for (const catalog of addon.catalogs) {
|
for (const catalog of addon.catalogs) {
|
||||||
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
||||||
// If setting doesn't exist, default to true for backward compatibility
|
|
||||||
const isEnabled = catalogSettings[settingKey] ?? true;
|
const isEnabled = catalogSettings[settingKey] ?? true;
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
try {
|
try {
|
||||||
// Get the items for this catalog
|
|
||||||
const addonManifest = await stremioService.getInstalledAddonsAsync();
|
const addonManifest = await stremioService.getInstalledAddonsAsync();
|
||||||
const manifest = addonManifest.find(a => a.id === addon.id);
|
const manifest = addonManifest.find(a => a.id === addon.id);
|
||||||
if (!manifest) continue;
|
if (!manifest) continue;
|
||||||
|
|
||||||
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
|
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
|
||||||
if (metas && metas.length > 0) {
|
if (metas && metas.length > 0) {
|
||||||
// Convert Meta to StreamingContent
|
|
||||||
const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
|
const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
|
||||||
|
|
||||||
// Format the catalog name
|
// Get potentially custom display name
|
||||||
let displayName = catalog.name;
|
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name);
|
||||||
|
|
||||||
// Remove duplicate words and clean up the name (case-insensitive)
|
// Remove duplicate words and clean up the name (case-insensitive)
|
||||||
const words = displayName.split(' ');
|
const words = displayName.split(' ');
|
||||||
const uniqueWords = [];
|
const uniqueWords = [];
|
||||||
const seenWords = new Set();
|
const seenWords = new Set();
|
||||||
|
|
||||||
for (const word of words) {
|
for (const word of words) {
|
||||||
const lowerWord = word.toLowerCase();
|
const lowerWord = word.toLowerCase();
|
||||||
if (!seenWords.has(lowerWord)) {
|
if (!seenWords.has(lowerWord)) {
|
||||||
uniqueWords.push(word); // Keep original case
|
uniqueWords.push(word);
|
||||||
seenWords.add(lowerWord);
|
seenWords.add(lowerWord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +205,6 @@ class CatalogService {
|
||||||
const addons = await this.getAllAddons();
|
const addons = await this.getAllAddons();
|
||||||
const catalogs: CatalogContent[] = [];
|
const catalogs: CatalogContent[] = [];
|
||||||
|
|
||||||
// Filter addons with catalogs of the specified type
|
|
||||||
const typeAddons = addons.filter(addon =>
|
const typeAddons = addons.filter(addon =>
|
||||||
addon.catalogs && addon.catalogs.some(catalog => catalog.type === type)
|
addon.catalogs && addon.catalogs.some(catalog => catalog.type === type)
|
||||||
);
|
);
|
||||||
|
|
@ -222,18 +218,20 @@ class CatalogService {
|
||||||
const manifest = addonManifest.find(a => a.id === addon.id);
|
const manifest = addonManifest.find(a => a.id === addon.id);
|
||||||
if (!manifest) continue;
|
if (!manifest) continue;
|
||||||
|
|
||||||
// Apply genre filter if provided
|
|
||||||
const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : [];
|
const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : [];
|
||||||
const metas = await stremioService.getCatalog(manifest, type, catalog.id, 1, filters);
|
const metas = await stremioService.getCatalog(manifest, type, catalog.id, 1, filters);
|
||||||
|
|
||||||
if (metas && metas.length > 0) {
|
if (metas && metas.length > 0) {
|
||||||
const items = metas.map(meta => this.convertMetaToStreamingContent(meta));
|
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({
|
catalogs.push({
|
||||||
addon: addon.id,
|
addon: addon.id,
|
||||||
type,
|
type,
|
||||||
id: catalog.id,
|
id: catalog.id,
|
||||||
name: catalog.name,
|
name: displayName,
|
||||||
genre: genreFilter,
|
genre: genreFilter,
|
||||||
items
|
items
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ export class MDBListService {
|
||||||
private static instance: MDBListService;
|
private static instance: MDBListService;
|
||||||
private apiKey: string | null = null;
|
private apiKey: string | null = null;
|
||||||
private enabled: boolean = true;
|
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() {
|
private constructor() {
|
||||||
logger.log('[MDBListService] Service initialized');
|
logger.log('[MDBListService] Service initialized');
|
||||||
|
|
@ -36,16 +39,32 @@ export class MDBListService {
|
||||||
try {
|
try {
|
||||||
// First check if MDBList is enabled
|
// First check if MDBList is enabled
|
||||||
const enabledSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
|
const enabledSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
|
||||||
|
const wasEnabled = this.enabled;
|
||||||
this.enabled = enabledSetting === null || enabledSetting === 'true';
|
this.enabled = enabledSetting === null || enabledSetting === 'true';
|
||||||
logger.log('[MDBListService] MDBList enabled:', this.enabled);
|
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) {
|
if (!this.enabled) {
|
||||||
logger.log('[MDBListService] MDBList is disabled, skipping API key loading');
|
logger.log('[MDBListService] MDBList is disabled, skipping API key loading');
|
||||||
this.apiKey = null;
|
this.apiKey = null;
|
||||||
return;
|
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');
|
logger.log('[MDBListService] Initialized with API key:', this.apiKey ? 'Present' : 'Not found');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[MDBListService] Failed to load settings:', 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> {
|
async getRatings(imdbId: string, mediaType: 'movie' | 'show'): Promise<MDBListRatings | null> {
|
||||||
logger.log(`[MDBListService] Fetching ratings for ${mediaType} with IMDB ID:`, imdbId);
|
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
|
// Check if MDBList is enabled before doing anything else
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
// Try to refresh enabled status in case it was changed
|
// Try to refresh enabled status in case it was changed
|
||||||
|
|
@ -139,7 +169,13 @@ export class MDBListService {
|
||||||
try {
|
try {
|
||||||
const errorJson = JSON.parse(errorText);
|
const errorJson = JSON.parse(errorText);
|
||||||
if (errorJson.error === "Invalid API key") {
|
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 {
|
} else {
|
||||||
logger.warn(`[MDBListService] 403 Forbidden, but not invalid key error:`, errorJson);
|
logger.warn(`[MDBListService] 403 Forbidden, but not invalid key error:`, errorJson);
|
||||||
}
|
}
|
||||||
|
|
@ -171,12 +207,37 @@ export class MDBListService {
|
||||||
|
|
||||||
const ratingCount = Object.keys(ratings).length;
|
const ratingCount = Object.keys(ratings).length;
|
||||||
logger.log(`[MDBListService] Fetched ${ratingCount} ratings successfully:`, ratings);
|
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) {
|
} catch (error) {
|
||||||
logger.error('[MDBListService] Error fetching MDBList ratings:', error);
|
logger.error('[MDBListService] Error fetching MDBList ratings:', error);
|
||||||
return null;
|
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();
|
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