Files
PeachDeployments/lib/Peach/designs/TransferCard.dart
alvocool 38881d1fe1 Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:15:30 +03:00

166 lines
5.0 KiB
Dart

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';
}
}