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:
44
app/(tabs)/_layout.tsx
Normal file
44
app/(tabs)/_layout.tsx
Normal file
@@ -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 (
|
||||||
|
<Tabs
|
||||||
|
tabBar={(props) => <GlassTabBar {...props} />}
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
tabBarStyle: { display: "none" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: "Créations",
|
||||||
|
tabBarIcon: ({ color, size }: { color: ColorValue; size: number }) => (
|
||||||
|
<House size={size} color={color as string} weight="light" />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="explore"
|
||||||
|
options={{
|
||||||
|
title: "Explorer",
|
||||||
|
tabBarIcon: ({ color, size }: { color: ColorValue; size: number }) => (
|
||||||
|
<Compass size={size} color={color as string} weight="light" />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="profile"
|
||||||
|
options={{
|
||||||
|
title: "Profil",
|
||||||
|
tabBarIcon: ({ color, size }: { color: ColorValue; size: number }) => (
|
||||||
|
<UserCircle size={size} color={color as string} weight="light" />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
app/(tabs)/explore.tsx
Normal file
60
app/(tabs)/explore.tsx
Normal file
@@ -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 (
|
||||||
|
<View style={[styles.container, { paddingTop: insets.top + spacing.md }]}>
|
||||||
|
<Animated.View entering={FadeInDown.duration(500)}>
|
||||||
|
<GlassCard variant="subtle" radius="lg" padding={16}>
|
||||||
|
<Text style={typography.title}>Explorer</Text>
|
||||||
|
<Text style={[typography.caption, { marginTop: 2 }]}>
|
||||||
|
Découvrez la communauté
|
||||||
|
</Text>
|
||||||
|
</GlassCard>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.delay(200).duration(600)}
|
||||||
|
style={styles.placeholder}
|
||||||
|
>
|
||||||
|
<GlassCard variant="medium" radius="xl" padding={32}>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<Text style={[typography.heading, { textAlign: "center" }]}>
|
||||||
|
Bientôt disponible
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.bodySecondary,
|
||||||
|
{ textAlign: "center", marginTop: 8 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Partagez vos créations et découvrez{"\n"}
|
||||||
|
celles de la communauté — V1.5
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</GlassCard>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
paddingHorizontal: layout.screenPadding,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
232
app/(tabs)/index.tsx
Normal file
232
app/(tabs)/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View style={[styles.container, { paddingTop: insets.top + spacing.md }]}>
|
||||||
|
{/* Header */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(500)}
|
||||||
|
style={styles.header}
|
||||||
|
>
|
||||||
|
<GlassCard variant="subtle" radius="lg" padding={16}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<View>
|
||||||
|
<Text style={typography.title}>Lively</Text>
|
||||||
|
<Text style={[typography.caption, { marginTop: 2 }]}>
|
||||||
|
Vos fonds d'écran animés
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</GlassCard>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{savedWallpapers.length === 0 ? (
|
||||||
|
<EmptyState onPress={handleNewWallpaper} />
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={savedWallpapers}
|
||||||
|
numColumns={2}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
contentContainerStyle={styles.grid}
|
||||||
|
columnWrapperStyle={styles.row}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
ListHeaderComponent={
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={typography.label}>Récentes</Text>
|
||||||
|
<Text style={[typography.caption, { color: colors.text.secondary }]}>
|
||||||
|
{savedWallpapers.length} création{savedWallpapers.length > 1 ? "s" : ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
renderItem={({ item, index }) => (
|
||||||
|
<WallpaperCard
|
||||||
|
config={item}
|
||||||
|
index={index}
|
||||||
|
onPress={() => handleOpenWallpaper(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Floating CTA */}
|
||||||
|
{savedWallpapers.length > 0 && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.delay(300).duration(400)}
|
||||||
|
style={[styles.fabContainer, { bottom: layout.tabBarHeight + insets.bottom + 16 }]}
|
||||||
|
>
|
||||||
|
<GlassButton
|
||||||
|
label="Nouveau"
|
||||||
|
onPress={handleNewWallpaper}
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
pulse
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyState({ onPress }: { onPress: () => void }) {
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.delay(200).duration(600)}
|
||||||
|
style={styles.emptyContainer}
|
||||||
|
>
|
||||||
|
<GlassCard variant="medium" radius="xl" padding={32}>
|
||||||
|
<View style={styles.emptyContent}>
|
||||||
|
<Text style={[typography.heading, { textAlign: "center" }]}>
|
||||||
|
Donnez vie à vos photos
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.bodySecondary,
|
||||||
|
{ textAlign: "center", marginTop: 8, marginBottom: 24 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Choisissez une image et appliquez{"\n"}une animation en quelques taps
|
||||||
|
</Text>
|
||||||
|
<GlassButton
|
||||||
|
label="Choisir une photo"
|
||||||
|
onPress={onPress}
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
pulse
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</GlassCard>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WallpaperCard({
|
||||||
|
config,
|
||||||
|
index,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
config: WallpaperConfig;
|
||||||
|
index: number;
|
||||||
|
onPress: () => void;
|
||||||
|
}) {
|
||||||
|
const meta = ANIMATION_META[config.animation];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View entering={FadeInDown.delay(index * 80).duration(400)}>
|
||||||
|
<Pressable onPress={onPress}>
|
||||||
|
<GlassCard variant="subtle" radius="lg" noPadding>
|
||||||
|
<Image
|
||||||
|
source={{ uri: config.sourceUri }}
|
||||||
|
style={styles.cardImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
<View style={styles.cardOverlay}>
|
||||||
|
<Text style={styles.cardLabel}>{meta.label}</Text>
|
||||||
|
</View>
|
||||||
|
</GlassCard>
|
||||||
|
</Pressable>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
242
app/(tabs)/profile.tsx
Normal file
242
app/(tabs)/profile.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
48
app/_layout.tsx
Normal file
48
app/_layout.tsx
Normal file
@@ -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 (
|
||||||
|
<GestureHandlerRootView style={styles.root}>
|
||||||
|
<StatusBar style="light" />
|
||||||
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
contentStyle: { backgroundColor: colors.background },
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen name="(tabs)" />
|
||||||
|
<Stack.Screen
|
||||||
|
name="picker"
|
||||||
|
options={{ animation: "slide_from_bottom", presentation: "modal" }}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="editor/[id]"
|
||||||
|
options={{ animation: "fade" }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user