From 22628894799322b5b4d0628a05f392941eb807af Mon Sep 17 00:00:00 2001 From: Amarjit Singh Date: Sun, 22 Mar 2026 23:17:18 -0700 Subject: [PATCH] Add catalog/collection reorder, enable/disable toggle, and fix catalog name display - Add CatalogOrderScreen for reordering catalogs and collections on home screen - Add catalogOrderService for persisting display order in MMKV - Add enable/disable toggle for collections in CollectionManagementScreen - Fix catalog name display in FolderDetailScreen tabs and CollectionEditorScreen - Filter disabled collections from reorder screen and home screen - Integrate display order into HomeScreen listData memo Co-Authored-By: Claude Opus 4.6 --- src/hooks/useCollections.ts | 9 +- src/navigation/AppNavigator.tsx | 14 + src/screens/CatalogOrderScreen.tsx | 350 ++++++++++++++++++ src/screens/CollectionEditorScreen.tsx | 5 +- src/screens/CollectionManagementScreen.tsx | 109 ++++-- src/screens/FolderDetailScreen.tsx | 17 +- src/screens/HomeScreen.tsx | 72 +++- .../ContentDiscoverySettingsScreen.tsx | 8 + src/services/catalogOrderService.ts | 72 ++++ src/services/collectionsService.ts | 34 ++ 10 files changed, 637 insertions(+), 53 deletions(-) create mode 100644 src/screens/CatalogOrderScreen.tsx create mode 100644 src/services/catalogOrderService.ts diff --git a/src/hooks/useCollections.ts b/src/hooks/useCollections.ts index e7618499..fad744c5 100644 --- a/src/hooks/useCollections.ts +++ b/src/hooks/useCollections.ts @@ -4,12 +4,17 @@ import { collectionsService, collectionsEmitter, COLLECTIONS_EVENTS } from '../s export function useCollections() { const [collections, setCollections] = useState([]); + const [enabledMap, setEnabledMap] = useState>({}); const [loading, setLoading] = useState(true); const loadCollections = useCallback(async () => { try { - const data = await collectionsService.getCollections(); + const [data, settings] = await Promise.all([ + collectionsService.getCollections(), + collectionsService.getCollectionSettings(), + ]); setCollections(data); + setEnabledMap(settings); } catch (error) { if (__DEV__) console.error('[useCollections] Error loading:', error); } finally { @@ -31,5 +36,5 @@ export function useCollections() { }; }, [loadCollections]); - return { collections, loading, refresh: loadCollections }; + return { collections, enabledMap, loading, refresh: loadCollections }; } diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 8c5f7444..6c30f888 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -80,6 +80,7 @@ import ContributorsScreen from '../screens/ContributorsScreen'; import CollectionManagementScreen from '../screens/CollectionManagementScreen'; import CollectionEditorScreen from '../screens/CollectionEditorScreen'; import FolderDetailScreen from '../screens/FolderDetailScreen'; +import CatalogOrderScreen from '../screens/CatalogOrderScreen'; import { ContentDiscoverySettingsScreen, @@ -230,6 +231,7 @@ export type RootStackParamList = { Collections: undefined; CollectionEditor: { collectionId?: string }; FolderDetail: { collectionId: string; folderId: string }; + CatalogOrder: undefined; // New organized settings screens ContentDiscoverySettings: undefined; @@ -1939,6 +1941,18 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta }, }} /> + diff --git a/src/screens/CatalogOrderScreen.tsx b/src/screens/CatalogOrderScreen.tsx new file mode 100644 index 00000000..e0be154f --- /dev/null +++ b/src/screens/CatalogOrderScreen.tsx @@ -0,0 +1,350 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ScrollView, + StatusBar, +} from 'react-native'; +import { useNavigation, useFocusEffect, NavigationProp } from '@react-navigation/native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { useTheme } from '../contexts/ThemeContext'; +import { useCollections } from '../hooks/useCollections'; +import { stremioService } from '../services/stremioService'; +import { mmkvStorage } from '../services/mmkvStorage'; +import { catalogOrderService, CatalogOrderService } from '../services/catalogOrderService'; +import ScreenHeader from '../components/common/ScreenHeader'; +import LoadingSpinner from '../components/common/LoadingSpinner'; +import { RootStackParamList } from '../navigation/AppNavigator'; + +interface OrderItem { + key: string; + label: string; + subtitle: string; + isCollection: boolean; +} + +const CATALOG_SETTINGS_KEY = 'catalog_settings'; + +const CatalogOrderScreen = () => { + const navigation = useNavigation>(); + const { currentTheme } = useTheme(); + const colors = currentTheme.colors; + const { collections, enabledMap: collectionEnabledMap } = useCollections(); + + const [orderedKeys, setOrderedKeys] = useState([]); + const [catalogItems, setCatalogItems] = useState([]); + const [loading, setLoading] = useState(true); + + // Build collection items from hook + const collectionItems = useMemo(() => { + const items: OrderItem[] = []; + for (const collection of collections) { + if (collectionEnabledMap[collection.id] === false) continue; + items.push({ + key: CatalogOrderService.collectionKey(collection.id), + label: collection.title, + subtitle: `${collection.folders.length} folder${collection.folders.length !== 1 ? 's' : ''}`, + isCollection: true, + }); + } + return items; + }, [collections]); + + // Build the full item map + const itemMap = useMemo(() => { + const map = new Map(); + for (const item of collectionItems) map.set(item.key, item); + for (const item of catalogItems) map.set(item.key, item); + return map; + }, [collectionItems, catalogItems]); + + // Load catalog metadata from installed addons (same filtering as HomeScreen) + const loadCatalogs = useCallback(async () => { + try { + const addons = await stremioService.getInstalledAddonsAsync(); + const savedSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY); + const catalogSettings: Record = savedSettingsJson ? JSON.parse(savedSettingsJson) : {}; + + const items: OrderItem[] = []; + const seen = new Set(); + + for (const addon of addons) { + if (!addon.catalogs) continue; + const addonUsesShowInHome = addon.catalogs.some((c: any) => c.showInHome === true); + + for (const catalog of addon.catalogs) { + // Skip search catalogs + if ( + (catalog.id && catalog.id.startsWith('search.')) || + (catalog.type && catalog.type.startsWith('search')) + ) continue; + + // Skip catalogs with required extras + const requiredExtras = (catalog.extra || []).filter((e: any) => e.isRequired); + if (requiredExtras.length > 0) continue; + + // Respect showInHome flag + if (addonUsesShowInHome && !(catalog as any).showInHome) continue; + + // Respect user enable/disable setting + const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`; + const isEnabled = catalogSettings[settingKey] ?? true; + if (!isEnabled) continue; + + // Deduplicate + const key = CatalogOrderService.catalogKey(addon.id, catalog.type, catalog.id); + if (seen.has(key)) continue; + seen.add(key); + + items.push({ + key, + label: catalog.name || catalog.id, + subtitle: `${addon.name || addon.id} · ${catalog.type}`, + isCollection: false, + }); + } + } + + setCatalogItems(items); + } catch { + // silent fail + } + }, []); + + // Load everything on focus + useFocusEffect( + useCallback(() => { + setLoading(true); + Promise.all([ + loadCatalogs(), + catalogOrderService.getOrder(), + ]).then(([_, savedOrder]) => { + // itemMap might not be updated yet, so we'll set keys and let the + // ordered list rebuild via the items memo below + setOrderedKeys(savedOrder); + setLoading(false); + }); + }, [loadCatalogs]) + ); + + // Merge saved order with available items + const items = useMemo(() => { + const allItems = [...collectionItems, ...catalogItems]; + const allMap = new Map(); + for (const item of allItems) allMap.set(item.key, item); + + if (orderedKeys.length === 0) return allItems; + + const result: OrderItem[] = []; + const seen = new Set(); + + // First: items in saved order + for (const key of orderedKeys) { + const item = allMap.get(key); + if (item && !seen.has(key)) { + result.push(item); + seen.add(key); + } + } + // Then: any new items not in saved order + for (const item of allItems) { + if (!seen.has(item.key)) { + result.push(item); + } + } + return result; + }, [orderedKeys, collectionItems, catalogItems]); + + const handleMoveUp = useCallback((index: number) => { + if (index <= 0) return; + const currentKeys = items.map(i => i.key); + [currentKeys[index - 1], currentKeys[index]] = [currentKeys[index], currentKeys[index - 1]]; + setOrderedKeys(currentKeys); + catalogOrderService.saveOrder(currentKeys); + }, [items]); + + const handleMoveDown = useCallback((index: number) => { + if (index >= items.length - 1) return; + const currentKeys = items.map(i => i.key); + [currentKeys[index], currentKeys[index + 1]] = [currentKeys[index + 1], currentKeys[index]]; + setOrderedKeys(currentKeys); + catalogOrderService.saveOrder(currentKeys); + }, [items]); + + const handleReset = useCallback(async () => { + await catalogOrderService.resetOrder(); + setOrderedKeys([]); + }, []); + + if (loading) { + return ( + + + navigation.goBack()} /> + + + + + ); + } + + return ( + + + navigation.goBack()} + /> + + + + Reset to Default + + + {items.length === 0 && ( + + + + No catalogs or collections to reorder + + + )} + + {items.map((item, index) => ( + + + + + + {item.label} + + + {item.subtitle} + + + + + handleMoveUp(index)} + disabled={index === 0} + style={[styles.iconButton, index === 0 && styles.disabledButton]} + > + + + handleMoveDown(index)} + disabled={index === items.length - 1} + style={[styles.iconButton, index === items.length - 1 && styles.disabledButton]} + > + + + + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + scrollContent: { + padding: 16, + paddingBottom: 40, + }, + resetButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: 12, + borderRadius: 10, + marginBottom: 16, + gap: 6, + }, + resetButtonText: { + fontSize: 14, + fontWeight: '500', + }, + emptyContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 60, + gap: 12, + }, + emptyText: { + fontSize: 15, + }, + itemCard: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 12, + borderRadius: 10, + borderWidth: 1, + marginBottom: 8, + }, + itemInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + gap: 10, + }, + itemTextContainer: { + flex: 1, + }, + itemLabel: { + fontSize: 15, + fontWeight: '500', + }, + itemSubtitle: { + fontSize: 12, + marginTop: 2, + }, + itemActions: { + flexDirection: 'row', + gap: 2, + }, + iconButton: { + padding: 6, + }, + disabledButton: { + opacity: 0.3, + }, +}); + +export default CatalogOrderScreen; diff --git a/src/screens/CollectionEditorScreen.tsx b/src/screens/CollectionEditorScreen.tsx index 05ecc6d5..7cc05281 100644 --- a/src/screens/CollectionEditorScreen.tsx +++ b/src/screens/CollectionEditorScreen.tsx @@ -407,6 +407,9 @@ const CollectionEditorScreen = () => { {editingFolder.catalogSources.map((source, idx) => { const isMissing = !installedAddonIds.has(source.addonId); + const catalogInfo = availableCatalogs.find( + c => c.addonId === source.addonId && c.type === source.type && c.catalogId === source.catalogId + ); return ( { > - {source.catalogId} + {catalogInfo?.name || source.catalogId} {isMissing ? `Addon not installed: ${source.addonId}` : `${source.addonId} · ${source.type}`} diff --git a/src/screens/CollectionManagementScreen.tsx b/src/screens/CollectionManagementScreen.tsx index 401747f8..34accadd 100644 --- a/src/screens/CollectionManagementScreen.tsx +++ b/src/screens/CollectionManagementScreen.tsx @@ -6,6 +6,7 @@ import { TouchableOpacity, ScrollView, StatusBar, + Switch, Platform, Alert, } from 'react-native'; @@ -36,13 +37,25 @@ const CollectionManagementScreen = () => { const [alertTitle, setAlertTitle] = useState(''); const [alertMessage, setAlertMessage] = useState(''); const [alertActions, setAlertActions] = useState([]); + const [enabledMap, setEnabledMap] = useState>({}); + + const loadEnabledState = useCallback(async () => { + const settings = await collectionsService.getCollectionSettings(); + setEnabledMap(settings); + }, []); useFocusEffect( useCallback(() => { refresh(); - }, [refresh]) + loadEnabledState(); + }, [refresh, loadEnabledState]) ); + const handleToggleEnabled = useCallback(async (id: string, value: boolean) => { + setEnabledMap(prev => ({ ...prev, [id]: value })); + await collectionsService.setCollectionEnabled(id, value); + }, []); + const handleNewCollection = useCallback(() => { navigation.navigate('CollectionEditor' as any, {}); }, [navigation]); @@ -151,45 +164,62 @@ const CollectionManagementScreen = () => { )} - {collections.map((collection, index) => ( - - - - - {collection.title} - - - {collection.folders.length} folder{collection.folders.length !== 1 ? 's' : ''} - - - - handleMoveUp(collection.id)} - disabled={index === 0} - style={[styles.iconButton, index === 0 && styles.disabledButton]} - > - - - handleMoveDown(collection.id)} - disabled={index === collections.length - 1} - style={[styles.iconButton, index === collections.length - 1 && styles.disabledButton]} - > - - - handleEdit(collection.id)} style={styles.iconButton}> - - - handleDelete(collection)} style={styles.iconButton}> - - + {collections.map((collection, index) => { + const isEnabled = enabledMap[collection.id] !== false; + return ( + + + + + {collection.title} + + + {collection.folders.length} folder{collection.folders.length !== 1 ? 's' : ''}{!isEnabled ? ' · Hidden' : ''} + + + + handleToggleEnabled(collection.id, value)} + trackColor={{ false: colors.elevation3, true: colors.primary + '80' }} + thumbColor={isEnabled ? colors.primary : colors.textMuted} + style={styles.switch} + /> + handleMoveUp(collection.id)} + disabled={index === 0} + style={[styles.iconButton, index === 0 && styles.disabledButton]} + > + + + handleMoveDown(collection.id)} + disabled={index === collections.length - 1} + style={[styles.iconButton, index === collections.length - 1 && styles.disabledButton]} + > + + + handleEdit(collection.id)} style={styles.iconButton}> + + + handleDelete(collection)} style={styles.iconButton}> + + + - - ))} + ); + })} {collections.length === 0 && ( @@ -302,6 +332,9 @@ const styles = StyleSheet.create({ alignItems: 'center', gap: 4, }, + switch: { + transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }], + }, iconButton: { padding: 6, }, diff --git a/src/screens/FolderDetailScreen.tsx b/src/screens/FolderDetailScreen.tsx index e532023e..a4837fa7 100644 --- a/src/screens/FolderDetailScreen.tsx +++ b/src/screens/FolderDetailScreen.tsx @@ -55,11 +55,18 @@ const FolderDetailScreen = () => { if (!f) return; setFolder(f); - // Build tab labels - const labels = f.catalogSources.map(source => ({ - name: source.catalogId, - type: source.type, - })); + // Build tab labels — resolve human-readable names from addon manifests + const addons = await stremioService.getInstalledAddonsAsync(); + const labels = f.catalogSources.map(source => { + const addon = addons.find((a: any) => a.id === source.addonId); + const catalog = addon?.catalogs?.find( + (c: any) => c.id === source.catalogId && c.type === source.type + ); + return { + name: catalog?.name || source.catalogId, + type: source.type, + }; + }); setTabLabels(labels); // Load first tab diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index ba57eac4..1b247fe0 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -74,6 +74,7 @@ import { useScrollToTop } from '../contexts/ScrollToTopContext'; import CollectionRowSection from '../components/home/CollectionRowSection'; import { useCollections } from '../hooks/useCollections'; import { collectionsEmitter, COLLECTIONS_EVENTS } from '../services/collectionsService'; +import { catalogOrderService, catalogOrderEmitter, CATALOG_ORDER_EVENTS, CatalogOrderService } from '../services/catalogOrderService'; // Constants const CATALOG_SETTINGS_KEY = 'catalog_settings'; @@ -147,7 +148,8 @@ const HomeScreen = () => { const { lastUpdate } = useCatalogContext(); // Add catalog context to listen for addon changes const { showInfo } = useToast(); const { setTrailerPlaying } = useTrailer(); - const { collections } = useCollections(); + const { collections, enabledMap: collectionEnabledMap } = useCollections(); + const [savedOrder, setSavedOrder] = useState([]); const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection); const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource); const refreshTimeoutRef = useRef(null); @@ -196,6 +198,14 @@ const HomeScreen = () => { return () => clearTimeout(timer); }, [insets.top]); + // Load saved catalog/collection display order + useEffect(() => { + catalogOrderService.getOrder().then(setSavedOrder); + const handler = () => catalogOrderService.getOrder().then(setSavedOrder); + catalogOrderEmitter.on(CATALOG_ORDER_EVENTS.CHANGED, handler); + return () => { catalogOrderEmitter.off(CATALOG_ORDER_EVENTS.CHANGED, handler); }; + }, []); + const { featuredContent, allFeaturedContent, @@ -763,26 +773,74 @@ const HomeScreen = () => { // Only show a limited number of catalogs initially for performance const catalogsToShow = catalogs.slice(0, visibleCatalogCount); - // Show collections at the top, before catalog rows + // Build maps of available items keyed for ordering (filter out disabled collections) + const collectionMap = new Map(); for (const collection of collections) { - data.push({ type: 'collection', collection, key: `collection-${collection.id}` }); + if (collectionEnabledMap[collection.id] === false) continue; + collectionMap.set(CatalogOrderService.collectionKey(collection.id), collection); } + const catalogMap = new Map(); catalogsToShow.forEach((catalog, index) => { if (catalog) { - data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` }); - } else if (catalogsLoading && pendingCatalogIndexes[index]) { - data.push({ type: 'placeholder', key: `placeholder-${index}` }); + catalogMap.set(CatalogOrderService.catalogKey(catalog.addon, catalog.type, catalog.id), { catalog, index }); } }); + // Apply saved order if available + if (savedOrder.length > 0) { + const seen = new Set(); + for (const key of savedOrder) { + if (seen.has(key)) continue; + seen.add(key); + if (collectionMap.has(key)) { + const col = collectionMap.get(key)!; + data.push({ type: 'collection', collection: col, key: `collection-${col.id}` }); + } else if (catalogMap.has(key)) { + const { catalog, index } = catalogMap.get(key)!; + data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` }); + } + } + // Append any items not in the saved order + for (const [key, col] of collectionMap) { + if (!seen.has(key)) { + data.push({ type: 'collection', collection: col, key: `collection-${col.id}` }); + } + } + for (const [key, { catalog, index }] of catalogMap) { + if (!seen.has(key)) { + data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` }); + } + } + } else { + // Default order: enabled collections first, then catalogs + for (const collection of collections) { + if (collectionEnabledMap[collection.id] === false) continue; + data.push({ type: 'collection', collection, key: `collection-${collection.id}` }); + } + catalogsToShow.forEach((catalog, index) => { + if (catalog) { + data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` }); + } + }); + } + + // Show placeholders for loading catalogs (not part of ordering) + if (catalogsLoading) { + catalogsToShow.forEach((catalog, index) => { + if (!catalog && pendingCatalogIndexes[index]) { + data.push({ type: 'placeholder', key: `placeholder-${index}` }); + } + }); + } + // Add a "Load More" button if there are more catalogs to show if (catalogs.length > visibleCatalogCount && catalogs.filter(c => c).length > visibleCatalogCount) { data.push({ type: 'loadMore', key: 'load-more' }); } return data; - }, [hasAddons, catalogs, catalogsLoading, pendingCatalogIndexes, visibleCatalogCount, settings.showThisWeekSection, collections]); + }, [hasAddons, catalogs, catalogsLoading, pendingCatalogIndexes, visibleCatalogCount, settings.showThisWeekSection, collections, collectionEnabledMap, savedOrder]); const handleLoadMoreCatalogs = useCallback(() => { setVisibleCatalogCount(prev => Math.min(prev + 3, catalogs.length)); diff --git a/src/screens/settings/ContentDiscoverySettingsScreen.tsx b/src/screens/settings/ContentDiscoverySettingsScreen.tsx index fd026095..9265b020 100644 --- a/src/screens/settings/ContentDiscoverySettingsScreen.tsx +++ b/src/screens/settings/ContentDiscoverySettingsScreen.tsx @@ -128,6 +128,14 @@ export const ContentDiscoverySettingsContent: React.FC navigation.navigate('Collections')} isTablet={isTablet} /> + } + onPress={() => navigation.navigate('CatalogOrder')} + isTablet={isTablet} + /> {isItemVisible('home_screen') && ( { + const scope = await mmkvStorage.getItem('@user:current') || 'local'; + return `@user:${scope}:${ORDER_STORAGE_KEY}`; + } + + async getOrder(): Promise { + try { + const key = await this.getStorageKey(); + const json = await mmkvStorage.getItem(key); + if (!json) return []; + const parsed = JSON.parse(json); + if (!Array.isArray(parsed)) return []; + return parsed; + } catch { + return []; + } + } + + async saveOrder(order: string[]): Promise { + try { + const key = await this.getStorageKey(); + await mmkvStorage.setItem(key, JSON.stringify(order)); + catalogOrderEmitter.emit(CATALOG_ORDER_EVENTS.CHANGED); + } catch { + // silent fail + } + } + + async resetOrder(): Promise { + try { + const key = await this.getStorageKey(); + await mmkvStorage.setItem(key, JSON.stringify([])); + catalogOrderEmitter.emit(CATALOG_ORDER_EVENTS.CHANGED); + } catch { + // silent fail + } + } + + /** Build a catalog key from addon/type/id */ + static catalogKey(addonId: string, type: string, catalogId: string): string { + return `${addonId}:${type}:${catalogId}`; + } + + /** Build a collection key from collection id */ + static collectionKey(collectionId: string): string { + return `collection_${collectionId}`; + } +} + +export const catalogOrderService = CatalogOrderService.getInstance(); diff --git a/src/services/collectionsService.ts b/src/services/collectionsService.ts index d3d27e08..327a9b12 100644 --- a/src/services/collectionsService.ts +++ b/src/services/collectionsService.ts @@ -4,6 +4,7 @@ import { logger } from '../utils/logger'; import EventEmitter from 'eventemitter3'; const COLLECTIONS_STORAGE_KEY = 'collections'; +const COLLECTION_SETTINGS_KEY = 'collection_settings'; export const collectionsEmitter = new EventEmitter(); export const COLLECTIONS_EVENTS = { @@ -120,6 +121,39 @@ class CollectionsService { } } + private async getSettingsKey(): Promise { + const scope = await mmkvStorage.getItem('@user:current') || 'local'; + return `@user:${scope}:${COLLECTION_SETTINGS_KEY}`; + } + + async getCollectionSettings(): Promise> { + try { + const key = await this.getSettingsKey(); + const json = await mmkvStorage.getItem(key); + if (!json) return {}; + return JSON.parse(json); + } catch { + return {}; + } + } + + async isCollectionEnabled(id: string): Promise { + const settings = await this.getCollectionSettings(); + return settings[id] !== false; // default enabled + } + + async setCollectionEnabled(id: string, enabled: boolean): Promise { + try { + const settings = await this.getCollectionSettings(); + settings[id] = enabled; + const key = await this.getSettingsKey(); + await mmkvStorage.setItem(key, JSON.stringify(settings)); + collectionsEmitter.emit(COLLECTIONS_EVENTS.CHANGED); + } catch { + // silent fail + } + } + generateId(): string { return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; }