From b8accd430298563fd8d8d59b3fe1323de8373d11 Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Wed, 10 Jan 2024 13:02:41 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=BB=E4=B8=AD=E7=94=BB=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../proxy/plugin/PictureInPicturePlugin.kt | 8 +- .../proxy/vpn/socket/ClientPacketWriter.kt | 4 +- ios/Podfile | 3 +- ios/Runner/AppDelegate.swift | 43 ++--- ios/Runner/Info.plist | 5 + ios/Runner/VpnManager.swift | 2 +- lib/native/pip.dart | 32 +++- lib/network/http/codec.dart | 3 + lib/network/http/http_parser.dart | 14 +- lib/ui/configuration.dart | 8 +- lib/ui/desktop/desktop.dart | 4 +- lib/ui/desktop/left/model/search_model.dart | 3 +- lib/ui/desktop/left/search_condition.dart | 2 +- .../toolbar/setting/request_rewrite.dart | 9 +- lib/ui/launch/launch.dart | 35 ++-- lib/ui/mobile/menu.dart | 12 +- lib/ui/mobile/mobile.dart | 101 +++++------ lib/ui/mobile/request/list.dart | 2 +- lib/ui/mobile/setting/request_rewrite.dart | 5 +- lib/ui/mobile/setting/ssl.dart | 55 +++--- lib/ui/mobile/setting/video_player.dart | 167 ++++++++++++++++++ lib/ui/mobile/{ => widgets}/about.dart | 0 .../mobile/{ => widgets}/connect_remote.dart | 0 lib/ui/mobile/widgets/pip.dart | 60 +++++++ pubspec.lock | 60 +++++++ pubspec.yaml | 23 +++ 26 files changed, 512 insertions(+), 148 deletions(-) create mode 100644 lib/ui/mobile/setting/video_player.dart rename lib/ui/mobile/{ => widgets}/about.dart (100%) rename lib/ui/mobile/{ => widgets}/connect_remote.dart (100%) create mode 100644 lib/ui/mobile/widgets/pip.dart diff --git a/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt b/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt index b645699..6bed557 100644 --- a/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt +++ b/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt @@ -22,6 +22,8 @@ import androidx.core.content.ContextCompat class PictureInPicturePlugin : AndroidFlutterPlugin() { private var registerBroadcast = false var channel: MethodChannel? = null + var proxyHost: String? = null + var proxyPort: Int? = null ///广播事件接受者 private val vpnBroadcastReceiver = object : BroadcastReceiver() { @@ -41,7 +43,7 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { if (isRunning) { activity.startService(ProxyVpnService.stopVpnIntent(activity)) } else { - activity.startService(ProxyVpnService.startVpnIntent(activity)) + activity.startService(ProxyVpnService.startVpnIntent(activity, proxyHost, proxyPort)) } //设置画中画参数 @@ -63,8 +65,10 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { when (call.method) { "enterPictureInPictureMode" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val param = updatePictureInPictureParams(ProxyVpnService.isRunning) + proxyHost = call.argument("proxyHost") + proxyPort = call.argument("proxyPort") + val param = updatePictureInPictureParams(ProxyVpnService.isRunning) if (!registerBroadcast) { registerBroadcast = true ContextCompat.registerReceiver( diff --git a/android/app/src/main/kotlin/com/network/proxy/vpn/socket/ClientPacketWriter.kt b/android/app/src/main/kotlin/com/network/proxy/vpn/socket/ClientPacketWriter.kt index 811b25c..badf88a 100644 --- a/android/app/src/main/kotlin/com/network/proxy/vpn/socket/ClientPacketWriter.kt +++ b/android/app/src/main/kotlin/com/network/proxy/vpn/socket/ClientPacketWriter.kt @@ -28,7 +28,7 @@ class ClientPacketWriter(private val clientWriter: FileOutputStream) : Runnable } override fun run() { - while (!this.shutdown) { + while (!this.shutdown && clientWriter.channel.isOpen) { try { val data: ByteArray = this.packetQueue.take() try { @@ -36,7 +36,7 @@ class ClientPacketWriter(private val clientWriter: FileOutputStream) : Runnable } catch (e: IOException) { Log.e(TAG, "Error writing $shutdown data.length bytes to the VPN") e.printStackTrace() - this.packetQueue.addFirst(data) // Put the data back, so it's resent +// this.packetQueue.addFirst(data) // Put the data back, so it's resent Thread.sleep(10) // Add an arbitrary tiny pause, in case that helps } } catch (ignored: InterruptedException) { diff --git a/ios/Podfile b/ios/Podfile index 164df53..31466bc 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -30,7 +30,8 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! - + + pod 'SnapKit', '~> 5.0.1' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 972af6c..994e5ce 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -5,24 +5,23 @@ import NetworkExtension @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { - var backgroundAudioEnable: Bool = false + var backgroundAudioEnable: Bool = true - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) + override func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + GeneratedPluginRegistrant.register(with: self) - let controller: FlutterViewController = window.rootViewController as! FlutterViewController ; - let batteryChannel = FlutterMethodChannel.init(name: "com.proxy/proxyVpn", binaryMessenger: controller as! FlutterBinaryMessenger); - batteryChannel.setMethodCallHandler({ - (call: FlutterMethodCall, result: FlutterResult) -> Void in - if ("stopVpn" == call.method) { - VpnManager.shared.disconnect() - } else { - let arguments = call.arguments as? Dictionary - self.backgroundAudioEnable = (arguments!["backgroundAudioEnable"]) as! Bool - VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int) + let controller: FlutterViewController = window.rootViewController as! FlutterViewController ; + let batteryChannel = FlutterMethodChannel.init(name: "com.proxy/proxyVpn", binaryMessenger: controller as! FlutterBinaryMessenger); + batteryChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in + if ("stopVpn" == call.method) { + VpnManager.shared.disconnect() + } else if ("isRunning" == call.method){ + result(Bool(VpnManager.shared.isRunning())) + } else { + let arguments = call.arguments as? Dictionary +// self.backgroundAudioEnable = (arguments!["backgroundAudioEnable"]) as! Bool + VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int) } }) @@ -30,17 +29,17 @@ import NetworkExtension } override func applicationWillTerminate(_ application: UIApplication) { - VpnManager.shared.disconnect() + VpnManager.shared.disconnect() } - var timer: Timer? - var bgTask: UIBackgroundTaskIdentifier? - + var timer: Timer? + var bgTask: UIBackgroundTaskIdentifier? override func applicationDidEnterBackground(_ application: UIApplication) { if (!VpnManager.shared.isRunning()) { return } + timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true) RunLoop.current.add(timer!, forMode: RunLoop.Mode.common) bgTask = application.beginBackgroundTask(expirationHandler: nil) @@ -49,6 +48,7 @@ import NetworkExtension @objc func timerAction() { print(UIApplication.shared.backgroundTimeRemaining) let application = UIApplication.shared + if (bgTask != nil) { application.endBackgroundTask(bgTask!); bgTask = nil; @@ -91,11 +91,12 @@ import NetworkExtension } var backgroundUpdateTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0) + func endBackgroundUpdateTask() { if (!VpnManager.shared.isRunning() || !AudioManager.shared.openBackgroundAudioAutoplay) { return } - + AudioManager.shared.openBackgroundAudioAutoplay = false UIApplication.shared.endBackgroundTask(self.backgroundUpdateTask) self.backgroundUpdateTask = UIBackgroundTaskIdentifier.invalid diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 39560ae..bd7c882 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -69,5 +69,10 @@ UIViewControllerBasedStatusBarAppearance + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/ios/Runner/VpnManager.swift b/ios/Runner/VpnManager.swift index 9026762..17e6f24 100755 --- a/ios/Runner/VpnManager.swift +++ b/ios/Runner/VpnManager.swift @@ -15,7 +15,7 @@ class VpnManager{ var activeVPN: NETunnelProviderManager?; public var proxyHost: String = "127.0.0.01" - public var proxyPort: Int = 8888 + public var proxyPort: Int = 9099 static let shared = VpnManager() var observerAdded: Bool = false diff --git a/lib/native/pip.dart b/lib/native/pip.dart index f92ee23..30f0309 100644 --- a/lib/native/pip.dart +++ b/lib/native/pip.dart @@ -1,27 +1,39 @@ import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:network_proxy/native/vpn.dart'; import 'package:network_proxy/network/util/logger.dart'; +import 'package:network_proxy/ui/launch/launch.dart'; import 'package:network_proxy/ui/mobile/mobile.dart'; +import 'package:network_proxy/utils/lang.dart'; ///画中画 class PictureInPicture { + static bool inPip = false; + static final MethodChannel _channel = const MethodChannel('com.proxy/pictureInPicture') ..setMethodCallHandler((call) async { logger.d("pictureInPicture MethodCallHandler ${call.method}"); if (call.method == 'cleanSession') { MobileHomeState.requestStateKey.currentState?.clean(); + } else if (call.method == 'exitPictureInPictureMode') { + inPip = false; + Vpn.isRunning().then((value) { + Vpn.isVpnStarted = value; + SocketLaunch.startStatus.value = ValueWrap.of(value); + }); } + return Future.value(); }); ///进入画中画模式 - static Future enterPictureInPictureMode() async { - if (Platform.isAndroid) { - final bool enterPictureInPictureMode = await _channel.invokeMethod('enterPictureInPictureMode'); - return enterPictureInPictureMode; - } - return false; + static Future enterPictureInPictureMode(String host, int port) async { + final bool enterPictureInPictureMode = + await _channel.invokeMethod('enterPictureInPictureMode', {"proxyHost": host, "proxyPort": port}); + inPip = true; + + return enterPictureInPictureMode; } ///退出画中画模式 @@ -29,4 +41,12 @@ class PictureInPicture { final bool exitPictureInPictureMode = await _channel.invokeMethod('exitPictureInPictureMode'); return exitPictureInPictureMode; } + + ///发送数据 + static Future addData(String text) async { + if (Platform.isIOS && inPip) { + _channel.invokeMethod('addData', text.fixAutoLines()); + } + return false; + } } diff --git a/lib/network/http/codec.dart b/lib/network/http/codec.dart index 766d20e..44f4808 100644 --- a/lib/network/http/codec.dart +++ b/lib/network/http/codec.dart @@ -101,6 +101,9 @@ abstract class HttpCodec implements Codec { //请求行 if (_state == State.readInitial) { init(); + if (data.readableBytes() < 128) { + return result; + } var initialLine = _readInitialLine(data); result.data = createMessage(initialLine); _state = State.readHeader; diff --git a/lib/network/http/http_parser.dart b/lib/network/http/http_parser.dart index 17b73df..b4f8d2d 100644 --- a/lib/network/http/http_parser.dart +++ b/lib/network/http/http_parser.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; -import 'package:network_proxy/network/http/codec.dart'; import 'package:network_proxy/network/http/constants.dart'; import 'package:network_proxy/network/http/http_headers.dart'; import 'package:network_proxy/network/util/byte_buf.dart'; @@ -12,7 +11,7 @@ class HttpParse { /// 解析请求行 List parseInitialLine(ByteBuf data, int size) { List initialLine = []; - + var startIndex = data.readerIndex; for (int i = data.readerIndex; i < size; i++) { if (_isLineEnd(data, i)) { //请求行结束 @@ -23,11 +22,16 @@ class HttpParse { } } - if (initialLine.length != 3) { - throw ParserException("parseLine error", String.fromCharCodes(data.bytes)); + if (initialLine.length == 3) { + return initialLine; } - return initialLine; + if (data.length > defaultMaxLength) { + throw Exception("request line too long"); + } + + data.readerIndex = startIndex; + return []; } //分割行 diff --git a/lib/ui/configuration.dart b/lib/ui/configuration.dart index 8e48971..054ae37 100644 --- a/lib/ui/configuration.dart +++ b/lib/ui/configuration.dart @@ -28,8 +28,8 @@ class AppConfiguration { //是否显示更新内容公告 bool upgradeNoticeV7 = true; - /// 是否启用小窗口 - bool smallWindow = false; + /// 是否启用画中画 + bool pipEnabled = true; /// bool headerExpanded = true; @@ -116,7 +116,7 @@ class AppConfiguration { _theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true); upgradeNoticeV7 = config['upgradeNoticeV7'] ?? true; _language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']); - smallWindow = config['smallWindow'] ?? Platform.isAndroid; + pipEnabled = config['pipEnabled'] ?? true; headerExpanded = config['headerExpanded'] ?? true; iosVpnBackgroundAudioEnable = config['iosVpnBackgroundAudioEnable']; } catch (e) { @@ -142,7 +142,7 @@ class AppConfiguration { 'useMaterial3': _theme.useMaterial3, 'upgradeNoticeV7': upgradeNoticeV7, "language": _language?.languageCode, - 'smallWindow': smallWindow, + 'pipEnabled': pipEnabled, "headerExpanded": headerExpanded, "iosVpnBackgroundAudioEnable": iosVpnBackgroundAudioEnable == false ? null : iosVpnBackgroundAudioEnable }; diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index 68a26f9..e99d49e 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -206,6 +206,7 @@ class _DesktopHomePagePageState extends State implements EventL '4. 请求编辑URL参数支持表单编辑;\n' '5. 增加高级重放;\n' '6. 域名过滤支持批量导出&编辑;\n' + '7. IOS支持画中画模式;\n' : 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n' 'Click HTTPS Capture packets(Lock icon),Choose to install the root certificate and follow the prompts to proceed。\n\n' '1. Increase multilingual support;\n' @@ -213,7 +214,8 @@ class _DesktopHomePagePageState extends State implements EventL '3. Details page Headers Expanded Config;\n' '4. Request Edit URL parameter support for form editing;\n' '5. Support advanced replay;\n' - '6. Domain name filtering supports batch export&editing;\n', + '6. Domain name filtering supports batch export&editing;\n' + '7. iOS Supports picture in picture mode;\n', style: const TextStyle(fontSize: 14))); }); } diff --git a/lib/ui/desktop/left/model/search_model.dart b/lib/ui/desktop/left/model/search_model.dart index bb75e10..bced810 100644 --- a/lib/ui/desktop/left/model/search_model.dart +++ b/lib/ui/desktop/left/model/search_model.dart @@ -90,7 +90,8 @@ class SearchModel { var entries = option == Option.requestHeader ? request.headers.entries : response?.headers.entries ?? []; for (var entry in entries) { - if (entry.value.any((element) => element.contains(keyword))) { + if (entry.key.toLowerCase() == keyword.toLowerCase() || + entry.value.any((element) => element.contains(keyword))) { return true; } } diff --git a/lib/ui/desktop/left/search_condition.dart b/lib/ui/desktop/left/search_condition.dart index 0e20ff5..db05d21 100644 --- a/lib/ui/desktop/left/search_condition.dart +++ b/lib/ui/desktop/left/search_condition.dart @@ -148,7 +148,7 @@ class SearchConditionsState extends State { Widget options(String title, Option option) { bool isCN = localizations.localeName == 'zh'; return Container( - constraints: BoxConstraints(maxWidth: isCN ? 100 : 150, minWidth: 100, maxHeight: 33), + constraints: BoxConstraints(maxWidth: isCN ? 100 : 152, minWidth: 100, maxHeight: 33), child: Row(children: [ Text(title, style: const TextStyle(fontSize: 12)), Checkbox( diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index 93b8bba..cf0f83f 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -194,7 +194,6 @@ class _RequestRuleListState extends State { rules = widget.requestRewrites.rules; } - @override Widget build(BuildContext context) { return GestureDetector( @@ -488,8 +487,12 @@ class _RuleAddDialogState extends State { text: localizations.useGuide, style: const TextStyle(color: Colors.blue, fontSize: 14), recognizer: TapGestureRecognizer() - ..onTap = () => DesktopMultiWindow.invokeMethod(0, "launchUrl", - 'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99'))), + ..onTap = () => DesktopMultiWindow.invokeMethod( + 0, + "launchUrl", + isCN + ? 'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99' + : 'https://github.com/wanghongenpin/network_proxy_flutter/wiki/Request-Rewrite'))), ]), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), content: Container( diff --git a/lib/ui/launch/launch.dart b/lib/ui/launch/launch.dart index 2225f7e..3095657 100644 --- a/lib/ui/launch/launch.dart +++ b/lib/ui/launch/launch.dart @@ -4,13 +4,13 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/native/app_lifecycle.dart'; import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/utils/lang.dart'; import 'package:network_proxy/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; class SocketLaunch extends StatefulWidget { - static bool started = false; + static ValueNotifier> startStatus = ValueNotifier(ValueWrap()); final ProxyServer proxyServer; final int size; @@ -30,20 +30,18 @@ class SocketLaunch extends StatefulWidget { this.serverLaunch = true}); @override - State createState() { - return _SocketLaunchState(); - } + State createState() => _SocketLaunchState(); } class _SocketLaunchState extends State with WindowListener, WidgetsBindingObserver { AppLocalizations get localizations => AppLocalizations.of(context)!; + bool started = false; @override void initState() { super.initState(); windowManager.addListener(this); WidgetsBinding.instance.addObserver(this); - AppLifecycleBinding.ensureInitialized(); //启动代理服务器 if (widget.startup) { start(); @@ -51,6 +49,11 @@ class _SocketLaunchState extends State with WindowListener, Widget if (Platforms.isDesktop()) { windowManager.setPreventClose(true); } + SocketLaunch.startStatus.addListener(() { + setState(() { + started = SocketLaunch.startStatus.value.get() ?? started; + }); + }); } @override @@ -68,7 +71,7 @@ class _SocketLaunchState extends State with WindowListener, Widget Future appExit() async { await widget.proxyServer.stop(); - SocketLaunch.started = false; + started = false; await windowManager.destroy(); exit(0); } @@ -85,22 +88,22 @@ class _SocketLaunchState extends State with WindowListener, Widget print('AppLifecycleState.detached'); widget.onStop?.call(); widget.proxyServer.stop(); - SocketLaunch.started = false; + started = false; } } @override Widget build(BuildContext context) { return IconButton( - tooltip: SocketLaunch.started ? localizations.stop : localizations.start, - icon: Icon(SocketLaunch.started ? Icons.stop : Icons.play_arrow_sharp, - color: SocketLaunch.started ? Colors.red : Colors.green, size: widget.size.toDouble()), + tooltip: started ? localizations.stop : localizations.start, + icon: Icon(started ? Icons.stop : Icons.play_arrow_sharp, + color: started ? Colors.red : Colors.green, size: widget.size.toDouble()), onPressed: () async { - if (SocketLaunch.started) { + if (started) { if (!widget.serverLaunch) { setState(() { widget.onStop?.call(); - SocketLaunch.started = !SocketLaunch.started; + started = !started; }); return; } @@ -108,7 +111,7 @@ class _SocketLaunchState extends State with WindowListener, Widget widget.proxyServer.stop().then((value) { widget.onStop?.call(); setState(() { - SocketLaunch.started = !SocketLaunch.started; + started = !started; }); }); } else { @@ -122,14 +125,14 @@ class _SocketLaunchState extends State with WindowListener, Widget if (!widget.serverLaunch) { await widget.onStart?.call(); setState(() { - SocketLaunch.started = true; + started = true; }); return; } widget.proxyServer.start().then((value) { setState(() { - SocketLaunch.started = true; + started = true; }); widget.onStart?.call(); }).catchError((e) { diff --git a/lib/ui/mobile/menu.dart b/lib/ui/mobile/menu.dart index 03f044c..ed92f6e 100644 --- a/lib/ui/mobile/menu.dart +++ b/lib/ui/mobile/menu.dart @@ -14,8 +14,8 @@ import 'package:network_proxy/ui/component/toolbox.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/ui/configuration.dart'; -import 'package:network_proxy/ui/mobile/about.dart'; -import 'package:network_proxy/ui/mobile/connect_remote.dart'; +import 'package:network_proxy/ui/mobile/widgets/about.dart'; +import 'package:network_proxy/ui/mobile/widgets/connect_remote.dart'; import 'package:network_proxy/ui/mobile/request/favorite.dart'; import 'package:network_proxy/ui/mobile/request/history.dart'; import 'package:network_proxy/ui/mobile/setting/app_whitelist.dart'; @@ -126,15 +126,13 @@ class SettingMenu extends StatelessWidget { onTap: () => _language(context), ), MobileThemeSetting(appConfiguration: appConfiguration), - Platform.isIOS - ? const SizedBox() - : ListTile( + ListTile( title: Text(localizations.windowMode), subtitle: Text(localizations.windowModeSubTitle, style: const TextStyle(fontSize: 12)), trailing: SwitchWidget( - value: appConfiguration.smallWindow, + value: appConfiguration.pipEnabled, onChanged: (value) { - appConfiguration.smallWindow = value; + appConfiguration.pipEnabled = value; appConfiguration.flushConfig(); })), ListTile( diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 6f7349d..e373639 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -15,14 +15,14 @@ import 'package:network_proxy/network/handler.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http/websocket.dart'; import 'package:network_proxy/network/http_client.dart'; -import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/launch/launch.dart'; -import 'package:network_proxy/ui/mobile/connect_remote.dart'; import 'package:network_proxy/ui/mobile/menu.dart'; import 'package:network_proxy/ui/mobile/request/list.dart'; import 'package:network_proxy/ui/mobile/request/search.dart'; +import 'package:network_proxy/ui/mobile/widgets/connect_remote.dart'; +import 'package:network_proxy/ui/mobile/widgets/pip.dart'; import 'package:network_proxy/utils/ip.dart'; class MobileHomePage extends StatefulWidget { @@ -53,16 +53,16 @@ class MobileHomeState extends State implements EventListener, Li @override void onUserLeaveHint() async { if (Vpn.isVpnStarted && !pictureInPictureNotifier.value) { - if (desktop.value.connect || !Platform.isAndroid || !(await (AppConfiguration.instance)).smallWindow) { + if (desktop.value.connect || !Platform.isAndroid || !(await (AppConfiguration.instance)).pipEnabled) { return; } - PictureInPicture.enterPictureInPictureMode(); + PictureInPicture.enterPictureInPictureMode(Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port); } } @override - onPictureInPictureModeChanged(bool isInPictureInPictureMode) { + onPictureInPictureModeChanged(bool isInPictureInPictureMode) async { if (isInPictureInPictureMode && !pictureInPictureNotifier.value) { while (Navigator.canPop(context)) { Navigator.pop(context); @@ -81,6 +81,7 @@ class MobileHomeState extends State implements EventListener, Li @override void onRequest(Channel channel, HttpRequest request) { + PictureInPicture.addData(request.requestUrl); requestStateKey.currentState!.add(channel, request); } @@ -154,18 +155,21 @@ class MobileHomeState extends State implements EventListener, Li } return Scaffold( - appBar: appBar(), - drawer: DrawerWidget(proxyServer: proxyServer), - floatingActionButton: _floatingActionButton(), - body: ValueListenableBuilder( - valueListenable: desktop, - builder: (context, value, _) { - return Column(children: [ - value.connect ? remoteConnect(value) : const SizedBox(), - Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)) - ]); - }), - ); + floatingActionButton: + widget.appConfiguration.pipEnabled ? PictureInPictureWindow(proxyServer) : const SizedBox(), + body: Scaffold( + appBar: appBar(), + drawer: DrawerWidget(proxyServer: proxyServer), + floatingActionButton: _launchActionButton(), + body: ValueListenableBuilder( + valueListenable: desktop, + builder: (context, value, _) { + return Column(children: [ + value.connect ? remoteConnect(value) : const SizedBox(), + Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)) + ]); + }), + )); })); } @@ -177,36 +181,25 @@ class MobileHomeState extends State implements EventListener, Li onPressed: () => requestStateKey.currentState?.clean()), const SizedBox(width: 2), MoreMenu(proxyServer: proxyServer, desktop: desktop), - const SizedBox(width: 10) + const SizedBox(width: 10), ]); } - FloatingActionButton _floatingActionButton() { + FloatingActionButton _launchActionButton() { return FloatingActionButton( onPressed: null, child: Center( - child: futureWidget(localIp(), (data) { - SocketLaunch.started = Vpn.isVpnStarted; - return SocketLaunch( - proxyServer: proxyServer, - size: 36, - startup: false, - serverLaunch: false, - onStart: () async { - // if (Platform.isIOS && widget.appConfiguration.iosVpnBackgroundAudioEnable == null) { - // widget.appConfiguration.iosVpnBackgroundAudioEnable = false; - // await showConfirmDialog(context, content: localizations.iosVpnBackgroundAudio, onConfirm: () { - // widget.appConfiguration.iosVpnBackgroundAudioEnable = true; - // widget.appConfiguration.flushConfig(); - // }); - // } - - Vpn.startVpn(Platform.isAndroid ? data : "127.0.0.1", proxyServer.port, - backgroundAudioEnable: widget.appConfiguration.iosVpnBackgroundAudioEnable, - appList: proxyServer.configuration.appWhitelist); - }, - onStop: () => Vpn.stopVpn()); - })), + child: SocketLaunch( + proxyServer: proxyServer, + size: 36, + startup: Vpn.isVpnStarted, + serverLaunch: false, + onStart: () async { + Vpn.startVpn(Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port, + backgroundAudioEnable: widget.appConfiguration.iosVpnBackgroundAudioEnable, + appList: proxyServer.configuration.appWhitelist); + }, + onStop: () => Vpn.stopVpn())), ); } @@ -215,19 +208,21 @@ class MobileHomeState extends State implements EventListener, Li String content = isCN ? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n\n' - '1. 增加多语言支持;\n' - '2. 请求重写支持文件选择;\n' - '3. 抓包详情页面Headers默认展开配置;\n' - '4. 请求编辑URL参数支持表单编辑;\n' - '5. 增加高级重放;\n' - '6. 域名过滤支持批量导出&编辑;\n' + '1. 支持画中画模式,可在设置中关闭;\n' + '2. 增加多语言支持;\n' + '3. 请求重写支持文件选择;\n' + '4. 抓包详情页面Headers默认展开配置;\n' + '5. 请求编辑URL参数支持表单编辑;\n' + '6. 增加高级重放;\n' + '7. 域名过滤支持批量导出&编辑;\n' : 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n' - '1. Increase multilingual support;\n' - '2. Request Rewrite support file selection;\n' - '3. Details page Headers Expanded Config;\n' - '4. Request Edit URL parameter support for form editing;\n' - '5. Support advanced replay;\n' - '6. Domain name filtering supports batch export&editing;\n'; + '1. Supports picture in picture mode, which can be turned off in settings;\n' + '2. Increase multilingual support;\n' + '3. Request Rewrite support file selection;\n' + '4. Details page Headers Expanded Config;\n' + '5. Request Edit URL parameter support for form editing;\n' + '6. Support advanced replay;\n' + '7. Domain name filtering supports batch export&editing;\n'; showAlertDialog(isCN ? '更新内容V1.0.7' : "Update content V1.0.7", content, () { widget.appConfiguration.upgradeNoticeV7 = false; widget.appConfiguration.flushConfig(); diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index 06105ec..1e24d68 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -40,7 +40,7 @@ class RequestListState extends State { void initState() { super.initState(); if (widget.list != null) { - container.addAll(widget.list!); + container = widget.list!; } } diff --git a/lib/ui/mobile/setting/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart index 2771798..e4a9e0f 100644 --- a/lib/ui/mobile/setting/request_rewrite.dart +++ b/lib/ui/mobile/setting/request_rewrite.dart @@ -444,8 +444,9 @@ class _RewriteRuleState extends State { text: localizations.useGuide, style: const TextStyle(color: Colors.blue, fontSize: 14), recognizer: TapGestureRecognizer() - ..onTap = () => launchUrl(Uri.parse( - 'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99')))), + ..onTap = () => launchUrl(Uri.parse(isCN + ? 'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99' + : 'https://github.com/wanghongenpin/network_proxy_flutter/wiki/Request-Rewrite')))), ]), actions: [ TextButton( diff --git a/lib/ui/mobile/setting/ssl.dart b/lib/ui/mobile/setting/ssl.dart index b62492b..9c7d951 100644 --- a/lib/ui/mobile/setting/ssl.dart +++ b/lib/ui/mobile/setting/ssl.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/util/crts.dart'; +import 'package:network_proxy/ui/mobile/setting/video_player.dart'; import 'package:url_launcher/url_launcher.dart'; class MobileSslWidget extends StatefulWidget { @@ -54,27 +55,39 @@ class _MobileSslState extends State { } Widget ios() { - return ExpansionTile( - title: Text(localizations.profileDownload), - initiallyExpanded: true, - childrenPadding: const EdgeInsets.only(left: 20), - expandedAlignment: Alignment.topLeft, - expandedCrossAxisAlignment: CrossAxisAlignment.start, - shape: const Border(), - children: [ - TextButton(onPressed: () => _downloadCert(), child: Text("1. ${localizations.downloadRootCa}")), - TextButton(onPressed: () {}, child: Text("2. ${localizations.installRootCa} -> ${localizations.trustCa}")), - TextButton(onPressed: () {}, child: Text("2.1 ${localizations.installCaDescribe}")), - Padding( - padding: const EdgeInsets.only(left: 15), - child: Image.network("https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png", - height: 400)), - TextButton(onPressed: () {}, child: Text("2.2 ${localizations.trustCaDescribe}")), - Padding( - padding: const EdgeInsets.only(left: 15), - child: Image.network("https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png", - height: 270)), - ]); + return Column(children: [ + if (localizations.localeName != 'zh') + ExpansionTile( + title: Text(localizations.useGuide), + shape: const Border(), + maintainState: true, + children: [ + Container( + height: 350, padding: const EdgeInsets.only(left: 15, right: 15), child: const VideoPlayerScreen()) + ], + ), + ExpansionTile( + title: Text(localizations.installRootCa), + initiallyExpanded: true, + childrenPadding: const EdgeInsets.only(left: 20), + expandedAlignment: Alignment.topLeft, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + shape: const Border(), + children: [ + TextButton(onPressed: () => _downloadCert(), child: Text("1. ${localizations.downloadRootCa}")), + TextButton(onPressed: () {}, child: Text("2. ${localizations.installRootCa} -> ${localizations.trustCa}")), + TextButton(onPressed: () {}, child: Text("2.1 ${localizations.installCaDescribe}")), + Padding( + padding: const EdgeInsets.only(left: 15), + child: Image.network("https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png", + height: 400)), + TextButton(onPressed: () {}, child: Text("2.2 ${localizations.trustCaDescribe}")), + Padding( + padding: const EdgeInsets.only(left: 15), + child: Image.network("https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png", + height: 270)), + ]) + ]); } Widget android() { diff --git a/lib/ui/mobile/setting/video_player.dart b/lib/ui/mobile/setting/video_player.dart new file mode 100644 index 0000000..f94ab8d --- /dev/null +++ b/lib/ui/mobile/setting/video_player.dart @@ -0,0 +1,167 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +void main() => runApp(const VideoPlayerApp()); + +class VideoPlayerApp extends StatelessWidget { + const VideoPlayerApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Video Player Demo', + home: VideoPlayerScreen(), + ); + } +} + +class VideoPlayerScreen extends StatefulWidget { + const VideoPlayerScreen({super.key}); + + @override + State createState() => _VideoPlayerScreenState(); +} + +class _VideoPlayerScreenState extends State { + late VideoPlayerController _controller; + late Future _initializeVideoPlayerFuture; + + @override + void initState() { + super.initState(); + + _controller = VideoPlayerController.network( + 'https://github.com/wanghongenpin/network_proxy_flutter/assets/24794200/38bc5a83-999f-4af2-9d74-863532a81cef', + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true, allowBackgroundPlayback: true), + ); + _initializeVideoPlayerFuture = _controller.initialize(); + _initializeVideoPlayerFuture.whenComplete(() { + final MediaQueryData data = MediaQuery.of(context); + + EdgeInsets paddingSafeArea = data.padding; + double widthScreen = data.size.width; + _controller.setPictureInPictureOverlayRect( + rect: Rect.fromLTWH(0, paddingSafeArea.top, widthScreen, 9 * widthScreen / 16)); + // _controller.setAutomaticallyStartPictureInPicture(enableStartPictureInPictureAutomaticallyFromInline: true); + }); + _controller.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + // Ensure disposing of the VideoPlayerController to free up resources. + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _initializeVideoPlayerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return AspectRatio( + aspectRatio: _controller.value.aspectRatio, + // Use the VideoPlayer widget to display the video. + child: Stack(alignment: Alignment.bottomCenter, children: [ + VideoPlayer(_controller), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator( + _controller, + allowScrubbing: true, + ), + ]), + ); + } else { + // If the VideoPlayerController is still initializing, show a + // loading spinner. + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + } +} + +class _ControlsOverlay extends StatelessWidget { + const _ControlsOverlay({required this.controller}); + + static const List _playbackRates = [ + 0.25, + 0.5, + 1.0, + 1.5, + 2.0, + ]; + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 50), + reverseDuration: const Duration(milliseconds: 200), + child: controller.value.isPlaying + ? const SizedBox.shrink() + : Container( + color: Colors.black26, + child: const Center( + child: Icon( + Icons.play_arrow, + color: Colors.white, + size: 80.0, + semanticLabel: 'Play', + ), + ), + ), + ), + GestureDetector( + onTap: () { + controller.value.isPlaying ? controller.pause() : controller.play(); + }, + ), + Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () async { + controller.startPictureInPicture(); + }, + icon: const Icon(Icons.picture_in_picture), + )), + Align( + alignment: Alignment.bottomRight, + child: PopupMenuButton( + initialValue: controller.value.playbackSpeed, + tooltip: 'Playback speed', + onSelected: (double speed) { + controller.setPlaybackSpeed(speed); + }, + itemBuilder: (BuildContext context) { + return >[ + for (final double speed in _playbackRates) + PopupMenuItem( + value: speed, + child: Text('${speed}x'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 16, + ), + child: Text('${controller.value.playbackSpeed}x', style: const TextStyle(color: Colors.white)), + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui/mobile/about.dart b/lib/ui/mobile/widgets/about.dart similarity index 100% rename from lib/ui/mobile/about.dart rename to lib/ui/mobile/widgets/about.dart diff --git a/lib/ui/mobile/connect_remote.dart b/lib/ui/mobile/widgets/connect_remote.dart similarity index 100% rename from lib/ui/mobile/connect_remote.dart rename to lib/ui/mobile/widgets/connect_remote.dart diff --git a/lib/ui/mobile/widgets/pip.dart b/lib/ui/mobile/widgets/pip.dart new file mode 100644 index 0000000..eab2c81 --- /dev/null +++ b/lib/ui/mobile/widgets/pip.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:network_proxy/native/pip.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/utils/ip.dart'; + +class PictureInPictureWindow extends StatefulWidget { + final ProxyServer proxyServer; + + const PictureInPictureWindow( + this.proxyServer, { + super.key, + }); + + @override + State createState() => _PictureInPictureState(); +} + +class _PictureInPictureState extends State { + static double xPosition = -1; + static double yPosition = -1; + static Size? size; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + Widget build(BuildContext context) { + size ??= MediaQuery.of(context).size; + if (xPosition == -1) { + xPosition = size!.width * 0.9; + yPosition = size!.height * 0.35; + } + + return Stack(children: [ + Positioned( + top: yPosition, + left: xPosition, + child: GestureDetector( + onPanUpdate: (tapInfo) { + if (xPosition + tapInfo.delta.dx < 0) return; + if (yPosition + tapInfo.delta.dy < 0) return; + + setState(() { + xPosition += tapInfo.delta.dx; + yPosition += tapInfo.delta.dy; + }); + }, + child: IconButton( + tooltip: localizations.windowMode, + onPressed: () async { + PictureInPicture.enterPictureInPictureMode( + Platform.isAndroid ? await localIp() : "127.0.0.1", widget.proxyServer.port); + }, + icon: const Icon(Icons.picture_in_picture))), + ) + ]); + } +} diff --git a/pubspec.lock b/pubspec.lock index 90f427e..01ac9c0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" cupertino_icons: dependency: "direct main" description: @@ -326,6 +334,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.4" http: dependency: transitive description: @@ -771,6 +787,50 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + path: "packages/video_player/video_player" + ref: "feature/ios-pip" + resolved-ref: b829adec12641d97bef672d8ddbbf0531dd3efe7 + url: "https://github.com/icapps/plugins.git" + source: git + version: "2.6.0" + video_player_android: + dependency: "direct overridden" + description: + path: "packages/video_player/video_player_android" + ref: "feature/ios-pip" + resolved-ref: b829adec12641d97bef672d8ddbbf0531dd3efe7 + url: "https://github.com/icapps/plugins.git" + source: git + version: "2.3.10" + video_player_avfoundation: + dependency: "direct overridden" + description: + path: "packages/video_player/video_player_avfoundation" + ref: "feature/ios-pip" + resolved-ref: b829adec12641d97bef672d8ddbbf0531dd3efe7 + url: "https://github.com/icapps/plugins.git" + source: git + version: "2.4.0" + video_player_platform_interface: + dependency: "direct overridden" + description: + path: "packages/video_player/video_player_platform_interface" + ref: "feature/ios-pip" + resolved-ref: b829adec12641d97bef672d8ddbbf0531dd3efe7 + url: "https://github.com/icapps/plugins.git" + source: git + version: "6.1.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7d0cbed..cf45a40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,29 @@ dependencies: flutter_js: ^0.8.0 flutter_code_editor: file_picker: ^6.1.1 + video_player: + git: + url: https://github.com/icapps/plugins.git + ref: feature/ios-pip + path: packages/video_player/video_player + + +dependency_overrides: + video_player_platform_interface: + git: + url: https://github.com/icapps/plugins.git + ref: feature/ios-pip + path: packages/video_player/video_player_platform_interface + video_player_avfoundation: + git: + url: https://github.com/icapps/plugins.git + ref: feature/ios-pip + path: packages/video_player/video_player_avfoundation + video_player_android: + git: + url: https://github.com/icapps/plugins.git + ref: feature/ios-pip + path: packages/video_player/video_player_android dev_dependencies: flutter_test: