mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-29 13:59:58 +00:00
user can now customzie rows in of catalogscreen.
This commit is contained in:
parent
408b1cb366
commit
375ea61b37
2 changed files with 136 additions and 35 deletions
|
|
@ -21,6 +21,7 @@ import { Image } from 'expo-image';
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
|
||||
|
||||
type CatalogScreenProps = {
|
||||
|
|
@ -143,20 +144,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
borderTopRightRadius: 12,
|
||||
backgroundColor: colors.elevation3,
|
||||
},
|
||||
itemContent: {
|
||||
padding: SPACING.sm,
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
lineHeight: 18,
|
||||
},
|
||||
releaseInfo: {
|
||||
fontSize: 12,
|
||||
marginTop: SPACING.xs,
|
||||
color: colors.mediumGray,
|
||||
},
|
||||
// removed bottom text container; keep spacing via item margin only
|
||||
footer: {
|
||||
padding: SPACING.lg,
|
||||
alignItems: 'center',
|
||||
|
|
@ -223,19 +211,33 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
...calculateCatalogLayout(width)
|
||||
};
|
||||
});
|
||||
const [mobileColumnsPref, setMobileColumnsPref] = useState<'auto' | 2 | 3>('auto');
|
||||
const { currentTheme } = useTheme();
|
||||
const colors = currentTheme.colors;
|
||||
const styles = createStyles(colors);
|
||||
const isDarkMode = true;
|
||||
const isInitialRender = React.useRef(true);
|
||||
|
||||
// Load mobile columns preference (phones only)
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const pref = await AsyncStorage.getItem('catalog_mobile_columns');
|
||||
if (pref === '2') setMobileColumnsPref(2);
|
||||
else if (pref === '3') setMobileColumnsPref(3);
|
||||
else setMobileColumnsPref('auto');
|
||||
} catch {}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// Handle screen dimension changes
|
||||
useEffect(() => {
|
||||
const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
||||
setScreenData({
|
||||
const base = calculateCatalogLayout(window.width);
|
||||
setScreenData(prev => ({
|
||||
width: window.width,
|
||||
...calculateCatalogLayout(window.width)
|
||||
});
|
||||
...base
|
||||
}));
|
||||
});
|
||||
|
||||
return () => subscription?.remove();
|
||||
|
|
@ -542,9 +544,26 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
}
|
||||
}, [loading, paginating, hasMore, page, loadItems, refreshing]);
|
||||
|
||||
const effectiveNumColumns = React.useMemo(() => {
|
||||
const isPhone = screenData.width < 600; // basic breakpoint; tablets generally above this
|
||||
if (!isPhone || mobileColumnsPref === 'auto') return screenData.numColumns;
|
||||
// clamp to 2 or 3 on phones
|
||||
return mobileColumnsPref === 2 ? 2 : 3;
|
||||
}, [screenData.width, screenData.numColumns, mobileColumnsPref]);
|
||||
|
||||
const effectiveItemWidth = React.useMemo(() => {
|
||||
if (effectiveNumColumns === screenData.numColumns) return screenData.itemWidth;
|
||||
// recompute width for custom columns on mobile to maintain spacing roughly similar
|
||||
const HORIZONTAL_PADDING = 16 * 2; // SPACING.lg * 2
|
||||
const ITEM_SPACING = 8; // SPACING.sm
|
||||
const availableWidth = screenData.width - HORIZONTAL_PADDING;
|
||||
const totalSpacing = ITEM_SPACING * (effectiveNumColumns - 1);
|
||||
return (availableWidth - totalSpacing) / effectiveNumColumns;
|
||||
}, [effectiveNumColumns, screenData.width, screenData.itemWidth]);
|
||||
|
||||
const renderItem = useCallback(({ item, index }: { item: Meta; index: number }) => {
|
||||
// Calculate if this is the last item in a row
|
||||
const isLastInRow = (index + 1) % screenData.numColumns === 0;
|
||||
const isLastInRow = (index + 1) % effectiveNumColumns === 0;
|
||||
// For proper spacing
|
||||
const rightMargin = isLastInRow ? 0 : SPACING.sm;
|
||||
|
||||
|
|
@ -554,7 +573,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
styles.item,
|
||||
{
|
||||
marginRight: rightMargin,
|
||||
width: screenData.itemWidth
|
||||
width: effectiveItemWidth
|
||||
}
|
||||
]}
|
||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })}
|
||||
|
|
@ -568,22 +587,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
transition={0}
|
||||
allowDownscaling
|
||||
/>
|
||||
<View style={styles.itemContent}>
|
||||
<Text
|
||||
style={styles.title}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.releaseInfo && (
|
||||
<Text style={styles.releaseInfo}>
|
||||
{item.releaseInfo}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}, [navigation, styles, screenData.numColumns, screenData.itemWidth]);
|
||||
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth]);
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<View style={styles.centered}>
|
||||
|
|
@ -681,8 +687,8 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
data={items}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => `${item.id}-${item.type}`}
|
||||
numColumns={screenData.numColumns}
|
||||
key={screenData.numColumns}
|
||||
numColumns={effectiveNumColumns}
|
||||
key={effectiveNumColumns}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ interface GroupedCatalogs {
|
|||
|
||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
|
||||
const CATALOG_MOBILE_COLUMNS_KEY = 'catalog_mobile_columns';
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
// Create a styles creator function that accepts the theme colors
|
||||
|
|
@ -133,6 +134,33 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
groupHeaderRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
optionRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
gap: 8,
|
||||
},
|
||||
optionChip: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
optionChipSelected: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
optionChipText: {
|
||||
color: colors.mediumGray,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
optionChipTextSelected: {
|
||||
color: colors.white,
|
||||
},
|
||||
hintRow: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -224,6 +252,7 @@ const CatalogSettingsScreen = () => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [settings, setSettings] = useState<CatalogSetting[]>([]);
|
||||
const [groupedSettings, setGroupedSettings] = useState<GroupedCatalogs>({});
|
||||
const [mobileColumns, setMobileColumns] = useState<'auto' | 2 | 3>('auto');
|
||||
const navigation = useNavigation();
|
||||
const { refreshCatalogs } = useCatalogContext();
|
||||
const { currentTheme } = useTheme();
|
||||
|
|
@ -320,6 +349,16 @@ const CatalogSettingsScreen = () => {
|
|||
|
||||
setSettings(availableCatalogs);
|
||||
setGroupedSettings(grouped);
|
||||
|
||||
// Load mobile columns preference (phones only)
|
||||
try {
|
||||
const pref = await AsyncStorage.getItem(CATALOG_MOBILE_COLUMNS_KEY);
|
||||
if (pref === '2') setMobileColumns(2);
|
||||
else if (pref === '3') setMobileColumns(3);
|
||||
else setMobileColumns('auto');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load catalog settings:', error);
|
||||
} finally {
|
||||
|
|
@ -470,6 +509,62 @@ const CatalogSettingsScreen = () => {
|
|||
<Text style={styles.headerTitle}>Catalogs</Text>
|
||||
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
|
||||
{/* Layout (Mobile only) */}
|
||||
{Platform.OS && (
|
||||
<View style={styles.addonSection}>
|
||||
<Text style={styles.addonTitle}>LAYOUT CATALOGSCREEN (PHONE)</Text>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.groupHeader}>
|
||||
<Text style={styles.groupTitle}>Posters per row</Text>
|
||||
<View style={styles.groupHeaderRight} />
|
||||
</View>
|
||||
{/* Only show on phones (approx width < 600) */}
|
||||
<View style={styles.optionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.optionChip, mobileColumns === 'auto' && styles.optionChipSelected]}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await AsyncStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, 'auto');
|
||||
setMobileColumns('auto');
|
||||
} catch {}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.optionChipText, mobileColumns === 'auto' && styles.optionChipTextSelected]}>Auto</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.optionChip, mobileColumns === 2 && styles.optionChipSelected]}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await AsyncStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '2');
|
||||
setMobileColumns(2);
|
||||
} catch {}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.optionChipText, mobileColumns === 2 && styles.optionChipTextSelected]}>2</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.optionChip, mobileColumns === 3 && styles.optionChipSelected]}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await AsyncStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '3');
|
||||
setMobileColumns(3);
|
||||
} catch {}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.optionChipText, mobileColumns === 3 && styles.optionChipTextSelected]}>3</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.hintRow}>
|
||||
<MaterialIcons name="info-outline" size={14} color={colors.mediumGray} />
|
||||
<Text style={styles.hintText}>Applies to phones only. Tablets keep adaptive layout.</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{Object.entries(groupedSettings).map(([addonId, group]) => (
|
||||
<View key={addonId} style={styles.addonSection}>
|
||||
<Text style={styles.addonTitle}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue