diff --git a/src/hooks/useAnimation.ts b/src/hooks/useAnimation.ts new file mode 100644 index 0000000..22ffd94 --- /dev/null +++ b/src/hooks/useAnimation.ts @@ -0,0 +1,50 @@ +import { useCallback, useMemo } from "react"; +import { + useSharedValue, + useFrameCallback, + SharedValue, +} from "react-native-reanimated"; +import type { SkRuntimeEffect } from "@shopify/react-native-skia"; +import { shaders } from "@/shaders"; +import type { AnimationType, AnimationUniforms } from "@/types/wallpaper"; + +interface AnimationState { + time: SharedValue; + isPlaying: SharedValue; + play: () => void; + pause: () => void; + reset: () => void; + shader: SkRuntimeEffect | null; +} + +export function useAnimation( + animationType: AnimationType, + uniforms: AnimationUniforms +): AnimationState { + const time = useSharedValue(0); + const isPlaying = useSharedValue(true); + const startTime = useSharedValue(0); + + useFrameCallback((frameInfo) => { + if (isPlaying.value) { + time.value = (frameInfo.timeSinceFirstFrame ?? 0) / 1000; + } + }); + + const play = useCallback(() => { + isPlaying.value = true; + }, []); + + const pause = useCallback(() => { + isPlaying.value = false; + }, []); + + const reset = useCallback(() => { + time.value = 0; + startTime.value = 0; + }, []); + + const shader = useMemo(() => shaders[animationType], [animationType]); + + return { time, isPlaying, play, pause, reset, shader }; +} diff --git a/src/hooks/useGyroscope.ts b/src/hooks/useGyroscope.ts new file mode 100644 index 0000000..3a52edb --- /dev/null +++ b/src/hooks/useGyroscope.ts @@ -0,0 +1,43 @@ +import { useEffect } from "react"; +import { useSharedValue, withSpring } from "react-native-reanimated"; +import { Gyroscope } from "expo-sensors"; + +interface GyroscopeData { + x: ReturnType>; + y: ReturnType>; + available: boolean; +} + +export function useGyroscope(enabled: boolean = true): GyroscopeData { + const x = useSharedValue(0); + const y = useSharedValue(0); + + useEffect(() => { + if (!enabled) return; + + let available = true; + Gyroscope.isAvailableAsync().then((result) => { + available = result; + }); + + Gyroscope.setUpdateInterval(50); // 20Hz — balanced power/responsiveness + + const subscription = Gyroscope.addListener((data) => { + // Accumulate rotation with damping + x.value = withSpring(x.value + data.x * 0.05, { + damping: 20, + stiffness: 100, + }); + y.value = withSpring(y.value + data.y * 0.05, { + damping: 20, + stiffness: 100, + }); + }); + + return () => { + subscription.remove(); + }; + }, [enabled]); + + return { x, y, available: true }; +} diff --git a/src/hooks/useWallpaperExport.ts b/src/hooks/useWallpaperExport.ts new file mode 100644 index 0000000..f480110 --- /dev/null +++ b/src/hooks/useWallpaperExport.ts @@ -0,0 +1,147 @@ +import { useState, useCallback } from "react"; +import { Platform, Alert } from "react-native"; +import type { WallpaperConfig } from "@/types/wallpaper"; + +type ExportStatus = "idle" | "preparing" | "exporting" | "success" | "error"; + +interface ExportResult { + status: ExportStatus; + progress: number; + error: string | null; + exportAsWallpaper: (config: WallpaperConfig) => Promise; +} + +export function useWallpaperExport(): ExportResult { + const [status, setStatus] = useState("idle"); + const [progress, setProgress] = useState(0); + const [error, setError] = useState(null); + + const exportAsWallpaper = useCallback(async (config: WallpaperConfig) => { + try { + setStatus("preparing"); + setProgress(0); + setError(null); + + if (Platform.OS === "android") { + await exportAndroid(config, setProgress, setStatus); + } else if (Platform.OS === "ios") { + await exportIOS(config, setProgress, setStatus); + } else { + throw new Error("Platform non supportée. Utilisez Android ou iOS."); + } + + setStatus("success"); + setProgress(1); + } catch (err) { + const message = err instanceof Error ? err.message : "Échec de l'export"; + setError(message); + setStatus("error"); + Alert.alert("Erreur d'export", message); + } + }, []); + + return { status, progress, error, exportAsWallpaper }; +} + +async function exportAndroid( + config: WallpaperConfig, + onProgress: (p: number) => void, + onStatus: (s: ExportStatus) => void +) { + // Lazy-load native module (only available on Android device builds) + let WallpaperAndroid: typeof import("../../modules/wallpaper-android/src/WallpaperAndroidModule").WallpaperAndroid; + try { + WallpaperAndroid = + require("../../modules/wallpaper-android/src/WallpaperAndroidModule").WallpaperAndroid; + } catch { + Alert.alert( + "Module non disponible", + "Le module natif Android n'est pas disponible dans Expo Go. Créez un development build avec `npx expo run:android`.", + ); + return; + } + + // Check support + const supported = WallpaperAndroid.isLiveWallpaperSupported(); + if (!supported) { + throw new Error("Les live wallpapers ne sont pas supportés sur cet appareil."); + } + + onProgress(0.2); + onStatus("exporting"); + + // Save config to SharedPreferences (read by LiveWallpaperService) + const configJson = JSON.stringify({ + sourceUri: config.sourceUri, + depthMapUri: config.depthMapUri ?? "", + animation: config.animation, + uniforms: config.uniforms, + fps: config.fps, + }); + + await WallpaperAndroid.saveConfig(configJson); + onProgress(0.6); + + // Launch system wallpaper picker + await WallpaperAndroid.setLiveWallpaper(); + onProgress(1.0); +} + +async function exportIOS( + config: WallpaperConfig, + onProgress: (p: number) => void, + onStatus: (s: ExportStatus) => void +) { + let LivePhoto: typeof import("../../modules/livephoto-ios/src/LivePhotoModule").LivePhoto; + try { + LivePhoto = + require("../../modules/livephoto-ios/src/LivePhotoModule").LivePhoto; + } catch { + Alert.alert( + "Module non disponible", + "Le module natif iOS n'est pas disponible dans Expo Go. Créez un development build avec `npx expo run:ios`.", + ); + return; + } + + // Check/request photo library permission + let permission = await LivePhoto.checkPermission(); + if (permission === "undetermined") { + permission = await LivePhoto.requestPermission(); + } + if (permission !== "granted") { + throw new Error( + "Accès à la photothèque refusé. Activez-le dans Réglages > Lively > Photos." + ); + } + + onProgress(0.1); + onStatus("exporting"); + + const configJson = JSON.stringify({ + sourceUri: config.sourceUri, + depthMapUri: config.depthMapUri ?? "", + animation: config.animation, + uniforms: config.uniforms, + fps: config.fps, + }); + + onProgress(0.3); + + // Generate Live Photo (HEIC + MOV) and save to photo library + await LivePhoto.exportLivePhoto(configJson); + + onProgress(0.9); + + // Guide user to apply the wallpaper + Alert.alert( + "Live Photo sauvegardée", + "Pour l'appliquer comme fond d'écran :\n\n" + + "Réglages > Fond d'écran > Ajouter\n" + + "> Photos > Sélectionnez votre Live Photo\n" + + "> Activez « Live Photo »", + [{ text: "OK" }] + ); + + onProgress(1.0); +}