diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6122f521d..97a32b9e3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ + + + diff --git a/lib/app/translations/app_translations.dart b/lib/app/translations/app_translations.dart index 4ccb05710..71bf8b7a0 100644 --- a/lib/app/translations/app_translations.dart +++ b/lib/app/translations/app_translations.dart @@ -36,8 +36,9 @@ class AppTranslations extends Translations { 'check_update': '检查更新', 'clear_cache': '清除缓存', 'home': '首页', - 'explore': '探索', + 'explore': '发现', 'profile': '我的', + 'device': '设备', }, 'en_US': { // General @@ -74,6 +75,7 @@ class AppTranslations extends Translations { 'home': 'Home', 'explore': 'Explore', 'profile': 'Profile', + 'device': 'Device', }, }; } \ No newline at end of file diff --git a/lib/core/routes/app_pages.dart b/lib/core/routes/app_pages.dart index b04ee1937..651b22bd6 100644 --- a/lib/core/routes/app_pages.dart +++ b/lib/core/routes/app_pages.dart @@ -22,10 +22,33 @@ import '../../modules/translation/views/translation_view.dart'; import '../../modules/translation/bindings/translation_binding.dart'; import '../../modules/translation/views/translation_history_view.dart'; import '../../modules/translation/bindings/translation_history_binding.dart'; +import '../../modules/login/views/login_view.dart'; +import '../../modules/login/bindings/login_binding.dart'; +import '../../modules/splash/views/splash_view.dart'; +import '../../modules/splash/bindings/splash_binding.dart'; +import '../../modules/pairing/views/pairing_view.dart'; +import '../../modules/pairing/bindings/pairing_binding.dart'; import './app_routes.dart'; +import '../../modules/settings/views/settings_view.dart'; +import '../../modules/settings/bindings/settings_binding.dart'; abstract class AppPages { static final pages = [ + GetPage( + name: Routes.splash, + page: () => const SplashView(), + binding: SplashBinding(), + ), + GetPage( + name: Routes.login, + page: () => const LoginView(), + binding: LoginBinding(), + ), + GetPage( + name: Routes.pairing, + page: () => const PairingView(), + binding: PairingBinding(), + ), GetPage( name: Routes.initial, page: () => const HomeView(), @@ -101,5 +124,10 @@ abstract class AppPages { page: () => const TranslationHistoryView(), binding: TranslationHistoryBinding(), ), + GetPage( + name: Routes.settings, + page: () => const SettingsView(), + binding: SettingsBinding(), + ), ]; } \ No newline at end of file diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index c112313bf..1d6c8a1ea 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -1,9 +1,13 @@ abstract class Routes { + static const splash = '/splash'; + static const login = '/login'; + static const pairing = '/pairing'; static const initial = '/home'; static const home = '/home'; static const explore = '/explore'; static const chat = '/chat'; static const profile = '/profile'; + static const settings = '/settings'; static const speechDemo = '/speech_demo'; static const TTS_TEST = '/tts_test'; static const ASR_TEST = '/asr_test'; diff --git a/lib/core/widgets/common_bottom_nav.dart b/lib/core/widgets/common_bottom_nav.dart index 1b35aed21..015feadfe 100644 --- a/lib/core/widgets/common_bottom_nav.dart +++ b/lib/core/widgets/common_bottom_nav.dart @@ -24,7 +24,7 @@ class CommonBottomNav extends StatelessWidget { Get.offAllNamed(Routes.explore); break; case 2: - Get.offAllNamed(Routes.profile); + Get.offAllNamed(Routes.settings); break; } }, @@ -38,8 +38,8 @@ class CommonBottomNav extends StatelessWidget { label: 'explore'.tr, ), BottomNavigationBarItem( - icon: const Icon(Icons.person), - label: 'profile'.tr, + icon: const Icon(Icons.settings), + label: 'settings'.tr, ), ], ); diff --git a/lib/data/services/bluetooth_service.dart b/lib/data/services/bluetooth_service.dart new file mode 100644 index 000000000..6bed30f5d --- /dev/null +++ b/lib/data/services/bluetooth_service.dart @@ -0,0 +1,300 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:get/get.dart'; +import '../../core/utils/logger.dart'; +import 'dart:io' show Platform; +import 'package:permission_handler/permission_handler.dart'; + +/// 蓝牙设备服务,用于发现和管理蓝牙设备 +class BluetoothService extends GetxService { + static BluetoothService get to => Get.find(); + + // 单例实例 + static final BluetoothService _instance = BluetoothService._internal(); + factory BluetoothService() => _instance; + BluetoothService._internal(); + + // 设备状态 + final _isAvailable = false.obs; + final _isOn = false.obs; + + // 已连接的设备 + final _connectedDevices = [].obs; + + // 扫描状态 + final _isScanning = false.obs; + + // 扫描结果 + final _scanResults = [].obs; + + // 当前选中的设备 + final _selectedDevice = Rx(null); + + // 连接状态 + final _connectionState = Rx(BluetoothConnectionState.disconnected); + + // 订阅 + StreamSubscription? _stateSubscription; + StreamSubscription? _scanSubscription; + StreamSubscription? _connectionSubscription; + + // 权限状态 + final _permissionsGranted = false.obs; + bool get permissionsGranted => _permissionsGranted.value; + + // Getters + bool get isAvailable => _isAvailable.value; + bool get isOn => _isOn.value; + bool get isScanning => _isScanning.value; + List get connectedDevices => _connectedDevices; + List get scanResults => _scanResults; + BluetoothDevice? get selectedDevice => _selectedDevice.value; + BluetoothConnectionState get connectionState => _connectionState.value; + + // Observable getters (for reactive programming) + RxBool get isAvailableObs => _isAvailable; + RxBool get isOnObs => _isOn; + RxList get connectedDevicesObs => _connectedDevices; + RxList get scanResultsObs => _scanResults; + Rx get selectedDeviceObs => _selectedDevice; + Rx get connectionStateObs => _connectionState; + + /// 初始化蓝牙服务 + Future init() async { + try { + // 不在初始化时检查权限,改为在需要时才检查 + // await _checkAndRequestPermissions(); + + // 监听蓝牙状态 + _stateSubscription = FlutterBluePlus.adapterState.listen((state) { + _isOn.value = state == BluetoothAdapterState.on; + _isAvailable.value = state != BluetoothAdapterState.unavailable; + + if (state == BluetoothAdapterState.on) { + // 蓝牙打开时,获取已连接设备,但不请求权限 + // 只有在用户主动扫描时才请求权限 + _getConnectedDevices(); + } else if (state == BluetoothAdapterState.off) { + // 蓝牙关闭时,清空设备列表 + _connectedDevices.clear(); + _scanResults.clear(); + _selectedDevice.value = null; + } + }); + + // 检查当前蓝牙状态,如果已开启则获取设备 + if (await FlutterBluePlus.isOn) { + _isOn.value = true; + _isAvailable.value = true; + _getConnectedDevices(); + } + + return this; + } catch (e) { + Logger.error('初始化蓝牙服务失败: $e'); + return this; + } + } + + /// 检查并请求蓝牙所需权限 + Future _checkAndRequestPermissions() async { + // 暂时跳过权限检查,直接返回true + _permissionsGranted.value = true; + return true; + } + + /// 再次请求权限 + Future requestPermissions() async { + // 暂时跳过权限检查,直接返回true + _permissionsGranted.value = true; + return true; + } + + /// 获取已连接的设备 + Future _getConnectedDevices() async { + try { + // 获取已连接的设备 + List devices = await FlutterBluePlus.connectedSystemDevices; + _connectedDevices.value = devices; + + // 如果有已连接的设备但没有选中设备,则选中第一个 + if (_connectedDevices.isNotEmpty && _selectedDevice.value == null) { + _selectedDevice.value = _connectedDevices[0]; + _monitorConnectionState(_selectedDevice.value!); + } + + Logger.info('已连接设备数量: ${_connectedDevices.length}'); + } catch (e) { + Logger.error('获取已连接设备失败: $e'); + } + } + + /// 获取系统中已配对的设备 + Future> getPairedDevices() async { + try { + // 获取已配对的设备(系统已知设备) + List systemDevices = await FlutterBluePlus.bondedDevices; + return systemDevices; + } catch (e) { + Logger.error('获取已配对设备失败: $e'); + return []; + } + } + + /// 开始扫描附近的蓝牙设备 + Future startScan({Duration? timeout}) async { + if (_isScanning.value) return; + + // 清空旧的扫描结果 + _scanResults.clear(); + + try { + // 开始扫描 + _scanSubscription?.cancel(); + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { + _scanResults.value = results; + }); + + // 设置扫描中状态 + _isScanning.value = true; + + // 开始扫描 + await FlutterBluePlus.startScan( + timeout: timeout ?? const Duration(seconds: 10), + ); + + // 扫描完成后更新状态 + _isScanning.value = false; + + Logger.info('蓝牙扫描完成,发现 ${_scanResults.length} 个设备'); + } catch (e) { + _isScanning.value = false; + Logger.error('蓝牙扫描失败: $e'); + } + } + + /// 停止扫描 + Future stopScan() async { + if (!_isScanning.value) return; + + try { + await FlutterBluePlus.stopScan(); + _isScanning.value = false; + } catch (e) { + Logger.error('停止蓝牙扫描失败: $e'); + } + } + + /// 连接到指定设备 + Future connectToDevice(BluetoothDevice device) async { + if (_selectedDevice.value == device) { + // 如果是当前选中设备,检查连接状态 + if (_connectionState.value == BluetoothConnectionState.connected) { + Logger.info('设备 ${device.platformName} 已经连接,无需重新连接'); + return true; + } + } + + try { + // 检查设备是否已连接 + bool isAlreadyConnected = false; + // 检查设备连接状态 + BluetoothConnectionState state = await device.connectionState.first; + if (state == BluetoothConnectionState.connected) { + Logger.info('设备 ${device.platformName} 已处于连接状态'); + isAlreadyConnected = true; + } + + // 如果设备已连接,直接更新状态而不尝试连接 + if (isAlreadyConnected) { + // 断开当前跟踪的设备(如果有) + if (_selectedDevice.value != null && _selectedDevice.value != device) { + _connectionSubscription?.cancel(); + } + + // 更新状态 + _selectedDevice.value = device; + _monitorConnectionState(device); + + return true; + } + + // 断开当前连接 + await disconnectFromDevice(); + + // 连接新设备 + await device.connect(); + + // 更新状态 + _selectedDevice.value = device; + _monitorConnectionState(device); + + return true; + } catch (e) { + Logger.error('连接蓝牙设备失败: $e'); + return false; + } + } + + /// 断开与当前设备的连接 + Future disconnectFromDevice() async { + if (_selectedDevice.value == null) return true; + + try { + await _selectedDevice.value!.disconnect(); + _connectionSubscription?.cancel(); + _connectionSubscription = null; + _selectedDevice.value = null; + + return true; + } catch (e) { + Logger.error('断开蓝牙设备连接失败: $e'); + return false; + } + } + + /// 监控连接状态 + void _monitorConnectionState(BluetoothDevice device) { + _connectionSubscription?.cancel(); + _connectionSubscription = device.connectionState.listen((state) { + _connectionState.value = state; + + if (state == BluetoothConnectionState.disconnected) { + _selectedDevice.value = null; + } + }); + } + + /// 获取已连接设备的信息 + Map getConnectedDeviceInfo() { + if (_selectedDevice.value == null) { + return { + 'connected': false, + 'device': null, + }; + } + + return { + 'connected': _connectionState.value == BluetoothConnectionState.connected, + 'device': { + 'name': _selectedDevice.value!.platformName.isEmpty ? + '未命名设备' : _selectedDevice.value!.platformName, + 'id': _selectedDevice.value!.remoteId.str, + 'isConnected': _connectionState.value == BluetoothConnectionState.connected, + 'rssi': _scanResults + .firstWhereOrNull((r) => r.device.remoteId == _selectedDevice.value!.remoteId) + ?.rssi + } + }; + } + + @override + void onClose() { + _stateSubscription?.cancel(); + _scanSubscription?.cancel(); + _connectionSubscription?.cancel(); + disconnectFromDevice(); + super.onClose(); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2304c63b9..3ddff4655 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,8 +12,18 @@ import 'app/translations/app_translations.dart'; import 'data/services/voice_interaction_service.dart'; void main() async { + // 确保Flutter绑定初始化 WidgetsFlutterBinding.ensureInitialized(); + // 先运行应用,显示启动页面 + runApp(const MainApp()); + + // 然后异步执行其他初始化操作 + _initializeApp(); +} + +// 异步初始化应用 +Future _initializeApp() async { // 设置窗口大小(仅在 macOS 下生效) if (Platform.isMacOS) { const double width = 375.0; @@ -46,7 +56,7 @@ void main() async { // AudioService 将在 AudioServiceManager 中初始化 // 不再需要在这里调用 AudioService.init - runApp(const MainApp()); + // 其他可能的初始化操作... } class MainApp extends StatelessWidget { @@ -66,7 +76,7 @@ class MainApp extends StatelessWidget { visualDensity: VisualDensity.adaptivePlatformDensity, ), initialBinding: InitialBinding(), - initialRoute: Routes.initial, + initialRoute: Routes.splash, getPages: AppPages.pages, debugShowCheckedModeBanner: false, translations: AppTranslations(), diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index a13432868..bbca18d55 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/lib/modules/explore/controllers/explore_controller.dart @@ -1,32 +1,170 @@ import 'package:get/get.dart'; -import '../../../data/providers/agent_provider.dart'; -import '../../../data/services/voice_interaction_service.dart'; -class ChatItem { - final String id; +import 'package:flutter/material.dart'; - ChatItem({ - required this.id, +class AIFeature { + final String title; + final String description; + final IconData icon; + final Color startColor; + final Color endColor; + final Color iconBgColor; + final Color iconColor; + + AIFeature({ + required this.title, + required this.description, + required this.icon, + required this.startColor, + required this.endColor, + required this.iconBgColor, + required this.iconColor, + }); +} + +class FunctionScene { + final String title; + final String description; + final IconData icon; + final Color startColor; + final Color endColor; + final Color iconColor; + + FunctionScene({ + required this.title, + required this.description, + required this.icon, + required this.startColor, + required this.endColor, + required this.iconColor, }); } class ExploreController extends GetxController { - static ExploreController get to => Get.find(); + // 搜索关键词 + final searchController = TextEditingController(); - final _chatList = [].obs; + // AI功能列表 + final List aiFeatures = [ + AIFeature( + title: '同声翻译', + description: '实时翻译40+种语言', + icon: Icons.translate, + startColor: Colors.blue.shade100, + endColor: Colors.blue.shade200, + iconBgColor: Colors.blue.withOpacity(0.2), + iconColor: Colors.blue.shade600, + ), + AIFeature( + title: 'AI女友', + description: '智能陪伴,随时聊天', + icon: Icons.favorite, + startColor: Colors.pink.shade100, + endColor: Colors.pink.shade200, + iconBgColor: Colors.pink.withOpacity(0.2), + iconColor: Colors.pink.shade600, + ), + AIFeature( + title: '音乐创作', + description: '用AI创作专属音乐', + icon: Icons.music_note, + startColor: Colors.green.shade100, + endColor: Colors.green.shade200, + iconBgColor: Colors.green.withOpacity(0.2), + iconColor: Colors.green.shade600, + ), + AIFeature( + title: '声音克隆', + description: '创建你的数字声音', + icon: Icons.mic, + startColor: Colors.amber.shade100, + endColor: Colors.amber.shade200, + iconBgColor: Colors.amber.withOpacity(0.2), + iconColor: Colors.amber.shade600, + ), + ]; - List get chatList => _chatList; + // 热门功能场景列表 + final List functionScenes = [ + FunctionScene( + title: '商务会议助手', + description: '实时记录会议纪要,多语言翻译', + icon: Icons.people, + startColor: Colors.blue.shade200, + endColor: Colors.blue.shade300, + iconColor: Colors.blue.shade600, + ), + FunctionScene( + title: '语言学习助手', + description: '发音纠正,实时练习口语', + icon: Icons.school, + startColor: Colors.green.shade200, + endColor: Colors.green.shade300, + iconColor: Colors.green.shade600, + ), + FunctionScene( + title: '旅行翻译助手', + description: '实时语音翻译,旅游景点介绍', + icon: Icons.language, + startColor: Colors.amber.shade200, + endColor: Colors.amber.shade300, + iconColor: Colors.amber.shade600, + ), + ]; - @override - void onInit() { - super.onInit(); - _loadInitialChats(); + // 搜索功能 + void search() { + if (searchController.text.isEmpty) return; + + Get.snackbar( + '搜索', + '正在搜索: ${searchController.text}', + snackPosition: SnackPosition.TOP, + ); + + // 实际应用中,这里应该实现真正的搜索逻辑 + searchController.clear(); } - void _loadInitialChats() { - final agents = AgentProvider.getAllAgents(); - _chatList.assignAll( - agents.map((agent) => ChatItem(id: agent.id)).toList(), + // 点击AI功能 + void onAIFeatureTap(int index) { + if (index < 0 || index >= aiFeatures.length) return; + + Get.snackbar( + '功能', + '您选择了: ${aiFeatures[index].title}', + snackPosition: SnackPosition.TOP, ); + + // 实际应用中,这里应该跳转到对应的功能页面 } + // 点击功能场景 + void onFunctionSceneTap(int index) { + if (index < 0 || index >= functionScenes.length) return; + + Get.snackbar( + '场景', + '您选择了: ${functionScenes[index].title}', + snackPosition: SnackPosition.TOP, + ); + + // 实际应用中,这里应该跳转到对应的场景页面 + } + + // 尝试新功能 + void tryNewFeature() { + Get.snackbar( + '新功能', + '您选择了尝试AI歌手模式', + snackPosition: SnackPosition.TOP, + ); + + // 实际应用中,这里应该跳转到新功能页面 + } + + @override + void onClose() { + searchController.dispose(); + super.onClose(); + } } \ No newline at end of file diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 3916ed86f..15651717a 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -1,111 +1,312 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'dart:ui'; import '../controllers/explore_controller.dart'; -import '../../chat/controllers/chat_controller.dart'; -import '../../chat/views/chat_view.dart'; -import '../../../data/providers/agent_provider.dart'; import '../../../core/widgets/common_bottom_nav.dart'; -import '../../../core/routes/app_routes.dart'; class ExploreView extends GetView { const ExploreView({Key? key}) : super(key: key); - void _onChatItemTap(ChatItem item) { - // 获取对应的Agent - final agent = AgentProvider.getAgentById(item.id); - if (agent == null) { - Get.snackbar( - 'Error', - 'Agent not found', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + body: SafeArea( + child: Column( + children: [ + // 标题和搜索 + _buildHeaderAndSearch(), + + // 主要内容(可滚动) + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 80.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 特色功能 + _buildFeaturedCard(), + + // AI功能列表 + SizedBox(height: 24.h), + _buildAIFeaturesSection(), + + // 热门功能场景 + SizedBox(height: 24.h), + _buildFunctionScenesSection(), + + // 新功能推荐 + SizedBox(height: 24.h), + _buildNewFeatureSection(), + ], + ), + ), + ), + ), + ], + ), + ), + bottomNavigationBar: const CommonBottomNav(currentIndex: 1), + ); + } - // 检查是否是同声翻译官 - if (agent.id == 'simultaneous_interpreter') { - // 导航到翻译页面,并传递代理信息 - Get.toNamed(Routes.translation, arguments: { - 'agentId': agent.id, - 'agentName': agent.name, - 'agentAvatar': agent.avatarUrl, - }); - return; - } + // 标题和搜索 + Widget _buildHeaderAndSearch() { + return Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + // 标题和菜单按钮 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '发现', + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.grey[200], + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon(Icons.more_vert, size: 16.sp), + padding: EdgeInsets.zero, + onPressed: () {}, + ), + ), + ], + ), + + // 搜索框 + SizedBox(height: 16.h), + Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), + ), + child: TextField( + controller: controller.searchController, + decoration: InputDecoration( + hintText: '搜索AI功能...', + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey[400], + ), + prefixIcon: Icon( + Icons.search, + size: 20.sp, + color: Colors.grey[400], + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 12.h), + ), + textInputAction: TextInputAction.search, + onSubmitted: (_) => controller.search(), + ), + ), + ], + ), + ); + } + + // 特色功能卡片 + Widget _buildFeaturedCard() { + return Container( + height: 180.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Stack( + children: [ + // 背景图片 + ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Container( + color: Colors.blue.shade900, // 预设颜色,图片加载失败时显示 + child: Image.network( + 'https://images.unsplash.com/photo-1526378722484-bd91ca387e72?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80', + width: double.infinity, + height: 180.h, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.blue.shade900, + width: double.infinity, + height: 180.h, + child: Icon( + Icons.image_not_supported, + color: Colors.white, + size: 40.sp, + ), + ); + }, + ), + ), + ), + + // 渐变遮罩 + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.r), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.1), + Colors.black.withOpacity(0.6), + ], + ), + ), + ), + + // 文本内容 + Padding( + padding: EdgeInsets.all(24.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '智能耳机,无限可能', + style: TextStyle( + fontSize: 22.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 4.h), + Text( + '探索DeepVoice提供的AI功能', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.8), + ), + ), + ], + ), + ), + ], + ), + ); + } - // 导航到聊天页面,让 binding 来处理控制器的创建 - Get.toNamed('/chat', arguments: { - 'agentId': agent.id, - 'agentName': agent.name, - 'agentVoice': agent.voice, - 'agentAvatar': agent.avatarUrl, - 'agentSubtitle': agent.description, - 'systemPrompt': agent.systemPrompt, - 'welcomeMessage': agent.welcomeMessage, - 'playVoiceOnEnter': false, - }); + // AI功能部分 + Widget _buildAIFeaturesSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AI功能', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[800], + ), + ), + SizedBox(height: 16.h), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16.w, + mainAxisSpacing: 16.h, + childAspectRatio: 1.0, + ), + itemCount: controller.aiFeatures.length, + itemBuilder: (context, index) { + final feature = controller.aiFeatures[index]; + return _buildAIFeatureItem(feature, index); + }, + ), + ], + ); } - Widget _buildChatItem(ChatItem item) { - // 获取对应的Agent - final agent = AgentProvider.getAgentById(item.id); - if (agent == null) return const SizedBox.shrink(); - - return InkWell( - onTap: () => _onChatItemTap(item), + // AI功能项 + Widget _buildAIFeatureItem(AIFeature feature, int index) { + return GestureDetector( + onTap: () => controller.onAIFeatureTap(index), child: Container( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), - child: Row( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + feature.startColor, + feature.endColor, + ], + ), + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Stack( children: [ - // 头像 - CircleAvatar( - radius: 25.r, - backgroundImage: AssetImage(agent.avatarUrl), - backgroundColor: const Color(0xFFEEF4FF), - ), - SizedBox(width: 12.w), - // 聊天内容 - Expanded( + // 图标圆圈 + Align( + alignment: Alignment.topLeft, + child: Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: feature.iconColor.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + feature.icon, + size: 24.sp, + color: feature.iconColor, + ), + ), + ), + // 文本内容 + Padding( + padding: EdgeInsets.only(top: 58.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - // 名称行 - Row( - children: [ - Text( - agent.name, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - ), - if (agent.tag == '推荐') ...[ - SizedBox(width: 8.w), - Container( - padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h), - decoration: BoxDecoration( - color: const Color(0xFFE8F5E9), - borderRadius: BorderRadius.circular(4.r), - ), - child: Text( - '推荐', - style: TextStyle( - fontSize: 12.sp, - color: const Color(0xFF4CAF50), - ), - ), - ), - ], - ], + // 标题 + Text( + feature.title, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[800], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), SizedBox(height: 4.h), // 描述 Text( - agent.description, + feature.description, style: TextStyle( - fontSize: 14.sp, + fontSize: 13.sp, color: Colors.grey[600], ), maxLines: 1, @@ -120,53 +321,191 @@ class ExploreView extends GetView { ); } - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - title: Text( - '对话', + // 热门功能场景部分 + Widget _buildFunctionScenesSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '热门功能场景', style: TextStyle( - fontSize: 17.sp, + fontSize: 18.sp, fontWeight: FontWeight.w600, - color: Colors.black, + color: Colors.grey[800], ), ), - centerTitle: true, - actions: [ - IconButton( - icon: Icon( - Icons.search, - color: Colors.black, - size: 24.sp, + SizedBox(height: 16.h), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.functionScenes.length, + itemBuilder: (context, index) { + final scene = controller.functionScenes[index]; + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildFunctionSceneItem(scene, index), + ); + }, + ), + ], + ); + } + + // 功能场景项 + Widget _buildFunctionSceneItem(FunctionScene scene, int index) { + return GestureDetector( + onTap: () => controller.onFunctionSceneTap(index), + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // 图标 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + scene.startColor, + scene.endColor, + ], + ), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + scene.icon, + size: 24.sp, + color: scene.iconColor, + ), + ), + SizedBox(width: 16.w), + + // 文本内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + scene.title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[800], + ), + ), + SizedBox(height: 4.h), + Text( + scene.description, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + ], + ), ), - onPressed: () {}, + ], + ), + ), + ); + } + + // 新功能推荐部分 + Widget _buildNewFeatureSection() { + return Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.purple.shade100, + Colors.purple.shade200, + ], + ), + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), ), - IconButton( - icon: Icon( - Icons.add_circle_outline, - color: Colors.black, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 图标 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: Colors.purple.shade200, + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + Icons.lightbulb_outline, size: 24.sp, + color: Colors.purple.shade600, + ), + ), + SizedBox(height: 16.h), + + // 标题 + Text( + 'AI歌手模式', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + SizedBox(height: 8.h), + + // 描述 + Text( + '用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + ), + SizedBox(height: 16.h), + + // 按钮 + ElevatedButton( + onPressed: controller.tryNewFeature, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.purple.shade500, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text( + '立即尝试', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), ), - onPressed: () {}, ), - SizedBox(width: 4.w), ], ), - body: Obx(() => ListView.separated( - itemCount: controller.chatList.length, - separatorBuilder: (context, index) => Divider( - height: 1, - indent: 16.w, - endIndent: 16.w, - color: const Color(0xFFEEEEEE), - ), - itemBuilder: (context, index) => _buildChatItem(controller.chatList[index]), - )), - bottomNavigationBar: const CommonBottomNav(currentIndex: 1), ); } } \ No newline at end of file diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index ef1b5ca81..f29767595 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,131 +1,467 @@ import 'package:get/get.dart'; -import 'dart:async'; -import 'dart:io'; +import '../../../core/routes/app_routes.dart'; +import '../../../data/services/bluetooth_service.dart'; +import '../../../core/utils/logger.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothService; import 'package:flutter/material.dart'; -import '../../../data/services/voice_interaction_service.dart'; -enum ConnectStatus { - initial, // 初始状态,显示添加设备 - searching, // 搜索中 - connected, // 连接成功 - failed // 连接失败 +// 连接状态枚举 +enum ConnectionStatus { + disconnected, // 未连接 + connecting, // 连接中 + connected, // 已连接 + failed // 连接失败 } class HomeController extends GetxController { static HomeController get to => Get.find(); - final _connectStatus = ConnectStatus.initial.obs; - final _searchDots = ''.obs; - Timer? _animationTimer; - bool _isDisposed = false; + // 设备连接状态 + final _isConnected = false.obs; + bool get isConnected => _isConnected.value; + + // 连接状态 + final _connectionStatus = ConnectionStatus.disconnected.obs; + ConnectionStatus get connectionStatus => _connectionStatus.value; + + // 电池状态 + final _leftEarbudBattery = 75.obs; + final _rightEarbudBattery = 52.obs; + final _caseBattery = 92.obs; + + int get leftEarbudBattery => _leftEarbudBattery.value; + int get rightEarbudBattery => _rightEarbudBattery.value; + int get caseBattery => _caseBattery.value; + + // 固件版本 + final String firmwareVersion = 'v2.4.1'; + + // 蓝牙设备信息 + final _deviceInfo = {}.obs; + Map get deviceInfo => _deviceInfo.value; + + // 蓝牙服务 + BluetoothService? _bluetoothService; + + // 连接错误信息 + final _connectionErrorMsg = ''.obs; + String get connectionErrorMsg => _connectionErrorMsg.value; - ConnectStatus get connectStatus => _connectStatus.value; - String get searchDots => _searchDots.value; - @override void onInit() { super.onInit(); - _connectStatus.value = ConnectStatus.initial; - VoiceInteractionService.to.initialize(); + // 检查是否有传入的蓝牙设备信息 + if (Get.arguments != null && Get.arguments is Map) { + _deviceInfo.assignAll(Get.arguments); + _updateConnectionStatus(); + } + + // 获取蓝牙服务 + _initBluetoothService(); + + // 模拟定期获取电池状态 + _simulateBatteryUpdates(); } - - - @override - void onClose() { - _isDisposed = true; - stopSearchingAnimation(); - super.onClose(); + + // 初始化蓝牙服务 + void _initBluetoothService() { + try { + if (Get.isRegistered()) { + _bluetoothService = Get.find(); + + // 如果已经有连接的设备但没有设备信息 + if (_deviceInfo.isEmpty && _bluetoothService!.selectedDevice != null) { + _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); + _updateConnectionStatus(); + } else if (_deviceInfo.isEmpty) { + // 如果没有设备信息,尝试获取已配对设备 + _checkPairedDevices(); + } + + // 监听连接状态变化 + ever(_bluetoothService!.connectionStateObs, (state) { + _processConnectionStateChange(state); + }); + + // 监听选中设备变化 + ever(_bluetoothService!.selectedDeviceObs, (device) { + if (device != null) { + _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); + _updateConnectionStatus(); + } else if (_isConnected.value) { + // 如果设备断开连接,更新状态 + _isConnected.value = false; + _deviceInfo.clear(); + _connectionStatus.value = ConnectionStatus.disconnected; + } + }); + } + } catch (e) { + Logger.error('获取蓝牙服务失败: $e'); + _connectionStatus.value = ConnectionStatus.failed; + _connectionErrorMsg.value = '获取蓝牙服务失败'; + } } - - void startSearchingAnimation() { - stopSearchingAnimation(); // Clear any existing timer + + // 处理连接状态变化 + void _processConnectionStateChange(BluetoothConnectionState state) { + _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); - if (_isDisposed) return; + switch (state) { + case BluetoothConnectionState.connected: + _connectionStatus.value = ConnectionStatus.connected; + _connectionErrorMsg.value = ''; + _showConnectionSuccessMessage(); + break; + case BluetoothConnectionState.disconnected: + _connectionStatus.value = ConnectionStatus.disconnected; + break; + case BluetoothConnectionState.connecting: + _connectionStatus.value = ConnectionStatus.connecting; + break; + default: + // 其他状态暂不处理 + break; + } - int count = 0; - _animationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { - if (_isDisposed || _connectStatus.value != ConnectStatus.searching) { - timer.cancel(); - return; + _updateConnectionStatus(); + } + + // 显示连接成功消息 + void _showConnectionSuccessMessage() { + if (_bluetoothService?.selectedDevice != null) { + String deviceName = _bluetoothService!.selectedDevice!.platformName.isEmpty ? + '未命名设备' : _bluetoothService!.selectedDevice!.platformName; + Get.snackbar( + '连接成功', + '已成功连接到设备: $deviceName', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green[100], + colorText: Colors.green[800], + duration: const Duration(seconds: 2), + ); + } + } + + // 尝试查找并连接已配对设备 + Future _checkPairedDevices() async { + try { + if (_bluetoothService != null) { + // 更新状态为正在连接 + _connectionStatus.value = ConnectionStatus.connecting; + + // 如果已有选中的设备并且已连接,直接使用 + if (_bluetoothService!.selectedDevice != null && + _bluetoothService!.connectionState == BluetoothConnectionState.connected) { + Logger.info('使用已连接的设备: ${_bluetoothService!.selectedDevice!.platformName}'); + _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); + _connectionStatus.value = ConnectionStatus.connected; + _updateConnectionStatus(); + return; + } + + // 获取已配对设备 + final pairedDevices = await _bluetoothService!.getPairedDevices(); + if (pairedDevices.isEmpty) { + Logger.info('没有找到已配对设备'); + _connectionStatus.value = ConnectionStatus.disconnected; + return; + } + + // 遍历配对设备,查找已连接的设备 + for (var device in pairedDevices) { + try { + final state = await device.connectionState.first; + if (state == BluetoothConnectionState.connected) { + // 找到已连接的设备,更新UI + final success = await _bluetoothService!.connectToDevice(device); + if (success) { + Logger.info('使用已连接的已配对设备: ${device.platformName}'); + _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); + _connectionStatus.value = ConnectionStatus.connected; + _updateConnectionStatus(); + return; + } + } + } catch (e) { + // 忽略单个设备检查错误 + Logger.error('检查设备连接状态失败: $e'); + } + } + + // 如果没有已连接的设备,尝试连接第一个配对设备 + if (pairedDevices.isNotEmpty) { + Logger.info('尝试连接第一个配对设备: ${pairedDevices.first.platformName}'); + final success = await _bluetoothService!.connectToDevice(pairedDevices.first); + if (success) { + Logger.info('自动连接到已配对设备: ${pairedDevices.first.platformName}'); + _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); + _connectionStatus.value = ConnectionStatus.connected; + _updateConnectionStatus(); + } else { + // 连接失败 + _connectionStatus.value = ConnectionStatus.failed; + _connectionErrorMsg.value = '连接设备失败,请重试'; + } + } else { + _connectionStatus.value = ConnectionStatus.disconnected; + } } - count = (count + 1) % 4; - _searchDots.value = '.' * count; - }); + } catch (e) { + Logger.error('检查已配对设备失败: $e'); + _connectionStatus.value = ConnectionStatus.failed; + _connectionErrorMsg.value = '检查设备失败: ${e.toString()}'; + } } - - void stopSearchingAnimation() { - _animationTimer?.cancel(); - _animationTimer = null; - if (!_isDisposed) { - _searchDots.value = ''; + + // 更新连接状态 + void _updateConnectionStatus() { + if (_deviceInfo.containsKey('connected')) { + _isConnected.value = _deviceInfo['connected'] ?? false; + + // 如果设备信息显示已连接,但状态不是已连接,则更新状态 + if (_isConnected.value && _connectionStatus.value != ConnectionStatus.connected) { + _connectionStatus.value = ConnectionStatus.connected; + } else if (!_isConnected.value && _connectionStatus.value == ConnectionStatus.connected) { + // 如果设备显示未连接,但状态是已连接,则更新状态 + _connectionStatus.value = ConnectionStatus.disconnected; + } + } else { + _isConnected.value = false; + if (_connectionStatus.value == ConnectionStatus.connected) { + _connectionStatus.value = ConnectionStatus.disconnected; + } } } - - Future connectDevice() async { - if (_connectStatus.value == ConnectStatus.searching) return; + + // 模拟电池状态更新 + void _simulateBatteryUpdates() { + // 在实际应用中,这里应该从设备获取真实的电池状态 + // 这里只是模拟随机波动 + } + + // 探索AI功能 + void exploreAIFeatures() { + Get.toNamed(Routes.explore); + } + + // 更新固件 + void updateFirmware() { + Get.snackbar( + '提示', + '开始下载固件更新...', + snackPosition: SnackPosition.TOP, + ); - _connectStatus.value = ConnectStatus.searching; - startSearchingAnimation(); - + // 模拟更新过程 + // 在真实应用中,这里应该有固件更新的实际逻辑 + } + + // 打开降噪功能 + void toggleNoiseReduction() { + Get.snackbar( + '提示', + '降噪功能已开启', + snackPosition: SnackPosition.TOP, + ); + } + + // 音效设置 + void openSoundEffects() { + Get.snackbar( + '提示', + '音效设置功能尚未实现', + snackPosition: SnackPosition.TOP, + ); + } + + // 环境音功能 + void toggleAmbientSound() { + Get.snackbar( + '提示', + '环境音模式已开启', + snackPosition: SnackPosition.TOP, + ); + } + + // 收藏的设置 + void openFavorites() { + Get.snackbar( + '提示', + '收藏功能尚未实现', + snackPosition: SnackPosition.TOP, + ); + } + + // 打开设置页面 + void openSettings() { + Get.toNamed(Routes.settings); + } + + // 重新扫描设备 + void rescanDevices() { + if (_bluetoothService != null) { + Get.toNamed(Routes.pairing); + } else { + Get.snackbar( + '提示', + '蓝牙服务不可用', + snackPosition: SnackPosition.TOP, + ); + } + } + + // 获取设备名称 + String getDeviceName() { + if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) { + return '未连接设备'; + } + + final device = _deviceInfo['device'] as Map; + return device['name'] ?? '未命名设备'; + } + + // 获取设备ID + String getDeviceId() { + if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) { + return ''; + } + + final device = _deviceInfo['device'] as Map; + return device['id'] ?? ''; + } + + // 获取设备信号强度 + String getSignalStrength() { + if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) { + return '无信号'; + } + + final device = _deviceInfo['device'] as Map; + final rssi = device['rssi']; + + if (rssi == null) { + return '未知'; + } + + // RSSI通常在-100到0之间,值越大信号越强 + if (rssi > -60) { + return '强'; + } else if (rssi > -80) { + return '中'; + } else { + return '弱'; + } + } + + // 刷新设备信息 + Future refreshDeviceInfo() async { try { - // 模拟搜索和连接过程 - await Future.delayed(const Duration(seconds: 3)); + // 检查蓝牙是否可用 + if (_bluetoothService == null) { + Get.snackbar( + '提示', + '蓝牙服务不可用', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red[100], + colorText: Colors.red[800], + ); + return; + } - if (_isDisposed) return; // Check if controller is disposed + // 设置正在刷新状态 + _connectionStatus.value = ConnectionStatus.connecting; - // 模拟随机成功或失败 - if (DateTime.now().millisecondsSinceEpoch % 2 == 0) { - _connectStatus.value = ConnectStatus.connected; - if (!_isDisposed) { - await Future.delayed(const Duration(seconds: 2)); // 显示成功状态2秒 + if (_bluetoothService!.selectedDevice != null) { + // 如果有选中的设备,刷新设备信息 + _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); + _updateConnectionStatus(); + + // 检查刷新后的连接状态 + if (_isConnected.value) { + Get.snackbar( + '提示', + '设备信息已更新', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green[100], + colorText: Colors.green[800], + ); + } else { + // 设备可能已断开连接 + _connectionStatus.value = ConnectionStatus.disconnected; Get.snackbar( - 'Success', - 'Device connected successfully', + '提示', + '设备已断开连接', snackPosition: SnackPosition.TOP, - backgroundColor: Colors.green.withOpacity(0.1), - colorText: Colors.green, - duration: const Duration(seconds: 3), + backgroundColor: Colors.amber[100], + colorText: Colors.amber[800], ); + + // 尝试重新连接 + await _checkPairedDevices(); } } else { - _connectStatus.value = ConnectStatus.failed; + // 如果没有选中的设备,尝试连接已配对设备 + await _checkPairedDevices(); + + if (_connectionStatus.value == ConnectionStatus.failed) { + Get.snackbar( + '提示', + _connectionErrorMsg.value.isNotEmpty ? _connectionErrorMsg.value : '连接设备失败', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red[100], + colorText: Colors.red[800], + ); + } else if (_connectionStatus.value == ConnectionStatus.disconnected) { + Get.snackbar( + '提示', + '未找到已连接设备,请连接设备', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.amber[100], + colorText: Colors.amber[800], + ); + } } } catch (e) { - if (!_isDisposed) { - _connectStatus.value = ConnectStatus.failed; - Get.snackbar( - 'Error', - 'Failed to connect to device', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red.withOpacity(0.1), - colorText: Colors.red, - duration: const Duration(seconds: 3), - ); - } - } finally { - if (!_isDisposed && _connectStatus.value == ConnectStatus.searching) { - stopSearchingAnimation(); - _connectStatus.value = ConnectStatus.failed; - } + Logger.error('刷新设备信息失败: $e'); + _connectionStatus.value = ConnectionStatus.failed; + _connectionErrorMsg.value = '刷新设备信息失败: ${e.toString()}'; + + Get.snackbar( + '提示', + '刷新设备信息失败', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red[100], + colorText: Colors.red[800], + ); } } - - void retryConnect() { - if (_isDisposed) return; - _connectStatus.value = ConnectStatus.initial; - connectDevice(); + + // 处理连接失败 + String getConnectionStatusText() { + switch (_connectionStatus.value) { + case ConnectionStatus.connected: + return "已连接"; + case ConnectionStatus.connecting: + return "连接中..."; + case ConnectionStatus.disconnected: + return "未连接"; + case ConnectionStatus.failed: + return "连接失败"; + } } - - void disconnectDevice() { - if (_isDisposed) return; - _connectStatus.value = ConnectStatus.initial; - Get.snackbar( - 'Disconnected', - 'Device has been disconnected', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.blue.withOpacity(0.1), - colorText: Colors.blue, - duration: const Duration(seconds: 3), - ); + + // 获取连接状态颜色 + Color getConnectionStatusColor() { + switch (_connectionStatus.value) { + case ConnectionStatus.connected: + return Colors.green; + case ConnectionStatus.connecting: + return Colors.blue; + case ConnectionStatus.disconnected: + return Colors.red; + case ConnectionStatus.failed: + return Colors.red; + } } } \ No newline at end of file diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 8b068ceb1..ca6a07e95 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -3,281 +3,685 @@ import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/home_controller.dart'; import '../../../core/widgets/common_bottom_nav.dart'; -import '../../../core/routes/app_routes.dart'; + +// 导入枚举 +import '../controllers/home_controller.dart' show ConnectionStatus; class HomeView extends GetView { const HomeView({Key? key}) : super(key: key); - Widget _buildStatusWidget() { - return Obx(() { - switch (controller.connectStatus) { - case ConnectStatus.searching: - return Column( + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + body: SafeArea( + child: Column( + children: [ + // 状态栏和标题区域 + _buildHeader(), + + // 主要内容区域(可滚动) + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 耳机图片 + _buildEarbudsImage(), + + // 设备状态卡片 + SizedBox(height: 16.h), + _buildDeviceStatusCard(), + + // AI功能部分 + SizedBox(height: 16.h), + _buildAIFunctionsCard(), + + // 固件更新提示 + SizedBox(height: 16.h), + _buildFirmwareUpdateCard(), + + // 快速功能访问 + SizedBox(height: 24.h), + _buildQuickFunctions(), + ], + ), + ), + ), + ), + ], + ), + ), + bottomNavigationBar: const CommonBottomNav(currentIndex: 0), + ); + } + + // 页面顶部:状态栏和标题 + Widget _buildHeader() { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: Column( + children: [ + // 删除状态栏 + + // 标题区域 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'searching'.tr + controller.searchDots, + 'DeepVoice Pro', style: TextStyle( - fontSize: 16.sp, - color: Colors.grey[600], + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], ), ), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.grey[200], + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + icon: Icon(Icons.more_horiz, size: 16.sp, color: Colors.grey[600]), + padding: EdgeInsets.zero, + onPressed: () { + // 打开更多选项菜单 + }, + ), + ), + ], + ), + ], + ), + ); + } + + // 耳机图片 + Widget _buildEarbudsImage() { + return Container( + height: 180.h, + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: Container( + color: Colors.white, + padding: EdgeInsets.all(16.w), + child: Icon( + Icons.headset, + size: 120.sp, + color: Colors.blueGrey, + ), + ), + ), + ), + ); + } + + // 设备状态卡片 + Widget _buildDeviceStatusCard() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + // 状态标题行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Text( - 'searching_hint'.tr, + '设备状态', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[400], - height: 2, + fontWeight: FontWeight.w500, + color: Colors.grey[700], ), ), - ], - ); - case ConnectStatus.connected: - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + Obx(() => Row( children: [ - Icon( - Icons.check_circle, - color: Colors.green, - size: 20.sp, + Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: controller.getConnectionStatusColor(), + shape: BoxShape.circle, + ), ), - SizedBox(width: 8.w), + SizedBox(width: 4.w), Text( - 'connected'.tr, + controller.getConnectionStatusText(), style: TextStyle( - fontSize: 16.sp, - color: Colors.green, - fontWeight: FontWeight.w500, + fontSize: 12.sp, + color: controller.getConnectionStatusColor(), + ), + ), + // 添加刷新按钮 + SizedBox(width: 8.w), + InkWell( + onTap: controller.refreshDeviceInfo, + child: Icon( + Icons.refresh, + size: 16.sp, + color: Colors.grey[600], ), ), ], - ), - Text( - 'device_ready'.tr, - style: TextStyle( - fontSize: 14.sp, - color: Colors.grey[400], - height: 2, - ), - ), + )), ], - ); - case ConnectStatus.failed: - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + ), + + // 设备信息 + SizedBox(height: 12.h), + Obx(() { + // 如果连接失败且有错误信息,显示错误提示 + if (controller.connectionStatus == ConnectionStatus.failed && + controller.connectionErrorMsg.isNotEmpty) { + return Column( children: [ - Icon( - Icons.error, - color: Colors.red[400], - size: 20.sp, + Container( + padding: EdgeInsets.all(8.w), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Colors.red[200]!), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red, + size: 16.sp, + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + controller.connectionErrorMsg, + style: TextStyle( + fontSize: 12.sp, + color: Colors.red[800], + ), + ), + ), + ], + ), ), - SizedBox(width: 8.w), - Text( - 'connection_failed'.tr, - style: TextStyle( - fontSize: 16.sp, - color: Colors.red[400], - fontWeight: FontWeight.w500, + SizedBox(height: 12.h), + // 连接按钮 + ElevatedButton( + onPressed: controller.rescanDevices, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + minimumSize: Size(double.infinity, 36.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), ), + child: Text('重新连接'), ), ], - ), - SizedBox(height: 12.h), + ); + } + + // 如果正在连接中,显示连接中提示 + if (controller.connectionStatus == ConnectionStatus.connecting) { + return Container( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: Center( + child: Column( + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: CircularProgressIndicator( + strokeWidth: 2.w, + valueColor: AlwaysStoppedAnimation(Colors.blue), + ), + ), + SizedBox(height: 8.h), + Text( + '正在连接设备...', + style: TextStyle( + fontSize: 12.sp, + color: Colors.blue, + ), + ), + ], + ), + ), + ); + } + + return controller.isConnected + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + '已连接到:${controller.getDeviceName()}', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.grey[700], + ), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(4.r), + ), + child: Text( + '信号: ${controller.getSignalStrength()}', + style: TextStyle( + fontSize: 10.sp, + color: Colors.blue[700], + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(8.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '设备ID:${controller.getDeviceId()}', + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey[500], + ), + ), + SizedBox(height: 4.h), + Text( + '固件版本:${controller.firmwareVersion}', + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey[500], + ), + ), + ], + ), + ), + ], + ) + : Container( + padding: EdgeInsets.symmetric(vertical: 8.h), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bluetooth_disabled, + size: 14.sp, + color: Colors.grey[700], + ), + SizedBox(width: 4.w), + Text( + '未连接设备,点击下方按钮连接', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[700], + ), + ), + ], + ), + ); + }), + + // 连接按钮(仅在未连接时显示) + SizedBox(height: 12.h), + Obx(() { + if (controller.connectionStatus == ConnectionStatus.disconnected) { + return ElevatedButton( + onPressed: controller.rescanDevices, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + minimumSize: Size(double.infinity, 36.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + child: Text('连接设备'), + ); + } else { + return SizedBox(); + } + }), + + // 电池状态 + SizedBox(height: 12.h), + Obx(() => controller.isConnected + ? Row( + children: [ + // 左耳机 + Expanded( + child: _buildBatteryItem('左耳机', controller.leftEarbudBattery, Icons.headset), + ), + SizedBox(width: 12.w), + // 右耳机 + Expanded( + child: _buildBatteryItem('右耳机', controller.rightEarbudBattery, Icons.headset), + ), + SizedBox(width: 12.w), + // 充电盒 + Expanded( + child: _buildBatteryItem('充电盒', controller.caseBattery, Icons.cases_outlined), + ), + ], + ) + : const SizedBox()), + ], + ), + ); + } + + // 电池状态项 + Widget _buildBatteryItem(String label, int percentage, IconData icon) { + Color batteryColor; + IconData batteryIcon; + + // 根据电量设置颜色和图标 + if (percentage >= 70) { + batteryColor = Colors.green; + batteryIcon = Icons.battery_full; + } else if (percentage >= 30) { + batteryColor = Colors.amber; + batteryIcon = Icons.battery_5_bar; + } else { + batteryColor = Colors.red; + batteryIcon = Icons.battery_1_bar; + } + + return Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + padding: EdgeInsets.all(12.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 24.sp, color: Colors.grey[700]), + SizedBox(height: 8.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[500], + ), + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(batteryIcon, size: 14.sp, color: batteryColor), + SizedBox(width: 4.w), Text( - 'connection_failed_hint'.tr, - textAlign: TextAlign.center, + '$percentage%', style: TextStyle( - fontSize: 14.sp, - color: Colors.grey[500], - height: 1.8, + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.grey[700], ), ), ], - ); - default: - return const SizedBox.shrink(); - } - }); + ), + ], + ), + ); } - Widget _buildActionButton() { - return Obx(() { - switch (controller.connectStatus) { - case ConnectStatus.searching: - return Container( - width: double.infinity, - height: 56.h, - margin: EdgeInsets.symmetric(horizontal: 20.w), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(28.r), - ), - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 20.w, - height: 20.w, - child: const CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Color(0xFF4A6FE5)), - ), - ), - SizedBox(width: 12.w), - Text( - 'searching'.tr, - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.grey[400], - ), - ), - ], + // AI功能卡片 + Widget _buildAIFunctionsCard() { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade100, + Colors.purple.shade100, + ], + ), + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AI助手', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.grey[800], + ), ), - ), - ); - case ConnectStatus.failed: - return TextButton( - onPressed: controller.retryConnect, - style: TextButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 40.w, vertical: 12.h), - backgroundColor: Colors.blue[50], + SizedBox(height: 4.h), + Text( + '探索DeepVoice的智能功能', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ElevatedButton( + onPressed: controller.exploreAIFeatures, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[600], + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28.r), + borderRadius: BorderRadius.circular(24.r), ), + elevation: 2, ), child: Text( - 'retry'.tr, + '探索', style: TextStyle( - fontSize: 18.sp, - color: const Color(0xFF4A6FE5), - fontWeight: FontWeight.w600, + fontSize: 14.sp, + fontWeight: FontWeight.w500, ), ), - ); - case ConnectStatus.connected: - return Container( - width: double.infinity, - height: 56.h, - margin: EdgeInsets.symmetric(horizontal: 20.w), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF4CAF50), Color(0xFF66BB6A)], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - borderRadius: BorderRadius.circular(28.r), - boxShadow: [ - BoxShadow( - color: const Color(0xFF4CAF50).withOpacity(0.3), - offset: const Offset(0, 4), - blurRadius: 12, + ), + ], + ), + ); + } + + // 固件更新卡片 + Widget _buildFirmwareUpdateCard() { + return Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 40.w, + height: 40.w, + decoration: BoxDecoration( + color: Colors.amber[100], + shape: BoxShape.circle, ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: controller.disconnectDevice, - borderRadius: BorderRadius.circular(28.r), - child: Center( - child: Text( - 'disconnect'.tr, + child: Icon( + Icons.download, + size: 20.sp, + color: Colors.amber[600], + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '固件更新', style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - letterSpacing: 0.5, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.grey[800], ), ), - ), - ), - ), - ); - default: - return Container( - width: double.infinity, - height: 56.h, - margin: EdgeInsets.symmetric(horizontal: 20.w), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF4A6FE5), Color(0xFF6B8FF7)], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - borderRadius: BorderRadius.circular(28.r), - boxShadow: [ - BoxShadow( - color: const Color(0xFF4A6FE5).withOpacity(0.3), - offset: const Offset(0, 4), - blurRadius: 12, - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: controller.connectDevice, - borderRadius: BorderRadius.circular(28.r), - child: Center( - child: Text( - 'add_device'.tr, + Text( + '${controller.firmwareVersion}可用', style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - letterSpacing: 0.5, + fontSize: 12.sp, + color: Colors.grey[500], ), ), - ), + ], + ), + ], + ), + ElevatedButton( + onPressed: controller.updateFirmware, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[200], + foregroundColor: Colors.grey[700], + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24.r), ), + elevation: 0, ), - ); - } - }); + child: Text( + '更新', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); } - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Column( - children: [ - SizedBox(height: 40.h), - Text( - 'app_name'.tr, - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - SizedBox(height: 40.h), - Image.asset( - 'assets/images/headphone.png', - width: 200.w, - height: 200.w, - fit: BoxFit.contain, - ), - Expanded( - child: Center( - child: _buildStatusWidget(), - ), + // 快速功能访问 + Widget _buildQuickFunctions() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildQuickFunctionItem('降噪', Icons.volume_off, Colors.blue[600]!, Colors.blue[100]!, controller.toggleNoiseReduction), + _buildQuickFunctionItem('音效', Icons.music_note, Colors.purple[600]!, Colors.purple[100]!, controller.openSoundEffects), + _buildQuickFunctionItem('环境音', Icons.eco, Colors.green[600]!, Colors.green[100]!, controller.toggleAmbientSound), + _buildQuickFunctionItem('收藏', Icons.favorite, Colors.red[600]!, Colors.red[100]!, controller.openFavorites), + ], + ); + } + + // 快速功能项 + Widget _buildQuickFunctionItem(String label, IconData icon, Color iconColor, Color bgColor, VoidCallback onTap) { + return Column( + children: [ + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: bgColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), ), - _buildActionButton(), - SizedBox(height: 40.h), ], ), + child: IconButton( + icon: Icon(icon, color: iconColor), + onPressed: onTap, + ), ), - ), - bottomNavigationBar: const CommonBottomNav(currentIndex: 0), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[700], + ), + ), + ], ); } } \ No newline at end of file diff --git a/lib/modules/login/bindings/login_binding.dart b/lib/modules/login/bindings/login_binding.dart new file mode 100644 index 000000000..6729681f9 --- /dev/null +++ b/lib/modules/login/bindings/login_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/login_controller.dart'; + +class LoginBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => LoginController()); + } +} \ No newline at end of file diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart new file mode 100644 index 000000000..c72d5f623 --- /dev/null +++ b/lib/modules/login/controllers/login_controller.dart @@ -0,0 +1,246 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import '../../../core/routes/app_routes.dart'; + +enum LoginViewState { + welcome, // 欢迎页面 + verification // 验证码页面 +} + +class LoginController extends GetxController { + static LoginController get to => Get.find(); + + // 状态管理 + final _viewState = LoginViewState.welcome.obs; + final _isLoading = false.obs; + final _countdownSeconds = 0.obs; + final _contactValue = ''.obs; + final _displayContact = ''.obs; + final _verificationCode = ['', '', '', ''].obs; + + // Getters + LoginViewState get viewState => _viewState.value; + bool get isLoading => _isLoading.value; + int get countdownSeconds => _countdownSeconds.value; + String get contactValue => _contactValue.value; + String get displayContact => _displayContact.value; + List get verificationCode => _verificationCode; + + // 文本控制器 + final contactController = TextEditingController(); + final verificationCodeController = TextEditingController(); + + @override + void onInit() { + super.onInit(); + _viewState.value = LoginViewState.welcome; + } + + @override + void onClose() { + contactController.dispose(); + verificationCodeController.dispose(); + super.onClose(); + } + + // 获取验证码 + void getVerificationCode() { + if (contactController.text.isEmpty) { + Get.snackbar( + '提示', + '请输入手机号码或邮箱', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + + _contactValue.value = contactController.text; + _formatDisplayContact(); + _startCountdown(); + _sendVerificationCode(); + } + + // 使用验证码直接登录 + void loginWithVerificationCode() { + //测试期间暂时关闭验证码登录 + // if (contactController.text.isEmpty) { + // Get.snackbar( + // '提示', + // '请输入手机号码或邮箱', + // snackPosition: SnackPosition.TOP, + // backgroundColor: Colors.red.withOpacity(0.1), + // colorText: Colors.red, + // ); + // return; + // } + + // if (verificationCodeController.text.isEmpty) { + // Get.snackbar( + // '提示', + // '请输入验证码', + // snackPosition: SnackPosition.TOP, + // backgroundColor: Colors.red.withOpacity(0.1), + // colorText: Colors.red, + // ); + // return; + // } + + // if (verificationCodeController.text.length < 6) { + // Get.snackbar( + // '提示', + // '请输入完整的验证码', + // snackPosition: SnackPosition.TOP, + // backgroundColor: Colors.red.withOpacity(0.1), + // colorText: Colors.red, + // ); + // return; + // } + + _isLoading.value = true; + + // 模拟验证过程 + Future.delayed(const Duration(seconds: 2), () { + _isLoading.value = false; + // 验证成功,跳转到配对页面 + Get.offAllNamed(Routes.pairing); + }); + } + + // 切换到验证码页面 + void showVerificationView() { + if (contactController.text.isEmpty) { + Get.snackbar( + '提示', + '请输入手机号码或邮箱', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + + _contactValue.value = contactController.text; + _formatDisplayContact(); + _viewState.value = LoginViewState.verification; + _startCountdown(); + _sendVerificationCode(); + } + + // 返回欢迎页面 + void showWelcomeView() { + _viewState.value = LoginViewState.welcome; + _resetVerificationCode(); + } + + // 格式化显示的联系方式(隐藏部分字符) + void _formatDisplayContact() { + String contact = _contactValue.value; + if (contact.contains('@')) { // 邮箱 + final parts = contact.split('@'); + if (parts[0].length > 3) { + _displayContact.value = parts[0].substring(0, 3) + '****@' + parts[1]; + } else { + _displayContact.value = contact; + } + } else if (RegExp(r'^\d+$').hasMatch(contact)) { // 手机号 + if (contact.length > 7) { + _displayContact.value = '+86 ' + contact.substring(0, 3) + '****' + contact.substring(contact.length - 4); + } else { + _displayContact.value = contact; + } + } else { + _displayContact.value = contact; + } + } + + // 开始倒计时 + void _startCountdown() { + _countdownSeconds.value = 60; + Future.doWhile(() async { + await Future.delayed(const Duration(seconds: 1)); + _countdownSeconds.value--; + return _countdownSeconds.value > 0; + }); + } + + // 发送验证码 + void _sendVerificationCode() { + // 这里应该是实际发送验证码的逻辑 + // 为了演示,我们只是模拟发送 + } + + // 重新发送验证码 + void resendVerificationCode() { + if (_countdownSeconds.value > 0) return; + _startCountdown(); + _sendVerificationCode(); + } + + // 更新验证码输入 + void updateVerificationCode(int index, String value) { + if (index >= 0 && index < 4) { + var newList = [..._verificationCode]; + newList[index] = value; + _verificationCode.value = newList; + } + } + + // 重置验证码 + void _resetVerificationCode() { + _verificationCode.value = ['', '', '', '']; + } + + // 检查验证码是否完整 + bool isVerificationCodeComplete() { + return !_verificationCode.contains(''); + } + + // 登录 + Future login() async { + // 暂时关闭验证码验证 + if (!isVerificationCodeComplete()) { + Get.snackbar( + '提示', + '请输入完整的验证码', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + + _isLoading.value = true; + + try { + // 模拟验证过程 + await Future.delayed(const Duration(seconds: 2)); + + // 验证成功,跳转到配对页面 + Get.offAllNamed(Routes.pairing); + } catch (e) { + Get.snackbar( + '错误', + '登录失败,请重试', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } finally { + _isLoading.value = false; + } + } + + // 使用密码登录 + void usePasswordLogin() { + // 这里可以实现密码登录的逻辑 + Get.snackbar( + '提示', + '密码登录功能尚未实现', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.blue.withOpacity(0.1), + colorText: Colors.blue, + ); + } +} \ No newline at end of file diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart new file mode 100644 index 000000000..931a243dd --- /dev/null +++ b/lib/modules/login/views/login_view.dart @@ -0,0 +1,628 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import '../controllers/login_controller.dart'; + +class LoginView extends GetView { + const LoginView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + body: SafeArea( + child: Obx(() => controller.viewState == LoginViewState.welcome + ? _buildWelcomeView() + : _buildVerificationView(context)), + ), + ); + } + + // 欢迎页面 + Widget _buildWelcomeView() { + return Padding( + padding: EdgeInsets.all(24.w), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + // 状态栏 + _buildStatusBar(), + + // 应用标志和标题 + SizedBox(height: 32.h), + _buildAppLogo(), + + // 欢迎文本 + SizedBox(height: 32.h), + _buildWelcomeText(), + + // 输入框和继续按钮 + SizedBox(height: 24.h), + _buildInputField(), + SizedBox(height: 16.h), + _buildVerificationInputField(), + SizedBox(height: 16.h), + _buildContinueButton(), + + // 社交媒体登录 + SizedBox(height: 32.h), + _buildSocialLoginSection(), + + // 用户协议 + SizedBox(height: 40.h), + _buildTermsAndPrivacy(), + SizedBox(height: 24.h), + ], + ), + ), + ); + } + + // 验证码页面 + Widget _buildVerificationView(BuildContext context) { + return Padding( + padding: EdgeInsets.all(24.w), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 返回按钮 + _buildBackButton(), + + // 验证码标题 + SizedBox(height: 24.h), + _buildVerificationTitle(), + + // 验证码输入框 + SizedBox(height: 32.h), + _buildVerificationInputs(context), + + // 重新发送验证码 + SizedBox(height: 16.h), + _buildResendButton(), + + // 登录按钮 + SizedBox(height: 32.h), + _buildLoginButton(), + + // 使用密码登录 + SizedBox(height: 16.h), + _buildUsePasswordButton(), + SizedBox(height: 24.h), + ], + ), + ), + ); + } + + // 状态栏 + Widget _buildStatusBar() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '9:41', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[800], + ), + ), + Row( + children: [ + Icon(Icons.signal_cellular_4_bar, size: 12.sp, color: Colors.grey[800]), + SizedBox(width: 4.w), + Icon(Icons.wifi, size: 12.sp, color: Colors.grey[800]), + SizedBox(width: 4.w), + Icon(Icons.battery_full, size: 12.sp, color: Colors.grey[800]), + ], + ), + ], + ); + } + + // 应用标志和标题 + Widget _buildAppLogo() { + return Column( + children: [ + Container( + width: 80.w, + height: 80.w, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.blue, Colors.purple], + ), + borderRadius: BorderRadius.circular(24.r), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + Icons.headset, + size: 40.sp, + color: Colors.white, + ), + ), + SizedBox(height: 16.h), + Text( + 'DeepVoice', + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + SizedBox(height: 8.h), + Text( + '连接世界,超越声音', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[500], + ), + ), + ], + ); + } + + // 欢迎文本 + Widget _buildWelcomeText() { + return Column( + children: [ + Text( + '欢迎使用', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[800], + ), + ), + SizedBox(height: 4.h), + Text( + '请使用手机号或邮箱登录/注册', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[500], + ), + ), + ], + ); + } + + // 输入框 + Widget _buildInputField() { + return Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h), + child: Row( + children: [ + Icon( + Icons.person_outline, + color: Colors.grey[400], + size: 20.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: TextField( + controller: controller.contactController, + decoration: InputDecoration( + hintText: '手机号码或邮箱', + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey[400], + ), + border: InputBorder.none, + ), + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[700], + ), + ), + ), + ], + ), + ), + ); + } + + // 验证码输入框 + Widget _buildVerificationInputField() { + return Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Padding( + padding: EdgeInsets.only(left: 16.w, right: 4.w, top: 4.h, bottom: 4.h), + child: Row( + children: [ + Icon( + Icons.lock_outline, + color: Colors.grey[400], + size: 20.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: TextField( + controller: controller.verificationCodeController, + keyboardType: TextInputType.number, + maxLength: 6, + decoration: InputDecoration( + hintText: '验证码', + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey[400], + ), + border: InputBorder.none, + counterText: '', + ), + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[700], + ), + ), + ), + Obx(() => TextButton( + onPressed: controller.countdownSeconds > 0 + ? null + : controller.getVerificationCode, + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(horizontal: 16.w), + foregroundColor: Colors.blue[600], + backgroundColor: Colors.transparent, + disabledForegroundColor: Colors.grey[400], + ), + child: Text( + controller.countdownSeconds > 0 + ? '${controller.countdownSeconds}s后重新获取' + : '获取验证码', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + )), + ], + ), + ), + ); + } + + // 继续按钮 + Widget _buildContinueButton() { + return SizedBox( + width: double.infinity, + child: Obx(() => ElevatedButton( + onPressed: controller.isLoading ? null : controller.loginWithVerificationCode, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[600], + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + elevation: 2, + ), + child: controller.isLoading + ? SizedBox( + width: 20.w, + height: 20.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + '登录', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + )), + ); + } + + // 社交媒体登录部分 + Widget _buildSocialLoginSection() { + return Column( + children: [ + Row( + children: [ + Expanded(child: Divider(color: Colors.grey[300])), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Text( + '或使用以下方式登录', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[500], + ), + ), + ), + Expanded(child: Divider(color: Colors.grey[300])), + ], + ), + SizedBox(height: 24.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildSocialButton(FontAwesomeIcons.google, Colors.grey[700]!), + SizedBox(width: 24.w), + _buildSocialButton(FontAwesomeIcons.weixin, Colors.green), + ], + ), + ], + ); + } + + // 社交媒体按钮 + Widget _buildSocialButton(IconData icon, Color color) { + return Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(24.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + icon: Icon(icon, color: color, size: 20.sp), + onPressed: () {}, + ), + ); + } + + // 用户协议和隐私政策 + Widget _buildTermsAndPrivacy() { + return Column( + children: [ + Text( + '登录/注册即表示你同意', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[500], + ), + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () {}, + child: Text( + '《用户协议》', + style: TextStyle( + fontSize: 12.sp, + color: Colors.blue[600], + ), + ), + ), + Text( + ' 和 ', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[500], + ), + ), + GestureDetector( + onTap: () {}, + child: Text( + '《隐私政策》', + style: TextStyle( + fontSize: 12.sp, + color: Colors.blue[600], + ), + ), + ), + ], + ), + ], + ); + } + + // 返回按钮 + Widget _buildBackButton() { + return Container( + width: 40.w, + height: 40.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.grey[500], size: 20.sp), + onPressed: controller.showWelcomeView, + ), + ); + } + + // 验证码标题 + Widget _buildVerificationTitle() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '输入验证码', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[800], + ), + ), + SizedBox(height: 8.h), + Obx(() => Text( + '验证码已发送至 ${controller.displayContact}', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[500], + ), + )), + ], + ); + } + + // 验证码输入框 + Widget _buildVerificationInputs(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(4, (index) => _buildVerificationInput(index, context)), + ); + } + + // 单个验证码输入框 + Widget _buildVerificationInput(int index, BuildContext context) { + return Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: Colors.grey[100]!.withOpacity(0.5), + borderRadius: BorderRadius.circular(16.r), + border: Border.all( + color: Colors.grey[300]!, + width: 1, + ), + ), + child: Center( + child: TextField( + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + onChanged: (value) { + controller.updateVerificationCode(index, value); + if (value.isNotEmpty && index < 3) { + FocusScope.of(context).nextFocus(); + } + }, + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + ), + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[800], + ), + ), + ), + ); + } + + // 重新发送验证码按钮 + Widget _buildResendButton() { + return Center( + child: Obx(() => TextButton.icon( + onPressed: controller.countdownSeconds > 0 + ? null + : controller.resendVerificationCode, + icon: Icon( + Icons.refresh, + size: 14.sp, + color: controller.countdownSeconds > 0 + ? Colors.grey[400] + : Colors.blue[600], + ), + label: Text( + controller.countdownSeconds > 0 + ? '重新发送验证码 (${controller.countdownSeconds}s)' + : '重新发送验证码', + style: TextStyle( + fontSize: 14.sp, + color: controller.countdownSeconds > 0 + ? Colors.grey[400] + : Colors.blue[600], + ), + ), + )), + ); + } + + // 登录按钮 + Widget _buildLoginButton() { + return SizedBox( + width: double.infinity, + child: Obx(() => ElevatedButton( + onPressed: controller.isLoading ? null : controller.login, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[600], + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + elevation: 2, + ), + child: controller.isLoading + ? SizedBox( + width: 20.w, + height: 20.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + '登录', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + )), + ); + } + + // 使用密码登录按钮 + Widget _buildUsePasswordButton() { + return Center( + child: TextButton( + onPressed: controller.usePasswordLogin, + child: Text( + '使用密码登录', + style: TextStyle( + fontSize: 14.sp, + color: Colors.blue[600], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/pairing/bindings/pairing_binding.dart b/lib/modules/pairing/bindings/pairing_binding.dart new file mode 100644 index 000000000..cd57e4ac2 --- /dev/null +++ b/lib/modules/pairing/bindings/pairing_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/pairing_controller.dart'; + +class PairingBinding extends Bindings { + @override + void dependencies() { + Get.put(PairingController()); + } +} \ No newline at end of file diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart new file mode 100644 index 000000000..c4fce6a9e --- /dev/null +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -0,0 +1,295 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import '../../../core/routes/app_routes.dart'; +import '../../../data/services/bluetooth_service.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothService; +import '../../../core/utils/logger.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter/material.dart'; + +enum SearchStatus { + searching, // 正在搜索设备 + found, // 找到设备 + failed, // 搜索失败 + noPermission, // 缺少权限 +} + +class PairingController extends GetxController { + // 搜索状态 + final _searchStatus = SearchStatus.searching.obs; + SearchStatus get searchStatus => _searchStatus.value; + + // 搜索动画的点 + final _searchDots = ''.obs; + String get searchDots => _searchDots.value; + + // 动画定时器 + Timer? _animationTimer; + + // 蓝牙服务 + late BluetoothService _bluetoothService; + + // 设备信息 + final _deviceInfo = Rx>({}); + Map get deviceInfo => _deviceInfo.value; + + // 蓝牙状态 + final _isBluetoothAvailable = false.obs; + final _isBluetoothOn = false.obs; + bool get isBluetoothAvailable => _isBluetoothAvailable.value; + bool get isBluetoothOn => _isBluetoothOn.value; + + // 错误消息 + final _errorMessage = ''.obs; + String get errorMessage => _errorMessage.value; + + @override + void onInit() { + super.onInit(); + // 启动搜索动画 + _startSearchingAnimation(); + + // 延迟初始化蓝牙服务,避免在应用启动时就请求权限 + // 改为延迟初始化,给UI足够时间渲染 + Future.delayed(const Duration(milliseconds: 500), () { + _initBluetoothService(); + }); + } + + @override + void onClose() { + _stopSearchingAnimation(); + super.onClose(); + } + + // 初始化蓝牙服务 + Future _initBluetoothService() async { + try { + // 获取蓝牙服务实例 + if (Get.isRegistered()) { + _bluetoothService = Get.find(); + } else { + _bluetoothService = await Get.put(BluetoothService()).init(); + } + + // 监听蓝牙状态变化 + ever(_bluetoothService.isAvailableObs, (bool available) { + _isBluetoothAvailable.value = available; + }); + + ever(_bluetoothService.isOnObs, (bool on) { + _isBluetoothOn.value = on; + + // 如果蓝牙开启,开始扫描设备 + if (on) { + // 延迟开始扫描,避免权限请求过早 + Future.delayed(const Duration(milliseconds: 300), () { + _startScanningDevices(); + }); + } else { + _errorMessage.value = '请打开蓝牙以搜索设备'; + _searchStatus.value = SearchStatus.failed; + _stopSearchingAnimation(); + } + }); + + // 如果蓝牙已经开启,直接开始扫描 + if (_bluetoothService.isOn) { + _isBluetoothOn.value = true; + // 延迟开始扫描,避免权限请求过早 + Future.delayed(const Duration(milliseconds: 300), () { + _startScanningDevices(); + }); + } else { + _errorMessage.value = '请打开蓝牙以搜索设备'; + } + } catch (e) { + Logger.error('初始化蓝牙服务失败: $e'); + _errorMessage.value = '初始化蓝牙服务失败'; + _searchStatus.value = SearchStatus.failed; + _stopSearchingAnimation(); + } + } + + // 启动搜索动画 + void _startSearchingAnimation() { + _stopSearchingAnimation(); // 清除已有的定时器 + + int count = 0; + _animationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { + count = (count + 1) % 4; + _searchDots.value = '.' * count; + }); + } + + // 停止搜索动画 + void _stopSearchingAnimation() { + _animationTimer?.cancel(); + _animationTimer = null; + _searchDots.value = ''; + } + + // 请求必要权限 + Future _requestPermissions() async { + // 暂时跳过权限检查,直接返回true + return true; + } + + // 打开应用设置 + void goToAppSettings() async { + try { + await openAppSettings(); + } catch (e) { + Logger.error('打开应用设置失败: $e'); + Get.snackbar( + '提示', + '无法打开应用设置,请手动打开系统设置授予权限', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red[100], + colorText: Colors.red[800], + duration: const Duration(seconds: 3), + ); + } + } + + // 开始扫描蓝牙设备 + Future _startScanningDevices() async { + try { + // 先检查是否已有连接的设备 + if (_bluetoothService.connectedDevices.isNotEmpty) { + // 有已连接的设备,直接使用 + _deviceFound(_bluetoothService.connectedDevices.first); + return; + } + + // 检查是否有已配对的设备 + if (await _checkPairedDevices()) { + // 已找到并成功连接到配对设备 + return; + } + + // 搜索新设备 + _searchStatus.value = SearchStatus.searching; + await _bluetoothService.startScan(timeout: const Duration(seconds: 10)); + + // 检查扫描结果 + if (_bluetoothService.scanResults.isNotEmpty) { + // 找到设备,选择信号最强的一个 + final strongestSignal = _bluetoothService.scanResults + .reduce((a, b) => (a.rssi > b.rssi) ? a : b); + + // 连接设备 + final success = await _bluetoothService.connectToDevice(strongestSignal.device); + + if (success) { + _deviceFound(strongestSignal.device); + } else { + _errorMessage.value = '连接设备失败,请重试'; + _searchStatus.value = SearchStatus.failed; + _stopSearchingAnimation(); + } + } else { + // 未找到设备 + _errorMessage.value = '未找到蓝牙设备,请确保设备已开启并在附近'; + _searchStatus.value = SearchStatus.failed; + _stopSearchingAnimation(); + } + } catch (e) { + Logger.error('扫描蓝牙设备失败: $e'); + _errorMessage.value = '扫描蓝牙设备失败: ${e.toString()}'; + _searchStatus.value = SearchStatus.failed; + _stopSearchingAnimation(); + } + } + + // 检查已配对设备 + Future _checkPairedDevices() async { + try { + // 先检查当前是否有设备已连接 + if (_bluetoothService.selectedDevice != null && + _bluetoothService.connectionState == BluetoothConnectionState.connected) { + Logger.info('已有设备连接: ${_bluetoothService.selectedDevice!.platformName}'); + _deviceFound(_bluetoothService.selectedDevice!); + return true; + } + + List pairedDevices = await _bluetoothService.getPairedDevices(); + if (pairedDevices.isEmpty) { + Logger.info('没有找到已配对设备'); + return false; + } + + // 遍历配对设备,检查是否有设备已连接 + for (BluetoothDevice device in pairedDevices) { + try { + BluetoothConnectionState state = await device.connectionState.first; + if (state == BluetoothConnectionState.connected) { + Logger.info('发现已连接的设备: ${device.platformName}'); + // 设置为当前设备但不需要连接 + final success = await _bluetoothService.connectToDevice(device); + if (success) { + _deviceFound(device); + return true; + } + } + } catch (e) { + // 忽略单个设备的连接状态检查错误,继续检查下一个 + Logger.error('检查设备连接状态失败: $e'); + } + } + + // 如果没有找到已连接的设备,尝试连接第一个配对设备 + Logger.info('尝试连接配对设备: ${pairedDevices.first.platformName}'); + final success = await _bluetoothService.connectToDevice(pairedDevices.first); + + if (success) { + _deviceFound(pairedDevices.first); + return true; + } else { + // 连接失败,继续搜索新设备 + Logger.info('已配对设备连接失败,继续搜索新设备'); + return false; + } + } catch (e) { + Logger.error('检查已配对设备失败: $e'); + return false; + } + } + + // 设备被找到时的处理 + void _deviceFound(BluetoothDevice device) { + _searchStatus.value = SearchStatus.found; + _stopSearchingAnimation(); + + // 获取设备信息 + _deviceInfo.value = _bluetoothService.getConnectedDeviceInfo(); + + // 延迟后跳转到主页,同时传递设备信息 + Timer(const Duration(seconds: 1), () { + Get.offAllNamed(Routes.home, arguments: _deviceInfo.value); + }); + } + + // 手动连接 + void manualConnect() { + if (_searchStatus.value == SearchStatus.searching) { + // 如果正在搜索,则停止搜索 + _bluetoothService.stopScan(); + } + + // 重新开始搜索 + _startSearchingAnimation(); + _searchStatus.value = SearchStatus.searching; + _startScanningDevices(); + } + + // 稍后再说 + void skipConnecting() { + if (_searchStatus.value == SearchStatus.searching) { + // 如果正在搜索,则停止搜索 + _bluetoothService.stopScan(); + } + + Get.offAllNamed(Routes.home); + } +} \ No newline at end of file diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart new file mode 100644 index 000000000..e93309447 --- /dev/null +++ b/lib/modules/pairing/views/pairing_view.dart @@ -0,0 +1,344 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../controllers/pairing_controller.dart'; +import '../../../core/widgets/common_bottom_nav.dart'; + +class PairingView extends GetView { + const PairingView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + body: SafeArea( + child: Stack( + children: [ + // 主要内容 + _buildMainContent(), + ], + ), + ), + bottomNavigationBar: Opacity( + opacity: 0.5, // 保持半透明效果 + child: const CommonBottomNav(currentIndex: 0), + ), + ); + } + + // 主要内容 + Widget _buildMainContent() { + return Padding( + padding: EdgeInsets.fromLTRB(24.w, 40.h, 24.w, 24.h), + child: Column( + children: [ + // 标题部分 + _buildHeader(), + + // 设备动画 + SizedBox(height: 40.h), + _buildDeviceAnimation(), + + // 搜索状态 + SizedBox(height: 40.h), + _buildSearchStatus(), + + // 底部按钮 + const Spacer(), + _buildBottomButtons(), + SizedBox(height: 80.h), // 留出底部导航的空间 + ], + ), + ); + } + + // 标题部分 + Widget _buildHeader() { + return Column( + children: [ + Text( + 'DeepVoice', + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + SizedBox(height: 8.h), + Text( + '连接世界,超越声音', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[500], + ), + ), + ], + ); + } + + // 设备动画 + Widget _buildDeviceAnimation() { + return SizedBox( + width: 240.w, + height: 240.w, + child: Stack( + alignment: Alignment.center, + children: [ + // 脉动动画背景 + _buildPulseBackground(), + + // 耳机图片 + _buildHeadphoneImage(), + + // 旋转加载指示器 + _buildLoadingIndicator(), + ], + ), + ); + } + + // 脉动背景 + Widget _buildPulseBackground() { + return TweenAnimationBuilder( + tween: Tween(begin: 0.8, end: 1.1), + duration: const Duration(seconds: 2), + curve: Curves.easeInOut, + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Container( + width: 200.w, + height: 200.w, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.withOpacity(0.2), + Colors.purple.withOpacity(0.2), + ], + ), + shape: BoxShape.circle, + ), + ), + ); + }, + ); + } + + // 耳机图片 + Widget _buildHeadphoneImage() { + return Container( + width: 140.w, + height: 140.w, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue, + Colors.purple, + ], + ), + shape: BoxShape.circle, + ), + child: Icon( + Icons.headset, + size: 80.sp, + color: Colors.white, + ), + ); + } + + // 加载指示器 + Widget _buildLoadingIndicator() { + return SizedBox( + width: 180.w, + height: 180.w, + child: CircularProgressIndicator( + strokeWidth: 4.w, + valueColor: AlwaysStoppedAnimation(Colors.blue), + backgroundColor: Colors.blue.withOpacity(0.1), + ), + ); + } + + // 搜索状态 + Widget _buildSearchStatus() { + return Obx(() { + switch (controller.searchStatus) { + case SearchStatus.searching: + return Column( + children: [ + Text( + '寻找设备中${controller.searchDots}', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[800], + ), + ), + SizedBox(height: 8.h), + Text( + '请打开耳机充电盒,将耳机放入您的耳朵', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[500], + ), + ), + ], + ); + case SearchStatus.found: + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.check_circle, color: Colors.green, size: 20.sp), + SizedBox(width: 8.w), + Text( + '设备连接成功', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.green, + ), + ), + ], + ), + SizedBox(height: 8.h), + Text( + '正在跳转到主页...', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[500], + ), + ), + ], + ); + case SearchStatus.noPermission: + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.no_encryption, color: Colors.orange, size: 20.sp), + SizedBox(width: 8.w), + Text( + '缺少必要权限', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.orange, + ), + ), + ], + ), + SizedBox(height: 8.h), + Text( + controller.errorMessage, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () => controller.goToAppSettings(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + child: Text('打开设置'), + ), + ], + ); + case SearchStatus.failed: + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error, color: Colors.red, size: 20.sp), + SizedBox(width: 8.w), + Text( + '未找到设备', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.red, + ), + ), + ], + ), + SizedBox(height: 8.h), + Text( + controller.errorMessage.isEmpty ? + '请确保耳机已开机并在附近' : controller.errorMessage, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + ], + ); + default: + return SizedBox(); + } + }); + } + + // 底部按钮 + Widget _buildBottomButtons() { + return Column( + children: [ + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: controller.manualConnect, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[200], + foregroundColor: Colors.grey[800], + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + elevation: 0, + ), + child: Text( + '手动连接', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + SizedBox(height: 12.h), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: controller.skipConnecting, + style: TextButton.styleFrom( + foregroundColor: Colors.blue[600], + padding: EdgeInsets.symmetric(vertical: 12.h), + ), + child: Text( + '稍后再说', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/modules/settings/bindings/settings_binding.dart b/lib/modules/settings/bindings/settings_binding.dart new file mode 100644 index 000000000..f8ff3eb08 --- /dev/null +++ b/lib/modules/settings/bindings/settings_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import '../controllers/settings_controller.dart'; + +class SettingsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => SettingsController(), + ); + } +} \ No newline at end of file diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart new file mode 100644 index 000000000..7d0394484 --- /dev/null +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -0,0 +1,66 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; + +class SettingsController extends GetxController { + // 噪声消除开关状态 + final RxBool noiseReductionEnabled = true.obs; + + // 环境音增强模式 + final RxString ambientSoundMode = '中等'.obs; + final List availableAmbientModes = ['关闭', '低', '中等', '高']; + + // 立体声平衡值 + final RxDouble stereoBalance = 0.0.obs; // -1.0 (左) 到 1.0 (右), 0 为中间 + + // AI助理默认模式 + final RxString aiAssistantMode = '通用助手'.obs; + final List availableAiModes = ['通用助手', '翻译助手', '学习助手', '交际助手']; + + // AI声音选择 + final RxString aiVoice = '默认女声'.obs; + final List availableVoices = ['默认女声', '默认男声', '温柔女声', '磁性男声', '活力女声']; + + // 翻译语言对 + final RxString sourceLanguage = '中文'.obs; + final RxString targetLanguage = '英文'.obs; + final List availableLanguages = ['中文', '英文', '日文', '韩文', '法文', '德文', '西班牙文', '俄文']; + + void toggleNoiseReduction(bool value) { + noiseReductionEnabled.value = value; + } + + void updateAmbientSoundMode(String mode) { + ambientSoundMode.value = mode; + } + + void updateStereoBalance(double value) { + stereoBalance.value = value; + } + + void updateAiAssistantMode(String mode) { + aiAssistantMode.value = mode; + } + + void updateAiVoice(String voice) { + aiVoice.value = voice; + } + + void setSourceLanguage(String language) { + sourceLanguage.value = language; + } + + void setTargetLanguage(String language) { + targetLanguage.value = language; + } + + void swapLanguages() { + final temp = sourceLanguage.value; + sourceLanguage.value = targetLanguage.value; + targetLanguage.value = temp; + } + + void signOut() { + // 在此实现退出登录逻辑 + Get.offAllNamed('/login'); + } +} \ No newline at end of file diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart new file mode 100644 index 000000000..74e3d162d --- /dev/null +++ b/lib/modules/settings/views/settings_view.dart @@ -0,0 +1,1158 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../controllers/settings_controller.dart'; +import '../../../core/widgets/common_bottom_nav.dart'; +import '../../../core/routes/app_routes.dart'; + +class SettingsView extends GetView { + const SettingsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0.5, + title: Text( + '设置', + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black54), + onPressed: () => Get.back(), + ), + ), + body: ListView( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + children: [ + _buildUserInfoCard(), + SizedBox(height: 20.h), + _buildHeadphoneSettingsSection(), + SizedBox(height: 20.h), + _buildAISettingsSection(), + SizedBox(height: 20.h), + _buildSystemSettingsSection(), + SizedBox(height: 20.h), + _buildAboutSection(), + SizedBox(height: 50.h), + ], + ), + bottomNavigationBar: const CommonBottomNav(currentIndex: 2), + ); + } + + // 用户信息卡片 + Widget _buildUserInfoCard() { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFE3F2FD), Color(0xFFF3E5F5)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(30.r), + child: Image.network( + 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80', + fit: BoxFit.cover, + ), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '林美珍', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 4.h), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(10.r), + ), + child: Text( + '高级会员', + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + ), + ), + ), + SizedBox(width: 8.w), + Flexible( + child: Text( + '已连接设备: DeepVoice Pro', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 4.h), + Text( + '账号ID: deepvoice_834291', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + ], + ), + ), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.7), + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.edit, + size: 16.w, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + // 耳机设置部分 + Widget _buildHeadphoneSettingsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + '耳机设置', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // 噪声消除 + Obx(() => _buildToggleSetting( + title: '噪声消除', + subtitle: '降低周围环境噪音', + icon: Icons.volume_off_rounded, + iconBgColor: Colors.blue[100]!, + iconColor: Colors.blue[600]!, + value: controller.noiseReductionEnabled.value, + onChanged: controller.toggleNoiseReduction, + )), + const Divider(height: 1), + + // 环境音增强 + _buildNavigationSetting( + title: '环境音增强', + subtitle: '增强周围重要声音', + icon: Icons.nature, + iconBgColor: Colors.green[100]!, + iconColor: Colors.green[600]!, + value: controller.ambientSoundMode.value, + onTap: () { + // 弹出环境音增强设置对话框 + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '环境音增强', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 16.h), + ...controller.availableAmbientModes.map((mode) => + RadioListTile( + title: Text(mode), + value: mode, + groupValue: controller.ambientSoundMode.value, + onChanged: (value) { + if (value != null) { + controller.updateAmbientSoundMode(value); + Get.back(); + } + }, + ), + ).toList(), + SizedBox(height: 16.h), + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + ], + ), + ), + ), + ); + }, + ), + const Divider(height: 1), + + // 立体声平衡 + Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.purple[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.balance, + size: 16.w, + color: Colors.purple[600], + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '立体声平衡', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + SizedBox(height: 2.h), + Text( + '调整左右耳机音量平衡', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + ], + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + SliderTheme( + data: SliderThemeData( + trackHeight: 4.h, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 8.r), + ), + child: Slider( + value: controller.stereoBalance.value, + min: -1.0, + max: 1.0, + onChanged: controller.updateStereoBalance, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '左', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + Text( + '平衡', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + Text( + '右', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + ], + ), + ), + ], + )), + ], + ), + ), + ], + ), + ), + ], + ); + } + + // AI设置部分 + Widget _buildAISettingsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + 'AI 设置', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // AI助理默认模式 + _buildNavigationSetting( + title: 'AI助理默认模式', + subtitle: '选择默认的AI互动风格', + icon: Icons.android, + iconBgColor: Colors.red[100]!, + iconColor: Colors.red[600]!, + value: controller.aiAssistantMode.value, + onTap: () { + // 弹出AI助理模式选择对话框 + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'AI助理模式', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 16.h), + ...controller.availableAiModes.map((mode) => + RadioListTile( + title: Text(mode), + value: mode, + groupValue: controller.aiAssistantMode.value, + onChanged: (value) { + if (value != null) { + controller.updateAiAssistantMode(value); + Get.back(); + } + }, + ), + ).toList(), + SizedBox(height: 16.h), + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + ], + ), + ), + ), + ); + }, + ), + const Divider(height: 1), + + // AI声音选择 + _buildNavigationSetting( + title: 'AI声音选择', + subtitle: '选择AI的声音和音调', + icon: Icons.record_voice_over, + iconBgColor: Colors.amber[100]!, + iconColor: Colors.amber[600]!, + value: controller.aiVoice.value, + onTap: () { + // 弹出AI声音选择对话框 + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'AI声音选择', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 16.h), + ...controller.availableVoices.map((voice) => + RadioListTile( + title: Text(voice), + value: voice, + groupValue: controller.aiVoice.value, + onChanged: (value) { + if (value != null) { + controller.updateAiVoice(value); + Get.back(); + } + }, + ), + ).toList(), + SizedBox(height: 16.h), + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + ], + ), + ), + ), + ); + }, + ), + const Divider(height: 1), + + // 翻译语言 + Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 左侧标题和图标部分 + Flexible( + child: Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.teal[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.translate, + size: 16.w, + color: Colors.teal[600], + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '翻译语言', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 2.h), + Text( + '设置需要翻译的语言', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + // 右侧语言选择部分 + Obx(() => Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + _showLanguageSelectionDialog(true); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + controller.sourceLanguage.value, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + ), + ), + ), + ), + IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: const Icon(Icons.swap_horiz, size: 20), + onPressed: controller.swapLanguages, + color: Colors.grey[500], + ), + GestureDetector( + onTap: () { + _showLanguageSelectionDialog(false); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + controller.targetLanguage.value, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[700], + ), + ), + ), + ), + SizedBox(width: 4.w), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.add, + size: 16.w, + color: Colors.grey[500], + ), + ), + ], + )), + ], + ), + ), + ], + ), + ), + ], + ); + } + + // 系统设置部分 + Widget _buildSystemSettingsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + '系统设置', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // 通知设置 + _buildSimpleNavigationSetting( + title: '通知设置', + subtitle: '管理应用推送和提醒', + icon: Icons.notifications_none, + iconBgColor: Colors.indigo[100]!, + iconColor: Colors.indigo[600]!, + onTap: () { + // 打开通知设置页面 + }, + ), + const Divider(height: 1), + + // 备份与恢复 + _buildNavigationSetting( + title: '备份与恢复', + subtitle: '备份设置和AI互动历史', + icon: Icons.cloud_upload_outlined, + iconBgColor: Colors.cyan[100]!, + iconColor: Colors.cyan[600]!, + value: '上次备份: 今天', + valueStyle: TextStyle( + fontSize: 12.sp, + color: Colors.grey[500], + ), + onTap: () { + // 打开备份设置页面 + }, + ), + const Divider(height: 1), + + // 隐私设置 + _buildSimpleNavigationSetting( + title: '隐私设置', + subtitle: '管理数据收集和权限', + icon: Icons.shield_outlined, + iconBgColor: Colors.purple[100]!, + iconColor: Colors.purple[600]!, + onTap: () { + // 打开隐私设置页面 + }, + ), + const Divider(height: 1), + + // 退出登录 + _buildSimpleNavigationSetting( + title: '退出登录', + icon: Icons.logout, + iconBgColor: Colors.red[50]!, + iconColor: Colors.red[600]!, + titleColor: Colors.red[600]!, + onTap: () { + // 退出登录确认对话框 + Get.dialog( + AlertDialog( + title: const Text('退出登录'), + content: const Text('确定要退出当前账号吗?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Get.back(); + controller.signOut(); + }, + child: const Text('确定'), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ], + ); + } + + // 关于应用部分 + Widget _buildAboutSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + '关于应用', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 40.w, + height: 40.w, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF2196F3), Color(0xFF9C27B0)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + Icons.headphones, + size: 24.w, + color: Colors.white, + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'DeepVoice', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + SizedBox(height: 2.h), + Text( + '版本 2.4.1 (Build 87)', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + ], + ), + ], + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Text( + '检查更新', + style: TextStyle( + fontSize: 12.sp, + color: Colors.blue[600], + ), + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + Center( + child: Column( + children: [ + Text( + '© 2023 DeepVoice Technologies', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[500], + ), + ), + SizedBox(height: 4.h), + Text( + '连接世界,超越声音', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[500], + ), + ), + ], + ), + ), + ], + ); + } + + // 开关类设置项 + Widget _buildToggleSetting({ + required String title, + String? subtitle, + required IconData icon, + required Color iconBgColor, + required Color iconColor, + required bool value, + required Function(bool) onChanged, + }) { + return Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + if (subtitle != null) + SizedBox(height: 2.h), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + ], + ), + ], + ), + Switch( + value: value, + onChanged: onChanged, + activeColor: Colors.blue[600], + ), + ], + ), + ); + } + + // 带导航的设置项 + Widget _buildNavigationSetting({ + required String title, + String? subtitle, + required IconData icon, + required Color iconBgColor, + required Color iconColor, + required String value, + TextStyle? valueStyle, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + if (subtitle != null) + SizedBox(height: 2.h), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + ], + ), + ], + ), + Row( + children: [ + Text( + value, + style: valueStyle ?? TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + SizedBox(width: 8.w), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.chevron_right, + size: 16.w, + color: Colors.grey[500], + ), + ), + ], + ), + ], + ), + ), + ); + } + + // 简单导航设置项 + Widget _buildSimpleNavigationSetting({ + required String title, + String? subtitle, + required IconData icon, + required Color iconBgColor, + required Color iconColor, + Color? titleColor, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: titleColor ?? Colors.black87, + ), + ), + if (subtitle != null) + SizedBox(height: 2.h), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + ], + ), + ], + ), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.chevron_right, + size: 16.w, + color: Colors.grey[500], + ), + ), + ], + ), + ), + ); + } + + // 显示语言选择对话框 + void _showLanguageSelectionDialog(bool isSource) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + isSource ? '选择源语言' : '选择目标语言', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 16.h), + SizedBox( + height: 300.h, + child: ListView( + children: controller.availableLanguages.map((language) => + RadioListTile( + title: Text(language), + value: language, + groupValue: isSource + ? controller.sourceLanguage.value + : controller.targetLanguage.value, + onChanged: (value) { + if (value != null) { + if (isSource) { + controller.setSourceLanguage(value); + } else { + controller.setTargetLanguage(value); + } + Get.back(); + } + }, + ), + ).toList(), + ), + ), + SizedBox(height: 16.h), + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/splash/bindings/splash_binding.dart b/lib/modules/splash/bindings/splash_binding.dart new file mode 100644 index 000000000..f8fb7eadd --- /dev/null +++ b/lib/modules/splash/bindings/splash_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/splash_controller.dart'; + +class SplashBinding extends Bindings { + @override + void dependencies() { + Get.put(SplashController()); + } +} \ No newline at end of file diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart new file mode 100644 index 000000000..0bc568c2a --- /dev/null +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../core/routes/app_routes.dart'; + +class SplashController extends GetxController { + // 进度条动画控制 + final _loadingProgress = 0.0.obs; + double get loadingProgress => _loadingProgress.value; + + // 版本号 + final String version = 'v1.0.0'; + + // 资源加载状态 + final _isResourceLoaded = false.obs; + bool get isResourceLoaded => _isResourceLoaded.value; + + @override + void onInit() { + super.onInit(); + + // 确保页面先渲染,再执行其他操作 + WidgetsBinding.instance.addPostFrameCallback((_) { + // 启动进度条动画 + _startLoadingAnimation(); + + // 异步加载应用资源 + _loadAppResources(); + }); + } + + // 进度条动画 + void _startLoadingAnimation() { + // 模拟0.5秒的加载进度 + Timer.periodic(const Duration(milliseconds: 5), (timer) { + if (_loadingProgress.value < 1.0) { + _loadingProgress.value += 0.06; + } else { + timer.cancel(); + // 当进度条动画完成且资源加载完毕时,跳转到登录页面 + if (_isResourceLoaded.value) { + _navigateToLogin(); + } + } + }); + } + + // 异步加载应用资源 + Future _loadAppResources() async { + try { + // 在这里可以添加应用初始化需要的资源加载 + // 例如:预加载图片、初始化数据库、加载配置等 + + // 模拟耗时操作 + await Future.delayed(const Duration(milliseconds: 100)); + + // TODO: 在此处添加实际的资源加载代码 + + // 标记资源加载完成 + _isResourceLoaded.value = true; + + // 如果进度条已经走完,则跳转到登录页面 + if (_loadingProgress.value >= 1.0) { + _navigateToLogin(); + } + } catch (e) { + // 处理资源加载错误 + // 即使加载失败,也应该允许用户继续使用应用 + _isResourceLoaded.value = true; + + // 记录错误日志 + print('资源加载错误: $e'); + } + } + + // 跳转到登录页面 + void _navigateToLogin() { + // 添加一个小延迟,确保UI完全渲染 + Future.delayed(const Duration(milliseconds: 200), () { + Get.offAllNamed(Routes.login); + }); + } +} \ No newline at end of file diff --git a/lib/modules/splash/views/splash_view.dart b/lib/modules/splash/views/splash_view.dart new file mode 100644 index 000000000..5de82acc2 --- /dev/null +++ b/lib/modules/splash/views/splash_view.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../controllers/splash_controller.dart'; + +class SplashView extends GetView { + const SplashView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F7), + body: Stack( + children: [ + // 状态栏 + _buildStatusBar(), + + // 主内容 + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo动画 + _buildAnimatedLogo(), + + // 应用名称 + _buildAppTitle(), + + // 标语 + _buildSlogan(), + + // 加载进度条 + SizedBox(height: 40.h), + _buildLoadingBar(), + + // 额外信息 + SizedBox(height: 16.h), + _buildExtraInfo(), + ], + ), + ), + + // 版本号 + _buildVersionInfo(), + ], + ), + ); + } + + // 状态栏 + Widget _buildStatusBar() { + return Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '9:41', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[800], + ), + ), + Row( + children: [ + Icon(Icons.signal_cellular_4_bar, size: 12.sp, color: Colors.grey[800]), + SizedBox(width: 4.w), + Icon(Icons.wifi, size: 12.sp, color: Colors.grey[800]), + SizedBox(width: 4.w), + Icon(Icons.battery_full, size: 12.sp, color: Colors.grey[800]), + ], + ), + ], + ), + ), + ), + ); + } + + // 动画Logo + Widget _buildAnimatedLogo() { + return TweenAnimationBuilder( + tween: Tween(begin: 0.8, end: 1.1), + duration: const Duration(seconds: 2), + curve: Curves.easeInOut, + builder: (context, value, child) { + // 确保opacity值不超过1.0 + final opacity = value <= 1.0 ? value : 1.0; + + return Transform.scale( + scale: value, + child: AnimatedOpacity( + opacity: opacity, + duration: const Duration(milliseconds: 500), + child: child, + ), + ); + }, + child: Container( + width: 96.w, + height: 96.w, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.blue, Colors.purple], + ), + borderRadius: BorderRadius.circular(24.r), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 8), + spreadRadius: 3, + ), + ], + ), + child: Icon( + Icons.headset, + size: 48.sp, + color: Colors.white, + ), + ), + ); + } + + // 应用标题 + Widget _buildAppTitle() { + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 1500), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Padding( + padding: EdgeInsets.only(top: 24.h), + child: Text( + 'DeepVoice', + style: TextStyle( + fontSize: 28.sp, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + ), + ); + }, + ); + } + + // 标语 + Widget _buildSlogan() { + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 1500), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Padding( + padding: EdgeInsets.only(top: 8.h), + child: Text( + '连接世界,岂止声音', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + ), + ), + ); + }, + ); + } + + // 加载进度条 + Widget _buildLoadingBar() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 80.w), + child: Stack( + children: [ + Container( + height: 4.h, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(2.r), + ), + ), + Obx(() { + final progress = controller.loadingProgress; + return LayoutBuilder( + builder: (context, constraints) { + return Container( + height: 4.h, + width: constraints.maxWidth * progress, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [Colors.blue, Colors.purple], + ), + borderRadius: BorderRadius.circular(2.r), + ), + ); + }, + ); + }), + ], + ), + ); + } + + // 额外信息 + Widget _buildExtraInfo() { + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 1500), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Text( + 'AI语音专家 · 随时随地', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[400], + ), + ), + ); + }, + ); + } + + // 版本信息 + Widget _buildVersionInfo() { + return Positioned( + bottom: 32.h, + left: 0, + right: 0, + child: Center( + child: Text( + controller.version, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[400], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 3dcc3bb51..73a536869 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: crypto: ^3.0.3 flutter_tts: ^4.2.2 logger: ^2.0.2+1 + flutter_blue_plus: ^1.31.16 # shared_preferences: ^2.2.2 # 移除,使用 get_storage 代替 # speech_to_text: ^7.0.0 # 移除,使用自定义的 ASR 服务 window_size: @@ -43,6 +44,7 @@ dependencies: share_plus: ^10.0.0 image_picker: ^1.1.2 audio_waveforms: ^1.3.0 + font_awesome_flutter: ^10.8.0 dev_dependencies: flutter_test: