feat: add navigation with tab bar and screen layouts

Expo Router file-based navigation with glass tab bar,
3 tabs (Créations, Explorer, Profil), Phosphor icons,
modal picker, and fade transitions.
This commit is contained in:
Mathis Pruvot
2026-05-28 11:49:39 +00:00
parent 5cddb440e6
commit fbf8094281
5 changed files with 626 additions and 0 deletions

242
app/(tabs)/profile.tsx Normal file
View File

@@ -0,0 +1,242 @@
import { ScrollView, StyleSheet, Switch, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, { FadeInDown } from "react-native-reanimated";
import { GlassCard } from "@/components/glass";
import { useSettingsStore } from "@/stores/settings.store";
import { useWallpaperStore } from "@/stores/wallpaper.store";
import { colors, typography, spacing, layout } from "@/theme";
export default function ProfileScreen() {
const insets = useSafeAreaInsets();
const settings = useSettingsStore();
const wallpaperCount = useWallpaperStore((s) => s.savedWallpapers.length);
return (
<ScrollView
style={[styles.container, { paddingTop: insets.top + spacing.md }]}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
<Animated.View entering={FadeInDown.duration(500)}>
<GlassCard variant="subtle" radius="lg" padding={16}>
<Text style={typography.title}>Profil</Text>
<Text style={[typography.caption, { marginTop: 2 }]}>
Préférences & réglages
</Text>
</GlassCard>
</Animated.View>
{/* Stats */}
<Animated.View entering={FadeInDown.delay(100).duration(400)} style={styles.section}>
<GlassCard variant="medium" radius="lg">
<Text style={typography.label}>Statistiques</Text>
<View style={styles.statsRow}>
<View style={styles.stat}>
<Text style={typography.title}>{wallpaperCount}</Text>
<Text style={typography.caption}>Créations</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.stat}>
<Text style={typography.title}>5</Text>
<Text style={typography.caption}>Animations</Text>
</View>
</View>
</GlassCard>
</Animated.View>
{/* Settings */}
<Animated.View entering={FadeInDown.delay(200).duration(400)} style={styles.section}>
<GlassCard variant="medium" radius="lg">
<Text style={[typography.label, { marginBottom: 16 }]}>Réglages</Text>
<SettingRow
label="Retour haptique"
description="Vibrations au toucher"
value={settings.hapticFeedback}
onToggle={settings.setHapticFeedback}
/>
<SettingRow
label="Depth map auto"
description="Générer automatiquement (V1.5)"
value={settings.autoDepthMap}
onToggle={settings.setAutoDepthMap}
/>
</GlassCard>
</Animated.View>
{/* Export Quality */}
<Animated.View entering={FadeInDown.delay(300).duration(400)} style={styles.section}>
<GlassCard variant="medium" radius="lg">
<Text style={[typography.label, { marginBottom: 16 }]}>
Qualité d'export
</Text>
{(["low", "medium", "high"] as const).map((q) => (
<QualityOption
key={q}
label={q === "low" ? "Économique" : q === "medium" ? "Standard" : "Maximum"}
description={
q === "low"
? "15 FPS, compression forte"
: q === "medium"
? "24 FPS, bon compromis"
: "30 FPS, qualité maximale"
}
selected={settings.exportQuality === q}
onPress={() => settings.setExportQuality(q)}
/>
))}
</GlassCard>
</Animated.View>
{/* About */}
<Animated.View entering={FadeInDown.delay(400).duration(400)} style={styles.section}>
<GlassCard variant="subtle" radius="lg">
<Text style={typography.label}>À propos</Text>
<Text style={[typography.bodySecondary, { marginTop: 8 }]}>
Lively v1.0.0
</Text>
<Text style={[typography.caption, { marginTop: 4 }]}>
Fonds d'écran animés depuis votre galerie
</Text>
</GlassCard>
</Animated.View>
<View style={{ height: layout.tabBarHeight + 40 }} />
</ScrollView>
);
}
function SettingRow({
label,
description,
value,
onToggle,
}: {
label: string;
description: string;
value: boolean;
onToggle: (v: boolean) => void;
}) {
return (
<View style={styles.settingRow}>
<View style={styles.settingText}>
<Text style={typography.body}>{label}</Text>
<Text style={typography.caption}>{description}</Text>
</View>
<Switch
value={value}
onValueChange={onToggle}
trackColor={{
false: "rgba(255,255,255,0.1)",
true: "rgba(255,255,255,0.3)",
}}
thumbColor={value ? colors.text.primary : colors.text.tertiary}
/>
</View>
);
}
function QualityOption({
label,
description,
selected,
onPress,
}: {
label: string;
description: string;
selected: boolean;
onPress: () => void;
}) {
return (
<View
style={[styles.qualityRow, selected && styles.qualitySelected]}
onTouchEnd={onPress}
>
<View style={styles.settingText}>
<Text style={[typography.body, selected && { color: colors.text.primary }]}>
{label}
</Text>
<Text style={typography.caption}>{description}</Text>
</View>
<View
style={[
styles.radio,
selected && styles.radioSelected,
]}
>
{selected && <View style={styles.radioDot} />}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
paddingHorizontal: layout.screenPadding,
},
content: {
paddingBottom: 40,
},
section: {
marginTop: spacing.md,
},
statsRow: {
flexDirection: "row",
marginTop: 16,
alignItems: "center",
},
stat: {
flex: 1,
alignItems: "center",
},
statDivider: {
width: 1,
height: 40,
backgroundColor: colors.glass.border,
},
settingRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.glass.highlight,
},
settingText: {
flex: 1,
marginRight: 16,
},
qualityRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 12,
borderRadius: 12,
marginBottom: 4,
},
qualitySelected: {
backgroundColor: "rgba(255, 255, 255, 0.06)",
},
radio: {
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 1.5,
borderColor: colors.text.tertiary,
alignItems: "center",
justifyContent: "center",
},
radioSelected: {
borderColor: colors.text.primary,
},
radioDot: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: colors.text.primary,
},
});