managarten/picture/apps/mobile/components/batch/BatchProgressTracker.tsx
Till-JS c712a2504a feat: integrate uload and picture, unify package naming
- Add uload project with apps/web structure
  - Reorganize from flat to monorepo structure
  - Remove PocketBase binary and local data
  - Update to pnpm and @uload/web namespace

- Add picture project to monorepo
  - Remove embedded git repository

- Unify all package names to @{project}/{app} schema:
  - @maerchenzauber/* (was @storyteller/*)
  - @manacore/* (was manacore-*, manacore)
  - @manadeck/* (was web, backend, manadeck)
  - @memoro/* (was memoro-web, landing, memoro)
  - @picture/* (already unified)
  - @uload/web

- Add convenient dev scripts for all apps:
  - pnpm dev:{project}:web
  - pnpm dev:{project}:landing
  - pnpm dev:{project}:mobile
  - pnpm dev:{project}:backend

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 04:00:36 +01:00

263 lines
No EOL
8 KiB
TypeScript

import React, { useEffect } from 'react';
import {
View,
Text,
ScrollView,
Pressable,
ActivityIndicator
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useBatchStore } from '~/store/batchStore';
import { router } from 'expo-router';
interface BatchProgressTrackerProps {
batchId: string;
onComplete?: () => void;
onItemClick?: (itemId: string) => void;
compact?: boolean;
}
export function BatchProgressTracker({
batchId,
onComplete,
onItemClick,
compact = false
}: BatchProgressTrackerProps) {
const {
activeBatches,
loadBatch,
subscribeToBatch,
unsubscribeFromBatch,
retryFailed,
cancelBatch
} = useBatchStore();
const batch = activeBatches.get(batchId);
useEffect(() => {
// Load and subscribe to batch
loadBatch(batchId);
subscribeToBatch(batchId);
return () => {
unsubscribeFromBatch(batchId);
};
}, [batchId]);
useEffect(() => {
// Check if batch is complete
if (batch && batch.status === 'completed' && onComplete) {
onComplete();
}
}, [batch?.status]);
if (!batch) {
return (
<View className="p-4 bg-dark-surface rounded-lg">
<ActivityIndicator size="small" color="#818cf8" />
</View>
);
}
const progressPercentage = batch.total_count > 0
? ((batch.completed_count + batch.failed_count) / batch.total_count) * 100
: 0;
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return <Ionicons name="checkmark-circle" size={20} color="#10b981" />;
case 'failed':
return <Ionicons name="close-circle" size={20} color="#ef4444" />;
case 'processing':
return <ActivityIndicator size="small" color="#818cf8" />;
case 'pending':
return <Ionicons name="time-outline" size={20} color="#6b7280" />;
default:
return null;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'text-green-500';
case 'failed':
return 'text-red-500';
case 'processing':
return 'text-indigo-500';
default:
return 'text-gray-500';
}
};
if (compact) {
// Compact view for in-screen display
return (
<View className="bg-dark-surface rounded-lg p-3 mb-2">
<View className="flex-row items-center justify-between mb-2">
<Text className="text-sm font-semibold text-white">
{batch.name || 'Batch Generation'}
</Text>
<Text className={`text-xs ${getStatusColor(batch.status)}`}>
{batch.completed_count}/{batch.total_count}
</Text>
</View>
{/* Progress Bar */}
<View className="h-2 bg-dark-input rounded-full overflow-hidden">
<View
className="h-full bg-indigo-600 rounded-full"
style={{ width: `${progressPercentage}%` }}
/>
</View>
{batch.failed_count > 0 && (
<Pressable
onPress={() => retryFailed(batchId)}
className="mt-2 flex-row items-center"
>
<Ionicons name="refresh" size={14} color="#ef4444" />
<Text className="text-xs text-red-500 ml-1">
{batch.failed_count} fehlgeschlagen - Wiederholen
</Text>
</Pressable>
)}
</View>
);
}
// Full view
return (
<View className="bg-dark-surface rounded-lg p-4">
{/* Header */}
<View className="mb-4">
<Text className="text-lg font-bold text-white mb-1">
{batch.name || 'Batch Generation'}
</Text>
<Text className="text-sm text-gray-400">
Status: <Text className={getStatusColor(batch.status)}>{batch.status}</Text>
</Text>
</View>
{/* Overall Progress */}
<View className="mb-4">
<View className="flex-row justify-between mb-2">
<Text className="text-sm text-gray-400">
Gesamt: {progressPercentage.toFixed(0)}%
</Text>
<Text className="text-sm text-gray-400">
{batch.completed_count + batch.failed_count}/{batch.total_count}
</Text>
</View>
<View className="h-3 bg-dark-input rounded-full overflow-hidden">
<View
className="h-full bg-indigo-600 rounded-full"
style={{ width: `${progressPercentage}%` }}
/>
</View>
</View>
{/* Statistics */}
<View className="flex-row justify-around mb-4 py-2 border-y border-dark-border">
<View className="items-center">
<Text className="text-2xl font-bold text-green-500">
{batch.completed_count}
</Text>
<Text className="text-xs text-gray-400">Fertig</Text>
</View>
<View className="items-center">
<Text className="text-2xl font-bold text-indigo-500">
{batch.processing_count || 0}
</Text>
<Text className="text-xs text-gray-400">Läuft</Text>
</View>
<View className="items-center">
<Text className="text-2xl font-bold text-gray-500">
{batch.pending_count || 0}
</Text>
<Text className="text-xs text-gray-400">Wartend</Text>
</View>
<View className="items-center">
<Text className="text-2xl font-bold text-red-500">
{batch.failed_count}
</Text>
<Text className="text-xs text-gray-400">Fehler</Text>
</View>
</View>
{/* Items List */}
{batch.items && batch.items.length > 0 && (
<ScrollView className="max-h-64">
{batch.items.map((item, index) => (
<Pressable
key={item.id}
onPress={() => {
if (item.status === 'completed' && onItemClick) {
onItemClick(item.id);
}
}}
disabled={item.status !== 'completed'}
className="flex-row items-center py-2 border-b border-dark-border"
>
<Text className="text-gray-400 mr-3 min-w-[20px]">
{index + 1}.
</Text>
<View className="mr-3">
{getStatusIcon(item.status)}
</View>
<Text
className="flex-1 text-sm text-gray-300"
numberOfLines={1}
>
{item.prompt}
</Text>
{item.retry_count && item.retry_count > 0 && (
<Text className="text-xs text-yellow-500 ml-2">
Retry {item.retry_count}
</Text>
)}
</Pressable>
))}
</ScrollView>
)}
{/* Actions */}
<View className="flex-row justify-between mt-4">
{batch.status === 'processing' && (
<Pressable
onPress={() => cancelBatch(batchId)}
className="flex-row items-center px-3 py-2 bg-red-900/20 rounded-lg"
>
<Ionicons name="stop-circle-outline" size={16} color="#ef4444" />
<Text className="ml-1 text-sm text-red-500">Abbrechen</Text>
</Pressable>
)}
{batch.failed_count > 0 && (
<Pressable
onPress={() => retryFailed(batchId)}
className="flex-row items-center px-3 py-2 bg-yellow-900/20 rounded-lg"
>
<Ionicons name="refresh-outline" size={16} color="#eab308" />
<Text className="ml-1 text-sm text-yellow-500">
Fehler wiederholen
</Text>
</Pressable>
)}
{batch.status === 'completed' && (
<Pressable
onPress={() => router.push('/(tabs)')}
className="flex-row items-center px-3 py-2 bg-green-900/20 rounded-lg"
>
<Ionicons name="images-outline" size={16} color="#10b981" />
<Text className="ml-1 text-sm text-green-500">
Zur Galerie
</Text>
</Pressable>
)}
</View>
</View>
);
}