diff --git a/src/screens/CatalogSettingsScreen.tsx b/src/screens/CatalogSettingsScreen.tsx index de053a91..d4a0a9d7 100644 --- a/src/screens/CatalogSettingsScreen.tsx +++ b/src/screens/CatalogSettingsScreen.tsx @@ -23,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 { clearCustomNameCache } from '../utils/catalogNameUtils'; import { BlurView } from 'expo-blur'; interface CatalogSetting { @@ -133,6 +134,17 @@ const createStyles = (colors: any) => StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, + hintRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 16, + paddingVertical: 8, + }, + hintText: { + fontSize: 12, + color: colors.mediumGray, + }, enabledCount: { fontSize: 15, color: colors.mediumGray, @@ -250,6 +262,26 @@ const CatalogSettingsScreen = () => { const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`; let displayName = catalog.name || catalog.id; const catalogType = catalog.type === 'movie' ? 'Movies' : catalog.type === 'series' ? 'TV Shows' : catalog.type.charAt(0).toUpperCase() + catalog.type.slice(1); + + // Clean duplicate words within the catalog name (e.g., "Popular Popular") + if (displayName) { + const words = displayName.split(' ').filter(Boolean); + const uniqueWords: string[] = []; + const seen = new Set(); + for (const w of words) { + const lw = w.toLowerCase(); + if (!seen.has(lw)) { + uniqueWords.push(w); + seen.add(lw); + } + } + displayName = uniqueWords.join(' '); + } + + // Append content type if not already present (case-insensitive) + if (!displayName.toLowerCase().includes(catalogType.toLowerCase())) { + displayName = `${displayName} ${catalogType}`.trim(); + } uniqueCatalogs.set(settingKey, { addonId: addon.id, @@ -379,9 +411,13 @@ const CatalogSettingsScreen = () => { } await AsyncStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames)); + // Clear in-memory cache so new name is used immediately + try { clearCustomNameCache(); } catch {} // --- Reload settings to reflect the change --- await loadSettings(); + // Also trigger home/catalog consumers to refresh + try { refreshCatalogs(); } catch {} // --- No need to manually update local state anymore --- } catch (error) { @@ -459,7 +495,13 @@ const CatalogSettingsScreen = () => { - {group.expanded && group.catalogs.map((setting, index) => ( + {group.expanded && ( + <> + + + Long-press a catalog to rename + + {group.catalogs.map((setting, index) => ( handleLongPress(setting)} // Added long press handler @@ -484,7 +526,9 @@ const CatalogSettingsScreen = () => { ios_backgroundColor="#505050" /> - ))} + ))} + + )} ))} diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index d2ef759a..ef5f768d 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -41,6 +41,7 @@ import * as Haptics from 'expo-haptics'; import { tmdbService } from '../services/tmdbService'; import { logger } from '../utils/logger'; import { storageService } from '../services/storageService'; +import { getCatalogDisplayName, clearCustomNameCache } from '../utils/catalogNameUtils'; import { useHomeCatalogs } from '../hooks/useHomeCatalogs'; import { useFeaturedContent } from '../hooks/useFeaturedContent'; import { useSettings, settingsEmitter } from '../hooks/useSettings'; @@ -188,7 +189,7 @@ const HomeScreen = () => { const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1); if (metas && metas.length > 0) { // Limit items per catalog to reduce memory usage - const limitedMetas = metas.slice(0, 8); // Further reduced for memory + const limitedMetas = metas.slice(0, 30); const items = limitedMetas.map((meta: any) => ({ id: meta.id, @@ -209,11 +210,27 @@ const HomeScreen = () => { })); // Skip prefetching to reduce memory pressure - - let displayName = catalog.name; - const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; - if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { - displayName = `${displayName} ${contentType}`; + // Resolve custom display name; if custom exists, use as-is + const originalName = catalog.name || catalog.id; + let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName); + const isCustom = displayName !== originalName; + + if (!isCustom) { + // De-duplicate repeated words (case-insensitive) + const words = displayName.split(' ').filter(Boolean); + const uniqueWords: string[] = []; + const seen = new Set(); + for (const w of words) { + const lw = w.toLowerCase(); + if (!seen.has(lw)) { uniqueWords.push(w); seen.add(lw); } + } + displayName = uniqueWords.join(' '); + + // Append content type if not present + const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; + if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { + displayName = `${displayName} ${contentType}`; + } } const catalogContent = { diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts index 65c2f5f5..6ff452eb 100644 --- a/src/services/SyncService.ts +++ b/src/services/SyncService.ts @@ -406,9 +406,39 @@ class SyncService { .eq('user_id', userId) .single(); if (us) { - await AsyncStorage.setItem(`@user:${userId}:app_settings`, JSON.stringify(us.app_settings || {})); - await AsyncStorage.setItem('app_settings', JSON.stringify(us.app_settings || {})); - await storageService.saveSubtitleSettings(us.subtitle_settings || {}); + // Merge remote settings with existing local settings, preferring remote values + // but preserving any local-only keys (e.g., newly added client-side settings + // not yet present on the server). This avoids losing local preferences on restart. + try { + const localScopedJson = (await AsyncStorage.getItem(`@user:${userId}:app_settings`)) || '{}'; + const localLegacyJson = (await AsyncStorage.getItem('app_settings')) || '{}'; + // Prefer scoped local if available; fall back to legacy + let localSettings: Record = {}; + try { localSettings = JSON.parse(localScopedJson); } catch {} + if (!localSettings || Object.keys(localSettings).length === 0) { + try { localSettings = JSON.parse(localLegacyJson); } catch { localSettings = {}; } + } + + const remoteRaw: Record = (us.app_settings || {}) as Record; + // Exclude episodeLayoutStyle from remote to keep it local-only + const { episodeLayoutStyle: _remoteEpisodeLayoutStyle, ...remoteSettingsSansLocalOnly } = remoteRaw || {}; + // Merge: start from local, override with remote (sans excluded keys) + const mergedSettings = { ...(localSettings || {}), ...(remoteSettingsSansLocalOnly || {}) }; + + await AsyncStorage.setItem(`@user:${userId}:app_settings`, JSON.stringify(mergedSettings)); + await AsyncStorage.setItem('app_settings', JSON.stringify(mergedSettings)); + await storageService.saveSubtitleSettings(us.subtitle_settings || {}); + // Notify listeners that settings changed due to sync + try { settingsEmitter.emit(); } catch {} + } catch (e) { + // Fallback to writing remote settings as-is if merge fails + const remoteRaw: Record = (us.app_settings || {}) as Record; + const { episodeLayoutStyle: _remoteEpisodeLayoutStyle, ...remoteSettingsSansLocalOnly } = remoteRaw || {}; + await AsyncStorage.setItem(`@user:${userId}:app_settings`, JSON.stringify(remoteSettingsSansLocalOnly)); + await AsyncStorage.setItem('app_settings', JSON.stringify(remoteSettingsSansLocalOnly)); + await storageService.saveSubtitleSettings(us.subtitle_settings || {}); + try { settingsEmitter.emit(); } catch {} + } } })(), this.pullAddonsSnapshot(userId), @@ -697,7 +727,9 @@ class SyncService { (await AsyncStorage.getItem(`@user:${scope}:app_settings`)) || (await AsyncStorage.getItem('app_settings')) || '{}'; - const appSettings = JSON.parse(appSettingsJson); + const parsed = JSON.parse(appSettingsJson) as Record; + // Exclude local-only settings from push + const { episodeLayoutStyle: _localEpisodeLayoutStyle, ...appSettings } = parsed || {}; const subtitleSettings = (await storageService.getSubtitleSettings()) || {}; const { error } = await supabase.from('user_settings').upsert({ user_id: userId, diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index babd0fe4..17dfe4dc 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -205,29 +205,33 @@ class CatalogService { const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1); if (metas && metas.length > 0) { // Cap items per catalog to reduce memory and rendering load - const limited = metas.slice(0, 8); // Further reduced for memory + const limited = metas.slice(0, 12); const items = limited.map(meta => this.convertMetaToStreamingContent(meta)); - // 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); - seenWords.add(lowerWord); + // Get potentially custom display name; if customized, respect it as-is + const originalName = catalog.name || catalog.id; + let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName); + const isCustom = displayName !== originalName; + + if (!isCustom) { + // Remove duplicate words and clean up the name (case-insensitive) + const words = displayName.split(' '); + const uniqueWords: string[] = []; + const seenWords = new Set(); + for (const word of words) { + const lowerWord = word.toLowerCase(); + if (!seenWords.has(lowerWord)) { + uniqueWords.push(word); + seenWords.add(lowerWord); + } + } + displayName = uniqueWords.join(' '); + + // Add content type if not present + const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; + if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { + displayName = `${displayName} ${contentType}`; } - } - displayName = uniqueWords.join(' '); - - // Add content type if not present - const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; - if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { - displayName = `${displayName} ${contentType}`; } return {