import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'video_batch_upload_queue.dart'; import 'video_batch_upload_task.dart'; class VideoBatchUploadPanel extends StatelessWidget { const VideoBatchUploadPanel({ super.key, required this.queue, }); final VideoBatchUploadQueue queue; @override Widget build(BuildContext context) { if (!queue.hasTasks) { return const SizedBox.shrink(); } return Positioned( left: 0, right: 0, bottom: 0, child: AnimatedSwitcher( duration: const Duration(milliseconds: 180), child: queue.isExpanded ? _ExpandedUploadPanel(queue: queue) : _CollapsedUploadBar(queue: queue), ), ); } } class _CollapsedUploadBar extends StatelessWidget { const _CollapsedUploadBar({required this.queue}); final VideoBatchUploadQueue queue; @override Widget build(BuildContext context) { return SafeArea( top: false, child: GestureDetector( onTap: queue.toggleExpanded, child: Container( height: 70.h, margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 18.h), padding: EdgeInsets.symmetric(horizontal: 22.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18.r), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.12), blurRadius: 18.r, offset: Offset(0, 6.h), ), ], ), child: Row( children: [ Icon( Icons.cloud_upload_outlined, color: const Color(0xFF46A5FF), size: 34.sp, ), SizedBox(width: 12.w), Expanded( child: Text( '正在上传${queue.assetType.label} ${queue.finishedCount}/${queue.totalCount}', style: TextStyle( fontSize: 24.sp, color: const Color(0xFF1F2937), fontWeight: FontWeight.w500, ), ), ), Icon( Icons.keyboard_arrow_up_rounded, color: const Color(0xFF667085), size: 34.sp, ), ], ), ), ), ); } } class _ExpandedUploadPanel extends StatelessWidget { const _ExpandedUploadPanel({required this.queue}); final VideoBatchUploadQueue queue; @override Widget build(BuildContext context) { final double bottomPadding = MediaQuery.of(context).padding.bottom; return Container( constraints: BoxConstraints(maxHeight: 560.h), padding: EdgeInsets.fromLTRB(24.w, 20.h, 24.w, 18.h + bottomPadding), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.12), blurRadius: 18.r, offset: Offset(0, -6.h), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded( child: Text( '上传${queue.assetType.label}', style: TextStyle( fontSize: 30.sp, color: const Color(0xFF111827), fontWeight: FontWeight.w600, ), ), ), Text( '${queue.finishedCount}/${queue.totalCount}', style: TextStyle( fontSize: 24.sp, color: const Color(0xFF667085), ), ), SizedBox(width: 10.w), GestureDetector( onTap: queue.toggleExpanded, behavior: HitTestBehavior.opaque, child: Icon( Icons.keyboard_arrow_down_rounded, color: const Color(0xFF667085), size: 38.sp, ), ), ], ), SizedBox(height: 16.h), Flexible( child: ListView.separated( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: queue.tasks.length, separatorBuilder: (_, __) => SizedBox(height: 12.h), itemBuilder: (_, int index) { final VideoBatchUploadTask task = queue.tasks[index]; return _UploadTaskCell( task: task, onRetry: () => queue.retryTask(task), onRemove: () => queue.removeTask(task), ); }, ), ), ], ), ); } } class _UploadTaskCell extends StatelessWidget { const _UploadTaskCell({ required this.task, required this.onRetry, required this.onRemove, }); final VideoBatchUploadTask task; final VoidCallback onRetry; final VoidCallback onRemove; @override Widget build(BuildContext context) { final Color statusColor = task.status == VideoBatchUploadStatus.failed ? const Color(0xFFE5484D) : const Color(0xFF667085); return Container( padding: EdgeInsets.all(12.w), decoration: BoxDecoration( color: const Color(0xFFF7F8FA), borderRadius: BorderRadius.circular(16.r), ), child: Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(12.r), child: SizedBox( width: 86.w, height: 86.w, child: task.thumbnail == null ? Container( color: const Color(0xFFEDEFF3), alignment: Alignment.center, child: Icon( task.assetType == VideoBatchUploadAssetType.video ? Icons.videocam_outlined : Icons.image_outlined, size: 46.sp, color: const Color(0xFF98A2B3), ), ) : Image.memory( task.thumbnail!, fit: BoxFit.cover, ), ), ), SizedBox(width: 14.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.fileName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 24.sp, color: const Color(0xFF1F2937), fontWeight: FontWeight.w500, ), ), SizedBox(height: 8.h), Text( _subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 20.sp, color: const Color(0xFF8D90A0), ), ), SizedBox(height: 10.h), ClipRRect( borderRadius: BorderRadius.circular(999), child: LinearProgressIndicator( value: task.progress.clamp(0, 1), minHeight: 6.h, backgroundColor: const Color(0xFFE5E7EB), valueColor: AlwaysStoppedAnimation( task.status == VideoBatchUploadStatus.failed ? const Color(0xFFE5484D) : const Color(0xFF46A5FF), ), ), ), ], ), ), SizedBox(width: 12.w), SizedBox( width: 96.w, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( task.status == VideoBatchUploadStatus.failed && task.errorMessage.isNotEmpty ? task.errorMessage : task.status.label, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, style: TextStyle( fontSize: 20.sp, color: statusColor, ), ), SizedBox(height: 12.h), _TaskActionButton( task: task, onRetry: onRetry, onRemove: onRemove, ), ], ), ), ], ), ); } String get _subtitle { final List parts = [ if (task.durationText.isNotEmpty) task.durationText, if (task.fileSizeText.isNotEmpty) task.fileSizeText, ]; return parts.isEmpty ? task.assetType.localLabel : parts.join(' · '); } } class _TaskActionButton extends StatelessWidget { const _TaskActionButton({ required this.task, required this.onRetry, required this.onRemove, }); final VideoBatchUploadTask task; final VoidCallback onRetry; final VoidCallback onRemove; @override Widget build(BuildContext context) { if (task.canRetry) { return GestureDetector( onTap: onRetry, behavior: HitTestBehavior.opaque, child: Text( '重试', style: TextStyle( fontSize: 22.sp, color: const Color(0xFF46A5FF), fontWeight: FontWeight.w500, ), ), ); } if (task.status == VideoBatchUploadStatus.completed) { return Icon( Icons.check_circle_rounded, size: 30.sp, color: const Color(0xFF12B76A), ); } return GestureDetector( onTap: onRemove, behavior: HitTestBehavior.opaque, child: Icon( task.status == VideoBatchUploadStatus.uploading ? Icons.close_rounded : Icons.delete_outline_rounded, size: 30.sp, color: const Color(0xFF98A2B3), ), ); } }