diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 998004a..ce641d0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -386,15 +386,29 @@ final class PipShadowCoordinator: NSObject, AVPictureInPictureControllerDelegate guard call.method == "shareText", let arguments = call.arguments as? [String: Any], let text = arguments["text"] as? String, - !text.isEmpty, - let controller = self?.window?.rootViewController else { + !text.isEmpty else { result(FlutterError(code: "share_unavailable", message: "Share sheet is unavailable", details: nil)) return } - let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil) - activity.popoverPresentationController?.sourceView = controller.view - activity.completionWithItemsHandler = { _, _, _, _ in result(nil) } - controller.present(activity, animated: true) + DispatchQueue.main.async { + guard let controller = self?.sharePresenter() else { + result(FlutterError(code: "share_unavailable", message: "Share sheet is unavailable", details: nil)) + return + } + let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil) + if let popover = activity.popoverPresentationController { + popover.sourceView = controller.view + popover.sourceRect = CGRect( + x: controller.view.bounds.midX, + y: controller.view.bounds.midY, + width: 0, + height: 0 + ) + popover.permittedArrowDirections = [] + } + activity.completionWithItemsHandler = { _, _, _, _ in result(nil) } + controller.present(activity, animated: true) + } } let thumbnailChannel = FlutterMethodChannel( @@ -459,6 +473,34 @@ final class PipShadowCoordinator: NSObject, AVPictureInPictureControllerDelegate } } + /// 当前前台 Scene 的根控制器之上,最适合展示系统分享面板的控制器。 + private func sharePresenter() -> UIViewController? { + let keyWindow = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first { $0.activationState == .foregroundActive }? + .windows.first { $0.isKeyWindow } + guard let rootController = keyWindow?.rootViewController else { return nil } + let presenter = topPresentedController(from: rootController) + guard presenter.viewIfLoaded?.window != nil else { return nil } + return presenter + } + + /// 解析已展示页面、导航栈和标签页中的当前可见控制器。 + private func topPresentedController(from controller: UIViewController) -> UIViewController { + if let presented = controller.presentedViewController { + return topPresentedController(from: presented) + } + if let navigation = controller as? UINavigationController, + let visible = navigation.visibleViewController { + return topPresentedController(from: visible) + } + if let tabs = controller as? UITabBarController, + let selected = tabs.selectedViewController { + return topPresentedController(from: selected) + } + return controller + } + /// iOS 画中画:影子播放器通道,与 Android 共用同一 channel 名。 private func registerPipChannel(_ engineBridge: FlutterImplicitEngineBridge) { let channel = FlutterMethodChannel( diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index ce8085a..9fc3f53 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -1,5 +1,7 @@ // Author: fruit.com.cn +import 'package:flutter/foundation.dart'; + import 'dev_config.dart'; import 'prod_config.dart'; @@ -9,12 +11,10 @@ import 'prod_config.dart'; /// - `dev_config.dart`:[DevConfig](本机联调) /// - `prod_config.dart`:[ProdConfig](生产) /// -/// 切换环境只改下面这一个布尔值,不用传任何构建参数、也不做网络探测。 +/// Debug 使用开发环境,Release 自动使用生产环境,不做网络探测。 abstract final class AppConfig { - /// **环境开关**:true 用开发环境,false 用生产环境。 - /// - /// 打生产包前必须改成 false;release 构建若仍为 true 会在启动时直接报错。 - static const bool useDevConfig = false; + /// **环境开关**:Debug 使用开发环境,Release 使用生产环境。 + static const bool useDevConfig = !kReleaseMode; /// 当前选中的环境地址。 static String get baseUrl => diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 4331df2..a85f3b4 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -27,6 +27,7 @@ const double _progressHeight = 2; const double _progressHitHeight = 16; const double _pageHorizontal = 20; const double _railRight = 14; +const double _railShiftRight = 8; const double _railGap = 12; const double _railWidth = 56; const double _infoWatchAllGap = 2; @@ -234,7 +235,10 @@ class _HomeFeedOverlayState extends State { padding: const EdgeInsets.only( bottom: _railBottomOffset, ), - child: _rail(), + child: Transform.translate( + offset: const Offset(_railShiftRight, 0), + child: _rail(), + ), ), ), ], diff --git a/test/core/app_config_environment_test.dart b/test/core/app_config_environment_test.dart new file mode 100644 index 0000000..2cce7e2 --- /dev/null +++ b/test/core/app_config_environment_test.dart @@ -0,0 +1,18 @@ +// Author: fruit.com.cn + +// 应用环境选择测试。 +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/config/app_config.dart'; +import 'package:fruitshorts/core/config/dev_config.dart'; +import 'package:fruitshorts/core/config/prod_config.dart'; + +void main() { + test('should_use_environment_matching_build_mode', () { + expect(AppConfig.useDevConfig, !kReleaseMode); + expect( + AppConfig.baseUrl, + kReleaseMode ? ProdConfig.baseUrl : DevConfig.baseUrl, + ); + }); +} diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index 6e94a94..a0429f6 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -373,7 +373,7 @@ void main() { expect( tester.getRect(find.byKey(const Key('home-feed-action-rail'))).right, closeTo( - tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 14, + tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 6, 0.1, ), );