From fbf80942819eb0a5978dc235b482278ec069b378 Mon Sep 17 00:00:00 2001 From: Mathis Pruvot Date: Thu, 28 May 2026 11:49:39 +0000 Subject: [PATCH] feat: add navigation with tab bar and screen layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expo Router file-based navigation with glass tab bar, 3 tabs (Créations, Explorer, Profil), Phosphor icons, modal picker, and fade transitions. --- app/(tabs)/_layout.tsx | 44 ++++++++ app/(tabs)/explore.tsx | 60 ++++++++++ app/(tabs)/index.tsx | 232 +++++++++++++++++++++++++++++++++++++++ app/(tabs)/profile.tsx | 242 +++++++++++++++++++++++++++++++++++++++++ app/_layout.tsx | 48 ++++++++ 5 files changed, 626 insertions(+) create mode 100644 app/(tabs)/_layout.tsx create mode 100644 app/(tabs)/explore.tsx create mode 100644 app/(tabs)/index.tsx create mode 100644 app/(tabs)/profile.tsx create mode 100644 app/_layout.tsx diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..00da53f --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,44 @@ +import { Tabs } from "expo-router"; +import { GlassTabBar } from "@/components/glass"; +import { House, Compass, UserCircle } from "phosphor-react-native"; +import type { ColorValue } from "react-native"; + +export default function TabLayout() { + return ( + } + screenOptions={{ + headerShown: false, + tabBarStyle: { display: "none" }, + }} + > + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx new file mode 100644 index 0000000..612fec1 --- /dev/null +++ b/app/(tabs)/explore.tsx @@ -0,0 +1,60 @@ +import { StyleSheet, 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 { colors, typography, spacing, layout } from "@/theme"; + +export default function ExploreScreen() { + const insets = useSafeAreaInsets(); + + return ( + + + + Explorer + + Découvrez la communauté + + + + + + + + + Bientôt disponible + + + Partagez vos créations et découvrez{"\n"} + celles de la communauté — V1.5 + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + paddingHorizontal: layout.screenPadding, + }, + placeholder: { + flex: 1, + justifyContent: "center", + paddingHorizontal: spacing.md, + }, + content: { + alignItems: "center", + }, +}); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..502cff4 --- /dev/null +++ b/app/(tabs)/index.tsx @@ -0,0 +1,232 @@ +import { useCallback } from "react"; +import { + Dimensions, + FlatList, + Image, + Pressable, + StyleSheet, + Text, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useRouter } from "expo-router"; +import Animated, { FadeInDown } from "react-native-reanimated"; +import { Plus, ImageSquare } from "phosphor-react-native"; +import { GlassCard, GlassButton } from "@/components/glass"; +import { useWallpaperStore } from "@/stores/wallpaper.store"; +import { colors, typography, spacing, layout } from "@/theme"; +import { ANIMATION_META, type WallpaperConfig } from "@/types/wallpaper"; + +const { width: SCREEN_WIDTH } = Dimensions.get("window"); +const CARD_SIZE = (SCREEN_WIDTH - layout.screenPadding * 2 - spacing.md) / 2; + +export default function HomeScreen() { + const insets = useSafeAreaInsets(); + const router = useRouter(); + const savedWallpapers = useWallpaperStore((s) => s.savedWallpapers); + + const handleNewWallpaper = useCallback(() => { + router.push("/picker"); + }, [router]); + + const handleOpenWallpaper = useCallback( + (config: WallpaperConfig) => { + const store = useWallpaperStore.getState(); + store.setSourceImage(config.sourceUri); + store.setAnimation(config.animation); + Object.entries(config.uniforms).forEach(([key, value]) => { + store.setUniform(key, value); + }); + router.push(`/editor/${config.id}`); + }, + [router] + ); + + return ( + + {/* Header */} + + + + + Lively + + Vos fonds d'écran animés + + + + + + + {/* Content */} + {savedWallpapers.length === 0 ? ( + + ) : ( + item.id} + contentContainerStyle={styles.grid} + columnWrapperStyle={styles.row} + showsVerticalScrollIndicator={false} + ListHeaderComponent={ + + Récentes + + {savedWallpapers.length} création{savedWallpapers.length > 1 ? "s" : ""} + + + } + renderItem={({ item, index }) => ( + handleOpenWallpaper(item)} + /> + )} + /> + )} + + {/* Floating CTA */} + {savedWallpapers.length > 0 && ( + + + + )} + + ); +} + +function EmptyState({ onPress }: { onPress: () => void }) { + return ( + + + + + Donnez vie à vos photos + + + Choisissez une image et appliquez{"\n"}une animation en quelques taps + + + + + + ); +} + +function WallpaperCard({ + config, + index, + onPress, +}: { + config: WallpaperConfig; + index: number; + onPress: () => void; +}) { + const meta = ANIMATION_META[config.animation]; + + return ( + + + + + + {meta.label} + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + paddingHorizontal: layout.screenPadding, + }, + header: { + marginBottom: spacing.lg, + }, + headerRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + sectionHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: spacing.md, + }, + grid: { + paddingBottom: 160, + }, + row: { + gap: spacing.md, + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + paddingHorizontal: spacing.md, + }, + emptyContent: { + alignItems: "center", + }, + cardImage: { + width: CARD_SIZE, + height: CARD_SIZE * 1.4, + borderRadius: 24, + }, + cardOverlay: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + padding: 12, + borderBottomLeftRadius: 24, + borderBottomRightRadius: 24, + backgroundColor: "rgba(0, 0, 0, 0.4)", + }, + cardLabel: { + ...typography.caption, + color: colors.text.primary, + fontWeight: "600", + }, + fabContainer: { + position: "absolute", + left: layout.screenPadding, + right: layout.screenPadding, + }, +}); diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx new file mode 100644 index 0000000..ea18f1b --- /dev/null +++ b/app/(tabs)/profile.tsx @@ -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 ( + + + + Profil + + Préférences & réglages + + + + + {/* Stats */} + + + Statistiques + + + {wallpaperCount} + Créations + + + + 5 + Animations + + + + + + {/* Settings */} + + + Réglages + + + + + + + + {/* Export Quality */} + + + + Qualité d'export + + {(["low", "medium", "high"] as const).map((q) => ( + settings.setExportQuality(q)} + /> + ))} + + + + {/* About */} + + + À propos + + Lively v1.0.0 + + + Fonds d'écran animés depuis votre galerie + + + + + + + ); +} + +function SettingRow({ + label, + description, + value, + onToggle, +}: { + label: string; + description: string; + value: boolean; + onToggle: (v: boolean) => void; +}) { + return ( + + + {label} + {description} + + + + ); +} + +function QualityOption({ + label, + description, + selected, + onPress, +}: { + label: string; + description: string; + selected: boolean; + onPress: () => void; +}) { + return ( + + + + {label} + + {description} + + + {selected && } + + + ); +} + +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, + }, +}); diff --git a/app/_layout.tsx b/app/_layout.tsx new file mode 100644 index 0000000..2cad593 --- /dev/null +++ b/app/_layout.tsx @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import { StatusBar } from "expo-status-bar"; +import { Stack } from "expo-router"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { StyleSheet } from "react-native"; +import { useWallpaperStore } from "@/stores/wallpaper.store"; +import { useSettingsStore } from "@/stores/settings.store"; +import { colors } from "@/theme"; + +export default function RootLayout() { + const loadWallpapers = useWallpaperStore((s) => s.loadSavedWallpapers); + const loadSettings = useSettingsStore((s) => s.loadSettings); + + useEffect(() => { + loadWallpapers(); + loadSettings(); + }, []); + + return ( + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: colors.background, + }, +});