import 'package:device_jieli/device_jieli.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/devices_controller.dart'; class DevicesView extends StatefulWidget { const DevicesView({Key? key}) : super(key: key); @override State createState() => _DevicesViewState(); } class _DevicesViewState extends State with TickerProviderStateMixin { late AnimationController _pulseController; late AnimationController _rotationController; late Animation _pulseAnimation; late Animation _rotationAnimation; DevicesController get controller => Get.find(); @override void initState() { super.initState(); _pulseController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, )..repeat(reverse: true); _rotationController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, )..repeat(); _pulseAnimation = Tween(begin: 0.8, end: 1.2).animate(CurvedAnimation( parent: _pulseController, curve: Curves.easeInOut, )); _rotationAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _rotationController, curve: Curves.linear), ); } @override void dispose() { _pulseController.dispose(); _rotationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: isDark ? Colors.black : Colors.white, body: SafeArea( child: Column( children: [ _buildTopSection(isDark), Expanded(child: _buildDeviceListSection(isDark)), _buildBottomActions(isDark), ], ), ), ); } // ─── 顶部:标题 + 扫描动画 + 状态文字 ──────────────────── Widget _buildTopSection(bool isDark) { return Padding( padding: EdgeInsets.only(top: 40.h, left: 24.w, right: 24.w), child: Column( children: [ Text( 'Wicue', style: TextStyle( fontSize: 28.sp, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black, ), ), SizedBox(height: 6.h), Text( 'homeSlogan'.tr, style: TextStyle( fontSize: 14.sp, color: isDark ? Colors.white60 : Colors.black54, ), ), SizedBox(height: 24.h), _buildScanAnimation(isDark), SizedBox(height: 16.h), Obx(() { if (!controller.isBluetoothOn.value) { return Text( 'pleaseEnableBluetooth'.tr, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, color: const Color(0xFFFF9500), ), ); } final scanning = controller.isScanning.value; return Text( scanning ? 'scanning'.tr : 'scanIdle'.tr, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w500, color: isDark ? Colors.white70 : Colors.black87, ), ); }), SizedBox(height: 6.h), Text( 'ensureHeadsetOnAndPairingMode'.tr, textAlign: TextAlign.center, style: TextStyle( fontSize: 12.sp, color: isDark ? Colors.white38 : Colors.black45, ), ), ], ), ); } Widget _buildScanAnimation(bool isDark) { const primaryColor = Color(0xFF0066CC); const secondaryColor = Color(0xFF5E5CE6); return SizedBox( width: 180.w, height: 180.w, child: Obx(() { final scanning = controller.isScanning.value; return Stack( alignment: Alignment.center, children: [ if (scanning) _buildPulse(primaryColor, secondaryColor), if (scanning) _buildRotatingBorder(primaryColor), Container( width: 92.w, height: 92.w, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [primaryColor, secondaryColor], ), shape: BoxShape.circle, ), child: Icon( Icons.headphones, color: Colors.white, size: 44.w, ), ), ], ); }), ); } Widget _buildPulse(Color primary, Color secondary) { return AnimatedBuilder( animation: _pulseAnimation, builder: (_, __) { final v = _pulseAnimation.value; final opacity = 0.4 - (_pulseController.value * 0.3); return Container( width: 160.w * v, height: 160.w * v, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ primary.withValues(alpha: opacity), secondary.withValues(alpha: opacity), ], ), shape: BoxShape.circle, ), ); }, ); } Widget _buildRotatingBorder(Color primary) { return AnimatedBuilder( animation: _rotationAnimation, builder: (_, __) { return Transform.rotate( angle: _rotationAnimation.value * 2 * 3.14159, child: SizedBox( width: 110.w, height: 110.w, child: CircularProgressIndicator( strokeWidth: 3.w, valueColor: AlwaysStoppedAnimation(primary), backgroundColor: Colors.grey.withValues(alpha: 0.18), value: 0.7, ), ), ); }, ); } // ─── 底部:扫描结果列表 ──────────────────────────────── Widget _buildDeviceListSection(bool isDark) { return Container( margin: EdgeInsets.only(top: 20.h, left: 16.w, right: 16.w), decoration: BoxDecoration( color: isDark ? Colors.white.withValues(alpha: 0.03) : const Color(0xFFF5F6FA), borderRadius: BorderRadius.circular(16.r), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 14.h, bottom: 8.h), child: Row( children: [ Text( 'discoveredDevices'.tr, style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w600, color: isDark ? Colors.white70 : Colors.black87, ), ), const Spacer(), Obx(() => Text( '${controller.scannedDevices.length}', style: TextStyle( fontSize: 12.sp, color: isDark ? Colors.white54 : Colors.black54, ), )), ], ), ), Expanded( child: Obx(() { final devices = controller.scannedDevices; if (devices.isEmpty) { return Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 32.h), child: Text( 'noDevicesFound'.tr, style: TextStyle( fontSize: 13.sp, color: isDark ? Colors.white38 : Colors.black45, ), ), ), ); } return ListView.separated( padding: EdgeInsets.fromLTRB(8.w, 4.h, 8.w, 12.h), itemCount: devices.length, separatorBuilder: (_, __) => SizedBox(height: 6.h), itemBuilder: (_, i) => _buildDeviceTile(devices[i], isDark), ); }), ), ], ), ); } Widget _buildDeviceTile(JieliDevice device, bool isDark) { return Obx(() { // 订阅连接状态、当前连接地址、正在连接/绑定的目标 + 已绑定列表 tick,让 tile 实时刷新 controller.connectionStateRx.value; controller.boundDevicesTick.value; final connected = controller.isConnected(device); final connectingAddr = controller.connectingAddress.value; final bindingAddr = controller.bindingAddress.value; final connecting = connectingAddr == device.address; final binding = bindingAddr == device.address; final disabled = (connectingAddr != null && !connecting) || (bindingAddr != null && !binding); final bound = controller.isDeviceBound(device); return _buildDeviceTileContent( device, isDark, connected: connected, connecting: connecting, binding: binding, disabled: disabled, bound: bound, ); }); } Widget _buildDeviceTileContent( JieliDevice device, bool isDark, { required bool connected, required bool connecting, required bool binding, required bool disabled, required bool bound, }) { const connectedColor = Color(0xFF22C55E); final canTap = !connecting && !binding && !disabled; void onPrimaryTap() { if (bound) { controller.connect(device); } else { controller.bindAndConnect(device); } } return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(12.r), onTap: canTap ? onPrimaryTap : null, child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.r), color: isDark ? Colors.white.withValues(alpha: 0.04) : Colors.white, border: Border.all( color: connected ? connectedColor.withValues(alpha: 0.3) : (isDark ? Colors.white.withValues(alpha: 0.05) : Colors.black.withValues(alpha: 0.05)), ), ), child: Row( children: [ Container( width: 36.w, height: 36.w, decoration: BoxDecoration( shape: BoxShape.circle, color: connected ? connectedColor.withValues(alpha: 0.15) : const Color(0xFF0066CC).withValues(alpha: 0.12), ), child: Icon( Icons.headphones, size: 18.sp, color: connected ? connectedColor : const Color(0xFF0066CC), ), ), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( device.name.isEmpty ? 'unnamedDevice'.tr : device.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), ), if (connected) Container( padding: EdgeInsets.symmetric( horizontal: 6.w, vertical: 2.h), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.r), color: connectedColor.withValues(alpha: 0.12), ), child: Text( 'connected'.tr, style: TextStyle( fontSize: 10.sp, fontWeight: FontWeight.w600, color: connectedColor, ), ), ), ], ), SizedBox(height: 2.h), Text( '${device.address}${device.rssi != null ? ' RSSI ${device.rssi}' : ''}', style: TextStyle( fontSize: 10.sp, color: isDark ? Colors.white38 : Colors.black45, fontFamily: 'Menlo', ), ), ], ), ), if (connected) TextButton( onPressed: controller.disconnect, style: TextButton.styleFrom( minimumSize: Size(0, 28.h), padding: EdgeInsets.symmetric(horizontal: 8.w), ), child: Text( 'disconnect'.tr, style: TextStyle(fontSize: 11.sp, color: Colors.red), ), ) else if (connecting || binding) SizedBox( width: 18.w, height: 18.w, child: const CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Color(0xFF0066CC)), ), ) else ElevatedButton( onPressed: disabled ? null : onPrimaryTap, style: ElevatedButton.styleFrom( backgroundColor: bound ? const Color(0xFF0066CC) : const Color(0xFF8B5CF6), foregroundColor: Colors.white, minimumSize: Size(0, 30.h), padding: EdgeInsets.symmetric(horizontal: 12.w), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.r), ), elevation: 0, ), child: Text( bound ? 'connect'.tr : 'bind'.tr, style: TextStyle( fontSize: 11.sp, fontWeight: FontWeight.w600, ), ), ), ], ), ), ), ); } // ─── 底部:扫描操作按钮 ──────────────────────────────── Widget _buildBottomActions(bool isDark) { return Padding( padding: EdgeInsets.fromLTRB(24.w, 12.h, 24.w, 20.h), child: Obx(() { final bluetoothOff = !controller.isBluetoothOn.value; final scanning = controller.isScanning.value; final IconData icon; final String label; if (bluetoothOff) { icon = Icons.bluetooth_disabled; label = 'enableBluetooth'.tr; } else if (scanning) { icon = Icons.stop; label = 'stopScan'.tr; } else { icon = Icons.refresh; label = 'startScan'.tr; } return SizedBox( width: double.infinity, height: 48.h, child: ElevatedButton.icon( onPressed: (!bluetoothOff && scanning) ? controller.stopScan : controller.startScan, icon: Icon(icon, size: 18.sp), label: Text( label, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600), ), style: ElevatedButton.styleFrom( backgroundColor: bluetoothOff ? const Color(0xFFFF9500) : const Color(0xFF0066CC), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14.r), ), elevation: 0, ), ), ); }), ); } }