import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/wakeup_voice_controller.dart'; class WakeupVoiceView extends GetView { const WakeupVoiceView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: isDark ? Colors.black : Colors.grey[50], body: Obx(() { // 顶部渐变头部 + 内容 return CustomScrollView( slivers: [ SliverAppBar( pinned: true, elevation: 0, centerTitle: true, backgroundColor: Colors.transparent, flexibleSpace: _buildHeader(isDark), expandedHeight: 180, leading: IconButton( icon: const Icon(Icons.arrow_back_ios, color: Colors.white), onPressed: () => Get.back(), ), title: const Text('唤醒词管理', style: TextStyle(color: Colors.white, fontSize: 18)), actions: const [ Padding( padding: EdgeInsets.only(right: 12), child: Icon(Icons.more_vert, color: Colors.white), ) ], ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCurrentWakeWordCard(isDark), const SizedBox(height: 16), _buildAvailableWakeWords(isDark), ], ), ), ), ], ); }), ); } // 顶部渐变头部 Widget _buildHeader(bool isDark) { final connected = controller.isConnected.value; final deviceName = controller.deviceName.value.isEmpty ? '未连接设备' : controller.deviceName.value; return Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF6A5AE0), Color(0xFF8A65E6)], ), ), child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 56, 16, 16), child: Row( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.headset, color: Colors.white, size: 28), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(deviceName, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(height: 6), Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( color: connected ? Colors.greenAccent : Colors.red, shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text(connected ? '已连接' : '未连接', style: const TextStyle( color: Colors.white70, fontSize: 12)), ], ) ], ), ), ], ), ), ), ); } // 当前唤醒词卡片 Widget _buildCurrentWakeWordCard(bool isDark) { final curId = controller.currentDeviceVoiceId.value; final curVer = controller.currentDeviceVoiceVersion.value; WakeupVoiceItem? curItem; for (final v in controller.voices) { if (v.id == curId) { curItem = v; break; } } final title = '当前唤醒词'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), Text(curVer != null ? 'v$curVer' : '', style: const TextStyle(color: Colors.grey, fontSize: 12)), ], ), const SizedBox(height: 8), _buildVoiceCard( isDark: isDark, item: curItem, isCurrent: true, hasUpdate: _hasUpdateForCurrent(curItem), ), ], ); } // 可用唤醒词列表 Widget _buildAvailableWakeWords(bool isDark) { final curId = controller.currentDeviceVoiceId.value; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('可用唤醒词', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), if (controller.loadError.value.isNotEmpty) Text(controller.loadError.value, style: const TextStyle(color: Colors.red)), ...controller.voices.where((v) => v.id != curId).map( (v) => _buildVoiceCard(isDark: isDark, item: v, isCurrent: false)), // 如果存在更新项(同ID但版本更高),也在此展示 ...controller.voices .where((v) => v.id == curId && _versionHigherThan( v.version, controller.currentDeviceVoiceVersion.value)) .map((v) => _buildVoiceCard( isDark: isDark, item: v, isCurrent: false, hasUpdate: true, )), ], ); } bool _versionHigherThan(String a, String? b) { if (b == null) return true; // 简易版本比较:按点位分割逐段比较 List pa = a.split('.').map((e) => int.tryParse(e) ?? 0).toList(); List pb = b.split('.').map((e) => int.tryParse(e) ?? 0).toList(); for (int i = 0; i < 3; i++) { final va = i < pa.length ? pa[i] : 0; final vb = i < pb.length ? pb[i] : 0; if (va != vb) return va > vb; } return false; } bool _hasUpdateForCurrent(WakeupVoiceItem? curItem) { if (curItem == null) return false; final curVer = controller.currentDeviceVoiceVersion.value; return _versionHigherThan(curItem.version, curVer); } Widget _buildVoiceCard({ required bool isDark, required WakeupVoiceItem? item, required bool isCurrent, bool hasUpdate = false, }) { if (item == null) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.grey[900] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: const Text('未设置'), ); } final accuracyText = controller.extractAccuracy(item) ?? '识别率 -'; final showCheck = isCurrent && !hasUpdate; final actionColor = hasUpdate ? const Color(0xFFFF7A00) : const Color(0xFF6A5AE0); final actionText = hasUpdate ? '更新唤醒词' : '切换唤醒词'; final isUpgrading = item != null && controller.upgradingVoiceId.value == item.id; final progress = controller.upgradeProgress.value; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.grey[900] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: (hasUpdate ? const Color(0xFFFFE8D5) : const Color(0xFFE8E5FF)), borderRadius: BorderRadius.circular(10), ), child: Icon( hasUpdate ? Icons.mic : Icons.auto_awesome, color: hasUpdate ? const Color(0xFFFF7A00) : const Color(0xFF6A5AE0), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text(item.name, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600)), ), Text('v${item.version}', style: const TextStyle( fontSize: 12, color: Colors.grey)), ], ), const SizedBox(height: 4), Text(accuracyText, style: const TextStyle(fontSize: 12, color: Colors.grey)), ], ), ), if (showCheck) ...[ const SizedBox(width: 8), const Icon(Icons.check_circle, color: Color(0xFF6A5AE0)), ] ], ), if (hasUpdate) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? Colors.grey[850] : const Color(0xFFFFF4E8), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: const [ Icon(Icons.upgrade, color: Color(0xFFFF7A00), size: 18), SizedBox(width: 6), Text('更新内容', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFFF7A00))), ], ), const SizedBox(height: 6), ...controller.extractUpdateNotes(item).map((note) => Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('• ', style: TextStyle(color: Colors.black54)), Expanded( child: Text(note, style: const TextStyle( fontSize: 12, color: Colors.black87)), ) ], ), )), ], ), ), ], const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: actionColor, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), onPressed: isUpgrading ? null : () { if (hasUpdate) { controller.upgradeWakeVoiceTo(item); } else { controller.switchWakeVoiceTo(item); } }, child: Text(actionText), ), ), if (isUpgrading) ...[ const SizedBox(height: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.sync, size: 16, color: Color(0xFF6A5AE0)), const SizedBox(width: 6), Text('升级中... $progress%', style: const TextStyle( fontSize: 12, color: Colors.black54)), ], ), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: (progress.clamp(0, 100)) / 100.0, minHeight: 8, backgroundColor: isDark ? Colors.grey[800] : Colors.grey[300], color: const Color(0xFF6A5AE0), ), ), ], ), ], ], ), ); } // 底部按钮已移除,改为每个卡片独立的操作按钮以贴合设计 }