mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
splash screen, fixed delete exercises and templates when deleting powr pack bug, ui improvement to import powr pack screen
This commit is contained in:
parent
f1411e8568
commit
6ac0e80e45
8
app.json
8
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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string | 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 [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: () => (
|
||||
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
|
||||
<X {...getIconProps('primary')} size={24} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -192,10 +220,10 @@ export default function ImportPOWRPackScreen() {
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Input section */}
|
||||
<Card>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<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>
|
||||
<CardDescription>
|
||||
<Text className="text-muted-foreground">Paste a POWR Pack naddr to import</Text>
|
||||
@ -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"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
onPress={handleFetchPack}
|
||||
disabled={isLoading || !naddrInput.trim()}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Text className="text-primary-foreground">Fetch Pack</Text>
|
||||
)}
|
||||
</Button>
|
||||
{packData ? (
|
||||
// Success indicator when pack is loaded
|
||||
<View className="w-full flex-row items-center justify-center rounded-md p-3" style={{
|
||||
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} />
|
||||
<Text style={{
|
||||
color: COLORS.success,
|
||||
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>
|
||||
</Card>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<View className="mb-4 mt-4 p-4 bg-destructive/10 border border-destructive rounded-md flex-row items-center">
|
||||
<Text className="text-destructive ml-2">{error}</Text>
|
||||
<View className="mb-4 p-4 rounded-md flex-row items-center" style={{
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* Success message */}
|
||||
{importSuccess && (
|
||||
<View className="mb-4 mt-4 p-4 bg-green-50 border border-green-200 rounded-md flex-row items-center">
|
||||
<Text className="ml-2 text-green-800">Pack successfully imported!</Text>
|
||||
<View className="mb-4 p-4 rounded-md flex-row items-center" style={{
|
||||
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>
|
||||
)}
|
||||
|
||||
@ -250,7 +311,7 @@ export default function ImportPOWRPackScreen() {
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Text className="text-xl font-semibold">{getPackTitle()}</Text>
|
||||
<Text className="text-xl font-semibold text-foreground">{getPackTitle()}</Text>
|
||||
</CardTitle>
|
||||
{getPackDescription() ? (
|
||||
<CardDescription>
|
||||
@ -259,7 +320,7 @@ export default function ImportPOWRPackScreen() {
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="mb-2">Select items to import:</Text>
|
||||
<Text className="mb-2 text-foreground">Select items to import:</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -268,7 +329,7 @@ export default function ImportPOWRPackScreen() {
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Text className="text-lg font-semibold">Workout Templates</Text>
|
||||
<Text className="text-lg font-semibold text-foreground">Workout Templates</Text>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Text className="text-muted-foreground">{packData.templates.length} templates available</Text>
|
||||
@ -285,10 +346,14 @@ export default function ImportPOWRPackScreen() {
|
||||
handleTemplateChange(template.id, checked === true)
|
||||
}
|
||||
id={`template-${template.id}`}
|
||||
className="border-input"
|
||||
/>
|
||||
<Text className="ml-2 flex-1" onPress={() =>
|
||||
handleTemplateChange(template.id, !selectedTemplates.includes(template.id))
|
||||
}>
|
||||
<Text
|
||||
className="ml-3 flex-1 text-foreground"
|
||||
onPress={() =>
|
||||
handleTemplateChange(template.id, !selectedTemplates.includes(template.id))
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
@ -309,7 +374,7 @@ export default function ImportPOWRPackScreen() {
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Text className="text-lg font-semibold">Exercises</Text>
|
||||
<Text className="text-lg font-semibold text-foreground">Exercises</Text>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Text className="text-muted-foreground">{packData.exercises.length} exercises available</Text>
|
||||
@ -329,9 +394,10 @@ export default function ImportPOWRPackScreen() {
|
||||
}
|
||||
disabled={isRequired}
|
||||
id={`exercise-${exercise.id}`}
|
||||
className="border-input"
|
||||
/>
|
||||
<Text
|
||||
className={`ml-2 flex-1 ${isRequired ? 'font-medium' : ''}`}
|
||||
className={`ml-3 flex-1 ${isRequired ? 'font-medium' : ''} text-foreground`}
|
||||
onPress={() => {
|
||||
if (!isRequired) {
|
||||
handleExerciseChange(exercise.id, !selectedExercises.includes(exercise.id))
|
||||
@ -341,9 +407,11 @@ export default function ImportPOWRPackScreen() {
|
||||
{title}
|
||||
</Text>
|
||||
{isRequired && (
|
||||
<View style={styles.requiredBadge}>
|
||||
<InfoIcon size={14} color="#6b7280" />
|
||||
<Text className="text-xs text-gray-500 ml-1">Required</Text>
|
||||
<View style={[styles.requiredBadge, {
|
||||
backgroundColor: isDarkColorScheme ? COLORS.dark.muted : COLORS.light.muted
|
||||
}]}>
|
||||
<InfoIcon {...getIconProps('muted')} size={14} />
|
||||
<Text className="text-xs text-muted-foreground ml-1">Required</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@ -363,12 +431,13 @@ export default function ImportPOWRPackScreen() {
|
||||
<Button
|
||||
onPress={handleImport}
|
||||
disabled={isImporting || (selectedTemplates.length === 0 && selectedExercises.length === 0)}
|
||||
className="w-full"
|
||||
className="w-full mb-8"
|
||||
style={{ backgroundColor: COLORS.purple.DEFAULT }}
|
||||
>
|
||||
{isImporting ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<Text className="text-primary-foreground">
|
||||
<Text style={{ color: '#fff', fontWeight: '500' }}>
|
||||
Import {selectedTemplates.length + selectedExercises.length} Items
|
||||
</Text>
|
||||
)}
|
||||
@ -389,25 +458,27 @@ const styles = StyleSheet.create({
|
||||
paddingBottom: 80,
|
||||
},
|
||||
input: {
|
||||
marginBottom: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
packContent: {
|
||||
marginTop: 16,
|
||||
marginTop: 8,
|
||||
},
|
||||
itemRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#f0f0f0',
|
||||
},
|
||||
requiredBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f9fafb',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
closeButton: {
|
||||
padding: 8,
|
||||
}
|
||||
});
|
@ -8,10 +8,12 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
import { POWRPackWithContent } from '@/types/powr-pack';
|
||||
// Fix database context import
|
||||
import { usePOWRPackService } from '@/components/DatabaseProvider';
|
||||
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() {
|
||||
const powrPackService = usePOWRPackService();
|
||||
@ -19,7 +21,8 @@ export default function ManagePOWRPacksScreen() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [keepItems, setKeepItems] = useState(true);
|
||||
const { getIconProps } = useIconColor();
|
||||
const { isDarkColorScheme } = useColorScheme();
|
||||
|
||||
// Load imported packs
|
||||
useEffect(() => {
|
||||
@ -44,6 +47,11 @@ export default function ManagePOWRPacksScreen() {
|
||||
router.push('/(packs)/import');
|
||||
};
|
||||
|
||||
// Handle close button press
|
||||
const handleClose = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// Handle delete button click
|
||||
const handleDeleteClick = (packId: string) => {
|
||||
setSelectedPackId(packId);
|
||||
@ -53,9 +61,10 @@ export default function ManagePOWRPacksScreen() {
|
||||
// Handle delete confirmation
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!selectedPackId) return;
|
||||
|
||||
|
||||
try {
|
||||
await powrPackService.deletePack(selectedPackId, keepItems);
|
||||
// Always delete everything (we no longer need the keepItems parameter)
|
||||
await powrPackService.deletePack(selectedPackId, false);
|
||||
// Refresh the list
|
||||
loadPacks();
|
||||
} catch (error) {
|
||||
@ -77,34 +86,41 @@ export default function ManagePOWRPacksScreen() {
|
||||
options={{
|
||||
title: 'Manage POWR Packs',
|
||||
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}>
|
||||
{/* Import button - fix icon usage */}
|
||||
{/* Import button */}
|
||||
<Button
|
||||
onPress={handleImport}
|
||||
className="mb-4"
|
||||
style={{ backgroundColor: COLORS.purple.DEFAULT }}
|
||||
>
|
||||
<Plus size={18} color="#fff" className="mr-2" />
|
||||
<Text className="text-primary-foreground">Import New Pack</Text>
|
||||
<Plus size={18} color="#fff" style={{ marginRight: 8 }} />
|
||||
<Text style={{ color: '#fff', fontWeight: '500' }}>Import New Pack</Text>
|
||||
</Button>
|
||||
|
||||
{/* No packs message */}
|
||||
{!isLoading && packs.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="py-8 items-center">
|
||||
<PackageOpen size={48} color="#6b7280" />
|
||||
<Text className="text-lg font-medium mt-4 text-center">No POWR Packs Imported</Text>
|
||||
<Text className="text-center mt-2 text-gray-500">
|
||||
<PackageOpen size={48} {...getIconProps('muted')} />
|
||||
<Text className="text-lg font-medium mt-4 text-center text-foreground">No POWR Packs Imported</Text>
|
||||
<Text className="text-center mt-2 text-muted-foreground">
|
||||
Import workout packs shared by the community to get started.
|
||||
</Text>
|
||||
<Button
|
||||
onPress={handleImport}
|
||||
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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -120,7 +136,7 @@ export default function ManagePOWRPacksScreen() {
|
||||
<View style={styles.cardHeaderContent}>
|
||||
<View style={styles.cardHeaderText}>
|
||||
<CardTitle>
|
||||
<Text className="text-lg font-semibold">{pack.title}</Text>
|
||||
<Text className="text-lg font-semibold text-foreground">{pack.title}</Text>
|
||||
</CardTitle>
|
||||
{pack.description && (
|
||||
<CardDescription>
|
||||
@ -132,18 +148,18 @@ export default function ManagePOWRPacksScreen() {
|
||||
onPress={() => handleDeleteClick(pack.id)}
|
||||
style={styles.deleteButton}
|
||||
>
|
||||
<Trash2 size={20} color="#ef4444" />
|
||||
<Trash2 {...getIconProps('destructive')} size={20} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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' : ''}
|
||||
</Text>
|
||||
</View>
|
||||
<Separator className="my-2" />
|
||||
<Text className="text-sm text-gray-500">
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
Imported {formatImportDate(pack.importDate)}
|
||||
</Text>
|
||||
</CardContent>
|
||||
@ -156,47 +172,21 @@ export default function ManagePOWRPacksScreen() {
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Pack?</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
<Text>Delete Pack</Text>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Text>
|
||||
This will remove the POWR Pack from your library. Do you want to keep the imported exercises and templates?
|
||||
</Text>
|
||||
<Text>This will remove the POWR Pack and all its associated templates and exercises from your library.</Text>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<View style={styles.dialogOptions}>
|
||||
<Button
|
||||
variant={keepItems ? "default" : "outline"}
|
||||
onPress={() => setKeepItems(true)}
|
||||
className="flex-1 mr-2"
|
||||
>
|
||||
<Text className={keepItems ? "text-primary-foreground" : ""}>
|
||||
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>
|
||||
<View className="flex-row justify-center gap-3 px-4 mt-2">
|
||||
<AlertDialogCancel onPress={() => setShowDeleteDialog(false)}>
|
||||
<Text>Cancel</Text>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onPress={handleDeleteConfirm} className='bg-destructive'>
|
||||
<Text className='text-destructive-foreground'>Delete Pack</Text>
|
||||
</AlertDialogAction>
|
||||
</View>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</View>
|
||||
@ -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,
|
||||
}
|
||||
});
|
118
app/_layout.tsx
118
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 (
|
||||
<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');
|
||||
|
||||
@ -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<Promise<void> | 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 <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) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background">
|
||||
<Text className="text-foreground">Initializing...</Text>
|
||||
<Text className="text-foreground">Finalizing setup...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Main app UI wrapped in error boundary
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<DatabaseProvider>
|
||||
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
||||
{/* Ensure SettingsDrawerProvider wraps everything */}
|
||||
<SettingsDrawerProvider>
|
||||
{/* Add RelayInitializer here - it loads relay data once NDK is available */}
|
||||
<RelayInitializer />
|
||||
|
BIN
assets/splash.mov
Normal file
BIN
assets/splash.mov
Normal file
Binary file not shown.
BIN
assets/v1-splash.mov
Normal file
BIN
assets/v1-splash.mov
Normal file
Binary file not shown.
64
components/SimpleSplashScreen.tsx
Normal file
64
components/SimpleSplashScreen.tsx
Normal 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;
|
102
components/VideoSplashScreen.tsx
Normal file
102
components/VideoSplashScreen.tsx
Normal 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;
|
@ -83,14 +83,12 @@ export default function POWRPackSection() {
|
||||
relays
|
||||
});
|
||||
|
||||
// Copy to clipboard
|
||||
Clipboard.setString(naddr);
|
||||
// Navigate to import screen with the naddr as a parameter
|
||||
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) {
|
||||
console.error('Error handling pack click:', error);
|
||||
alert('Failed to prepare pack for import. Please try again.');
|
||||
|
@ -1,6 +1,9 @@
|
||||
PODS:
|
||||
- boost (1.84.0)
|
||||
- DoubleConversion (1.1.6)
|
||||
- EXAV (15.0.2):
|
||||
- ExpoModulesCore
|
||||
- ReactCommon/turbomodule/core
|
||||
- EXConstants (17.0.6):
|
||||
- ExpoModulesCore
|
||||
- EXJSONUtils (0.14.0)
|
||||
@ -2110,6 +2113,7 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.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`)
|
||||
- EXJSONUtils (from `../node_modules/expo-json-utils/ios`)
|
||||
- EXManifests (from `../node_modules/expo-manifests/ios`)
|
||||
@ -2215,6 +2219,8 @@ EXTERNAL SOURCES:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||
DoubleConversion:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
EXAV:
|
||||
:path: "../node_modules/expo-av/ios"
|
||||
EXConstants:
|
||||
:path: "../node_modules/expo-constants/ios"
|
||||
EXJSONUtils:
|
||||
@ -2406,6 +2412,7 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
|
||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||
EXAV: 4c41f0a6ae54a0b96597abc90e37962ab2ee0d85
|
||||
EXConstants: f5c27bfa40ba650257535ab958f4f5876ee93edd
|
||||
EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93
|
||||
EXManifests: 807ab5394ca9f8dd5e64283f02876b2f85c4eb72
|
||||
|
@ -537,7 +537,10 @@ export class NostrIntegration {
|
||||
exerciseRefs: string[]
|
||||
): Promise<void> {
|
||||
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
|
||||
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`);
|
||||
}
|
||||
|
||||
// Clear out any existing entries for this template
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM template_exercises WHERE template_id = ?`,
|
||||
[templateId]
|
||||
);
|
||||
|
||||
// Create template exercise records
|
||||
for (let i = 0; i < exerciseIds.length; 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(`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) {
|
||||
console.error('Error saving template exercises with parameters:', error);
|
||||
throw error;
|
||||
|
@ -417,7 +417,8 @@ export default class POWRPackService {
|
||||
*/
|
||||
async importPack(packImport: POWRPackImport, selection: POWRPackSelection): Promise<void> {
|
||||
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
|
||||
const exerciseIdMap = new Map<string, string>();
|
||||
@ -425,7 +426,10 @@ export default class POWRPackService {
|
||||
// First, import the selected exercises
|
||||
for (const exerciseId of selection.selectedExercises) {
|
||||
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
|
||||
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(`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
|
||||
for (const templateId of selection.selectedTemplates) {
|
||||
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
|
||||
const templateModel = this.nostrIntegration.convertNostrTemplateToLocal(templateEvent);
|
||||
@ -481,6 +490,10 @@ export default class POWRPackService {
|
||||
const templateExerciseIds: 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) {
|
||||
// Extract the base reference (before any parameters)
|
||||
const refParts = ref.split('::');
|
||||
@ -496,12 +509,12 @@ export default class POWRPackService {
|
||||
templateExerciseIds.push(localExerciseId);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
const refSegments = baseRef.split(':');
|
||||
@ -510,44 +523,53 @@ export default class POWRPackService {
|
||||
const refPubkey = refSegments[1];
|
||||
const refDTag = refSegments[2];
|
||||
|
||||
// Try to find the matching exercise by looking at both event ID and d-tag
|
||||
for (const [key, value] of exerciseIdMap.entries()) {
|
||||
// Check if this is potentially the same exercise with a different reference format
|
||||
if (key.includes(refPubkey) && (key.includes(refDTag) || key.endsWith(refDTag))) {
|
||||
templateExerciseIds.push(value);
|
||||
matchedRefs.push(ref);
|
||||
|
||||
// Also add this reference format to map for future lookups
|
||||
exerciseIdMap.set(baseRef, value);
|
||||
|
||||
console.log(`Found potential match using partial comparison: ${key} -> ${value}`);
|
||||
break;
|
||||
}
|
||||
// Log all entries in exerciseIdMap for debugging
|
||||
console.log(`Looking among ${exerciseIdMap.size} existing mappings:`);
|
||||
[...exerciseIdMap.entries()].forEach(([key, value]) => {
|
||||
console.log(` - ${key} -> ${value}`);
|
||||
});
|
||||
|
||||
// Try to find the matching exercise by direct dTag match
|
||||
const directDTagMatch = [...exerciseIdMap.entries()].find(([key, _]) => {
|
||||
return key.includes(refPubkey) && key.includes(refDTag);
|
||||
});
|
||||
|
||||
if (directDTagMatch) {
|
||||
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
|
||||
if (templateExerciseIds.length === templateExerciseIds.lastIndexOf(refDTag) + 1) {
|
||||
// Didn't add anything in the above loop, try direct event ID lookup
|
||||
const matchingEvent = packImport.exercises.find(e => e.id === refDTag);
|
||||
// Try to find the matching exercise by event ID
|
||||
const matchingEvent = packImport.exercises.find(e => e.id === refDTag);
|
||||
|
||||
if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) {
|
||||
const localExerciseId = exerciseIdMap.get(matchingEvent.id) || '';
|
||||
templateExerciseIds.push(localExerciseId);
|
||||
matchedRefs.push(ref);
|
||||
|
||||
if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) {
|
||||
const localExerciseId = exerciseIdMap.get(matchingEvent.id) || '';
|
||||
templateExerciseIds.push(localExerciseId);
|
||||
matchedRefs.push(ref);
|
||||
|
||||
// Add this reference to our map for future use
|
||||
exerciseIdMap.set(baseRef, localExerciseId);
|
||||
|
||||
console.log(`Found match by event ID: ${matchingEvent.id} -> ${localExerciseId}`);
|
||||
} else {
|
||||
console.log(`No matching exercise found for reference components: kind=${refKind}, pubkey=${refPubkey}, d-tag=${refDTag}`);
|
||||
}
|
||||
// Add this reference to our map for future use
|
||||
exerciseIdMap.set(baseRef, localExerciseId);
|
||||
|
||||
console.log(`✅ Found match by event ID: ${matchingEvent.id} -> ${localExerciseId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`❌ No matching exercise found for reference: ${baseRef}`);
|
||||
} 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
|
||||
if (templateExerciseIds.length > 0) {
|
||||
await this.nostrIntegration.saveTemplateExercisesWithParams(
|
||||
@ -564,7 +586,7 @@ export default class POWRPackService {
|
||||
);
|
||||
console.log(`Template ${templateModel.title} has ${templateExercises.length} exercises associated`);
|
||||
} 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'`
|
||||
);
|
||||
|
||||
console.log(`Template IDs:`);
|
||||
console.log(`Imported Template IDs:`);
|
||||
templates.forEach(t => {
|
||||
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) {
|
||||
console.error('Error importing pack:', 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 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 {
|
||||
if (!keepItems) {
|
||||
// Get all templates and exercises from this pack
|
||||
const templates = await this.db.getAllAsync<{ id: string }>(
|
||||
`SELECT t.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'`,
|
||||
console.log(`[POWRPackService] Starting deletion of pack ${packId}`);
|
||||
|
||||
// Always delete everything, ignore keepItems parameter
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
// First, get all templates and exercises from this pack
|
||||
console.log(`[POWRPackService] Finding templates associated with pack ${packId}`);
|
||||
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]
|
||||
);
|
||||
console.log(`[POWRPackService] Found ${templates.length} templates to delete`);
|
||||
|
||||
const exercises = await this.db.getAllAsync<{ id: string }>(
|
||||
`SELECT e.id
|
||||
FROM exercises e
|
||||
JOIN powr_pack_items ppi ON ppi.item_id = e.nostr_event_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise'`,
|
||||
console.log(`[POWRPackService] Finding exercises associated with pack ${packId}`);
|
||||
const exercises = await this.db.getAllAsync<{ id: string, nostr_event_id: string }>(
|
||||
`SELECT e.id, e.nostr_event_id
|
||||
FROM exercises e
|
||||
JOIN powr_pack_items ppi ON ppi.item_id = e.nostr_event_id
|
||||
WHERE ppi.pack_id = ? AND ppi.item_type = 'exercise'`,
|
||||
[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) {
|
||||
console.log(`[POWRPackService] Deleting template ${template.id}`);
|
||||
|
||||
// Delete template exercises first
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM template_exercises WHERE template_id = ?`,
|
||||
[template.id]
|
||||
);
|
||||
|
||||
// Then delete the template
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM templates WHERE 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
|
||||
for (const exercise of exercises) {
|
||||
for (const exerciseId of exerciseIdsToDelete) {
|
||||
console.log(`[POWRPackService] Deleting exercise ${exerciseId}`);
|
||||
|
||||
// Delete exercise tags first
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM exercise_tags WHERE exercise_id = ?`,
|
||||
[exercise.id]
|
||||
[exerciseId]
|
||||
);
|
||||
|
||||
// Then delete the exercise
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM exercises WHERE id = ?`,
|
||||
[exercise.id]
|
||||
[exerciseId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the pack items
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM powr_pack_items WHERE pack_id = ?`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
// Finally, delete the pack itself
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM powr_packs WHERE id = ?`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
// Delete the pack items
|
||||
console.log(`[POWRPackService] Deleting pack items for pack ${packId}`);
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM powr_pack_items WHERE pack_id = ?`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
// Finally, delete the pack itself
|
||||
console.log(`[POWRPackService] Deleting pack ${packId}`);
|
||||
await this.db.runAsync(
|
||||
`DELETE FROM powr_packs WHERE id = ?`,
|
||||
[packId]
|
||||
);
|
||||
|
||||
console.log(`[POWRPackService] Successfully deleted pack ${packId} with ${templates.length} templates and ${exerciseIdsToDelete.size} exercises`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting pack:', error);
|
||||
console.error(`[POWRPackService] Error deleting pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -487,6 +487,14 @@ export class TemplateService {
|
||||
try {
|
||||
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<{
|
||||
id: string;
|
||||
exercise_id: string;
|
||||
@ -507,6 +515,12 @@ export class TemplateService {
|
||||
console.log(`Found ${exercises.length} template exercises in database`);
|
||||
|
||||
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 [];
|
||||
}
|
||||
|
||||
@ -514,6 +528,7 @@ export class TemplateService {
|
||||
const result: TemplateExerciseWithData[] = [];
|
||||
|
||||
for (const exerciseRow of exercises) {
|
||||
console.log(`Looking up exercise with ID: ${exerciseRow.exercise_id}`);
|
||||
const exerciseData = await this.exerciseService.getExercise(exerciseRow.exercise_id);
|
||||
|
||||
if (exerciseData) {
|
||||
@ -527,6 +542,8 @@ export class TemplateService {
|
||||
notes: exerciseRow.notes ?? 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,9 +62,10 @@ export function usePOWRPacks() {
|
||||
}, [powrPackService, loadPacks]);
|
||||
|
||||
// Delete a pack
|
||||
const deletePack = useCallback(async (packId: string, keepItems: boolean = false) => {
|
||||
const deletePack = useCallback(async (packId: string) => {
|
||||
try {
|
||||
await powrPackService.deletePack(packId, keepItems);
|
||||
// Always delete everything
|
||||
await powrPackService.deletePack(packId, false);
|
||||
await loadPacks(); // Refresh the list
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -50,6 +50,7 @@
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"expo": "^52.0.35",
|
||||
"expo-av": "~15.0.2",
|
||||
"expo-crypto": "~14.0.2",
|
||||
"expo-dev-client": "~5.0.12",
|
||||
"expo-file-system": "~18.0.10",
|
||||
@ -59,7 +60,7 @@
|
||||
"expo-random": "^14.0.1",
|
||||
"expo-router": "~4.0.16",
|
||||
"expo-secure-store": "~14.0.1",
|
||||
"expo-splash-screen": "~0.29.20",
|
||||
"expo-splash-screen": "~0.29.22",
|
||||
"expo-sqlite": "~15.1.2",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "~4.0.8",
|
||||
@ -12208,6 +12209,23 @@
|
||||
"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": {
|
||||
"version": "17.0.6",
|
||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.6.tgz",
|
||||
|
@ -73,7 +73,7 @@
|
||||
"expo-random": "^14.0.1",
|
||||
"expo-router": "~4.0.16",
|
||||
"expo-secure-store": "~14.0.1",
|
||||
"expo-splash-screen": "~0.29.20",
|
||||
"expo-splash-screen": "~0.29.22",
|
||||
"expo-sqlite": "~15.1.2",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-system-ui": "~4.0.8",
|
||||
@ -97,7 +97,8 @@
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss": "3.3.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.5.6"
|
||||
"zustand": "^4.5.6",
|
||||
"expo-av": "~15.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
|
Loading…
x
Reference in New Issue
Block a user