From 0e1716738e1e18def84cc3948adc614d4174c0ef Mon Sep 17 00:00:00 2001 From: Mathis Pruvot Date: Thu, 28 May 2026 11:49:52 +0000 Subject: [PATCH] feat: add wallpaper editor with live shader preview Full-screen Skia canvas with real-time shader preview, glass bottom sheet controls (animation selector, intensity/ speed/direction sliders, particle preset picker), save and export actions with status feedback. --- app/editor/[id].tsx | 308 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 app/editor/[id].tsx diff --git a/app/editor/[id].tsx b/app/editor/[id].tsx new file mode 100644 index 0000000..9de7175 --- /dev/null +++ b/app/editor/[id].tsx @@ -0,0 +1,308 @@ +import { useCallback, useMemo } from "react"; +import { + ActivityIndicator, + Dimensions, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useRouter, useLocalSearchParams } from "expo-router"; +import { + Canvas, + Image as SkImage, + Shader, + Fill, + useImage, +} from "@shopify/react-native-skia"; +import Animated, { FadeIn, FadeInUp } from "react-native-reanimated"; +import { ArrowLeft, Check, Export } from "phosphor-react-native"; +import { GlassBottomSheet, GlassButton, GlassCard, GlassPill, GlassSlider } from "@/components/glass"; +import { useWallpaperStore } from "@/stores/wallpaper.store"; +import { useAnimation } from "@/hooks/useAnimation"; +import { useGyroscope } from "@/hooks/useGyroscope"; +import { useWallpaperExport } from "@/hooks/useWallpaperExport"; +import { shaders } from "@/shaders"; +import { + ANIMATION_META, + type AnimationType, +} from "@/types/wallpaper"; +import { colors, typography, spacing, layout } from "@/theme"; + +const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window"); + +const AVAILABLE_ANIMATIONS: AnimationType[] = [ + "ken-burns", + "color-shift", + "particles", + "vignette-pulse", + "glitch", +]; + +export default function EditorScreen() { + const insets = useSafeAreaInsets(); + const router = useRouter(); + const { id } = useLocalSearchParams<{ id: string }>(); + + const { + sourceUri, + animation, + uniforms, + particlePreset, + setAnimation, + setUniform, + setParticlePreset, + saveWallpaper, + } = useWallpaperStore(); + + const { time } = useAnimation(animation, uniforms); + const gyro = useGyroscope(animation === "depth-parallax"); + const { status, exportAsWallpaper } = useWallpaperExport(); + + const image = useImage(sourceUri); + const shader = useMemo(() => shaders[animation], [animation]); + + const handleApply = useCallback(async () => { + const config = saveWallpaper(); + if (config) { + await exportAsWallpaper(config); + } + }, [saveWallpaper, exportAsWallpaper]); + + const handleSaveOnly = useCallback(() => { + saveWallpaper(); + router.back(); + }, [saveWallpaper, router]); + + // No image selected — redirect back + if (!sourceUri) { + return ( + + + + Aucune image sélectionnée + + + Retournez à la galerie pour choisir une photo + + router.back()} + variant="primary" + size="md" + /> + + + ); + } + + // Image loading + if (!image) { + return ( + + + + Chargement de l'image... + + + ); + } + + return ( + + {/* Full-screen animated preview */} + + {shader && ( + + + + + + )} + + + {/* Top bar overlay */} + + router.back()} style={styles.iconButton}> + + + + + + + + + {/* Bottom sheet with controls */} + + + {/* Animation selector */} + Animation + + {AVAILABLE_ANIMATIONS.map((type) => ( + setAnimation(type)} + /> + ))} + + + {/* Particle preset selector (only for particles animation) */} + {animation === "particles" && ( + <> + Style + + {(["snow", "rain", "bokeh"] as const).map((preset) => ( + setParticlePreset(preset)} + /> + ))} + + + )} + + {/* Uniform controls */} + + setUniform("intensity", v)} + /> + setUniform("speed", v)} + /> + {(animation === "ken-burns" || animation === "glitch") && ( + setUniform("direction", v)} + formatValue={(v) => `${Math.round((v * 180) / Math.PI)}°`} + /> + )} + + + {/* Export button */} + + + ) : undefined + } + /> + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + centered: { + justifyContent: "center", + alignItems: "center", + paddingHorizontal: layout.screenPadding, + }, + canvas: { + position: "absolute", + top: 0, + left: 0, + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + topBar: { + position: "absolute", + left: 16, + right: 16, + flexDirection: "row", + justifyContent: "space-between", + zIndex: 10, + }, + iconButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "rgba(0, 0, 0, 0.4)", + borderWidth: 1, + borderColor: colors.glass.border, + alignItems: "center", + justifyContent: "center", + }, + animationScroll: { + marginBottom: spacing.lg, + }, + controls: { + marginBottom: spacing.lg, + }, + ctaContainer: { + marginTop: spacing.sm, + }, +});