From 601a4a0f1d5fd69bbff786e4633ed4bdd74c3b28 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 16 Dec 2025 15:38:29 +0530 Subject: [PATCH] landscape poster support --- src/components/home/CatalogSection.tsx | 14 --- src/components/home/ContentItem.tsx | 84 ++++++++------ src/screens/CatalogScreen.tsx | 41 ++++++- src/screens/CatalogSettingsScreen.tsx | 155 +++++++++++++------------ src/screens/LibraryScreen.tsx | 80 +++++++------ src/screens/SearchScreen.tsx | 27 ++++- src/services/catalogService.ts | 6 +- src/services/stremioService.ts | 1 + 8 files changed, 236 insertions(+), 172 deletions(-) diff --git a/src/components/home/CatalogSection.tsx b/src/components/home/CatalogSection.tsx index 3834781..14018c0 100644 --- a/src/components/home/CatalogSection.tsx +++ b/src/components/home/CatalogSection.tsx @@ -96,20 +96,7 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => { // Memoize the keyExtractor to prevent re-creation const keyExtractor = useCallback((item: StreamingContent) => `${item.id}-${item.type}`, []); - // Calculate item width for getItemLayout - use base POSTER_WIDTH for consistent spacing - // Note: ContentItem may apply size multipliers based on settings, but base width ensures consistent layout - const itemWidth = useMemo(() => POSTER_WIDTH, []); - // getItemLayout for consistent spacing and better performance - const getItemLayout = useCallback((data: any, index: number) => { - const length = itemWidth + separatorWidth; - const paddingHorizontal = isTV ? 32 : isLargeTablet ? 28 : isTablet ? 24 : 16; - return { - length, - offset: paddingHorizontal + (length * index), - index, - }; - }, [itemWidth, separatorWidth, isTV, isLargeTablet, isTablet]); return ( { } ])} ItemSeparatorComponent={ItemSeparator} - getItemLayout={getItemLayout} removeClippedSubviews={true} initialNumToRender={isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3} maxToRenderPerBatch={isTV ? 4 : isLargeTablet ? 4 : 3} diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index ef6aeb9..a7e4e60 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -41,7 +41,7 @@ const getDeviceType = (screenWidth: number) => { // Dynamic poster calculation based on screen width - show 1/4 of next poster const calculatePosterLayout = (screenWidth: number) => { const deviceType = getDeviceType(screenWidth); - + // Responsive sizing based on device type const MIN_POSTER_WIDTH = deviceType === 'tv' ? 180 : deviceType === 'largeTablet' ? 160 : deviceType === 'tablet' ? 140 : 100; const MAX_POSTER_WIDTH = deviceType === 'tv' ? 220 : deviceType === 'largeTablet' ? 200 : deviceType === 'tablet' ? 180 : 130; @@ -52,9 +52,9 @@ const calculatePosterLayout = (screenWidth: number) => { const availableWidth = screenWidth - LEFT_PADDING; // Try different numbers of full posters to find the best fit - let bestLayout = { - numFullPosters: 3, - posterWidth: deviceType === 'tv' ? 200 : deviceType === 'largeTablet' ? 180 : deviceType === 'tablet' ? 160 : 120 + let bestLayout = { + numFullPosters: 3, + posterWidth: deviceType === 'tv' ? 200 : deviceType === 'largeTablet' ? 180 : deviceType === 'tablet' ? 160 : 120 }; for (let n = 3; n <= 6; n++) { @@ -96,7 +96,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe return () => unsubscribe(); }, [item.id, item.type]); - // Load watched state from AsyncStorage when item changes + // Load watched state from AsyncStorage when item changes useEffect(() => { const updateWatched = () => { mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then((val: string | null) => setIsWatched(val === 'true')); @@ -126,7 +126,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe const posterWidth = React.useMemo(() => { const deviceType = getDeviceType(width); const sizeMultiplier = deviceType === 'tv' ? 1.2 : deviceType === 'largeTablet' ? 1.1 : deviceType === 'tablet' ? 1.0 : 0.9; - + switch (settings.posterSize) { case 'small': return Math.max(90, POSTER_WIDTH - 15) * sizeMultiplier; @@ -139,6 +139,30 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe } }, [settings.posterSize, width]); + // Determine dimensions based on poster shape + const { finalWidth, finalAspectRatio, borderRadius } = React.useMemo(() => { + const shape = item.posterShape || 'poster'; + const baseHeight = posterWidth / (2 / 3); // Standard height derived from portrait width + + let w = posterWidth; + let ratio = 2 / 3; + + if (shape === 'landscape') { + ratio = 16 / 9; + // Maintain same height as portrait posters + w = baseHeight * ratio; + } else if (shape === 'square') { + ratio = 1; + w = baseHeight; + } + + return { + finalWidth: w, + finalAspectRatio: ratio, + borderRadius: typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12 + }; + }, [posterWidth, item.posterShape, settings.posterBorderRadius]); + // Intersection observer simulation for lazy loading const itemRef = useRef(null); @@ -169,7 +193,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe setIsWatched(targetWatched); try { await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false'); - } catch {} + } catch { } showInfo(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched', targetWatched ? 'Item marked as watched' : 'Item marked as unwatched'); setTimeout(() => { DeviceEventEmitter.emit('watchedStatusChanged'); @@ -185,7 +209,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe undefined, { forceNotify: true, forceWrite: true } ); - } catch {} + } catch { } if (item.type === 'movie') { try { @@ -194,9 +218,9 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe await trakt.addToWatchedMovies(item.id); try { await storageService.updateTraktSyncStatus(item.id, item.type, true, 100); - } catch {} + } catch { } } - } catch {} + } catch { } } } setMenuVisible(false); @@ -242,44 +266,34 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe setMenuVisible(false); }, []); - // Memoize optimized poster URL to prevent recalculating const optimizedPosterUrl = React.useMemo(() => { if (!item.poster || item.poster.includes('placeholder')) { return 'https://via.placeholder.com/154x231/333/666?text=No+Image'; } - - // For TMDB images, use smaller sizes if (item.poster.includes('image.tmdb.org')) { - // Replace any size with w154 (fits 100-130px tiles perfectly) return item.poster.replace(/\/w\d+\//, '/w154/'); } - - // For metahub images, use smaller sizes if (item.poster.includes('placeholder')) { return item.poster.replace('/medium/', '/small/'); } - - // Return original URL for other sources to avoid breaking them return item.poster; }, [item.poster, item.id]); - // While settings load, render a placeholder with reserved space (poster aspect + title) if (!isLoaded) { - const placeholderRadius = 12; return ( - + - {/* Reserve space for title to keep section spacing stable */} ); @@ -287,24 +301,24 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe return ( <> - + - + {/* Image with FastImage for aggressive caching */} {item.poster ? ( { setImageError(false); @@ -316,14 +330,14 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe /> ) : ( // Show placeholder for items without posters - + {item.name.substring(0, 20)}... )} {imageError && ( - + )} @@ -350,14 +364,14 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe {settings.showPosterTitles && ( - {item.name} diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx index a29f99f..5932980 100644 --- a/src/screens/CatalogScreen.tsx +++ b/src/screens/CatalogScreen.tsx @@ -289,6 +289,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { const [catalogExtras, setCatalogExtras] = useState([]); const [selectedFilters, setSelectedFilters] = useState>({}); const [activeGenreFilter, setActiveGenreFilter] = useState(genreFilter); + const [showTitles, setShowTitles] = useState(true); // Default to showing titles const { currentTheme } = useTheme(); const colors = currentTheme.colors; const styles = createStyles(colors); @@ -302,6 +303,10 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { if (pref === '2') setMobileColumnsPref(2); else if (pref === '3') setMobileColumnsPref(3); else setMobileColumnsPref('auto'); + + // Load show titles preference (default: true) + const titlesPref = await mmkvStorage.getItem('catalog_show_titles'); + setShowTitles(titlesPref !== 'false'); // Default to true if not set } catch { } })(); }, []); @@ -556,11 +561,14 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { let nextHasMore = false; try { const svcHasMore = addonId ? stremioService.getCatalogHasMore(addonId, type, id) : undefined; - // If service explicitly provides hasMore, use it; otherwise assume there's more if we got any items - // This handles addons with different page sizes (not just 50 items per page) - nextHasMore = typeof svcHasMore === 'boolean' ? svcHasMore : (catalogItems.length > 0); + // If service explicitly provides hasMore, use it + // Otherwise, only assume there's more if we got a reasonable number of items (>= 5) + // This prevents infinite loops when addons return just 1-2 items per page + const MIN_ITEMS_FOR_MORE = 5; + nextHasMore = typeof svcHasMore === 'boolean' ? svcHasMore : (catalogItems.length >= MIN_ITEMS_FOR_MORE); } catch { - nextHasMore = catalogItems.length > 0; + // Fallback: only assume more if we got at least 5 items + nextHasMore = catalogItems.length >= 5; } setHasMore(nextHasMore); logger.log('[CatalogScreen] Updated items and hasMore', { @@ -749,6 +757,10 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { // For proper spacing const rightMargin = isLastInRow ? 0 : ((screenData as any).itemSpacing ?? SPACING.sm); + // Calculate aspect ratio based on posterShape + const shape = item.posterShape || 'poster'; + const aspectRatio = shape === 'landscape' ? 16 / 9 : (shape === 'square' ? 1 : 2 / 3); + return ( = ({ route, navigation }) => { > @@ -808,9 +820,26 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { ) )} + + {/* Poster Title */} + {showTitles && ( + + {item.name} + + )} ); - }, [navigation, styles, effectiveNumColumns, effectiveItemWidth, type, nowPlayingMovies, colors.white, optimizePosterUrl]); + }, [navigation, styles, effectiveNumColumns, effectiveItemWidth, screenData, type, nowPlayingMovies, colors.white, colors.mediumGray, optimizePosterUrl, addonId, isDarkMode, showTitles]); const renderEmptyState = () => ( diff --git a/src/screens/CatalogSettingsScreen.tsx b/src/screens/CatalogSettingsScreen.tsx index 1318f87..d583814 100644 --- a/src/screens/CatalogSettingsScreen.tsx +++ b/src/screens/CatalogSettingsScreen.tsx @@ -177,17 +177,17 @@ const createStyles = (colors: any) => StyleSheet.create({ optionChipTextSelected: { color: colors.white, }, - hintRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - paddingHorizontal: 16, - paddingVertical: 8, - }, - hintText: { - fontSize: 12, - color: colors.mediumGray, - }, + hintRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 16, + paddingVertical: 8, + }, + hintText: { + fontSize: 12, + color: colors.mediumGray, + }, enabledCount: { fontSize: 15, color: colors.mediumGray, @@ -268,6 +268,7 @@ const CatalogSettingsScreen = () => { const [settings, setSettings] = useState([]); const [groupedSettings, setGroupedSettings] = useState({}); const [mobileColumns, setMobileColumns] = useState<'auto' | 2 | 3>('auto'); + const [showTitles, setShowTitles] = useState(true); // Default to showing titles const navigation = useNavigation(); const { refreshCatalogs } = useCatalogContext(); const { currentTheme } = useTheme(); @@ -288,11 +289,11 @@ const CatalogSettingsScreen = () => { const loadSettings = useCallback(async () => { try { setLoading(true); - + // Get installed addons and their catalogs const addons = await stremioService.getInstalledAddonsAsync(); const availableCatalogs: CatalogSetting[] = []; - + // Get saved enable/disable settings const savedSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY); const savedEnabledSettings: { [key: string]: boolean } = savedSettingsJson ? JSON.parse(savedSettingsJson) : {}; @@ -300,12 +301,12 @@ const CatalogSettingsScreen = () => { // Get saved custom names const savedCustomNamesJson = await mmkvStorage.getItem(CATALOG_CUSTOM_NAMES_KEY); const savedCustomNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {}; - + // Process each addon's catalogs addons.forEach(addon => { if (addon.catalogs && addon.catalogs.length > 0) { const uniqueCatalogs = new Map(); - + addon.catalogs.forEach(catalog => { const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`; let displayName = catalog.name || catalog.id; @@ -330,7 +331,7 @@ const CatalogSettingsScreen = () => { if (!displayName.toLowerCase().includes(catalogType.toLowerCase())) { displayName = `${displayName} ${catalogType}`.trim(); } - + uniqueCatalogs.set(settingKey, { addonId: addon.id, catalogId: catalog.id, @@ -340,32 +341,32 @@ const CatalogSettingsScreen = () => { customName: savedCustomNames[settingKey] }); }); - + availableCatalogs.push(...uniqueCatalogs.values()); } }); - + // Group settings by addon name const grouped: GroupedCatalogs = {}; availableCatalogs.forEach(setting => { const addon = addons.find(a => a.id === setting.addonId); if (!addon) return; - + if (!grouped[setting.addonId]) { grouped[setting.addonId] = { name: addon.name, catalogs: [], - expanded: true, + expanded: true, enabledCount: 0 }; } - + grouped[setting.addonId].catalogs.push(setting); if (setting.enabled) { grouped[setting.addonId].enabledCount++; } }); - + setSettings(availableCatalogs); setGroupedSettings(grouped); @@ -375,6 +376,10 @@ const CatalogSettingsScreen = () => { if (pref === '2') setMobileColumns(2); else if (pref === '3') setMobileColumns(3); else setMobileColumns('auto'); + + // Load show titles preference (default: true) + const titlesPref = await mmkvStorage.getItem('catalog_show_titles'); + setShowTitles(titlesPref !== 'false'); // Default to true if not set } catch (e) { // ignore } @@ -396,7 +401,7 @@ const CatalogSettingsScreen = () => { settingsObj[key] = setting.enabled; }); await mmkvStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj)); - + // Small delay to ensure AsyncStorage has fully persisted before triggering refresh setTimeout(() => { refreshCatalogs(); // Trigger catalog refresh after saving settings @@ -411,26 +416,26 @@ const CatalogSettingsScreen = () => { const newSettings = [...settings]; const catalogsForAddon = groupedSettings[addonId].catalogs; const setting = catalogsForAddon[index]; - + const updatedSetting = { ...setting, enabled: !setting.enabled }; - - const flatIndex = newSettings.findIndex(s => - s.addonId === setting.addonId && - s.type === setting.type && + + const flatIndex = newSettings.findIndex(s => + s.addonId === setting.addonId && + s.type === setting.type && s.catalogId === setting.catalogId ); - + if (flatIndex !== -1) { newSettings[flatIndex] = updatedSetting; } - + const newGroupedSettings = { ...groupedSettings }; newGroupedSettings[addonId].catalogs[index] = updatedSetting; newGroupedSettings[addonId].enabledCount += updatedSetting.enabled ? 1 : -1; - + setSettings(newSettings); setGroupedSettings(newGroupedSettings); saveEnabledSettings(newSettings); // Use specific save function @@ -459,11 +464,11 @@ const CatalogSettingsScreen = () => { if (!catalogToRename || !currentRenameValue) return; const settingKey = `${catalogToRename.addonId}:${catalogToRename.type}:${catalogToRename.catalogId}`; - + try { const savedCustomNamesJson = await mmkvStorage.getItem(CATALOG_CUSTOM_NAMES_KEY); const customNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {}; - + const trimmedNewName = currentRenameValue.trim(); if (trimmedNewName === catalogToRename.name || trimmedNewName === '') { @@ -471,22 +476,22 @@ const CatalogSettingsScreen = () => { } else { customNames[settingKey] = trimmedNewName; } - + await mmkvStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames)); // Clear in-memory cache so new name is used immediately - try { clearCustomNameCache(); } catch {} + try { clearCustomNameCache(); } catch { } // --- Reload settings to reflect the change --- - await loadSettings(); + await loadSettings(); // Also trigger home/catalog consumers to refresh - try { refreshCatalogs(); } catch {} + try { refreshCatalogs(); } catch { } // --- No need to manually update local state anymore --- } catch (error) { logger.error('Failed to save custom catalog name:', error); setAlertTitle('Error'); setAlertMessage('Could not save the custom name.'); - setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertActions([{ label: 'OK', onPress: () => { } }]); setAlertVisible(true); } finally { setIsRenameModalVisible(false); @@ -533,7 +538,7 @@ const CatalogSettingsScreen = () => { Catalogs - + {/* Layout (Mobile only) */} {Platform.OS && ( @@ -552,7 +557,7 @@ const CatalogSettingsScreen = () => { try { await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, 'auto'); setMobileColumns('auto'); - } catch {} + } catch { } }} activeOpacity={0.7} > @@ -564,7 +569,7 @@ const CatalogSettingsScreen = () => { try { await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '2'); setMobileColumns(2); - } catch {} + } catch { } }} activeOpacity={0.7} > @@ -576,7 +581,7 @@ const CatalogSettingsScreen = () => { try { await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '3'); setMobileColumns(3); - } catch {} + } catch { } }} activeOpacity={0.7} > @@ -596,9 +601,9 @@ const CatalogSettingsScreen = () => { {group.name.toUpperCase()} - + - toggleExpansion(addonId)} activeOpacity={0.7} @@ -608,14 +613,14 @@ const CatalogSettingsScreen = () => { {group.enabledCount} of {group.catalogs.length} enabled - - + {group.expanded && ( <> @@ -623,30 +628,30 @@ const CatalogSettingsScreen = () => { Long-press a catalog to rename {group.catalogs.map((setting, index) => ( - handleLongPress(setting)} // Added long press handler - style={({ pressed }) => [ - styles.catalogItem, - pressed && styles.catalogItemPressed, // Optional pressed style - ]} - > - - - {setting.customName || setting.name} {/* Display custom or default name */} - - - {setting.type.charAt(0).toUpperCase() + setting.type.slice(1)} - - - toggleCatalog(addonId, index)} - trackColor={{ false: '#505050', true: colors.primary }} - thumbColor={Platform.OS === 'android' ? colors.white : undefined} - ios_backgroundColor="#505050" - /> - + handleLongPress(setting)} // Added long press handler + style={({ pressed }) => [ + styles.catalogItem, + pressed && styles.catalogItemPressed, // Optional pressed style + ]} + > + + + {setting.customName || setting.name} {/* Display custom or default name */} + + + {setting.type.charAt(0).toUpperCase() + setting.type.slice(1)} + + + toggleCatalog(addonId, index)} + trackColor={{ false: '#505050', true: colors.primary }} + thumbColor={Platform.OS === 'android' ? colors.white : undefined} + ios_backgroundColor="#505050" + /> + ))} )} @@ -706,8 +711,8 @@ const CatalogSettingsScreen = () => { )} ) : ( - setIsRenameModalVisible(false)}> - e.stopPropagation()}> + setIsRenameModalVisible(false)}> + e.stopPropagation()}> Rename Catalog { return folders.filter(folder => folder.itemCount > 0); }, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); - const renderItem = ({ item }: { item: LibraryItem }) => ( - navigation.navigate('Metadata', { id: item.id, type: item.type })} - onLongPress={() => { - setSelectedItem(item); - setMenuVisible(true); - }} - activeOpacity={0.7} - > - - - - {item.watched && ( - - - - )} - {item.progress !== undefined && item.progress < 1 && ( - - - - )} + const renderItem = ({ item }: { item: LibraryItem }) => { + const aspectRatio = item.posterShape === 'landscape' ? 16 / 9 : (item.posterShape === 'square' ? 1 : 2 / 3); + + return ( + navigation.navigate('Metadata', { id: item.id, type: item.type })} + onLongPress={() => { + setSelectedItem(item); + setMenuVisible(true); + }} + activeOpacity={0.7} + > + + + + {item.watched && ( + + + + )} + {item.progress !== undefined && item.progress < 1 && ( + + + + )} + + + {item.name} + - - {item.name} - - - - ); + + ); + }; // Render individual Trakt collection folder const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => ( diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 9acbacf..82a2711 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -615,6 +615,25 @@ const SearchScreen = () => { }) => { const [inLibrary, setInLibrary] = React.useState(!!item.inLibrary); const [watched, setWatched] = React.useState(false); + + // Calculate dimensions based on poster shape + const { itemWidth, aspectRatio } = useMemo(() => { + const shape = item.posterShape || 'poster'; + const baseHeight = HORIZONTAL_POSTER_HEIGHT; + + let w = HORIZONTAL_ITEM_WIDTH; + let r = 2 / 3; + + if (shape === 'landscape') { + r = 16 / 9; + w = baseHeight * r; + } else if (shape === 'square') { + r = 1; + w = baseHeight; + } + return { itemWidth: w, aspectRatio: r }; + }, [item.posterShape]); + React.useEffect(() => { const updateWatched = () => { mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true')); @@ -630,9 +649,10 @@ const SearchScreen = () => { }); return () => unsubscribe(); }, [item.id, item.type]); + return ( { navigation.navigate('Metadata', { id: item.id, type: item.type }); }} @@ -645,6 +665,11 @@ const SearchScreen = () => { activeOpacity={0.7} > diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index 589a9d1..c24108d 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -56,7 +56,7 @@ export interface StreamingContent { name: string; tmdbId?: number; poster: string; - posterShape?: string; + posterShape?: 'poster' | 'square' | 'landscape'; banner?: string; logo?: string; imdbRating?: string; @@ -835,7 +835,7 @@ class CatalogService { type: meta.type, name: meta.name, poster: posterUrl, - posterShape: 'poster', + posterShape: meta.posterShape || 'poster', // Use addon's shape or default to poster type banner: meta.background, logo: logoUrl, imdbRating: meta.imdbRating, @@ -857,7 +857,7 @@ class CatalogService { type: meta.type, name: meta.name, poster: meta.poster || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image', - posterShape: 'poster', + posterShape: meta.posterShape || 'poster', banner: meta.background, // Use addon's logo if available, otherwise undefined logo: (meta as any).logo || undefined, diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 9ba4cc3..378585e 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -20,6 +20,7 @@ export interface Meta { type: string; name: string; poster?: string; + posterShape?: 'poster' | 'square' | 'landscape'; // For variable aspect ratios background?: string; logo?: string; description?: string;