import 'package:flutter/material.dart'; import '../about/internal/application/TextType.dart'; import '../comms/Comms.dart' show TransferProgress; import '../utils/Colors.dart'; import 'text/Text.dart'; /// A descriptive, non-flat transfer panel: big percentage, a gradient progress /// bar, and live stats (transferred, speed, ETA). class TransferCard extends StatelessWidget { final TransferProgress progress; const TransferCard({super.key, required this.progress}); @override Widget build(BuildContext context) { final jp = context.jp; final pct = progress.fraction == null ? null : (progress.fraction! * 100).clamp(0, 100); final verb = progress.uploading ? 'Uploading' : 'Downloading'; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: jp.surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: PeachTheme.seed.withValues(alpha: 0.5)), boxShadow: [ ...PeachTheme.softShadow(Theme.of(context).brightness), ...PeachTheme.glow(PeachTheme.seed, opacity: 0.18, blur: 30), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Icon( progress.uploading ? Icons.cloud_upload : Icons.cloud_download, color: PeachTheme.seed, size: 22, ), const SizedBox(width: 8), text( verb, 15, TextType.Regular, color: jp.text, weight: FontWeight.w700, ), const Spacer(), text( pct == null ? '—' : '${pct.toStringAsFixed(0)}%', 26, TextType.Regular, color: PeachTheme.seed, height: 1, weight: FontWeight.w800, ), ], ), const SizedBox(height: 12), ClipRRect( borderRadius: BorderRadius.circular(6), child: Stack( children: [ Container(height: 10, color: jp.surface2), FractionallySizedBox( widthFactor: (progress.fraction ?? 0).clamp(0.0, 1.0), child: Container( height: 10, decoration: const BoxDecoration( gradient: LinearGradient( colors: [PeachTheme.seed, PeachTheme.accent], ), ), ), ), ], ), ), const SizedBox(height: 14), Row( children: [ _stat( jp, Icons.data_usage, 'Transferred', '${_bytes(progress.sent)} / ${_bytes(progress.total)}', ), _sep(jp), _stat( jp, Icons.speed, 'Speed', '${_bytes(progress.bytesPerSec.round())}/s', ), _sep(jp), _stat(jp, Icons.timer_outlined, 'ETA', _eta(progress.eta)), ], ), ], ), ); } Widget _sep(JpColors jp) => Container(width: 1, height: 30, color: jp.border); Widget _stat(JpColors jp, IconData icon, String label, String value) { return Expanded( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 12, color: jp.textFaint), const SizedBox(width: 4), text( label.toUpperCase(), 9, TextType.Regular, color: jp.textFaint, letterSpacing: 0.8, weight: FontWeight.w700, ), ], ), const SizedBox(height: 3), Text( value, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12.5, fontFamily: 'monospace', fontWeight: FontWeight.w600, color: jp.text, ), ), ], ), ); } static String _bytes(int b) { if (b < 1024) return '$b B'; if (b < 1024 * 1024) return '${(b / 1024).toStringAsFixed(0)} KB'; if (b < 1024 * 1024 * 1024) { return '${(b / (1024 * 1024)).toStringAsFixed(1)} MB'; } return '${(b / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB'; } static String _eta(Duration? d) { if (d == null) return '—'; final s = d.inSeconds; if (s <= 0) return '0s'; if (s < 60) return '${s}s'; final m = s ~/ 60; final rem = s % 60; if (m < 60) return '${m}m ${rem}s'; final h = m ~/ 60; return '${h}h ${m % 60}m'; } }