mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-29 04:13:03 +00:00
landscape poster support
This commit is contained in:
parent
59cb902658
commit
601a4a0f1d
8 changed files with 236 additions and 172 deletions
|
|
@ -96,20 +96,7 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
||||||
// Memoize the keyExtractor to prevent re-creation
|
// Memoize the keyExtractor to prevent re-creation
|
||||||
const keyExtractor = useCallback((item: StreamingContent) => `${item.id}-${item.type}`, []);
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
|
|
@ -194,7 +181,6 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
||||||
}
|
}
|
||||||
])}
|
])}
|
||||||
ItemSeparatorComponent={ItemSeparator}
|
ItemSeparatorComponent={ItemSeparator}
|
||||||
getItemLayout={getItemLayout}
|
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
initialNumToRender={isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3}
|
initialNumToRender={isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3}
|
||||||
maxToRenderPerBatch={isTV ? 4 : isLargeTablet ? 4 : 3}
|
maxToRenderPerBatch={isTV ? 4 : isLargeTablet ? 4 : 3}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, [item.id, item.type]);
|
}, [item.id, item.type]);
|
||||||
|
|
||||||
// Load watched state from AsyncStorage when item changes
|
// Load watched state from AsyncStorage when item changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateWatched = () => {
|
const updateWatched = () => {
|
||||||
mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then((val: string | null) => setIsWatched(val === 'true'));
|
mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then((val: string | null) => setIsWatched(val === 'true'));
|
||||||
|
|
@ -139,6 +139,30 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
}
|
}
|
||||||
}, [settings.posterSize, width]);
|
}, [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
|
// Intersection observer simulation for lazy loading
|
||||||
const itemRef = useRef<View>(null);
|
const itemRef = useRef<View>(null);
|
||||||
|
|
||||||
|
|
@ -169,7 +193,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
setIsWatched(targetWatched);
|
setIsWatched(targetWatched);
|
||||||
try {
|
try {
|
||||||
await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
|
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');
|
showInfo(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched', targetWatched ? 'Item marked as watched' : 'Item marked as unwatched');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
DeviceEventEmitter.emit('watchedStatusChanged');
|
DeviceEventEmitter.emit('watchedStatusChanged');
|
||||||
|
|
@ -185,7 +209,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
undefined,
|
undefined,
|
||||||
{ forceNotify: true, forceWrite: true }
|
{ forceNotify: true, forceWrite: true }
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
if (item.type === 'movie') {
|
if (item.type === 'movie') {
|
||||||
try {
|
try {
|
||||||
|
|
@ -194,9 +218,9 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
await trakt.addToWatchedMovies(item.id);
|
await trakt.addToWatchedMovies(item.id);
|
||||||
try {
|
try {
|
||||||
await storageService.updateTraktSyncStatus(item.id, item.type, true, 100);
|
await storageService.updateTraktSyncStatus(item.id, item.type, true, 100);
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setMenuVisible(false);
|
setMenuVisible(false);
|
||||||
|
|
@ -242,44 +266,34 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
setMenuVisible(false);
|
setMenuVisible(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Memoize optimized poster URL to prevent recalculating
|
// Memoize optimized poster URL to prevent recalculating
|
||||||
const optimizedPosterUrl = React.useMemo(() => {
|
const optimizedPosterUrl = React.useMemo(() => {
|
||||||
if (!item.poster || item.poster.includes('placeholder')) {
|
if (!item.poster || item.poster.includes('placeholder')) {
|
||||||
return 'https://via.placeholder.com/154x231/333/666?text=No+Image';
|
return 'https://via.placeholder.com/154x231/333/666?text=No+Image';
|
||||||
}
|
}
|
||||||
|
|
||||||
// For TMDB images, use smaller sizes
|
|
||||||
if (item.poster.includes('image.tmdb.org')) {
|
if (item.poster.includes('image.tmdb.org')) {
|
||||||
// Replace any size with w154 (fits 100-130px tiles perfectly)
|
|
||||||
return item.poster.replace(/\/w\d+\//, '/w154/');
|
return item.poster.replace(/\/w\d+\//, '/w154/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// For metahub images, use smaller sizes
|
|
||||||
if (item.poster.includes('placeholder')) {
|
if (item.poster.includes('placeholder')) {
|
||||||
return item.poster.replace('/medium/', '/small/');
|
return item.poster.replace('/medium/', '/small/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return original URL for other sources to avoid breaking them
|
|
||||||
return item.poster;
|
return item.poster;
|
||||||
}, [item.poster, item.id]);
|
}, [item.poster, item.id]);
|
||||||
|
|
||||||
// While settings load, render a placeholder with reserved space (poster aspect + title)
|
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
const placeholderRadius = 12;
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.itemContainer, { width: posterWidth }]}>
|
<View style={[styles.itemContainer, { width: finalWidth }]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.contentItem,
|
styles.contentItem,
|
||||||
{
|
{
|
||||||
width: posterWidth,
|
width: finalWidth,
|
||||||
borderRadius: placeholderRadius,
|
aspectRatio: finalAspectRatio,
|
||||||
|
borderRadius,
|
||||||
backgroundColor: currentTheme.colors.elevation1,
|
backgroundColor: currentTheme.colors.elevation1,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{/* Reserve space for title to keep section spacing stable */}
|
|
||||||
<View style={{ height: 18, marginTop: 4 }} />
|
<View style={{ height: 18, marginTop: 4 }} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -287,15 +301,15 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Animated.View style={[styles.itemContainer, { width: posterWidth }]} entering={FadeIn.duration(300)}>
|
<Animated.View style={[styles.itemContainer, { width: finalWidth }]} entering={FadeIn.duration(300)}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.contentItem, { width: posterWidth, borderRadius: posterRadius }]}
|
style={[styles.contentItem, { width: finalWidth, aspectRatio: finalAspectRatio, borderRadius }]}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
onLongPress={handleLongPress}
|
onLongPress={handleLongPress}
|
||||||
delayLongPress={300}
|
delayLongPress={300}
|
||||||
>
|
>
|
||||||
<View ref={itemRef} style={[styles.contentItemContainer, { borderRadius: posterRadius }] }>
|
<View ref={itemRef} style={[styles.contentItemContainer, { borderRadius }]}>
|
||||||
{/* Image with FastImage for aggressive caching */}
|
{/* Image with FastImage for aggressive caching */}
|
||||||
{item.poster ? (
|
{item.poster ? (
|
||||||
<FastImage
|
<FastImage
|
||||||
|
|
@ -304,7 +318,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
priority: FastImage.priority.normal,
|
priority: FastImage.priority.normal,
|
||||||
cache: FastImage.cacheControl.immutable
|
cache: FastImage.cacheControl.immutable
|
||||||
}}
|
}}
|
||||||
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
|
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
setImageError(false);
|
setImageError(false);
|
||||||
|
|
@ -316,7 +330,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
// Show placeholder for items without posters
|
// Show placeholder for items without posters
|
||||||
<View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center', borderRadius: posterRadius }] }>
|
<View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center', borderRadius: posterRadius }]}>
|
||||||
<Text style={{ color: currentTheme.colors.textMuted, fontSize: 10, textAlign: 'center' }}>
|
<Text style={{ color: currentTheme.colors.textMuted, fontSize: 10, textAlign: 'center' }}>
|
||||||
{item.name.substring(0, 20)}...
|
{item.name.substring(0, 20)}...
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
const [catalogExtras, setCatalogExtras] = useState<CatalogExtra[]>([]);
|
const [catalogExtras, setCatalogExtras] = useState<CatalogExtra[]>([]);
|
||||||
const [selectedFilters, setSelectedFilters] = useState<Record<string, string>>({});
|
const [selectedFilters, setSelectedFilters] = useState<Record<string, string>>({});
|
||||||
const [activeGenreFilter, setActiveGenreFilter] = useState<string | undefined>(genreFilter);
|
const [activeGenreFilter, setActiveGenreFilter] = useState<string | undefined>(genreFilter);
|
||||||
|
const [showTitles, setShowTitles] = useState(true); // Default to showing titles
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const colors = currentTheme.colors;
|
const colors = currentTheme.colors;
|
||||||
const styles = createStyles(colors);
|
const styles = createStyles(colors);
|
||||||
|
|
@ -302,6 +303,10 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
if (pref === '2') setMobileColumnsPref(2);
|
if (pref === '2') setMobileColumnsPref(2);
|
||||||
else if (pref === '3') setMobileColumnsPref(3);
|
else if (pref === '3') setMobileColumnsPref(3);
|
||||||
else setMobileColumnsPref('auto');
|
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 { }
|
} catch { }
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -556,11 +561,14 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
let nextHasMore = false;
|
let nextHasMore = false;
|
||||||
try {
|
try {
|
||||||
const svcHasMore = addonId ? stremioService.getCatalogHasMore(addonId, type, id) : undefined;
|
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
|
// If service explicitly provides hasMore, use it
|
||||||
// This handles addons with different page sizes (not just 50 items per page)
|
// Otherwise, only assume there's more if we got a reasonable number of items (>= 5)
|
||||||
nextHasMore = typeof svcHasMore === 'boolean' ? svcHasMore : (catalogItems.length > 0);
|
// 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 {
|
} catch {
|
||||||
nextHasMore = catalogItems.length > 0;
|
// Fallback: only assume more if we got at least 5 items
|
||||||
|
nextHasMore = catalogItems.length >= 5;
|
||||||
}
|
}
|
||||||
setHasMore(nextHasMore);
|
setHasMore(nextHasMore);
|
||||||
logger.log('[CatalogScreen] Updated items and hasMore', {
|
logger.log('[CatalogScreen] Updated items and hasMore', {
|
||||||
|
|
@ -749,6 +757,10 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
// For proper spacing
|
// For proper spacing
|
||||||
const rightMargin = isLastInRow ? 0 : ((screenData as any).itemSpacing ?? SPACING.sm);
|
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 (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -763,7 +775,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
>
|
>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: optimizePosterUrl(item.poster) }}
|
source={{ uri: optimizePosterUrl(item.poster) }}
|
||||||
style={styles.poster}
|
style={[styles.poster, { aspectRatio }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -808,9 +820,26 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Poster Title */}
|
||||||
|
{showTitles && (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: colors.mediumGray,
|
||||||
|
marginTop: 6,
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
}}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}, [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 = () => (
|
const renderEmptyState = () => (
|
||||||
<View style={styles.centered}>
|
<View style={styles.centered}>
|
||||||
|
|
|
||||||
|
|
@ -177,17 +177,17 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
optionChipTextSelected: {
|
optionChipTextSelected: {
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
},
|
},
|
||||||
hintRow: {
|
hintRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 6,
|
gap: 6,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
},
|
},
|
||||||
hintText: {
|
hintText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: colors.mediumGray,
|
color: colors.mediumGray,
|
||||||
},
|
},
|
||||||
enabledCount: {
|
enabledCount: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: colors.mediumGray,
|
color: colors.mediumGray,
|
||||||
|
|
@ -268,6 +268,7 @@ const CatalogSettingsScreen = () => {
|
||||||
const [settings, setSettings] = useState<CatalogSetting[]>([]);
|
const [settings, setSettings] = useState<CatalogSetting[]>([]);
|
||||||
const [groupedSettings, setGroupedSettings] = useState<GroupedCatalogs>({});
|
const [groupedSettings, setGroupedSettings] = useState<GroupedCatalogs>({});
|
||||||
const [mobileColumns, setMobileColumns] = useState<'auto' | 2 | 3>('auto');
|
const [mobileColumns, setMobileColumns] = useState<'auto' | 2 | 3>('auto');
|
||||||
|
const [showTitles, setShowTitles] = useState(true); // Default to showing titles
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { refreshCatalogs } = useCatalogContext();
|
const { refreshCatalogs } = useCatalogContext();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
@ -375,6 +376,10 @@ const CatalogSettingsScreen = () => {
|
||||||
if (pref === '2') setMobileColumns(2);
|
if (pref === '2') setMobileColumns(2);
|
||||||
else if (pref === '3') setMobileColumns(3);
|
else if (pref === '3') setMobileColumns(3);
|
||||||
else setMobileColumns('auto');
|
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) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
@ -474,19 +479,19 @@ const CatalogSettingsScreen = () => {
|
||||||
|
|
||||||
await mmkvStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames));
|
await mmkvStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames));
|
||||||
// Clear in-memory cache so new name is used immediately
|
// Clear in-memory cache so new name is used immediately
|
||||||
try { clearCustomNameCache(); } catch {}
|
try { clearCustomNameCache(); } catch { }
|
||||||
|
|
||||||
// --- Reload settings to reflect the change ---
|
// --- Reload settings to reflect the change ---
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
// Also trigger home/catalog consumers to refresh
|
// Also trigger home/catalog consumers to refresh
|
||||||
try { refreshCatalogs(); } catch {}
|
try { refreshCatalogs(); } catch { }
|
||||||
// --- No need to manually update local state anymore ---
|
// --- No need to manually update local state anymore ---
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to save custom catalog name:', error);
|
logger.error('Failed to save custom catalog name:', error);
|
||||||
setAlertTitle('Error');
|
setAlertTitle('Error');
|
||||||
setAlertMessage('Could not save the custom name.');
|
setAlertMessage('Could not save the custom name.');
|
||||||
setAlertActions([{ label: 'OK', onPress: () => {} }]);
|
setAlertActions([{ label: 'OK', onPress: () => { } }]);
|
||||||
setAlertVisible(true);
|
setAlertVisible(true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRenameModalVisible(false);
|
setIsRenameModalVisible(false);
|
||||||
|
|
@ -552,7 +557,7 @@ const CatalogSettingsScreen = () => {
|
||||||
try {
|
try {
|
||||||
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, 'auto');
|
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, 'auto');
|
||||||
setMobileColumns('auto');
|
setMobileColumns('auto');
|
||||||
} catch {}
|
} catch { }
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
|
|
@ -564,7 +569,7 @@ const CatalogSettingsScreen = () => {
|
||||||
try {
|
try {
|
||||||
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '2');
|
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '2');
|
||||||
setMobileColumns(2);
|
setMobileColumns(2);
|
||||||
} catch {}
|
} catch { }
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
|
|
@ -576,7 +581,7 @@ const CatalogSettingsScreen = () => {
|
||||||
try {
|
try {
|
||||||
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '3');
|
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '3');
|
||||||
setMobileColumns(3);
|
setMobileColumns(3);
|
||||||
} catch {}
|
} catch { }
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
|
|
@ -623,30 +628,30 @@ const CatalogSettingsScreen = () => {
|
||||||
<Text style={styles.hintText}>Long-press a catalog to rename</Text>
|
<Text style={styles.hintText}>Long-press a catalog to rename</Text>
|
||||||
</View>
|
</View>
|
||||||
{group.catalogs.map((setting, index) => (
|
{group.catalogs.map((setting, index) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={`${setting.addonId}:${setting.type}:${setting.catalogId}`}
|
key={`${setting.addonId}:${setting.type}:${setting.catalogId}`}
|
||||||
onLongPress={() => handleLongPress(setting)} // Added long press handler
|
onLongPress={() => handleLongPress(setting)} // Added long press handler
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
styles.catalogItem,
|
styles.catalogItem,
|
||||||
pressed && styles.catalogItemPressed, // Optional pressed style
|
pressed && styles.catalogItemPressed, // Optional pressed style
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View style={styles.catalogInfo}>
|
<View style={styles.catalogInfo}>
|
||||||
<Text style={styles.catalogName}>
|
<Text style={styles.catalogName}>
|
||||||
{setting.customName || setting.name} {/* Display custom or default 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)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={setting.enabled}
|
value={setting.enabled}
|
||||||
onValueChange={() => toggleCatalog(addonId, index)}
|
onValueChange={() => toggleCatalog(addonId, index)}
|
||||||
trackColor={{ false: '#505050', true: colors.primary }}
|
trackColor={{ false: '#505050', true: colors.primary }}
|
||||||
thumbColor={Platform.OS === 'android' ? colors.white : undefined}
|
thumbColor={Platform.OS === 'android' ? colors.white : undefined}
|
||||||
ios_backgroundColor="#505050"
|
ios_backgroundColor="#505050"
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -393,45 +393,49 @@ const LibraryScreen = () => {
|
||||||
return folders.filter(folder => folder.itemCount > 0);
|
return folders.filter(folder => folder.itemCount > 0);
|
||||||
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: LibraryItem }) => (
|
const renderItem = ({ item }: { item: LibraryItem }) => {
|
||||||
<TouchableOpacity
|
const aspectRatio = item.posterShape === 'landscape' ? 16 / 9 : (item.posterShape === 'square' ? 1 : 2 / 3);
|
||||||
style={[styles.itemContainer, { width: itemWidth }]}
|
|
||||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
return (
|
||||||
onLongPress={() => {
|
<TouchableOpacity
|
||||||
setSelectedItem(item);
|
style={[styles.itemContainer, { width: itemWidth }]}
|
||||||
setMenuVisible(true);
|
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||||
}}
|
onLongPress={() => {
|
||||||
activeOpacity={0.7}
|
setSelectedItem(item);
|
||||||
>
|
setMenuVisible(true);
|
||||||
<View>
|
}}
|
||||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
activeOpacity={0.7}
|
||||||
<FastImage
|
>
|
||||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
<View>
|
||||||
style={styles.poster}
|
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black, aspectRatio }]}>
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
<FastImage
|
||||||
/>
|
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||||
{item.watched && (
|
style={styles.poster}
|
||||||
<View style={styles.watchedIndicator}>
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success || '#4CAF50'} />
|
/>
|
||||||
</View>
|
{item.watched && (
|
||||||
)}
|
<View style={styles.watchedIndicator}>
|
||||||
{item.progress !== undefined && item.progress < 1 && (
|
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success || '#4CAF50'} />
|
||||||
<View style={styles.progressBarContainer}>
|
</View>
|
||||||
<View
|
)}
|
||||||
style={[
|
{item.progress !== undefined && item.progress < 1 && (
|
||||||
styles.progressBar,
|
<View style={styles.progressBarContainer}>
|
||||||
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
|
<View
|
||||||
]}
|
style={[
|
||||||
/>
|
styles.progressBar,
|
||||||
</View>
|
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
|
||||||
)}
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
</TouchableOpacity>
|
||||||
{item.name}
|
);
|
||||||
</Text>
|
};
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Render individual Trakt collection folder
|
// Render individual Trakt collection folder
|
||||||
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
||||||
|
|
|
||||||
|
|
@ -615,6 +615,25 @@ const SearchScreen = () => {
|
||||||
}) => {
|
}) => {
|
||||||
const [inLibrary, setInLibrary] = React.useState(!!item.inLibrary);
|
const [inLibrary, setInLibrary] = React.useState(!!item.inLibrary);
|
||||||
const [watched, setWatched] = React.useState(false);
|
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(() => {
|
React.useEffect(() => {
|
||||||
const updateWatched = () => {
|
const updateWatched = () => {
|
||||||
mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true'));
|
mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true'));
|
||||||
|
|
@ -630,9 +649,10 @@ const SearchScreen = () => {
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, [item.id, item.type]);
|
}, [item.id, item.type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.horizontalItem}
|
style={[styles.horizontalItem, { width: itemWidth }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Metadata', { id: item.id, type: item.type });
|
navigation.navigate('Metadata', { id: item.id, type: item.type });
|
||||||
}}
|
}}
|
||||||
|
|
@ -645,6 +665,11 @@ const SearchScreen = () => {
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={[styles.horizontalItemPosterContainer, {
|
<View style={[styles.horizontalItemPosterContainer, {
|
||||||
|
width: itemWidth,
|
||||||
|
height: undefined, // Let aspect ratio control height or keep fixed height with width?
|
||||||
|
// Actually, since we derived width from fixed height, we can keep height fixed or use aspect.
|
||||||
|
// Using aspect ratio is safer if baseHeight changes.
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
borderColor: 'rgba(255,255,255,0.05)'
|
borderColor: 'rgba(255,255,255,0.05)'
|
||||||
}]}>
|
}]}>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export interface StreamingContent {
|
||||||
name: string;
|
name: string;
|
||||||
tmdbId?: number;
|
tmdbId?: number;
|
||||||
poster: string;
|
poster: string;
|
||||||
posterShape?: string;
|
posterShape?: 'poster' | 'square' | 'landscape';
|
||||||
banner?: string;
|
banner?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
imdbRating?: string;
|
imdbRating?: string;
|
||||||
|
|
@ -835,7 +835,7 @@ class CatalogService {
|
||||||
type: meta.type,
|
type: meta.type,
|
||||||
name: meta.name,
|
name: meta.name,
|
||||||
poster: posterUrl,
|
poster: posterUrl,
|
||||||
posterShape: 'poster',
|
posterShape: meta.posterShape || 'poster', // Use addon's shape or default to poster type
|
||||||
banner: meta.background,
|
banner: meta.background,
|
||||||
logo: logoUrl,
|
logo: logoUrl,
|
||||||
imdbRating: meta.imdbRating,
|
imdbRating: meta.imdbRating,
|
||||||
|
|
@ -857,7 +857,7 @@ class CatalogService {
|
||||||
type: meta.type,
|
type: meta.type,
|
||||||
name: meta.name,
|
name: meta.name,
|
||||||
poster: meta.poster || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image',
|
poster: meta.poster || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image',
|
||||||
posterShape: 'poster',
|
posterShape: meta.posterShape || 'poster',
|
||||||
banner: meta.background,
|
banner: meta.background,
|
||||||
// Use addon's logo if available, otherwise undefined
|
// Use addon's logo if available, otherwise undefined
|
||||||
logo: (meta as any).logo || undefined,
|
logo: (meta as any).logo || undefined,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export interface Meta {
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
|
posterShape?: 'poster' | 'square' | 'landscape'; // For variable aspect ratios
|
||||||
background?: string;
|
background?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue