// connect_button.dart — 核心连接键(三态:off / connecting / on) import 'package:flutter/material.dart'; import '../pangolin_theme.dart'; import 'pangolin_icons.dart'; enum VpnStatus { off, connecting, on } class ConnectButton extends StatefulWidget { const ConnectButton({ super.key, required this.status, required this.onTap, this.elapsed = Duration.zero, this.size = 208, }); final VpnStatus status; final VoidCallback onTap; final Duration elapsed; final double size; @override State createState() => _ConnectButtonState(); } class _ConnectButtonState extends State with SingleTickerProviderStateMixin { late final AnimationController _spin = AnimationController(vsync: this, duration: const Duration(milliseconds: 1400))..repeat(); @override void dispose() { _spin.dispose(); super.dispose(); } String _fmt(Duration d) { String two(int n) => n.toString().padLeft(2, '0'); return '${two(d.inHours)}:${two(d.inMinutes % 60)}:${two(d.inSeconds % 60)}'; } @override Widget build(BuildContext context) { final c = context.pangolin; final s = widget.status; final Color fill = switch (s) { VpnStatus.off => c.bgSubtle, VpnStatus.connecting => c.accent, VpnStatus.on => c.success, }; final Color fg = s == VpnStatus.off ? c.accent : PangolinColors.white; final IconData icon = switch (s) { VpnStatus.off => PangolinIcons.power, VpnStatus.connecting => PangolinIcons.loader, VpnStatus.on => PangolinIcons.shieldCheck, }; final List glow = s == VpnStatus.off ? PangolinShadow.md : [ BoxShadow( color: (s == VpnStatus.on ? c.success : c.accent).withOpacity(0.18), blurRadius: 0, spreadRadius: 9, ), ...PangolinShadow.lg, ]; return GestureDetector( onTap: widget.onTap, child: AnimatedContainer( duration: PangolinMotion.slow, curve: PangolinMotion.easeOut, width: widget.size, height: widget.size, decoration: BoxDecoration(color: fill, shape: BoxShape.circle, boxShadow: glow), child: Stack( alignment: Alignment.center, children: [ Positioned.fill( child: AnimatedBuilder( animation: _spin, builder: (_, __) => CustomPaint( painter: _RingPainter( status: s, track: c.sand200, progress: PangolinColors.white, turns: s == VpnStatus.connecting ? _spin.value : 0, ), ), ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 50, color: fg), const SizedBox(height: 6), if (s == VpnStatus.on) Text(_fmt(widget.elapsed), style: PangolinText.mono.copyWith(color: fg, fontSize: 18, fontWeight: FontWeight.w600)) else Text(s == VpnStatus.connecting ? '···' : 'TAP', style: PangolinText.mono.copyWith(color: fg, fontSize: 14, fontWeight: FontWeight.w700, letterSpacing: 1.1)), ], ), ], ), ), ); } } class _RingPainter extends CustomPainter { _RingPainter({required this.status, required this.track, required this.progress, required this.turns}); final VpnStatus status; final Color track; final Color progress; final double turns; @override void paint(Canvas canvas, Size size) { final center = size.center(Offset.zero); final radius = size.width / 2 - 8; final rect = Rect.fromCircle(center: center, radius: radius); if (status == VpnStatus.off) { final p = Paint()..style = PaintingStyle.stroke..strokeWidth = 3..strokeCap = StrokeCap.round..color = track; const dash = 0.045, gap = 0.11; for (double a = 0; a < 6.283; a += dash + gap) { canvas.drawArc(rect, a, dash, false, p); } } else if (status == VpnStatus.connecting) { final base = Paint()..style = PaintingStyle.stroke..strokeWidth = 4..color = progress.withOpacity(0.3); canvas.drawArc(rect, 0, 6.283, false, base); final arc = Paint()..style = PaintingStyle.stroke..strokeWidth = 4..strokeCap = StrokeCap.round..color = progress; canvas.drawArc(rect, turns * 6.283, 1.6, false, arc); } else { final p = Paint()..style = PaintingStyle.stroke..strokeWidth = 4..strokeCap = StrokeCap.round..color = progress.withOpacity(0.85); canvas.drawArc(rect, 0, 6.283, false, p); } } @override bool shouldRepaint(_RingPainter old) => old.turns != turns || old.status != status; }