diff --git a/app.json b/app.json index ddaf78c..5ffd94d 100644 --- a/app.json +++ b/app.json @@ -11,7 +11,7 @@ "splash": { "image": "./assets/images/splash.png", "resizeMode": "contain", - "backgroundColor": "#ffffff" + "backgroundColor": "#000000" }, "assetBundlePatterns": [ "**/*" @@ -56,7 +56,9 @@ } } ], - "expo-secure-store" + "expo-secure-store", + "expo-av", + "expo-splash-screen" ], "experiments": { "typedRoutes": true @@ -77,4 +79,4 @@ }, "owner": "promotus" } -} +} \ No newline at end of file diff --git a/app/(packs)/import.tsx b/app/(packs)/import.tsx index 08cb1ca..e25acb3 100644 --- a/app/(packs)/import.tsx +++ b/app/(packs)/import.tsx @@ -1,24 +1,27 @@ // app/(packs)/import.tsx import React, { useState, useEffect } from 'react'; -import { View, ScrollView, StyleSheet, ActivityIndicator, Platform } from 'react-native'; -import { router, Stack } from 'expo-router'; +import { View, ScrollView, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native'; +import { router, Stack, useLocalSearchParams } from 'expo-router'; import { useNDK } from '@/lib/hooks/useNDK'; import { Text } from '@/components/ui/text'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; -import { nip19 } from 'nostr-tools'; // Fix import from nostr-tools +import { nip19 } from 'nostr-tools'; import { findTagValue } from '@/utils/nostr-utils'; -import POWRPackService from '@/lib/db/services/POWRPackService'; -import { usePOWRPackService } from '@/components/DatabaseProvider'; // Use the proper hook +import { usePOWRPackService } from '@/components/DatabaseProvider'; import { POWRPackImport, POWRPackSelection } from '@/types/powr-pack'; -import { InfoIcon } from 'lucide-react-native'; +import { InfoIcon, X, CheckCircle2 } from 'lucide-react-native'; +import { useIconColor } from '@/lib/theme/iconUtils'; +import { useColorScheme } from '@/lib/theme/useColorScheme'; +import { COLORS } from '@/lib/theme/colors'; export default function ImportPOWRPackScreen() { const { ndk } = useNDK(); const powrPackService = usePOWRPackService(); - const [naddrInput, setNaddrInput] = useState(''); + const params = useLocalSearchParams<{ naddr?: string }>(); + const [naddrInput, setNaddrInput] = useState(params.naddr || ''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [packData, setPackData] = useState(null); @@ -27,6 +30,25 @@ export default function ImportPOWRPackScreen() { const [dependencies, setDependencies] = useState>({}); const [isImporting, setIsImporting] = useState(false); const [importSuccess, setImportSuccess] = useState(false); + const { getIconProps } = useIconColor(); + const { isDarkColorScheme } = useColorScheme(); + + // Auto-fetch pack when naddr is provided via params + useEffect(() => { + if (params.naddr && !isLoading && !packData) { + setIsLoading(true); + handleFetchPack() + .catch(err => { + console.error("Auto-fetch error:", err); + setIsLoading(false); + }); + } + }, [params.naddr]); + + // Handle close button press + const handleClose = () => { + router.back(); + }; // Handle fetch button click const handleFetchPack = async () => { @@ -184,6 +206,12 @@ export default function ImportPOWRPackScreen() { options={{ title: 'Import POWR Pack', headerShown: true, + // Add a close button for iOS + headerRight: () => ( + + + + ), }} /> @@ -192,10 +220,10 @@ export default function ImportPOWRPackScreen() { keyboardShouldPersistTaps="handled" > {/* Input section */} - + - Enter POWR Pack Address + Enter POWR Pack Address Paste a POWR Pack naddr to import @@ -212,35 +240,68 @@ export default function ImportPOWRPackScreen() { placeholder="naddr1..." value={naddrInput} onChangeText={setNaddrInput} - style={styles.input} + style={[styles.input, { height: 80 }]} + multiline={true} + numberOfLines={3} + textAlignVertical="top" + className="border-input" /> - + {packData ? ( + // Success indicator when pack is loaded + + + + POWR Pack loaded successfully! + + + ) : ( + // Fetch button when no pack is loaded + + )} {/* Error message */} {error && ( - - {error} + + {error} )} {/* Success message */} {importSuccess && ( - - Pack successfully imported! + + + Pack successfully imported! )} @@ -250,7 +311,7 @@ export default function ImportPOWRPackScreen() { - {getPackTitle()} + {getPackTitle()} {getPackDescription() ? ( @@ -259,7 +320,7 @@ export default function ImportPOWRPackScreen() { ) : null} - Select items to import: + Select items to import: @@ -268,7 +329,7 @@ export default function ImportPOWRPackScreen() { - Workout Templates + Workout Templates {packData.templates.length} templates available @@ -285,10 +346,14 @@ export default function ImportPOWRPackScreen() { handleTemplateChange(template.id, checked === true) } id={`template-${template.id}`} + className="border-input" /> - - handleTemplateChange(template.id, !selectedTemplates.includes(template.id)) - }> + + handleTemplateChange(template.id, !selectedTemplates.includes(template.id)) + } + > {title} @@ -309,7 +374,7 @@ export default function ImportPOWRPackScreen() { - Exercises + Exercises {packData.exercises.length} exercises available @@ -329,9 +394,10 @@ export default function ImportPOWRPackScreen() { } disabled={isRequired} id={`exercise-${exercise.id}`} + className="border-input" /> { if (!isRequired) { handleExerciseChange(exercise.id, !selectedExercises.includes(exercise.id)) @@ -341,9 +407,11 @@ export default function ImportPOWRPackScreen() { {title} {isRequired && ( - - - Required + + + Required )} @@ -363,12 +431,13 @@ export default function ImportPOWRPackScreen() { {/* No packs message */} {!isLoading && packs.length === 0 && ( - - No POWR Packs Imported - + + No POWR Packs Imported + Import workout packs shared by the community to get started. @@ -120,7 +136,7 @@ export default function ManagePOWRPacksScreen() { - {pack.title} + {pack.title} {pack.description && ( @@ -132,18 +148,18 @@ export default function ManagePOWRPacksScreen() { onPress={() => handleDeleteClick(pack.id)} style={styles.deleteButton} > - + - + {templates.length} template{templates.length !== 1 ? 's' : ''} • {exercises.length} exercise{exercises.length !== 1 ? 's' : ''} - + Imported {formatImportDate(pack.importDate)} @@ -156,47 +172,21 @@ export default function ManagePOWRPacksScreen() { - Delete Pack? + + Delete Pack + - - This will remove the POWR Pack from your library. Do you want to keep the imported exercises and templates? - + This will remove the POWR Pack and all its associated templates and exercises from your library. - - - - - - - - - - - - - - + + setShowDeleteDialog(false)}> + Cancel + + + Delete Pack + + @@ -228,12 +218,11 @@ const styles = StyleSheet.create({ alignItems: 'center', marginBottom: 4, }, - dialogOptions: { - flexDirection: 'row', - marginBottom: 16, - }, dialogActions: { flexDirection: 'row', justifyContent: 'flex-end', + }, + closeButton: { + padding: 8, } }); \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index 07a3dbf..853c801 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -18,6 +18,42 @@ import SettingsDrawer from '@/components/SettingsDrawer'; import RelayInitializer from '@/components/RelayInitializer'; import { useNDKStore } from '@/lib/stores/ndk'; import { useWorkoutStore } from '@/stores/workoutStore'; +// Import splash screens with fallback mechanism +let SplashComponent: React.ComponentType<{onFinish: () => void}>; + +// First try to import the video splash screen +try { + // Try to dynamically import the Video component + const Video = require('expo-av').Video; + // If successful, import the VideoSplashScreen + SplashComponent = require('@/components/VideoSplashScreen').default; + console.log('Successfully imported VideoSplashScreen'); +} catch (e) { + console.warn('Failed to import VideoSplashScreen or expo-av:', e); + // If that fails, use the simple splash screen + try { + SplashComponent = require('@/components/SimpleSplashScreen').default; + console.log('Using SimpleSplashScreen as fallback'); + } catch (simpleSplashError) { + console.warn('Failed to import SimpleSplashScreen:', simpleSplashError); + // Last resort fallback is an inline component + SplashComponent = ({onFinish}) => { + React.useEffect(() => { + // Call onFinish after a short delay + const timer = setTimeout(() => { + onFinish(); + }, 500); + return () => clearTimeout(timer); + }, [onFinish]); + + return ( + + Loading POWR... + + ); + }; + } +} console.log('_layout.tsx loaded'); @@ -33,45 +69,89 @@ const DARK_THEME = { export default function RootLayout() { const [isInitialized, setIsInitialized] = React.useState(false); + const [isSplashFinished, setIsSplashFinished] = React.useState(false); const { colorScheme, isDarkColorScheme } = useColorScheme(); const { init } = useNDKStore(); + const initializationPromise = React.useRef | null>(null); + // Start app initialization immediately React.useEffect(() => { - async function initApp() { - try { - if (Platform.OS === 'web') { - document.documentElement.classList.add('bg-background'); + if (!initializationPromise.current) { + initializationPromise.current = (async () => { + try { + console.log('Starting app initialization in background...'); + if (Platform.OS === 'web') { + document.documentElement.classList.add('bg-background'); + } + setAndroidNavigationBar(colorScheme); + + // Initialize NDK + await init(); + + // Load favorites from SQLite + await useWorkoutStore.getState().loadFavorites(); + + console.log('App initialization completed!'); + setIsInitialized(true); + } catch (error) { + console.error('Failed to initialize:', error); } - setAndroidNavigationBar(colorScheme); - - // Initialize NDK - await init(); - - // Load favorites from SQLite - await useWorkoutStore.getState().loadFavorites(); - - setIsInitialized(true); - } catch (error) { - console.error('Failed to initialize:', error); - } + })(); } - - initApp(); + + return () => { + // This is just for cleanup, the promise will continue executing + initializationPromise.current = null; + }; }, []); + // Function to handle splash finish - will check if initialization is also complete + const handleSplashFinish = React.useCallback(() => { + console.log('Splash video finished playing'); + setIsSplashFinished(true); + + // If initialization isn't done yet, we'll show a loading indicator + if (!isInitialized) { + console.log('Waiting for initialization to complete...'); + } + }, [isInitialized]); + + // Show splash screen if not finished + if (!isSplashFinished) { + try { + return ; + } catch (e) { + console.error('Error rendering splash screen:', e); + // Skip splash screen if there's an error + if (!isInitialized) { + return ( + + Loading... + + ); + } + // Force continue to main app + setIsSplashFinished(true); + return null; + } + } + + // If splash is done but initialization isn't, show loading if (!isInitialized) { return ( - Initializing... + Finalizing setup... ); } + // Main app UI wrapped in error boundary return ( + {/* Ensure SettingsDrawerProvider wraps everything */} {/* Add RelayInitializer here - it loads relay data once NDK is available */} diff --git a/assets/splash.mov b/assets/splash.mov new file mode 100644 index 0000000..a45a1b9 Binary files /dev/null and b/assets/splash.mov differ diff --git a/assets/v1-splash.mov b/assets/v1-splash.mov new file mode 100644 index 0000000..0c73928 Binary files /dev/null and b/assets/v1-splash.mov differ diff --git a/components/SimpleSplashScreen.tsx b/components/SimpleSplashScreen.tsx new file mode 100644 index 0000000..d5835c6 --- /dev/null +++ b/components/SimpleSplashScreen.tsx @@ -0,0 +1,64 @@ +// components/SimpleSplashScreen.tsx +import React, { useEffect } from 'react'; +import { View, Image, ActivityIndicator, StyleSheet } from 'react-native'; +import * as SplashScreen from 'expo-splash-screen'; + +// Keep the splash screen visible while we fetch resources +SplashScreen.preventAutoHideAsync().catch(() => { + /* ignore error */ +}); + +interface SplashScreenProps { + onFinish: () => void; +} + +const SimpleSplashScreen: React.FC = ({ onFinish }) => { + useEffect(() => { + // Hide the native splash screen + SplashScreen.hideAsync().catch(() => { + /* ignore error */ + }); + + // Simulate video duration with a timeout + const timer = setTimeout(() => { + onFinish(); + }, 2000); // 2 seconds splash display + + return () => clearTimeout(timer); + }, [onFinish]); + + return ( + + {/* Use a static image as fallback */} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000000', + alignItems: 'center', + justifyContent: 'center', + }, + image: { + width: '80%', + height: '80%', + }, + loader: { + position: 'absolute', + bottom: 100, + }, +}); + +export default SimpleSplashScreen; \ No newline at end of file diff --git a/components/VideoSplashScreen.tsx b/components/VideoSplashScreen.tsx new file mode 100644 index 0000000..0886086 --- /dev/null +++ b/components/VideoSplashScreen.tsx @@ -0,0 +1,102 @@ +// components/VideoSplashScreen.tsx +import React, { useEffect, useRef, useState } from 'react'; +import { View, StyleSheet, ActivityIndicator } from 'react-native'; +import { Video, ResizeMode, AVPlaybackStatus } from 'expo-av'; +import * as SplashScreen from 'expo-splash-screen'; + +// Keep the splash screen visible while we fetch resources +SplashScreen.preventAutoHideAsync(); + +interface VideoSplashScreenProps { + onFinish: () => void; +} + +const VideoSplashScreen: React.FC = ({ onFinish }) => { + const videoRef = useRef