diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 4681a6fba..22fc128f0 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -196,7 +196,17 @@ class _AgentViewState extends State with WidgetsBindingObserver { IntroStepBuilder( group: 'ai', order: 3, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, + getOverlayPosition: ({ + required Offset offset, + required Size screenSize, + required Size size, + }) { + return OverlayPosition( + top: 200.h, + width: screenSize.width, + crossAxisAlignment: CrossAxisAlignment.end, + ); + }, overlayBuilder: (params) { return Obx( () => IntroOverlayBuilder.homeBuilder( @@ -242,8 +252,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { ? IntroStepBuilder( group: 'ai', order: 4, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, @@ -405,8 +413,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(height: 12.h), ], ); - } else if (controller.isTextInputMode.value && - !controller.hasTextInput.value) { + } else { // 文本输入模式下,且输入框为空 return Column( children: [ @@ -449,8 +456,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(height: 12.h), ], ); - } else { - return const SizedBox.shrink(); } }, ), @@ -506,8 +511,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { return IntroStepBuilder( group: 'ai', order: 1, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, @@ -670,19 +673,30 @@ class _AgentViewState extends State with WidgetsBindingObserver { : Colors.blue[100]) : (isDarkMode ? Colors.grey[800] - : Colors.grey[200]), + : const Color.fromARGB( + 255, 255, 255, 255)), borderRadius: BorderRadius.circular(22.w), - border: Border.all( - color: isPushToTalkActive - ? Colors.blue - : (isDarkMode - ? Colors.grey[800]! - : Colors.grey - .withOpacity(0.1)), - width: - isPushToTalkActive ? 2.w : 1.w, - ), + boxShadow: [ + BoxShadow( + color: + Colors.black.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + // border: Border.all( + // color: isPushToTalkActive + // ? Colors.blue + // : (isDarkMode + // ? Colors.grey[800]! + // : const Color.fromARGB( + // 255, 255, 255, 255) + // .withOpacity(0.1)), + // width: + // isPushToTalkActive ? 2.w : 1.w, + // ), ), child: Row( children: [ @@ -742,143 +756,149 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - IntroStepBuilder( - group: 'ai', - order: 2, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', - alignment: false, - onTap: () async { - await controller - .noviceGuideService.aiStepsList[ - controller.noviceGuideService - .aiStep]['method'](); - params.onNext!(); - controller.sendMessage(); - }, - ); - }, - builder: (BuildContext context, GlobalKey key) => - Obx( - key: key, - () { - final hasText = controller.hasTextInput.value; - final hasImage = - controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + Builder(builder: (context) { + StepWidgetParams? widgetParams; + return IntroStepBuilder( + group: 'ai', + order: 2, + onHighlightWidgetTap: () async { + await controller + .noviceGuideService.aiStepsList[ + controller.noviceGuideService.aiStep] + ['method'](); + widgetParams!.onNext!(); + controller.sendMessage(); + }, + overlayBuilder: (params) { + widgetParams = params; + return IntroOverlayBuilder.aiBuilder( + params, + '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', + alignment: false, + ); + }, + builder: + (BuildContext context, GlobalKey key) => + Obx( + key: key, + () { + final hasText = + controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = hasText || hasImage; + final shouldShowExpand = + controller.shouldShowExpandButton.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: - EdgeInsets.only(bottom: 40.h), - child: GestureDetector( - onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: - Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: + EdgeInsets.only(bottom: 40.h), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 controller - .closeFullScreenEdit(); - }); - }, - child: Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], - borderRadius: - BorderRadius.circular( - 16.w), - ), - child: Icon( - Icons.expand_less, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 20.sp, + .openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller + .closeFullScreenEdit(); + }); + }, + child: Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular( + 16.w), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20.sp, + ), ), ), ), + // 发送按钮 + GestureDetector( + onTap: () => + controller.sendMessage(), + child: Container( + width: 44.w, + height: 44.w, + margin: EdgeInsets.symmetric( + vertical: 2.w), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: + BorderRadius.circular(22.w), + ), + child: Icon( + Icons.send, + color: Colors.white, + size: 22.sp, + ), + ), ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = controller + .isPlusOptionsExpanded.value; + return GestureDetector( + onTap: () { + controller.togglePlusOptions(); + }, child: Container( width: 44.w, height: 44.w, margin: EdgeInsets.symmetric( vertical: 2.w), decoration: BoxDecoration( - color: primaryColor, + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22.w), ), child: Icon( - Icons.send, - color: Colors.white, + isExpanded + ? Icons.close + : Icons.add, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], size: 22.sp, ), ), - ), - ], - ); - } else { - // 显示'+'选择按钮 - return Obx(() { - final isExpanded = controller - .isPlusOptionsExpanded.value; - return GestureDetector( - onTap: () { - controller.togglePlusOptions(); - }, - child: Container( - width: 44.w, - height: 44.w, - margin: EdgeInsets.symmetric( - vertical: 2.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - isExpanded - ? Icons.close - : Icons.add, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22.sp, - ), - ), - ); - }); - } - }, - ), - ), + ); + }); + } + }, + ), + ); + }), ], ), diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index df4d421ca..e51be533d 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -72,86 +72,81 @@ class HomeView extends GetView { @override Widget build(BuildContext context) { - // 预缓存图片 - precacheImage( - const AssetImage('assets/images/headphone_new3.png'), context); - precacheImage( - const AssetImage('assets/images/headphone_new4.png'), context); final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFC), body: SafeArea( - child: Padding( + child: SingleChildScrollView( padding: EdgeInsets.all(16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部标题栏 _buildAppBar(isDarkMode), - // 可变高度的顶部图片区域 - 使用Expanded让它占用剩余空间 - Expanded( - child: Column( - children: [ - Obx(() => _buildAdaptiveEarbudsImage()), - SizedBox(height: 20.h), - // 设备状态卡片 - IntroStepBuilder( - group: 'home', - order: 1, - onWidgetLoad: () { - controller.noviceGuideService.startHomeGuide(); - }, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', - onNext: () { - controller.noviceGuideService.startWakeupListen(); - }, - ); - }, - builder: (BuildContext context, GlobalKey key) => - SizedBox( - key: key, - child: _buildAnimatedDeviceStatusCard(isDarkMode), - ), - ), - SizedBox(height: 20.h), - IntroStepBuilder( - group: 'home', - order: 2, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return Obx( - () => IntroOverlayBuilder.homeBuilder( - params, - controller.noviceGuideService.isWakeup.value, - onNext: () { - controller.noviceGuideService.closeWakeupListen(); - }, - ), - ); + + // 顶部图片 + _buildEarbudsImage(), + + SizedBox(height: 20.h), + + // 设备状态卡片 + IntroStepBuilder( + group: 'home', + order: 1, + onWidgetLoad: () { + controller.noviceGuideService.startHomeGuide(); + }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', + onNext: () { + controller.noviceGuideService.startWakeupListen(); + }, + ); + }, + builder: (BuildContext context, GlobalKey key) => SizedBox( + key: key, + child: _buildDeviceStatusCard(isDarkMode), + ), + ), + + SizedBox(height: 20.h), + + IntroStepBuilder( + group: 'home', + order: 2, + getOverlayPosition: ({ + required Offset offset, + required Size screenSize, + required Size size, + }) { + return OverlayPosition( + top: 200.h, + width: screenSize.width, + crossAxisAlignment: CrossAxisAlignment.end, + ); + }, + overlayBuilder: (params) { + return Obx( + () => IntroOverlayBuilder.homeBuilder( + params, + controller.noviceGuideService.isWakeup.value, + onNext: () { + controller.noviceGuideService.closeWakeupListen(); }, - builder: (BuildContext context, GlobalKey key) => - SizedBox( - key: key, - ), ), - ], + ); + }, + builder: (BuildContext context, GlobalKey key) => SizedBox( + key: key, ), ), - // 热门AI体验 - 固定在底部,基于内容的固定高度 - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, // 重要:让高度基于内容 - children: [ - _buildSectionTitle(isDarkMode), - SizedBox(height: 16.h), - // 给AI布局一个固定的高度,或者让它基于内容自适应 - _buildPremiumAILayout(), - ], - ), + + // 热门AI体验 + _buildSectionTitle(isDarkMode), + SizedBox(height: 16.h), + _buildPremiumAILayout(), ], ), ), @@ -201,47 +196,70 @@ class HomeView extends GetView { ); } - // 自适应高度的耳机图片 - 根据设备连接状态动态调整高度 - // 在自适应图片组件中添加RepaintBoundary - Widget _buildAdaptiveEarbudsImage() { + // 耳机图片 + Widget _buildEarbudsImage() { final bool isDarkMode = Get.isDarkMode; - final double imageHeight = - controller.isFullyConnectedRx.value ? 160.h : 220.h; - - return RepaintBoundary( - // 添加重绘边界,优化动画性能 - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - height: imageHeight, - child: PageView( - children: [ - Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(16.r), - child: Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', - height: imageHeight, - fit: BoxFit.contain, - ), + return SizedBox( + height: 220.h, + child: PageView( + children: [ + // Center( + // child: ClipRRect( + // // 添加 ClipRRect 来实现圆角 + // borderRadius: BorderRadius.circular(16.r), // 使用与其他部件一致的圆角大小 + // child: SizedBox( + // width: double.infinity, + // height: 200.h, + // child: Image.asset( + // isDarkMode + // ? 'assets/images/robot_light.png' + // : 'assets/images/robot_light.png', + // fit: BoxFit.fitWidth, + // ), + // ), + // ), + // ), + Center( + child: ClipRRect( + // 添加 ClipRRect + borderRadius: BorderRadius.circular(16.r), + child: Image.asset( + isDarkMode + ? 'assets/images/headphone_new3.png' + : 'assets/images/headphone_new4.png', + height: 220.h, + fit: BoxFit.contain, ), ), - ], - ), + ), + // Center( + // child: ClipRRect( + // // 添加 ClipRRect + // borderRadius: BorderRadius.circular(16.r), + // child: SizedBox( + // width: double.infinity, + // height: 200.h, + // child: Image.asset( + // isDarkMode + // ? 'assets/images/glasses_light.png' + // : 'assets/images/glasses_light.png', + // fit: BoxFit.fitWidth, + // ), + // ), + // ), + // ), + ], ), ); } - // 带动画的设备状态卡片 - 实现平滑的高度变化过渡 - Widget _buildAnimatedDeviceStatusCard(bool isDarkMode) { + // 设备状态卡片 + Widget _buildDeviceStatusCard(bool isDarkMode) { return Obx(() { + // 使用连接状态作为key,强制在状态改变时重建widget final isConnected = controller.isFullyConnectedRx.value; - - return AnimatedContainer( - duration: const Duration(milliseconds: 500), // 平滑过渡动画 - curve: Curves.easeInOut, + return Container( + key: ValueKey('device_status_$isConnected'), decoration: BoxDecoration( color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, borderRadius: BorderRadius.circular(16.r), @@ -255,58 +273,30 @@ class HomeView extends GetView { ), padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 16.h), child: Column( - mainAxisSize: MainAxisSize.min, // 重要:让容器高度自适应内容 children: [ // 状态标题行 _buildDeviceStatusHeader(isDarkMode), - // 设备信息 - 使用 AnimatedSwitcher 实现内容切换动画 + // 设备信息 SizedBox(height: 12.h), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.1), - end: Offset.zero, - ).animate(animation), - child: child, - ), - ); - }, - child: Container( - key: ValueKey('device_info_$isConnected'), - child: isConnected - ? _buildConnectedDeviceInfo(isDarkMode) - : _buildDisconnectedInfo(isDarkMode), - ), - ), - - // 连接按钮 - 使用 AnimatedSwitcher 实现按钮切换动画 + Obx(() { + // 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断 + // 使用响应式访问确保UI更新 + return controller.isFullyConnectedRx.value + ? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息 + : _buildDisconnectedInfo(isDarkMode); //未连接提示 + }), + + // 连接按钮(仅在未连接时显示) SizedBox(height: 12.h), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.1), - end: Offset.zero, - ).animate(animation), - child: child, - ), - ); - }, - child: Container( - key: ValueKey('device_button_$isConnected'), - child: !isConnected - ? _buildConnectButton() - : _buildDeviceButton(isDarkMode), - ), - ), + Obx(() { + // 使用响应式访问确保UI更新 + if (!controller.isFullyConnectedRx.value) { + return _buildConnectButton(); + } else { + return _buildDeviceButton(isDarkMode); + } + }), ], ), ); @@ -740,15 +730,14 @@ class HomeView extends GetView { child: IntroStepBuilder( group: 'home', order: 3, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, + onHighlightWidgetTap: () { + Intro.of(Get.context!).dispose(); + controller.exploreAIFeatures(); + }, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', - onTap: () { - params.onFinish(); - controller.exploreAIFeatures(); - }, ); }, builder: (BuildContext context, GlobalKey key) => SizedBox( diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 50639a33e..ed21cefaa 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -1487,7 +1487,7 @@ class AudioPlayer { private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) // 等待音效超时时间(秒)- 改为 var 以支持动态修改 - private var awaitTimeoutSeconds: TimeInterval = 10.0 + private var awaitTimeoutSeconds: TimeInterval = 30.0 private var resourcesBundle: Bundle { #if SWIFT_PACKAGE diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift index 2e492fb3b..001176fe5 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift @@ -131,39 +131,63 @@ private class QQMusicSearchHandler: FunctionHandler { } return await withCheckedContinuation { continuation in - // 直接调用QPlayAutoManager的搜索接口 - QPlayAutoManager.shared.search(keyword: keyword, searchType: .song) { items in - var result: [String: Any] - - if let items = items, !items.isEmpty { - let songs = items.map { item -> [String: Any] in - return [ - "mid": item.mid ?? "", - "name": item.name ?? "", - "artist": item.singer?.name ?? "", - "album": item.album ?? "", - "duration": item.duration, - "pic": item.coverUri ?? "" + // 调用更新后的搜索接口,包含错误处理 + QPlayAutoManager.shared.search( + keyword: keyword, + searchType: .song, + completion: { items in + var result: [String: Any] + + if let items = items, !items.isEmpty { + let songs = items.map { item -> [String: Any] in + return [ + "mid": item.mid ?? "", + "name": item.name ?? "", + "artist": item.singer?.name ?? "", + "album": item.album ?? "", + "duration": item.duration, + "pic": item.coverUri ?? "" + ] + } + result = [ + "success": true, + "message": "Found \(items.count) songs", + "broadcast": false, + "data": songs ] + } else { + result = ["success": false, "error": "No songs found"] } - result = [ - "success": true, - "message": "Found \(items.count) songs and started playing", - "broadcast":false, - "data": songs + + do { + let jsonData = try JSONSerialization.data(withJSONObject: result) + let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" + continuation.resume(returning: jsonString) + } catch { + continuation.resume(returning: "{\"success\": false, \"error\": \"JSON serialization failed\"}") + } + }, + onError: { error in + // 处理搜索错误 + var errorMessage = "Search failed" + if let error = error { + errorMessage = "Search failed: \(error.localizedDescription)" + } + + let result: [String: Any] = [ + "success": false, + "error": errorMessage ] - } else { - result = ["success": false, "error": "No songs found"] - } - - do { - let jsonData = try JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" - continuation.resume(returning: jsonString) - } catch { - continuation.resume(returning: "{\"success\": false, \"error\": \"JSON serialization failed\"}") + + do { + let jsonData = try JSONSerialization.data(withJSONObject: result) + let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" + continuation.resume(returning: jsonString) + } catch { + continuation.resume(returning: "{\"success\": false, \"error\": \"Search failed and JSON serialization failed\"}") + } } - } + ) } } } diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt index bf217f258..7b785c320 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt @@ -141,12 +141,15 @@ object LocationService { callback.onLocationError(-1, "定位服务未初始化") return } - - if (!checkPermissions(ctx)) { - callback.onLocationError(-2, "没有定位权限") + val p = checkPermissions(ctx) + if (p == 1) { + callback.onLocationError(-2, "定位权限没有") + return + } + if (p == 2) { + callback.onLocationError(-2, "后台定位权限没有") return } - if (!initialized) { callback.onLocationError(-3, "定位服务未初始化") return @@ -238,7 +241,7 @@ object LocationService { override fun run() { val ctx = appcontext ?: return val hasPermission = checkPermissions(ctx) - if (hasPermission && !initialized) { + if (hasPermission ==0 && !initialized) { Log.d(TAG, "权限已授予,重新初始化定位服务") initialize(ctx) stopPermissionMonitor() @@ -256,12 +259,26 @@ object LocationService { permissionRetryRunnable = null } - private fun checkPermissions(context: Context): Boolean { - return if (isAppInForeground(context)) { - hasForegroundLocationPermission(context) - } else { - hasBackgroundLocationPermission(context) + + /** + * 检查位置权限状态并返回对应编码 + * @return 0:已获得所需全部权限;1:未获得前台权限;2:前台权限已获得但后台权限未授予(仅Android 10+) + */ + private fun checkPermissions(context: Context): Int { + // 先检查前台权限是否已授予 + val hasForegroundPermission = hasForegroundLocationPermission(context) + if (!hasForegroundPermission) { + return 1 // 前台权限未授予 } + + // 前台权限已授予,再根据前后台状态检查后台权限 + if (!isAppInForeground(context)) { + // 应用在后台:Android 10+ 需要检查后台权限 + return 2 // 后台权限未授予 + } + + // 所有所需权限均已授予 + return 0 } private fun hasForegroundLocationPermission(context: Context): Boolean { @@ -306,7 +323,7 @@ object LocationService { val ctx = appcontext ?: activity // 如果已经有权限,直接返回成功 - if (checkPermissions(ctx)) { + if (checkPermissions(ctx) == 0) { result.success(mapOf("granted" to true)) return } diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift index a626a1e37..b55764459 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift @@ -195,8 +195,15 @@ public class LocationService: NSObject { // 检查是否已有请求在进行 if self.currentLocationCallback != nil { - completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "位置请求正在进行中"]))) - return + print("[LocationService] 检测到重复请求,重置之前的请求") + self.resetLocationRequest() + } + + // 实时检查权限状态 + let currentStatus = CLLocationManager.authorizationStatus() + if currentStatus != self.authorizationStatus { + print("[LocationService] 权限状态已变更,更新缓存") + self.authorizationStatus = currentStatus } // 检查权限 @@ -205,6 +212,12 @@ public class LocationService: NSObject { return } + // 检查位置服务是否启用 + if !CLLocationManager.locationServicesEnabled() { + completion(.failure(NSError(domain: "LocationService", code: -6, userInfo: [NSLocalizedDescriptionKey: "位置服务未启用"]))) + return + } + // 设置回调 self.currentLocationCallback = { result in switch result { @@ -228,7 +241,15 @@ public class LocationService: NSObject { } } - print("[LocationService] 开始正常定位") + // 设置超时定时器 + self.locationTimeoutTimer = Timer.scheduledTimer(withTimeInterval: self.locationTimeout, repeats: false) { [weak self] _ in + print("[LocationService] 位置请求超时") + let callback = self?.currentLocationCallback + self?.resetLocationRequest() + callback?(.failure(NSError(domain: "LocationService", code: -8, userInfo: [NSLocalizedDescriptionKey: "位置请求超时"]))) + } + + print("[LocationService] 开始正常定位,超时时间: \(self.locationTimeout)秒") // 使用单次定位 locationManager.requestLocation() } @@ -253,7 +274,7 @@ public class LocationService: NSObject { ) let callback = currentLocationCallback - currentLocationCallback = nil + resetLocationRequest() // 使用重置方法 callback?(.success(locationInfo)) } @@ -266,6 +287,18 @@ public class LocationService: NSObject { "timestamp": locationData.timestamp ] } + + // 添加超时定时器 + private var locationTimeoutTimer: Timer? + private let locationTimeout: TimeInterval = 15.0 // 15秒超时 + + // 添加重置方法 + private func resetLocationRequest() { + print("[LocationService] 重置位置请求状态") + currentLocationCallback = nil + locationTimeoutTimer?.invalidate() + locationTimeoutTimer = nil + } } // MARK: - CLLocationManagerDelegate @@ -304,7 +337,7 @@ extension LocationService: CLLocationManagerDelegate { print("[LocationService] 位置获取失败: \(error.localizedDescription)") let callback = currentLocationCallback - currentLocationCallback = nil + resetLocationRequest() // 使用重置方法 callback?(.failure(error)) } @@ -356,5 +389,3 @@ extension LocationService: CLLocationManagerDelegate { } } -// Remove the duplicate LocationData definition from here -// The LocationData struct is already defined in LocationData.swift diff --git a/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift b/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift index 04998a22c..f78bdf4c3 100644 --- a/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift +++ b/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift @@ -316,7 +316,27 @@ import QPlayAutoSDK } // MARK: - 登录状态管理 - // 在第206行之前添加新的方法 + // Add the error enum at the top of the class + enum QPlayAutoError: Error { + case qqMusicNotInstalled + case loginFailed + case connectionFailed + case searchFailed(String) // 新增搜索失败错误 + + var localizedDescription: String { + switch self { + case .qqMusicNotInstalled: + return "QQ音乐未安装" + case .loginFailed: + return "登录失败" + case .connectionFailed: + return "连接失败" + case .searchFailed(let reason): + return "搜索失败: \(reason)" + } + } + } + private func ensureQQMusicInstalledAndLoginAndExecute( _ operation: @escaping () -> Void, onQQMusicNotInstalled: (() -> Void)? = nil, @@ -330,15 +350,37 @@ import QPlayAutoSDK } // QQ音乐已安装,继续检查登录状态 - ensureLoginAndExecute(operation, onLoginFailed: onLoginFailed) + ensureLoginAndExecute(operation) { error in + switch error { + case .loginFailed: + onLoginFailed?() + case .qqMusicNotInstalled: + // This shouldn't happen since we already checked above + onQQMusicNotInstalled?() + default: + onLoginFailed?() + } + } } - // 修改现有的ensureLoginAndExecute方法(第206行开始) - private func ensureLoginAndExecute(_ operation: @escaping () -> Void, onLoginFailed: (() -> Void)? = nil) { + // 修改现有的ensureLoginAndExecute方法 + private func ensureLoginAndExecute( + _ operation: @escaping () -> Void, + onFailure: ((QPlayAutoError) -> Void)? = nil + ) { + // 首先检查QQ音乐是否已安装 + guard isQQMusicInstalled else { + print("QPlayAutoManager: QQ音乐未安装,无法执行操作") + onFailure?(.qqMusicNotInstalled) + return + } + if isLoginOK { // 已登录,直接执行操作 operation() } else { + + // 未登录,先登录 print("QPlayAutoManager: 未登录,正在尝试登录...") performLogin { [weak self] success in @@ -347,7 +389,7 @@ import QPlayAutoSDK operation() } else { print("QPlayAutoManager: 登录失败,无法执行操作") - onLoginFailed?() + onFailure?(.loginFailed) } } } @@ -533,33 +575,34 @@ import QPlayAutoSDK } @objc public func pause() { - ensureLoginAndExecute { + ensureLoginAndExecute({ QPlayAutoSDK.playerPlayPause() - } onLoginFailed: { - print("QPlayAutoManager: 暂停失败 - 登录失败") - } + }, onFailure: { error in + print("QPlayAutoManager: 暂停失败 - \(error)") + }) } + // Fix seek method (lines 571-578) @objc public func seek(to position: TimeInterval) { - ensureLoginAndExecute { - QPlayAutoSDK.playerSeek(Int(position)) - } onLoginFailed: { - print("QPlayAutoManager: 跳转播放位置失败 - 登录失败") - } + ensureLoginAndExecute { + QPlayAutoSDK.playerSeek(Int(position)) + } onFailure: { error in + print("QPlayAutoManager: 跳转播放位置失败 - \(error)") + } } // MARK: - 扩展功能方法(需要添加登录检查) /// 设置播放模式 @objc public func setPlayMode(_ playMode: QPlayAutoPlayMode, completion: @escaping (Bool) -> Void) { - ensureLoginAndExecute { - QPlayAutoSDK.setPlayMode(playMode) { (success, dict) in - completion(success) - } - } onLoginFailed: { - print("QPlayAutoManager: 设置播放模式失败 - 登录失败") - completion(false) - } + ensureLoginAndExecute { + QPlayAutoSDK.setPlayMode(playMode) { (success, dict) in + completion(success) + } + } onFailure: { error in + print("QPlayAutoManager: 设置播放模式失败 - \(error)") + completion(false) + } } /// 收藏/取消收藏歌曲 @@ -568,11 +611,12 @@ import QPlayAutoSDK QPlayAutoSDK.setFavoriteStateWithSong(song, isFavorite: isFavorite) { errorCode in completion(errorCode == 0) } - } onLoginFailed: { - print("QPlayAutoManager: 设置收藏状态失败 - 登录失败") - completion(false) + }onFailure: { error in + print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)") + completion(false) } - } + } + /// 获取相似歌曲 @objc public func requestSimilarSongs(song: QPlayAutoListItem, completion: @escaping ([QPlayAutoListItem]?, Error?) -> Void) { @@ -585,12 +629,12 @@ import QPlayAutoSDK completion(nil, error) } } - } onLoginFailed: { - print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败") - let error = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "QQ音乐未安装"]) + }onFailure: { error in + print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)") completion(nil, error) - } - } + } + } + /// 获取歌词 @objc public func requestLyric(song: QPlayAutoListItem, completion: @escaping (QPlayAutoLyric?, Error?) -> Void) { @@ -603,12 +647,11 @@ import QPlayAutoSDK completion(nil, error) } } - } onLoginFailed: { - print("QPlayAutoManager: 请求歌词失败 - 登录失败") - let error = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "QQ音乐未安装"]) + } onFailure: { error in + print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)") completion(nil, error) - } - } + } + } /// 获取歌曲列表 @objc public func requestSonglist(type: QPlayAutoSongListType, completion: @escaping ([QPlayAutoListItem]?, Error?) -> Void) { @@ -621,12 +664,11 @@ import QPlayAutoSDK completion(nil, error) } } - } onLoginFailed: { - print("QPlayAutoManager: 请求歌单失败 - 登录失败") - let error = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "QQ音乐未安装"]) + } onFailure: { error in + print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)") completion(nil, error) - } - } + } + } /// 播放指定歌曲列表 @objc public func playAtIndex(songList: [QPlayAutoListItem], playIndex: Int, completion: @escaping (Bool) -> Void) { @@ -634,11 +676,11 @@ import QPlayAutoSDK QPlayAutoSDK.play(atIndex: songList, play: playIndex) { errorCode in completion(errorCode == 0) } - } onLoginFailed: { - print("QPlayAutoManager: 播放指定索引歌曲失败 - QQ音乐未安装") + } onFailure: { error in + print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)") completion(false) - } - } + } + } // MARK: - 状态查询 @objc public var isConnected: Bool { @@ -729,17 +771,21 @@ import QPlayAutoSDK @objc public func search( keyword: String, searchType: QPlayAutoSearchType, - completion: @escaping ([QPlayAutoListItem]?) -> Void + completion: @escaping ([QPlayAutoListItem]?) -> Void, + onError: ((Error?) -> Void)? = nil ) { ensureLoginAndExecute { QPlayAutoSDK.search(keyword, type: searchType, firstPage: true) { (error, items) in completion(items) } - } onLoginFailed: { - print("QPlayAutoManager: 搜索失败 - 登录失败") - completion(nil) + }onFailure: { error in + print("QPlayAutoManager: 搜索失败 - 登录失败 - \(error)") + let loginError = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [ + NSLocalizedDescriptionKey: "登录失败: \(error)" + ]) + onError?(loginError) + } } - } @objc public func getItems( playlistId: String, @@ -756,11 +802,12 @@ import QPlayAutoSDK completion(nil) } } - } onLoginFailed: { - print("QPlayAutoManager: 获取播放列表失败 - 登录失败") + } onFailure: { error in + print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)") completion(nil) - } - } + } + } + // 添加公开的登录方法 @objc public func login(completion: @escaping (Bool) -> Void) {