splash screen, fixed delete exercises and templates when deleting powr pack bug, ui improvement to import powr pack screen

This commit is contained in:
DocNR 2025-03-17 20:20:16 -04:00
parent f1411e8568
commit 6ac0e80e45
16 changed files with 650 additions and 208 deletions

View File

@ -11,7 +11,7 @@
"splash": { "splash": {
"image": "./assets/images/splash.png", "image": "./assets/images/splash.png",
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#000000"
}, },
"assetBundlePatterns": [ "assetBundlePatterns": [
"**/*" "**/*"
@ -56,7 +56,9 @@
} }
} }
], ],
"expo-secure-store" "expo-secure-store",
"expo-av",
"expo-splash-screen"
], ],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true

View File

@ -1,24 +1,27 @@
// app/(packs)/import.tsx // app/(packs)/import.tsx
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { View, ScrollView, StyleSheet, ActivityIndicator, Platform } from 'react-native'; import { View, ScrollView, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
import { router, Stack } from 'expo-router'; import { router, Stack, useLocalSearchParams } from 'expo-router';
import { useNDK } from '@/lib/hooks/useNDK'; import { useNDK } from '@/lib/hooks/useNDK';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox'; 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 { findTagValue } from '@/utils/nostr-utils';
import POWRPackService from '@/lib/db/services/POWRPackService'; import { usePOWRPackService } from '@/components/DatabaseProvider';
import { usePOWRPackService } from '@/components/DatabaseProvider'; // Use the proper hook
import { POWRPackImport, POWRPackSelection } from '@/types/powr-pack'; 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() { export default function ImportPOWRPackScreen() {
const { ndk } = useNDK(); const { ndk } = useNDK();
const powrPackService = usePOWRPackService(); const powrPackService = usePOWRPackService();
const [naddrInput, setNaddrInput] = useState(''); const params = useLocalSearchParams<{ naddr?: string }>();
const [naddrInput, setNaddrInput] = useState(params.naddr || '');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [packData, setPackData] = useState<POWRPackImport | null>(null); const [packData, setPackData] = useState<POWRPackImport | null>(null);
@ -27,6 +30,25 @@ export default function ImportPOWRPackScreen() {
const [dependencies, setDependencies] = useState<Record<string, string[]>>({}); const [dependencies, setDependencies] = useState<Record<string, string[]>>({});
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
const [importSuccess, setImportSuccess] = 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 // Handle fetch button click
const handleFetchPack = async () => { const handleFetchPack = async () => {
@ -184,6 +206,12 @@ export default function ImportPOWRPackScreen() {
options={{ options={{
title: 'Import POWR Pack', title: 'Import POWR Pack',
headerShown: true, headerShown: true,
// Add a close button for iOS
headerRight: () => (
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
<X {...getIconProps('primary')} size={24} />
</TouchableOpacity>
),
}} }}
/> />
@ -192,10 +220,10 @@ export default function ImportPOWRPackScreen() {
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
{/* Input section */} {/* Input section */}
<Card> <Card className="mb-4">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<Text className="text-xl font-semibold">Enter POWR Pack Address</Text> <Text className="text-xl font-semibold text-foreground">Enter POWR Pack Address</Text>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
<Text className="text-muted-foreground">Paste a POWR Pack naddr to import</Text> <Text className="text-muted-foreground">Paste a POWR Pack naddr to import</Text>
@ -212,35 +240,68 @@ export default function ImportPOWRPackScreen() {
placeholder="naddr1..." placeholder="naddr1..."
value={naddrInput} value={naddrInput}
onChangeText={setNaddrInput} onChangeText={setNaddrInput}
style={styles.input} style={[styles.input, { height: 80 }]}
multiline={true}
numberOfLines={3}
textAlignVertical="top"
className="border-input"
/> />
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<Button {packData ? (
onPress={handleFetchPack} // Success indicator when pack is loaded
disabled={isLoading || !naddrInput.trim()} <View className="w-full flex-row items-center justify-center rounded-md p-3" style={{
className="w-full" backgroundColor: isDarkColorScheme ? 'rgba(34, 197, 94, 0.1)' : 'rgba(34, 197, 94, 0.1)',
> borderWidth: 1,
{isLoading ? ( borderColor: isDarkColorScheme ? 'rgba(34, 197, 94, 0.2)' : 'rgba(34, 197, 94, 0.2)'
<ActivityIndicator size="small" color="#fff" /> }}>
) : ( <CheckCircle2 {...getIconProps('success')} size={16} />
<Text className="text-primary-foreground">Fetch Pack</Text> <Text style={{
)} color: COLORS.success,
</Button> marginLeft: 8,
fontWeight: '500'
}}>
POWR Pack loaded successfully!
</Text>
</View>
) : (
// Fetch button when no pack is loaded
<Button
onPress={handleFetchPack}
disabled={isLoading || !naddrInput.trim()}
className="w-full"
style={{ backgroundColor: COLORS.purple.DEFAULT }}
>
{isLoading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={{ color: '#fff', fontWeight: '500' }}>Fetch Pack</Text>
)}
</Button>
)}
</CardFooter> </CardFooter>
</Card> </Card>
{/* Error message */} {/* Error message */}
{error && ( {error && (
<View className="mb-4 mt-4 p-4 bg-destructive/10 border border-destructive rounded-md flex-row items-center"> <View className="mb-4 p-4 rounded-md flex-row items-center" style={{
<Text className="text-destructive ml-2">{error}</Text> backgroundColor: isDarkColorScheme ? 'rgba(239, 68, 68, 0.1)' : 'rgba(239, 68, 68, 0.1)',
borderWidth: 1,
borderColor: isDarkColorScheme ? 'rgba(239, 68, 68, 0.2)' : 'rgba(239, 68, 68, 0.2)'
}}>
<Text style={{ color: COLORS.destructive, marginLeft: 8 }}>{error}</Text>
</View> </View>
)} )}
{/* Success message */} {/* Success message */}
{importSuccess && ( {importSuccess && (
<View className="mb-4 mt-4 p-4 bg-green-50 border border-green-200 rounded-md flex-row items-center"> <View className="mb-4 p-4 rounded-md flex-row items-center" style={{
<Text className="ml-2 text-green-800">Pack successfully imported!</Text> backgroundColor: isDarkColorScheme ? 'rgba(34, 197, 94, 0.1)' : 'rgba(34, 197, 94, 0.1)',
borderWidth: 1,
borderColor: isDarkColorScheme ? 'rgba(34, 197, 94, 0.2)' : 'rgba(34, 197, 94, 0.2)'
}}>
<CheckCircle2 {...getIconProps('success')} size={16} style={{ marginRight: 8 }} />
<Text style={{ color: COLORS.success, fontWeight: '500' }}>Pack successfully imported!</Text>
</View> </View>
)} )}
@ -250,7 +311,7 @@ export default function ImportPOWRPackScreen() {
<Card className="mb-4"> <Card className="mb-4">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<Text className="text-xl font-semibold">{getPackTitle()}</Text> <Text className="text-xl font-semibold text-foreground">{getPackTitle()}</Text>
</CardTitle> </CardTitle>
{getPackDescription() ? ( {getPackDescription() ? (
<CardDescription> <CardDescription>
@ -259,7 +320,7 @@ export default function ImportPOWRPackScreen() {
) : null} ) : null}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Text className="mb-2">Select items to import:</Text> <Text className="mb-2 text-foreground">Select items to import:</Text>
</CardContent> </CardContent>
</Card> </Card>
@ -268,7 +329,7 @@ export default function ImportPOWRPackScreen() {
<Card className="mb-4"> <Card className="mb-4">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<Text className="text-lg font-semibold">Workout Templates</Text> <Text className="text-lg font-semibold text-foreground">Workout Templates</Text>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
<Text className="text-muted-foreground">{packData.templates.length} templates available</Text> <Text className="text-muted-foreground">{packData.templates.length} templates available</Text>
@ -285,10 +346,14 @@ export default function ImportPOWRPackScreen() {
handleTemplateChange(template.id, checked === true) handleTemplateChange(template.id, checked === true)
} }
id={`template-${template.id}`} id={`template-${template.id}`}
className="border-input"
/> />
<Text className="ml-2 flex-1" onPress={() => <Text
handleTemplateChange(template.id, !selectedTemplates.includes(template.id)) className="ml-3 flex-1 text-foreground"
}> onPress={() =>
handleTemplateChange(template.id, !selectedTemplates.includes(template.id))
}
>
{title} {title}
</Text> </Text>
</View> </View>
@ -309,7 +374,7 @@ export default function ImportPOWRPackScreen() {
<Card className="mb-4"> <Card className="mb-4">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<Text className="text-lg font-semibold">Exercises</Text> <Text className="text-lg font-semibold text-foreground">Exercises</Text>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
<Text className="text-muted-foreground">{packData.exercises.length} exercises available</Text> <Text className="text-muted-foreground">{packData.exercises.length} exercises available</Text>
@ -329,9 +394,10 @@ export default function ImportPOWRPackScreen() {
} }
disabled={isRequired} disabled={isRequired}
id={`exercise-${exercise.id}`} id={`exercise-${exercise.id}`}
className="border-input"
/> />
<Text <Text
className={`ml-2 flex-1 ${isRequired ? 'font-medium' : ''}`} className={`ml-3 flex-1 ${isRequired ? 'font-medium' : ''} text-foreground`}
onPress={() => { onPress={() => {
if (!isRequired) { if (!isRequired) {
handleExerciseChange(exercise.id, !selectedExercises.includes(exercise.id)) handleExerciseChange(exercise.id, !selectedExercises.includes(exercise.id))
@ -341,9 +407,11 @@ export default function ImportPOWRPackScreen() {
{title} {title}
</Text> </Text>
{isRequired && ( {isRequired && (
<View style={styles.requiredBadge}> <View style={[styles.requiredBadge, {
<InfoIcon size={14} color="#6b7280" /> backgroundColor: isDarkColorScheme ? COLORS.dark.muted : COLORS.light.muted
<Text className="text-xs text-gray-500 ml-1">Required</Text> }]}>
<InfoIcon {...getIconProps('muted')} size={14} />
<Text className="text-xs text-muted-foreground ml-1">Required</Text>
</View> </View>
)} )}
</View> </View>
@ -363,12 +431,13 @@ export default function ImportPOWRPackScreen() {
<Button <Button
onPress={handleImport} onPress={handleImport}
disabled={isImporting || (selectedTemplates.length === 0 && selectedExercises.length === 0)} disabled={isImporting || (selectedTemplates.length === 0 && selectedExercises.length === 0)}
className="w-full" className="w-full mb-8"
style={{ backgroundColor: COLORS.purple.DEFAULT }}
> >
{isImporting ? ( {isImporting ? (
<ActivityIndicator size="small" color="#fff" /> <ActivityIndicator size="small" color="#fff" />
) : ( ) : (
<Text className="text-primary-foreground"> <Text style={{ color: '#fff', fontWeight: '500' }}>
Import {selectedTemplates.length + selectedExercises.length} Items Import {selectedTemplates.length + selectedExercises.length} Items
</Text> </Text>
)} )}
@ -389,25 +458,27 @@ const styles = StyleSheet.create({
paddingBottom: 80, paddingBottom: 80,
}, },
input: { input: {
marginBottom: 16, marginBottom: 8,
}, },
packContent: { packContent: {
marginTop: 16, marginTop: 8,
}, },
itemRow: { itemRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: 8, paddingVertical: 10,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f0f0f0', borderBottomColor: '#f0f0f0',
}, },
requiredBadge: { requiredBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#f9fafb',
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 2, paddingVertical: 2,
borderRadius: 12, borderRadius: 12,
marginLeft: 8, marginLeft: 8,
},
closeButton: {
padding: 8,
} }
}); });

View File

@ -8,10 +8,12 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { POWRPackWithContent } from '@/types/powr-pack'; import { POWRPackWithContent } from '@/types/powr-pack';
// Fix database context import
import { usePOWRPackService } from '@/components/DatabaseProvider'; import { usePOWRPackService } from '@/components/DatabaseProvider';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { Trash2, PackageOpen, Plus } from 'lucide-react-native'; import { Trash2, PackageOpen, Plus, X } 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 ManagePOWRPacksScreen() { export default function ManagePOWRPacksScreen() {
const powrPackService = usePOWRPackService(); const powrPackService = usePOWRPackService();
@ -19,7 +21,8 @@ export default function ManagePOWRPacksScreen() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [selectedPackId, setSelectedPackId] = useState<string | null>(null); const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [keepItems, setKeepItems] = useState(true); const { getIconProps } = useIconColor();
const { isDarkColorScheme } = useColorScheme();
// Load imported packs // Load imported packs
useEffect(() => { useEffect(() => {
@ -44,6 +47,11 @@ export default function ManagePOWRPacksScreen() {
router.push('/(packs)/import'); router.push('/(packs)/import');
}; };
// Handle close button press
const handleClose = () => {
router.back();
};
// Handle delete button click // Handle delete button click
const handleDeleteClick = (packId: string) => { const handleDeleteClick = (packId: string) => {
setSelectedPackId(packId); setSelectedPackId(packId);
@ -55,7 +63,8 @@ export default function ManagePOWRPacksScreen() {
if (!selectedPackId) return; if (!selectedPackId) return;
try { try {
await powrPackService.deletePack(selectedPackId, keepItems); // Always delete everything (we no longer need the keepItems parameter)
await powrPackService.deletePack(selectedPackId, false);
// Refresh the list // Refresh the list
loadPacks(); loadPacks();
} catch (error) { } catch (error) {
@ -77,34 +86,41 @@ export default function ManagePOWRPacksScreen() {
options={{ options={{
title: 'Manage POWR Packs', title: 'Manage POWR Packs',
headerShown: true, headerShown: true,
// Add a close button for iOS
headerRight: () => (
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
<X {...getIconProps('primary')} size={24} />
</TouchableOpacity>
),
}} }}
/> />
<ScrollView contentContainerStyle={styles.scrollContent}> <ScrollView contentContainerStyle={styles.scrollContent}>
{/* Import button - fix icon usage */} {/* Import button */}
<Button <Button
onPress={handleImport} onPress={handleImport}
className="mb-4" className="mb-4"
style={{ backgroundColor: COLORS.purple.DEFAULT }}
> >
<Plus size={18} color="#fff" className="mr-2" /> <Plus size={18} color="#fff" style={{ marginRight: 8 }} />
<Text className="text-primary-foreground">Import New Pack</Text> <Text style={{ color: '#fff', fontWeight: '500' }}>Import New Pack</Text>
</Button> </Button>
{/* No packs message */} {/* No packs message */}
{!isLoading && packs.length === 0 && ( {!isLoading && packs.length === 0 && (
<Card> <Card>
<CardContent className="py-8 items-center"> <CardContent className="py-8 items-center">
<PackageOpen size={48} color="#6b7280" /> <PackageOpen size={48} {...getIconProps('muted')} />
<Text className="text-lg font-medium mt-4 text-center">No POWR Packs Imported</Text> <Text className="text-lg font-medium mt-4 text-center text-foreground">No POWR Packs Imported</Text>
<Text className="text-center mt-2 text-gray-500"> <Text className="text-center mt-2 text-muted-foreground">
Import workout packs shared by the community to get started. Import workout packs shared by the community to get started.
</Text> </Text>
<Button <Button
onPress={handleImport} onPress={handleImport}
className="mt-4" className="mt-4"
variant="outline" style={{ backgroundColor: COLORS.purple.DEFAULT }}
> >
<Text>Import Your First Pack</Text> <Text style={{ color: '#fff', fontWeight: '500' }}>Import Your First Pack</Text>
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@ -120,7 +136,7 @@ export default function ManagePOWRPacksScreen() {
<View style={styles.cardHeaderContent}> <View style={styles.cardHeaderContent}>
<View style={styles.cardHeaderText}> <View style={styles.cardHeaderText}>
<CardTitle> <CardTitle>
<Text className="text-lg font-semibold">{pack.title}</Text> <Text className="text-lg font-semibold text-foreground">{pack.title}</Text>
</CardTitle> </CardTitle>
{pack.description && ( {pack.description && (
<CardDescription> <CardDescription>
@ -132,18 +148,18 @@ export default function ManagePOWRPacksScreen() {
onPress={() => handleDeleteClick(pack.id)} onPress={() => handleDeleteClick(pack.id)}
style={styles.deleteButton} style={styles.deleteButton}
> >
<Trash2 size={20} color="#ef4444" /> <Trash2 {...getIconProps('destructive')} size={20} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<View style={styles.statsRow}> <View style={styles.statsRow}>
<Text className="text-gray-500"> <Text className="text-muted-foreground">
{templates.length} template{templates.length !== 1 ? 's' : ''} {exercises.length} exercise{exercises.length !== 1 ? 's' : ''} {templates.length} template{templates.length !== 1 ? 's' : ''} {exercises.length} exercise{exercises.length !== 1 ? 's' : ''}
</Text> </Text>
</View> </View>
<Separator className="my-2" /> <Separator className="my-2" />
<Text className="text-sm text-gray-500"> <Text className="text-sm text-muted-foreground">
Imported {formatImportDate(pack.importDate)} Imported {formatImportDate(pack.importDate)}
</Text> </Text>
</CardContent> </CardContent>
@ -156,47 +172,21 @@ export default function ManagePOWRPacksScreen() {
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete Pack?</AlertDialogTitle> <AlertDialogTitle>
<Text>Delete Pack</Text>
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<Text> <Text>This will remove the POWR Pack and all its associated templates and exercises from your library.</Text>
This will remove the POWR Pack from your library. Do you want to keep the imported exercises and templates?
</Text>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <View className="flex-row justify-center gap-3 px-4 mt-2">
<View style={styles.dialogOptions}> <AlertDialogCancel onPress={() => setShowDeleteDialog(false)}>
<Button <Text>Cancel</Text>
variant={keepItems ? "default" : "outline"} </AlertDialogCancel>
onPress={() => setKeepItems(true)} <AlertDialogAction onPress={handleDeleteConfirm} className='bg-destructive'>
className="flex-1 mr-2" <Text className='text-destructive-foreground'>Delete Pack</Text>
> </AlertDialogAction>
<Text className={keepItems ? "text-primary-foreground" : ""}> </View>
Keep Items
</Text>
</Button>
<Button
variant={!keepItems ? "default" : "outline"}
onPress={() => setKeepItems(false)}
className="flex-1"
>
<Text className={!keepItems ? "text-primary-foreground" : ""}>
Delete All
</Text>
</Button>
</View>
<View style={styles.dialogActions}>
<AlertDialogCancel asChild>
<Button variant="outline" className="mr-2">
<Text>Cancel</Text>
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button variant="destructive" onPress={handleDeleteConfirm}>
<Text className="text-destructive-foreground">Confirm</Text>
</Button>
</AlertDialogAction>
</View>
</AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</View> </View>
@ -228,12 +218,11 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginBottom: 4, marginBottom: 4,
}, },
dialogOptions: {
flexDirection: 'row',
marginBottom: 16,
},
dialogActions: { dialogActions: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-end', justifyContent: 'flex-end',
},
closeButton: {
padding: 8,
} }
}); });

View File

@ -18,6 +18,42 @@ import SettingsDrawer from '@/components/SettingsDrawer';
import RelayInitializer from '@/components/RelayInitializer'; import RelayInitializer from '@/components/RelayInitializer';
import { useNDKStore } from '@/lib/stores/ndk'; import { useNDKStore } from '@/lib/stores/ndk';
import { useWorkoutStore } from '@/stores/workoutStore'; 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 (
<View className="flex-1 items-center justify-center bg-black">
<Text className="text-white text-xl">Loading POWR...</Text>
</View>
);
};
}
}
console.log('_layout.tsx loaded'); console.log('_layout.tsx loaded');
@ -33,45 +69,89 @@ const DARK_THEME = {
export default function RootLayout() { export default function RootLayout() {
const [isInitialized, setIsInitialized] = React.useState(false); const [isInitialized, setIsInitialized] = React.useState(false);
const [isSplashFinished, setIsSplashFinished] = React.useState(false);
const { colorScheme, isDarkColorScheme } = useColorScheme(); const { colorScheme, isDarkColorScheme } = useColorScheme();
const { init } = useNDKStore(); const { init } = useNDKStore();
const initializationPromise = React.useRef<Promise<void> | null>(null);
// Start app initialization immediately
React.useEffect(() => { React.useEffect(() => {
async function initApp() { if (!initializationPromise.current) {
try { initializationPromise.current = (async () => {
if (Platform.OS === 'web') { try {
document.documentElement.classList.add('bg-background'); 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 <SplashComponent onFinish={handleSplashFinish} />;
} catch (e) {
console.error('Error rendering splash screen:', e);
// Skip splash screen if there's an error
if (!isInitialized) {
return (
<View className="flex-1 items-center justify-center bg-background">
<Text className="text-foreground">Loading...</Text>
</View>
);
}
// Force continue to main app
setIsSplashFinished(true);
return null;
}
}
// If splash is done but initialization isn't, show loading
if (!isInitialized) { if (!isInitialized) {
return ( return (
<View className="flex-1 items-center justify-center bg-background"> <View className="flex-1 items-center justify-center bg-background">
<Text className="text-foreground">Initializing...</Text> <Text className="text-foreground">Finalizing setup...</Text>
</View> </View>
); );
} }
// Main app UI wrapped in error boundary
return ( return (
<ErrorBoundary> <ErrorBoundary>
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<DatabaseProvider> <DatabaseProvider>
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}> <ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
{/* Ensure SettingsDrawerProvider wraps everything */}
<SettingsDrawerProvider> <SettingsDrawerProvider>
{/* Add RelayInitializer here - it loads relay data once NDK is available */} {/* Add RelayInitializer here - it loads relay data once NDK is available */}
<RelayInitializer /> <RelayInitializer />

BIN
assets/splash.mov Normal file

Binary file not shown.

BIN
assets/v1-splash.mov Normal file

Binary file not shown.

View File

@ -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<SplashScreenProps> = ({ 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 (
<View style={styles.container}>
{/* Use a static image as fallback */}
<Image
source={require('../assets/images/splash.png')}
style={styles.image}
resizeMode="contain"
/>
<ActivityIndicator
size="large"
color="#ffffff"
style={styles.loader}
/>
</View>
);
};
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;

View File

@ -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<VideoSplashScreenProps> = ({ onFinish }) => {
const videoRef = useRef<Video>(null);
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Hide the native splash screen once our component is ready
if (isVideoLoaded) {
console.log('Video loaded, hiding native splash screen');
SplashScreen.hideAsync().catch(e => console.log('Error hiding splash screen:', e));
}
}, [isVideoLoaded]);
const handleVideoLoad = () => {
console.log('Video loaded successfully');
setIsVideoLoaded(true);
// Start playing the video once it's loaded
videoRef.current?.playAsync().catch(e => {
console.error('Error playing video:', e);
// If video fails to play, just call onFinish
onFinish();
});
};
const handleVideoError = (error: string) => {
console.error('Video error:', error);
setError(error);
// On error, skip the video and move to the app
onFinish();
};
const handlePlaybackStatusUpdate = (status: AVPlaybackStatus) => {
// Type-safe handling of playback status
if (!status.isLoaded) {
// Handle error state
if (status.error) {
handleVideoError(`Video error: ${status.error}`);
}
return;
}
// When video finishes playing, call the onFinish callback
if (status.didJustFinish) {
console.log('Video finished playing');
onFinish();
}
};
return (
<View style={styles.container}>
<Video
ref={videoRef}
source={require('../assets/splash.mov')}
style={styles.video}
resizeMode={ResizeMode.COVER}
onLoad={handleVideoLoad}
onError={(error) => handleVideoError(error.toString())}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
shouldPlay={false} // We'll play it manually after load
progressUpdateIntervalMillis={50} // More frequent updates
/>
{!isVideoLoaded && (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" color="#ffffff" />
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
video: {
flex: 1,
width: '100%',
height: '100%',
},
loaderContainer: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
});
export default VideoSplashScreen;

View File

@ -83,14 +83,12 @@ export default function POWRPackSection() {
relays relays
}); });
// Copy to clipboard // Navigate to import screen with the naddr as a parameter
Clipboard.setString(naddr); router.push({
pathname: '/(packs)/import',
params: { naddr }
});
// Navigate to import screen
router.push('/(packs)/import');
// Alert user that the address has been copied
alert('Pack address copied to clipboard. Paste it in the import field.');
} catch (error) { } catch (error) {
console.error('Error handling pack click:', error); console.error('Error handling pack click:', error);
alert('Failed to prepare pack for import. Please try again.'); alert('Failed to prepare pack for import. Please try again.');

View File

@ -1,6 +1,9 @@
PODS: PODS:
- boost (1.84.0) - boost (1.84.0)
- DoubleConversion (1.1.6) - DoubleConversion (1.1.6)
- EXAV (15.0.2):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXConstants (17.0.6): - EXConstants (17.0.6):
- ExpoModulesCore - ExpoModulesCore
- EXJSONUtils (0.14.0) - EXJSONUtils (0.14.0)
@ -2110,6 +2113,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXAV (from `../node_modules/expo-av/ios`)
- EXConstants (from `../node_modules/expo-constants/ios`) - EXConstants (from `../node_modules/expo-constants/ios`)
- EXJSONUtils (from `../node_modules/expo-json-utils/ios`) - EXJSONUtils (from `../node_modules/expo-json-utils/ios`)
- EXManifests (from `../node_modules/expo-manifests/ios`) - EXManifests (from `../node_modules/expo-manifests/ios`)
@ -2215,6 +2219,8 @@ EXTERNAL SOURCES:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
DoubleConversion: DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EXAV:
:path: "../node_modules/expo-av/ios"
EXConstants: EXConstants:
:path: "../node_modules/expo-constants/ios" :path: "../node_modules/expo-constants/ios"
EXJSONUtils: EXJSONUtils:
@ -2406,6 +2412,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
boost: 1dca942403ed9342f98334bf4c3621f011aa7946 boost: 1dca942403ed9342f98334bf4c3621f011aa7946
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
EXAV: 4c41f0a6ae54a0b96597abc90e37962ab2ee0d85
EXConstants: f5c27bfa40ba650257535ab958f4f5876ee93edd EXConstants: f5c27bfa40ba650257535ab958f4f5876ee93edd
EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93 EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93
EXManifests: 807ab5394ca9f8dd5e64283f02876b2f85c4eb72 EXManifests: 807ab5394ca9f8dd5e64283f02876b2f85c4eb72

View File

@ -537,7 +537,10 @@ export class NostrIntegration {
exerciseRefs: string[] exerciseRefs: string[]
): Promise<void> { ): Promise<void> {
try { try {
console.log(`Saving ${exerciseIds.length} exercise relationships for template ${templateId}`); console.log(`=== SAVING TEMPLATE EXERCISES ===`);
console.log(`Template ID: ${templateId}`);
console.log(`Exercise IDs (${exerciseIds.length}):`, exerciseIds);
console.log(`Exercise Refs (${exerciseRefs.length}):`, exerciseRefs);
// Check if nostr_reference column exists // Check if nostr_reference column exists
const hasNostrReference = await this.columnExists('template_exercises', 'nostr_reference'); const hasNostrReference = await this.columnExists('template_exercises', 'nostr_reference');
@ -555,6 +558,12 @@ export class NostrIntegration {
await this.db.execAsync(`ALTER TABLE template_exercises ADD COLUMN relay_hints TEXT`); await this.db.execAsync(`ALTER TABLE template_exercises ADD COLUMN relay_hints TEXT`);
} }
// Clear out any existing entries for this template
await this.db.runAsync(
`DELETE FROM template_exercises WHERE template_id = ?`,
[templateId]
);
// Create template exercise records // Create template exercise records
for (let i = 0; i < exerciseIds.length; i++) { for (let i = 0; i < exerciseIds.length; i++) {
const exerciseId = exerciseIds[i]; const exerciseId = exerciseIds[i];
@ -615,7 +624,15 @@ export class NostrIntegration {
console.log(`Saved template-exercise relationship: template=${templateId}, exercise=${exerciseId} with ${relayHints.length} relay hints`); console.log(`Saved template-exercise relationship: template=${templateId}, exercise=${exerciseId} with ${relayHints.length} relay hints`);
} }
console.log(`Successfully saved ${exerciseIds.length} template-exercise relationships for template ${templateId}`); // Verify the relationships were saved
const savedRelationships = await this.db.getAllAsync<{id: string, exercise_id: string}>(
`SELECT id, exercise_id FROM template_exercises WHERE template_id = ?`,
[templateId]
);
console.log(`Saved ${savedRelationships.length} template-exercise relationships for template ${templateId}`);
savedRelationships.forEach(rel => console.log(` - Exercise ID: ${rel.exercise_id}`));
console.log(`=== END SAVING TEMPLATE EXERCISES ===`);
} catch (error) { } catch (error) {
console.error('Error saving template exercises with parameters:', error); console.error('Error saving template exercises with parameters:', error);
throw error; throw error;

View File

@ -417,7 +417,8 @@ export default class POWRPackService {
*/ */
async importPack(packImport: POWRPackImport, selection: POWRPackSelection): Promise<void> { async importPack(packImport: POWRPackImport, selection: POWRPackSelection): Promise<void> {
try { try {
console.log(`Importing ${selection.selectedExercises.length} exercises...`); console.log(`=== STARTING PACK IMPORT ===`);
console.log(`Importing ${selection.selectedExercises.length} exercises and ${selection.selectedTemplates.length} templates...`);
// Map to track imported exercise IDs by various reference formats // Map to track imported exercise IDs by various reference formats
const exerciseIdMap = new Map<string, string>(); const exerciseIdMap = new Map<string, string>();
@ -425,7 +426,10 @@ export default class POWRPackService {
// First, import the selected exercises // First, import the selected exercises
for (const exerciseId of selection.selectedExercises) { for (const exerciseId of selection.selectedExercises) {
const exerciseEvent = packImport.exercises.find(e => e.id === exerciseId); const exerciseEvent = packImport.exercises.find(e => e.id === exerciseId);
if (!exerciseEvent) continue; if (!exerciseEvent) {
console.log(`Exercise event ${exerciseId} not found in pack data`);
continue;
}
// Get the d-tag value from the event // Get the d-tag value from the event
const dTag = exerciseEvent.tagValue('d'); const dTag = exerciseEvent.tagValue('d');
@ -456,12 +460,17 @@ export default class POWRPackService {
console.log(`Imported exercise: ${exerciseModel.title} (${localId}) from Nostr event ${exerciseId}`); console.log(`Imported exercise: ${exerciseModel.title} (${localId}) from Nostr event ${exerciseId}`);
} }
console.log(`Importing ${selection.selectedTemplates.length} templates...`); console.log(`=== EXERCISE IMPORT COMPLETE ===`);
console.log(`Total exercise reference mappings: ${exerciseIdMap.size}`);
console.log(`Now importing ${selection.selectedTemplates.length} templates...`);
// Then, import the selected templates // Then, import the selected templates
for (const templateId of selection.selectedTemplates) { for (const templateId of selection.selectedTemplates) {
const templateEvent = packImport.templates.find(t => t.id === templateId); const templateEvent = packImport.templates.find(t => t.id === templateId);
if (!templateEvent) continue; if (!templateEvent) {
console.log(`Template event ${templateId} not found in pack data`);
continue;
}
// Convert to local model // Convert to local model
const templateModel = this.nostrIntegration.convertNostrTemplateToLocal(templateEvent); const templateModel = this.nostrIntegration.convertNostrTemplateToLocal(templateEvent);
@ -481,6 +490,10 @@ export default class POWRPackService {
const templateExerciseIds: string[] = []; const templateExerciseIds: string[] = [];
const matchedRefs: string[] = []; const matchedRefs: string[] = [];
console.log(`=== DEBUG: Template ID Mapping Process ===`);
console.log(`Template Event: ${templateEvent.id}`);
console.log(`Template Local ID: ${localTemplateId}`);
for (const ref of exerciseRefs) { for (const ref of exerciseRefs) {
// Extract the base reference (before any parameters) // Extract the base reference (before any parameters)
const refParts = ref.split('::'); const refParts = ref.split('::');
@ -496,12 +509,12 @@ export default class POWRPackService {
templateExerciseIds.push(localExerciseId); templateExerciseIds.push(localExerciseId);
matchedRefs.push(ref); matchedRefs.push(ref);
console.log(`Mapped reference ${baseRef} to local exercise ID ${localExerciseId}`); console.log(`✅ Direct match found! Mapped reference ${baseRef} to local exercise ID ${localExerciseId}`);
continue; continue;
} }
// If not found by direct reference, try to match by examining individual components // If not found by direct reference, try to match by examining individual components
console.log(`No direct match for reference: ${baseRef}. Trying to match by components...`); console.log(`⚠️ No direct match for reference: ${baseRef}. Trying to match by components...`);
// Parse the reference for fallback matching // Parse the reference for fallback matching
const refSegments = baseRef.split(':'); const refSegments = baseRef.split(':');
@ -510,44 +523,53 @@ export default class POWRPackService {
const refPubkey = refSegments[1]; const refPubkey = refSegments[1];
const refDTag = refSegments[2]; const refDTag = refSegments[2];
// Try to find the matching exercise by looking at both event ID and d-tag // Log all entries in exerciseIdMap for debugging
for (const [key, value] of exerciseIdMap.entries()) { console.log(`Looking among ${exerciseIdMap.size} existing mappings:`);
// Check if this is potentially the same exercise with a different reference format [...exerciseIdMap.entries()].forEach(([key, value]) => {
if (key.includes(refPubkey) && (key.includes(refDTag) || key.endsWith(refDTag))) { console.log(` - ${key} -> ${value}`);
templateExerciseIds.push(value); });
matchedRefs.push(ref);
// Also add this reference format to map for future lookups // Try to find the matching exercise by direct dTag match
exerciseIdMap.set(baseRef, value); const directDTagMatch = [...exerciseIdMap.entries()].find(([key, _]) => {
return key.includes(refPubkey) && key.includes(refDTag);
});
console.log(`Found potential match using partial comparison: ${key} -> ${value}`); if (directDTagMatch) {
break; const [matchKey, matchValue] = directDTagMatch;
} templateExerciseIds.push(matchValue);
matchedRefs.push(ref);
// Also add this reference format to map for future lookups
exerciseIdMap.set(baseRef, matchValue);
console.log(`✅ Found match by dTag: ${matchKey} -> ${matchValue}`);
continue;
} }
// If no match found yet, check if there's a direct event ID match // Try to find the matching exercise by event ID
if (templateExerciseIds.length === templateExerciseIds.lastIndexOf(refDTag) + 1) { const matchingEvent = packImport.exercises.find(e => e.id === refDTag);
// Didn't add anything in the above loop, try direct event ID lookup
const matchingEvent = packImport.exercises.find(e => e.id === refDTag);
if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) { if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) {
const localExerciseId = exerciseIdMap.get(matchingEvent.id) || ''; const localExerciseId = exerciseIdMap.get(matchingEvent.id) || '';
templateExerciseIds.push(localExerciseId); templateExerciseIds.push(localExerciseId);
matchedRefs.push(ref); matchedRefs.push(ref);
// Add this reference to our map for future use // Add this reference to our map for future use
exerciseIdMap.set(baseRef, localExerciseId); exerciseIdMap.set(baseRef, localExerciseId);
console.log(`Found match by event ID: ${matchingEvent.id} -> ${localExerciseId}`); console.log(`✅ Found match by event ID: ${matchingEvent.id} -> ${localExerciseId}`);
} else { continue;
console.log(`No matching exercise found for reference components: kind=${refKind}, pubkey=${refPubkey}, d-tag=${refDTag}`);
}
} }
console.log(`❌ No matching exercise found for reference: ${baseRef}`);
} else { } else {
console.log(`Invalid reference format: ${baseRef}`); console.log(`Invalid reference format: ${baseRef}`);
} }
} }
console.log(`Mapped ${templateExerciseIds.length}/${exerciseRefs.length} references for template ${templateModel.title}`);
console.log(`=== END DEBUG ===`);
// Save template-exercise relationships with parameters // Save template-exercise relationships with parameters
if (templateExerciseIds.length > 0) { if (templateExerciseIds.length > 0) {
await this.nostrIntegration.saveTemplateExercisesWithParams( await this.nostrIntegration.saveTemplateExercisesWithParams(
@ -564,7 +586,7 @@ export default class POWRPackService {
); );
console.log(`Template ${templateModel.title} has ${templateExercises.length} exercises associated`); console.log(`Template ${templateModel.title} has ${templateExercises.length} exercises associated`);
} else { } else {
console.log(`No exercise relationships to save for template ${localTemplateId}`); console.log(`⚠️ No exercise relationships to save for template ${localTemplateId}`);
} }
} }
@ -583,10 +605,21 @@ export default class POWRPackService {
`SELECT id, title FROM templates WHERE source = 'nostr'` `SELECT id, title FROM templates WHERE source = 'nostr'`
); );
console.log(`Template IDs:`); console.log(`Imported Template IDs:`);
templates.forEach(t => { templates.forEach(t => {
console.log(` - ${t.title}: ${t.id}`); console.log(` - ${t.title}: ${t.id}`);
}); });
// Verify template-exercise relationships
for (const template of templates) {
const relationships = await this.db.getAllAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM template_exercises WHERE template_id = ?`,
[template.id]
);
console.log(`Template ${template.title}: ${relationships[0]?.count || 0} exercises`);
}
console.log(`=== PACK IMPORT COMPLETE ===`);
} catch (error) { } catch (error) {
console.error('Error importing pack:', error); console.error('Error importing pack:', error);
throw error; throw error;
@ -814,70 +847,112 @@ export default class POWRPackService {
} }
/** /**
* Delete a POWR Pack * Delete a POWR Pack and all its associated content
* @param packId The ID of the pack to delete * @param packId The ID of the pack to delete
* @param keepItems Whether to keep the imported templates and exercises * @param keepItems DEPRECATED parameter kept for backward compatibility
*/ */
async deletePack(packId: string, keepItems: boolean = true): Promise<void> { async deletePack(packId: string, keepItems: boolean = false): Promise<void> {
try { try {
if (!keepItems) { console.log(`[POWRPackService] Starting deletion of pack ${packId}`);
// Get all templates and exercises from this pack
const templates = await this.db.getAllAsync<{ id: string }>( // Always delete everything, ignore keepItems parameter
`SELECT t.id await this.db.withTransactionAsync(async () => {
FROM templates t // First, get all templates and exercises from this pack
JOIN powr_pack_items ppi ON ppi.item_id = t.nostr_event_id console.log(`[POWRPackService] Finding templates associated with pack ${packId}`);
WHERE ppi.pack_id = ? AND ppi.item_type = 'template'`, const templates = await this.db.getAllAsync<{ id: string, nostr_event_id: string }>(
`SELECT t.id, t.nostr_event_id
FROM templates t
JOIN powr_pack_items ppi ON ppi.item_id = t.nostr_event_id
WHERE ppi.pack_id = ? AND ppi.item_type = 'template'`,
[packId] [packId]
); );
console.log(`[POWRPackService] Found ${templates.length} templates to delete`);
const exercises = await this.db.getAllAsync<{ id: string }>( console.log(`[POWRPackService] Finding exercises associated with pack ${packId}`);
`SELECT e.id const exercises = await this.db.getAllAsync<{ id: string, nostr_event_id: string }>(
FROM exercises e `SELECT e.id, e.nostr_event_id
JOIN powr_pack_items ppi ON ppi.item_id = e.nostr_event_id FROM exercises e
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise'`, JOIN powr_pack_items ppi ON ppi.item_id = e.nostr_event_id
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise'`,
[packId] [packId]
); );
console.log(`[POWRPackService] Found ${exercises.length} exercises to delete`);
// Delete the templates // Find any additional exercises that are only referenced by templates in this pack
console.log(`[POWRPackService] Finding template exercises`);
const templateIds = templates.map(t => t.id);
let additionalExerciseIds: string[] = [];
if (templateIds.length > 0) {
const templateExercises = await this.db.getAllAsync<{ exercise_id: string }>(
`SELECT DISTINCT exercise_id
FROM template_exercises
WHERE template_id IN (${templateIds.map(() => '?').join(',')})`,
templateIds
);
additionalExerciseIds = templateExercises.map(te => te.exercise_id);
console.log(`[POWRPackService] Found ${additionalExerciseIds.length} additional exercises from templates`);
}
// Delete the templates and their exercises
for (const template of templates) { for (const template of templates) {
console.log(`[POWRPackService] Deleting template ${template.id}`);
// Delete template exercises first
await this.db.runAsync( await this.db.runAsync(
`DELETE FROM template_exercises WHERE template_id = ?`, `DELETE FROM template_exercises WHERE template_id = ?`,
[template.id] [template.id]
); );
// Then delete the template
await this.db.runAsync( await this.db.runAsync(
`DELETE FROM templates WHERE id = ?`, `DELETE FROM templates WHERE id = ?`,
[template.id] [template.id]
); );
} }
// Build a set of all exercise IDs to delete
const exerciseIdsToDelete = new Set([
...exercises.map(e => e.id),
...additionalExerciseIds
]);
// Delete the exercises // Delete the exercises
for (const exercise of exercises) { for (const exerciseId of exerciseIdsToDelete) {
console.log(`[POWRPackService] Deleting exercise ${exerciseId}`);
// Delete exercise tags first
await this.db.runAsync( await this.db.runAsync(
`DELETE FROM exercise_tags WHERE exercise_id = ?`, `DELETE FROM exercise_tags WHERE exercise_id = ?`,
[exercise.id] [exerciseId]
); );
// Then delete the exercise
await this.db.runAsync( await this.db.runAsync(
`DELETE FROM exercises WHERE id = ?`, `DELETE FROM exercises WHERE id = ?`,
[exercise.id] [exerciseId]
); );
} }
}
// Delete the pack items // Delete the pack items
await this.db.runAsync( console.log(`[POWRPackService] Deleting pack items for pack ${packId}`);
`DELETE FROM powr_pack_items WHERE pack_id = ?`, await this.db.runAsync(
[packId] `DELETE FROM powr_pack_items WHERE pack_id = ?`,
); [packId]
);
// Finally, delete the pack itself // Finally, delete the pack itself
await this.db.runAsync( console.log(`[POWRPackService] Deleting pack ${packId}`);
`DELETE FROM powr_packs WHERE id = ?`, await this.db.runAsync(
[packId] `DELETE FROM powr_packs WHERE id = ?`,
); [packId]
);
console.log(`[POWRPackService] Successfully deleted pack ${packId} with ${templates.length} templates and ${exerciseIdsToDelete.size} exercises`);
});
} catch (error) { } catch (error) {
console.error('Error deleting pack:', error); console.error(`[POWRPackService] Error deleting pack ${packId}:`, error);
throw error; throw error;
} }
} }

View File

@ -487,6 +487,14 @@ export class TemplateService {
try { try {
console.log(`Fetching exercises for template ${templateId}`); console.log(`Fetching exercises for template ${templateId}`);
// First, just count how many there should be
const countResult = await this.db.getFirstAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM template_exercises WHERE template_id = ?`,
[templateId]
);
console.log(`Expected template exercises: ${countResult?.count || 0}`);
// Now get the actual records
const exercises = await this.db.getAllAsync<{ const exercises = await this.db.getAllAsync<{
id: string; id: string;
exercise_id: string; exercise_id: string;
@ -507,6 +515,12 @@ export class TemplateService {
console.log(`Found ${exercises.length} template exercises in database`); console.log(`Found ${exercises.length} template exercises in database`);
if (exercises.length === 0) { if (exercises.length === 0) {
console.log(`No exercises found for template ${templateId} - verifying template exists...`);
const templateExists = await this.db.getFirstAsync<{ id: string }>(
`SELECT id FROM templates WHERE id = ?`,
[templateId]
);
console.log(`Template exists: ${templateExists ? 'Yes' : 'No'}`);
return []; return [];
} }
@ -514,6 +528,7 @@ export class TemplateService {
const result: TemplateExerciseWithData[] = []; const result: TemplateExerciseWithData[] = [];
for (const exerciseRow of exercises) { for (const exerciseRow of exercises) {
console.log(`Looking up exercise with ID: ${exerciseRow.exercise_id}`);
const exerciseData = await this.exerciseService.getExercise(exerciseRow.exercise_id); const exerciseData = await this.exerciseService.getExercise(exerciseRow.exercise_id);
if (exerciseData) { if (exerciseData) {
@ -527,6 +542,8 @@ export class TemplateService {
notes: exerciseRow.notes ?? undefined, // Convert null to undefined notes: exerciseRow.notes ?? undefined, // Convert null to undefined
nostrReference: exerciseRow.nostr_reference ?? undefined, // Convert null to undefined nostrReference: exerciseRow.nostr_reference ?? undefined, // Convert null to undefined
}); });
} else {
console.log(`⚠️ Could not find exercise with ID: ${exerciseRow.exercise_id}`);
} }
} }

View File

@ -62,9 +62,10 @@ export function usePOWRPacks() {
}, [powrPackService, loadPacks]); }, [powrPackService, loadPacks]);
// Delete a pack // Delete a pack
const deletePack = useCallback(async (packId: string, keepItems: boolean = false) => { const deletePack = useCallback(async (packId: string) => {
try { try {
await powrPackService.deletePack(packId, keepItems); // Always delete everything
await powrPackService.deletePack(packId, false);
await loadPacks(); // Refresh the list await loadPacks(); // Refresh the list
return true; return true;
} catch (error) { } catch (error) {

20
package-lock.json generated
View File

@ -50,6 +50,7 @@
"clsx": "^2.1.0", "clsx": "^2.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"expo": "^52.0.35", "expo": "^52.0.35",
"expo-av": "~15.0.2",
"expo-crypto": "~14.0.2", "expo-crypto": "~14.0.2",
"expo-dev-client": "~5.0.12", "expo-dev-client": "~5.0.12",
"expo-file-system": "~18.0.10", "expo-file-system": "~18.0.10",
@ -59,7 +60,7 @@
"expo-random": "^14.0.1", "expo-random": "^14.0.1",
"expo-router": "~4.0.16", "expo-router": "~4.0.16",
"expo-secure-store": "~14.0.1", "expo-secure-store": "~14.0.1",
"expo-splash-screen": "~0.29.20", "expo-splash-screen": "~0.29.22",
"expo-sqlite": "~15.1.2", "expo-sqlite": "~15.1.2",
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.8", "expo-system-ui": "~4.0.8",
@ -12208,6 +12209,23 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-av": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/expo-av/-/expo-av-15.0.2.tgz",
"integrity": "sha512-AHIHXdqLgK1dfHZF0JzX3YSVySGMrWn9QtPzaVjw54FAzvXfMt4sIoq4qRL/9XWCP9+ICcCs/u3EcvmxQjrfcA==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*",
"react-native-web": "*"
},
"peerDependenciesMeta": {
"react-native-web": {
"optional": true
}
}
},
"node_modules/expo-constants": { "node_modules/expo-constants": {
"version": "17.0.6", "version": "17.0.6",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.6.tgz", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.6.tgz",

View File

@ -73,7 +73,7 @@
"expo-random": "^14.0.1", "expo-random": "^14.0.1",
"expo-router": "~4.0.16", "expo-router": "~4.0.16",
"expo-secure-store": "~14.0.1", "expo-secure-store": "~14.0.1",
"expo-splash-screen": "~0.29.20", "expo-splash-screen": "~0.29.22",
"expo-sqlite": "~15.1.2", "expo-sqlite": "~15.1.2",
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.8", "expo-system-ui": "~4.0.8",
@ -97,7 +97,8 @@
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss": "3.3.5", "tailwindcss": "3.3.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zustand": "^4.5.6" "zustand": "^4.5.6",
"expo-av": "~15.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",