import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/upload_task.dart'; import 'package:island/pods/upload_tasks.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:easy_localization/easy_localization.dart'; class UploadOverlay extends HookConsumerWidget { const UploadOverlay({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final uploadTasks = ref.watch(uploadTasksProvider); final activeTasks = uploadTasks .where( (task) => task.status == UploadTaskStatus.pending || task.status == UploadTaskStatus.inProgress || task.status == UploadTaskStatus.paused, ) .toList(); if (activeTasks.isEmpty) { return const SizedBox.shrink(); } return Positioned( bottom: 16, right: 16, child: Material( elevation: 8, borderRadius: BorderRadius.circular(12), color: Theme.of(context).colorScheme.surfaceContainer, child: Container( width: 320, constraints: BoxConstraints(maxHeight: 400), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), ), ), child: Row( children: [ Icon( Symbols.upload, size: 20, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Text( 'uploadTasks'.tr(), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const Spacer(), Text( '${activeTasks.length}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w600, ), ), ], ), ), // Task list Flexible( child: ListView.builder( shrinkWrap: true, itemCount: activeTasks.length, itemBuilder: (context, index) { final task = activeTasks[index]; return UploadTaskTile(task: task); }, ), ), ], ), ), ), ); } } class UploadTaskTile extends HookConsumerWidget { final UploadTask task; const UploadTaskTile({super.key, required this.task}); @override Widget build(BuildContext context, WidgetRef ref) { final isExpanded = useState(false); return InkWell( onTap: () => isExpanded.value = !isExpanded.value, child: Container( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // Status icon _buildStatusIcon(context), const SizedBox(width: 8), // File info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.fileName, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( _formatFileSize(task.fileSize), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), // Progress indicator const SizedBox(width: 8), SizedBox( width: 40, height: 40, child: CircularProgressIndicator( value: task.progress, strokeWidth: 3, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, ), ), // Expand/collapse button IconButton( icon: Icon( isExpanded.value ? Symbols.expand_less : Symbols.expand_more, size: 16, ), onPressed: () => isExpanded.value = !isExpanded.value, padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), // Expanded details if (isExpanded.value) ...[ const SizedBox(height: 8), _buildExpandedDetails(context), ], ], ), ), ); } Widget _buildStatusIcon(BuildContext context) { IconData icon; Color color; switch (task.status) { case UploadTaskStatus.pending: icon = Symbols.schedule; color = Theme.of(context).colorScheme.secondary; break; case UploadTaskStatus.inProgress: icon = Symbols.upload; color = Theme.of(context).colorScheme.primary; break; case UploadTaskStatus.paused: icon = Symbols.pause_circle; color = Theme.of(context).colorScheme.tertiary; break; case UploadTaskStatus.completed: icon = Symbols.check_circle; color = Colors.green; break; case UploadTaskStatus.failed: icon = Symbols.error; color = Theme.of(context).colorScheme.error; break; case UploadTaskStatus.cancelled: icon = Symbols.cancel; color = Theme.of(context).colorScheme.error; break; case UploadTaskStatus.expired: icon = Symbols.timer_off; color = Theme.of(context).colorScheme.error; break; } return Icon(icon, size: 16, color: color); } Widget _buildExpandedDetails(BuildContext context) { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Progress text Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${(task.progress * 100).toStringAsFixed(1)}%', style: Theme.of( context, ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600), ), Text( '${task.uploadedChunks}/${task.totalChunks} chunks', style: Theme.of(context).textTheme.bodySmall, ), ], ), const SizedBox(height: 4), // Progress bar LinearProgressIndicator( value: task.progress, backgroundColor: Theme.of(context).colorScheme.surface, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 4), // Speed and ETA Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _formatBytesPerSecond(task), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), if (task.status == UploadTaskStatus.inProgress) Text( 'ETA: ${_formatDuration(task.estimatedTimeRemaining)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), // Error message if failed if (task.errorMessage != null) ...[ const SizedBox(height: 4), Text( task.errorMessage!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.error, ), ), ], ], ), ); } String _formatFileSize(int bytes) { if (bytes >= 1073741824) { return '${(bytes / 1073741824).toStringAsFixed(1)} GB'; } else if (bytes >= 1048576) { return '${(bytes / 1048576).toStringAsFixed(1)} MB'; } else if (bytes >= 1024) { return '${(bytes / 1024).toStringAsFixed(1)} KB'; } else { return '$bytes bytes'; } } String _formatBytesPerSecond(UploadTask task) { if (task.uploadedBytes == 0) return '0 B/s'; final elapsedSeconds = DateTime.now().difference(task.createdAt).inSeconds; if (elapsedSeconds == 0) return '0 B/s'; final bytesPerSecond = task.uploadedBytes / elapsedSeconds; return '${_formatFileSize(bytesPerSecond.toInt())}/s'; } String _formatDuration(Duration duration) { if (duration.inHours > 0) { return '${duration.inHours}h ${duration.inMinutes.remainder(60)}m'; } else if (duration.inMinutes > 0) { return '${duration.inMinutes}m ${duration.inSeconds.remainder(60)}s'; } else { return '${duration.inSeconds}s'; } } }