diff --git a/src/components/TabletStreamsLayout.tsx b/src/components/TabletStreamsLayout.tsx index f94d2d16..96456243 100644 --- a/src/components/TabletStreamsLayout.tsx +++ b/src/components/TabletStreamsLayout.tsx @@ -208,8 +208,8 @@ const TabletStreamsLayout: React.FC = ({ duration: 600, easing: Easing.out(Easing.cubic) })); - } else if (!backdropSource?.uri) { - // No backdrop available, animate content panels immediately + } else if (!backdropSource?.uri || backdropError) { + // No backdrop available OR backdrop failed to load - animate content panels immediately leftPanelOpacity.value = withTiming(1, { duration: 600, easing: Easing.out(Easing.cubic) @@ -228,7 +228,7 @@ const TabletStreamsLayout: React.FC = ({ easing: Easing.out(Easing.cubic) })); } - }, [backdropSource?.uri, backdropLoaded]); + }, [backdropSource?.uri, backdropLoaded, backdropError]); // Reset animation when episode changes useEffect(() => { diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 341acd08..29e0f404 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -275,8 +275,14 @@ const LibraryScreen = () => { try { const items = await catalogService.getLibraryItems(); + logger.log(`[LibraryScreen] Loaded ${items.length} library items`); + + if (items.length === 0) { + logger.warn('[LibraryScreen] Library is empty - this might indicate a loading issue'); + } + // Sort by date added (most recent first) - const sortedItems = items.sort((a, b) => { + const sortedItems = [...items].sort((a, b) => { const timeA = (a as any).addedToLibraryAt || 0; const timeB = (b as any).addedToLibraryAt || 0; return timeB - timeA; // Descending order (newest first) @@ -309,8 +315,10 @@ const LibraryScreen = () => { // Subscribe to library updates const unsubscribe = catalogService.subscribeToLibraryUpdates(async (items) => { + logger.log(`[LibraryScreen] Library update received with ${items.length} items`); + // Sort by date added (most recent first) - const sortedItems = items.sort((a, b) => { + const sortedItems = [...items].sort((a, b) => { const timeA = (a as any).addedToLibraryAt || 0; const timeB = (b as any).addedToLibraryAt || 0; return timeB - timeA; // Descending order (newest first) @@ -728,7 +736,7 @@ const LibraryScreen = () => { } // Sort by last watched/added date (most recent first) using raw timestamps - return items.sort((a, b) => { + return [...items].sort((a, b) => { const dateA = a.lastWatched ? new Date(a.lastWatched).getTime() : 0; const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0; return dateB - dateA; diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 564ea5de..e55f55e9 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -1632,6 +1632,41 @@ export const StreamsScreen = () => { // Helper to create gradient colors from dominant color const createGradientColors = useCallback((baseColor: string | null): [string, string, string, string, string] => { + // Always use black gradient when backdrop is enabled + if (settings.enableStreamsBackdrop) { + return ['rgba(0,0,0,0)', 'rgba(0,0,0,0.3)', 'rgba(0,0,0,0.6)', 'rgba(0,0,0,0.85)', 'rgba(0,0,0,0.95)']; + } + + // When backdrop is disabled, use theme background gradient + const themeBg = colors.darkBackground; + + // Handle hex color format (e.g., #1a1a1a) + if (themeBg.startsWith('#')) { + const r = parseInt(themeBg.substr(1, 2), 16); + const g = parseInt(themeBg.substr(3, 2), 16); + const b = parseInt(themeBg.substr(5, 2), 16); + return [ + `rgba(${r},${g},${b},0)`, + `rgba(${r},${g},${b},0.3)`, + `rgba(${r},${g},${b},0.6)`, + `rgba(${r},${g},${b},0.85)`, + `rgba(${r},${g},${b},0.95)`, + ]; + } + + // Handle rgb color format (e.g., rgb(26, 26, 26)) + const rgbMatch = themeBg.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch; + return [ + `rgba(${r},${g},${b},0)`, + `rgba(${r},${g},${b},0.3)`, + `rgba(${r},${g},${b},0.6)`, + `rgba(${r},${g},${b},0.85)`, + `rgba(${r},${g},${b},0.95)`, + ]; + } + if (!baseColor || baseColor === '#1a1a1a') { // Fallback to black gradient with stronger bottom edge return ['rgba(0,0,0,0)', 'rgba(0,0,0,0.3)', 'rgba(0,0,0,0.6)', 'rgba(0,0,0,0.85)', 'rgba(0,0,0,0.95)']; @@ -1650,7 +1685,7 @@ export const StreamsScreen = () => { `rgba(${r},${g},${b},0.85)`, `rgba(${r},${g},${b},0.95)`, ]; - }, []); + }, [settings.enableStreamsBackdrop, colors.darkBackground]); const gradientColors = useMemo(() => createGradientColors(dominantColor), @@ -1805,7 +1840,7 @@ export const StreamsScreen = () => { ) : ( @@ -1819,10 +1854,7 @@ export const StreamsScreen = () => { {Platform.OS === 'ios' && ( )} @@ -1923,19 +1955,19 @@ export const StreamsScreen = () => { )} {/* Gradient overlay to blend hero section with streams container */} - {metadata?.videos && metadata.videos.length > 1 && selectedEpisode && settings.enableStreamsBackdrop && ( + {metadata?.videos && metadata.videos.length > 1 && selectedEpisode && ( @@ -2388,6 +2420,7 @@ const createStyles = (colors: any) => StyleSheet.create({ position: 'relative', backgroundColor: 'transparent', pointerEvents: 'box-none', + zIndex: 1, }, streamsHeroBackground: { width: '100%', @@ -2728,10 +2761,10 @@ const createStyles = (colors: any) => StyleSheet.create({ }, heroBlendOverlay: { position: 'absolute', - top: 220, // Height of hero container + top: 140, // Start at ~64% of hero section, giving 80px of blend within hero left: 0, right: 0, - height: 60, // Extend gradient 60px into streams area + height: Platform.OS === 'android' ? 150 : 180, // Reduce gradient area on Android zIndex: 0, pointerEvents: 'none', }, diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index 999a1e49..f68ac1ce 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -157,10 +157,23 @@ class CatalogService { private libraryRemoveListeners: ((type: string, id: string) => void)[] = []; private constructor() { + this.initializeScope(); this.loadLibrary(); this.loadRecentContent(); } + private async initializeScope(): Promise { + try { + const currentScope = await AsyncStorage.getItem('@user:current'); + if (!currentScope) { + await AsyncStorage.setItem('@user:current', 'local'); + logger.log('[CatalogService] Initialized @user:current scope to "local"'); + } + } catch (error) { + logger.error('[CatalogService] Failed to initialize scope:', error); + } + } + static getInstance(): CatalogService { if (!CatalogService.instance) { CatalogService.instance = new CatalogService(); @@ -182,9 +195,16 @@ class CatalogService { } if (storedLibrary) { this.library = JSON.parse(storedLibrary); + logger.log(`[CatalogService] Library loaded successfully with ${Object.keys(this.library).length} items from scope: ${scope}`); + } else { + logger.log(`[CatalogService] No library data found for scope: ${scope}`); + this.library = {}; } + // Ensure @user:current is set to prevent future scope issues + await AsyncStorage.setItem('@user:current', scope); } catch (error: any) { logger.error('Failed to load library:', error); + this.library = {}; } } @@ -192,8 +212,10 @@ class CatalogService { try { const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; const scopedKey = `@user:${scope}:stremio-library`; - await AsyncStorage.setItem(scopedKey, JSON.stringify(this.library)); - await AsyncStorage.setItem(this.LEGACY_LIBRARY_KEY, JSON.stringify(this.library)); + const libraryData = JSON.stringify(this.library); + await AsyncStorage.setItem(scopedKey, libraryData); + await AsyncStorage.setItem(this.LEGACY_LIBRARY_KEY, libraryData); + logger.log(`[CatalogService] Library saved successfully with ${Object.keys(this.library).length} items to scope: ${scope}`); } catch (error: any) { logger.error('Failed to save library:', error); }