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), ], ), ), ), ], ); }), ); } // 顶部渐变头部 /// 构建顶部设备状态头部 /// 参数:`isDark` 是否深色模式 /// 返回:`Widget` 构建好的头部组件 /// 异常:无 Widget _buildHeader(bool isDark) { final connected = controller.isConnected.value; final deviceName = controller.deviceName.value.isEmpty ? '未连接设备' : controller.deviceName.value; final mismatch = controller.wakeupVersionMismatch.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)), if (mismatch) ...[ const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Colors.red.withOpacity(0.2), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.redAccent), ), child: const Text('设备异常', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w500)), ), ] ], ) ], ), ), ], ), ), ), ); } // 当前唤醒词卡片 /// 构建当前设备唤醒词卡片 /// 参数:`isDark` 是否深色模式 /// 返回:`Widget` 构建好的卡片 /// 异常:无 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), if (curItem != null) _buildVoiceCard( isDark: isDark, item: curItem, isCurrent: true, hasUpdate: curItem != null && controller.actionTextForItem(curItem) == '升级', ), // 版本一致的唤醒词放在“当前唤醒词”下面 ...controller.voices .where((v) => controller.actionTextForItem(v) == '已是最新') .where((v) => !identical(v, curItem)) .map((v) => _buildVoiceCard( isDark: isDark, item: v, isCurrent: true, hasUpdate: false, )), ], ); } // 可用唤醒词列表 /// 构建可用唤醒词列表 /// 参数:`isDark` 是否深色模式 /// 返回:`Widget` 构建好的列表 /// 异常:无 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) => controller.actionTextForItem(v) != '已是最新') .map((v) => _buildVoiceCard( isDark: isDark, item: v, isCurrent: false, hasUpdate: controller.actionTextForItem(v) == '升级', )), ], ); } /// 比较版本是否更高(三段比较) /// 参数:`a` 候选版本;`b` 当前版本(可空) /// 返回:`bool` 若 `a` 高于 `b` 返回 `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; } /// 判断当前卡片是否存在可升级版本 /// 参数:`curItem` 当前项(可空) /// 返回:`bool` 存在升级返回 `true` /// 异常:无 bool _hasUpdateForCurrent(WakeupVoiceItem? curItem) { if (curItem == null) return false; final curVer = controller.currentDeviceVoiceVersion.value; return _versionHigherThan(curItem.version, curVer); } /// 构建通用语音卡片 /// 参数:`isDark` 是否深色模式;`item` 语音项;`isCurrent` 是否当前项;`hasUpdate` 是否升级卡片 /// 返回:`Widget` 卡片组件 /// 异常:无 Widget _buildVoiceCard({ required bool isDark, required WakeupVoiceItem? item, required bool isCurrent, bool hasUpdate = false, }) { if (item == null) { if (isCurrent) { 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('未设置'), ); } else { return const SizedBox.shrink(); } } final accuracyText = controller.extractAccuracy(item) ?? '识别率 -'; final showCheck = isCurrent && !hasUpdate; final actionColor = hasUpdate ? const Color(0xFFFF7A00) : const Color(0xFF6A5AE0); final actionText = controller.actionTextForItem(item); final actionEnabled = controller.actionEnabledForItem(item); 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 || !actionEnabled ? null : () { if (actionText == '升级') { controller.upgradeWakeVoiceTo(item); } else if (actionText == '切换') { 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), ), ), ], ), ], ], ), ); } // 底部按钮已移除,改为每个卡片独立的操作按钮以贴合设计 }