diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b0228c2..787bcc2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -29,6 +29,8 @@ "themeLight": "Light", "themeDark": "Dark", "language": "Language", + "autoStartup": "Auto Start Recording Traffic", + "autoStartupDescribe": "Automatically start recording traffic when the program starts", "copied": "Copied to clipboard", "cancel": "Cancel", @@ -215,8 +217,10 @@ "windowMode": "Window Mode", "windowModeSubTitle": "Enabled Packet Capture, Enter the background, Display a small window", + "windowIcon": "Window shortcut icon", + "windowIconDescribe": "Show quick access to small window Icon", "headerExpanded": "Headers Expanded", - "headerExpandedSubtitle": "Details page Headers column is expanded by default", + "headerExpandedSubtitle": "Details page Headers is expanded by default", "externalProxyConnectFailure": "External Proxy Connect failure", "externalProxyFailureConfirm": "Access to all http will fail due to network connectivity issues,Do you want to continue setting up external proxies。", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 801dc25..13bfd2b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -29,6 +29,8 @@ "themeLight": "浅色", "themeDark": "深色", "language": "语言", + "autoStartup": "自动开启抓包", + "autoStartupDescribe": "程序启动时自动开始记录流量", "copied": "已复制到剪贴板", "cancel": "取消", @@ -215,8 +217,10 @@ "windowMode": "窗口模式", "windowModeSubTitle": "开启抓包后 如果应用退回到后台,显示一个小窗口", - "headerExpanded": "Headers默认展开", - "headerExpandedSubtitle": "详情页Headers栏是否默认展开", + "windowIcon": "窗口快捷图标", + "windowIconDescribe": "展示快捷进入小窗口Icon", + "headerExpanded": "Headers自动展开", + "headerExpandedSubtitle": "详情页Headers栏是否自动展开", "externalProxyConnectFailure": "外部代理连接失败", "externalProxyFailureConfirm": "网络不通所有接口将会访问失败,是否继续设置外部代理。", diff --git a/lib/network/bin/configuration.dart b/lib/network/bin/configuration.dart index c3abbf4..1d3065c 100644 --- a/lib/network/bin/configuration.dart +++ b/lib/network/bin/configuration.dart @@ -22,6 +22,7 @@ import 'package:network_proxy/network/util/file_read.dart'; import 'package:network_proxy/network/components/host_filter.dart'; import 'package:network_proxy/network/util/logger.dart'; import 'package:network_proxy/network/util/system_proxy.dart'; +import 'package:network_proxy/utils/platform.dart'; class Configuration { ///代理相关配置 @@ -50,6 +51,9 @@ class Configuration { //历史记录缓存时间 int historyCacheTime = 0; + //默认是否启动 + bool startup = false; + Configuration._(); /// 单例 @@ -72,6 +76,7 @@ class Configuration { Configuration.fromJson(Map config) { port = config['port'] ?? port; enableSsl = config['enableSsl'] == true; + startup = config['startup'] ?? Platforms.isDesktop(); enableSystemProxy = config['enableSystemProxy'] ?? (config['enableDesktop'] ?? true); proxyPassDomains = config['proxyPassDomains'] ?? SystemProxy.proxyPassDomains; historyCacheTime = config['historyCacheTime'] ?? 0; @@ -121,6 +126,7 @@ class Configuration { return { 'port': port, 'enableSsl': enableSsl, + 'startup': startup, 'enableSystemProxy': enableSystemProxy, 'proxyPassDomains': proxyPassDomains, 'externalProxy': externalProxy?.toJson(), diff --git a/lib/ui/configuration.dart b/lib/ui/configuration.dart index a69f9a2..2ed8090 100644 --- a/lib/ui/configuration.dart +++ b/lib/ui/configuration.dart @@ -31,7 +31,10 @@ class AppConfiguration { /// 是否启用画中画 ValueNotifier pipEnabled = ValueNotifier(true); - /// + /// 显示画中画图标 + ValueNotifier pipIcon = ValueNotifier(true); + + /// header默认展示 bool headerExpanded = true; bool? iosVpnBackgroundAudioEnable; @@ -117,6 +120,7 @@ class AppConfiguration { upgradeNoticeV8 = config['upgradeNoticeV8'] ?? true; _language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']); pipEnabled.value = config['pipEnabled'] ?? true; + pipIcon.value = config['pipIcon'] ?? false; headerExpanded = config['headerExpanded'] ?? true; iosVpnBackgroundAudioEnable = config['iosVpnBackgroundAudioEnable']; } catch (e) { @@ -143,6 +147,7 @@ class AppConfiguration { 'upgradeNoticeV8': upgradeNoticeV8, "language": _language?.languageCode, 'pipEnabled': pipEnabled.value, + 'pipIcon': pipIcon.value ? true : null, "headerExpanded": headerExpanded, "iosVpnBackgroundAudioEnable": iosVpnBackgroundAudioEnable == false ? null : iosVpnBackgroundAudioEnable }; diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index 886b266..53fe15b 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -142,7 +142,7 @@ class _DesktopHomePagePageState extends State implements EventL preferBelow: false, child: IconButton( onPressed: () { - showDialog(context: context, builder: (_) => Preference(widget.appConfiguration)); + showDialog(context: context, builder: (_) => Preference(widget.appConfiguration, proxyServer.configuration)); }, icon: Icon(Icons.settings_outlined, color: Colors.grey.shade500))), const SizedBox(height: 5), @@ -204,12 +204,14 @@ class _DesktopHomePagePageState extends State implements EventL '2. 关键词匹配高亮;\n' '3. 脚本批量操作和导入导出;\n' '4. 脚本支持日志查看,通过console.log()输出;\n' + '5. 设置增加自动开启抓包;\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. Display the application icon initiated by the request;\n' '2. Keyword matching highlights;\n' '3. Script batch operations and import/export;\n' - '4. The script supports log viewing, output through console.log();\n', + '4. The script supports log viewing, output through console.log();\n' + '5. Setting Auto Start Recording Traffic;\n', style: const TextStyle(fontSize: 14))); }); } diff --git a/lib/ui/desktop/preference.dart b/lib/ui/desktop/preference.dart index e26f5b6..2e423e1 100644 --- a/lib/ui/desktop/preference.dart +++ b/lib/ui/desktop/preference.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/ui/configuration.dart'; /// @author wanghongen /// 2024/1/2 class Preference extends StatelessWidget { + final Configuration configuration; final AppConfiguration appConfiguration; - const Preference(this.appConfiguration, {super.key}); + const Preference(this.appConfiguration, this.configuration, {super.key}); @override Widget build(BuildContext context) { @@ -23,7 +25,7 @@ class Preference extends StatelessWidget { const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton())) ]), content: SizedBox( - width: 280, + width: 300, child: Column(children: [ Row(children: [ SizedBox(width: 100, child: Text("${localizations.language}: ", style: titleMedium)), @@ -60,7 +62,17 @@ class Preference extends StatelessWidget { )), const Divider(), ListTile( - contentPadding: const EdgeInsets.only(), + contentPadding: EdgeInsets.zero, + title: Text(localizations.autoStartup), //默认是否启动 + subtitle: Text(localizations.autoStartupDescribe, style: const TextStyle(fontSize: 14)), + trailing: SwitchWidget( + value: configuration.startup, + onChanged: (value) { + configuration.startup = value; + configuration.flushConfig(); + })), + ListTile( + contentPadding: EdgeInsets.zero, title: Text(localizations.headerExpanded), subtitle: Text(localizations.headerExpandedSubtitle, style: const TextStyle(fontSize: 14)), trailing: SwitchWidget( diff --git a/lib/ui/desktop/toolbar/ssl/ssl.dart b/lib/ui/desktop/toolbar/ssl/ssl.dart index 20212fb..28d481e 100644 --- a/lib/ui/desktop/toolbar/ssl/ssl.dart +++ b/lib/ui/desktop/toolbar/ssl/ssl.dart @@ -207,10 +207,10 @@ class _SslState extends State { length: 2, child: Scaffold( appBar: TabBar( - tabs: [ + tabs: [ Tab(text: localizations.androidRoot), Tab(text: localizations.androidUserCA), - ] as List), + ]), body: Padding( padding: const EdgeInsets.all(10), child: TabBarView(children: [ diff --git a/lib/ui/launch/launch.dart b/lib/ui/launch/launch.dart index 3095657..54e1a78 100644 --- a/lib/ui/launch/launch.dart +++ b/lib/ui/launch/launch.dart @@ -50,6 +50,9 @@ class _SocketLaunchState extends State with WindowListener, Widget windowManager.setPreventClose(true); } SocketLaunch.startStatus.addListener(() { + if (SocketLaunch.startStatus.value.get() == started) { + return; + } setState(() { started = SocketLaunch.startStatus.value.get() ?? started; }); diff --git a/lib/ui/mobile/menu.dart b/lib/ui/mobile/menu.dart deleted file mode 100644 index 6ef9839..0000000 --- a/lib/ui/mobile/menu.dart +++ /dev/null @@ -1,450 +0,0 @@ -import 'dart:io'; - -import 'package:date_format/date_format.dart'; -import 'package:easy_permission/easy_permission.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/native/vpn.dart'; -import 'package:network_proxy/network/bin/server.dart'; -import 'package:network_proxy/network/components/host_filter.dart'; -import 'package:network_proxy/network/components/request_block_manager.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; -import 'package:network_proxy/network/http/http.dart'; -import 'package:network_proxy/network/http_client.dart'; -import 'package:network_proxy/storage/histories.dart'; -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/mobile.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'; -import 'package:network_proxy/ui/mobile/setting/filter.dart'; -import 'package:network_proxy/ui/mobile/setting/proxy.dart'; -import 'package:network_proxy/ui/mobile/setting/request_block.dart'; -import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart'; -import 'package:network_proxy/ui/mobile/setting/script.dart'; -import 'package:network_proxy/ui/mobile/setting/ssl.dart'; -import 'package:network_proxy/ui/mobile/setting/theme.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/widgets/highlight.dart'; -import 'package:network_proxy/utils/ip.dart'; -import 'package:network_proxy/utils/listenable_list.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:qrscan/qrscan.dart' as scanner; -import 'package:url_launcher/url_launcher.dart'; - -///左侧抽屉 -class DrawerWidget extends StatelessWidget { - final ProxyServer proxyServer; - final ListenableList container; - final HistoryTask historyTask; - - DrawerWidget({super.key, required this.proxyServer, required this.container}) - : historyTask = HistoryTask.ensureInstance(proxyServer.configuration, container); - - @override - Widget build(BuildContext context) { - AppLocalizations localizations = AppLocalizations.of(context)!; - - return Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer), - child: const Text(''), - ), - ListTile( - leading: const Icon(Icons.favorite), - title: Text(localizations.favorites), - onTap: () => navigator(context, MobileFavorites(proxyServer: proxyServer))), - ListTile( - leading: const Icon(Icons.history), - title: Text(localizations.history), - onTap: () => navigator( - context, MobileHistory(proxyServer: proxyServer, container: container, historyTask: historyTask)), - ), - const Divider(thickness: 0.3, height: 0), - ListTile( - leading: const Icon(Icons.construction), - title: Text(localizations.toolbox), - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(localizations.toolbox), centerTitle: true), - body: Toolbox(proxyServer: proxyServer)); - }), - )), - ListTile( - title: Text(localizations.httpsProxy), - leading: const Icon(Icons.https), - onTap: () => navigator(context, MobileSslWidget(proxyServer: proxyServer))), - const Divider(thickness: 0.3, height: 0), - ListTile( - title: Text(localizations.filter), - leading: const Icon(Icons.filter_alt_outlined), - onTap: () => navigator(context, FilterMenu(proxyServer: proxyServer))), - ListTile( - title: Text(localizations.requestRewrite), - leading: const Icon(Icons.replay_outlined), - onTap: () async { - var requestRewrites = await RequestRewrites.instance; - if (context.mounted) { - navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites)); - } - }), - ListTile( - title: Text(localizations.requestBlock), - leading: const Icon(Icons.block_flipped), - onTap: () async { - var requestBlockManager = await RequestBlockManager.instance; - if (context.mounted) { - navigator(context, MobileRequestBlock(requestBlockManager: requestBlockManager)); - } - }), - ListTile( - title: Text(localizations.script), - leading: const Icon(Icons.code), - onTap: () => navigator(context, const MobileScript())), - ListTile( - title: Text(localizations.setting), - leading: const Icon(Icons.settings), - onTap: () => navigator( - context, - futureWidget(AppConfiguration.instance, - (appConfiguration) => SettingMenu(proxyServer: proxyServer, appConfiguration: appConfiguration)))), - ListTile( - title: Text(localizations.about), - leading: const Icon(Icons.info_outline), - onTap: () => navigator(context, const About())), - const SizedBox(height: 20) - ], - )); - } -} - -///跳转页面 -navigator(BuildContext context, Widget widget) { - Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) { - return widget; - }), - ); -} - -///设置 -class SettingMenu extends StatelessWidget { - final ProxyServer proxyServer; - final AppConfiguration appConfiguration; - - const SettingMenu({super.key, required this.proxyServer, required this.appConfiguration}); - - @override - Widget build(BuildContext context) { - AppLocalizations localizations = AppLocalizations.of(context)!; - - return Scaffold( - appBar: AppBar(title: Text(localizations.setting, style: const TextStyle(fontSize: 16)), centerTitle: true), - body: Padding( - padding: const EdgeInsets.all(5), - child: ListView(children: [ - ListTile( - title: Text(localizations.proxy), - trailing: const Icon(Icons.arrow_right), - onTap: () => navigator(context, ProxySetting(proxyServer: proxyServer))), - ListTile( - title: Text(localizations.language), - trailing: const Icon(Icons.arrow_right), - onTap: () => _language(context), - ), - MobileThemeSetting(appConfiguration: appConfiguration), - ListTile( - title: Text(localizations.windowMode), - subtitle: Text(localizations.windowModeSubTitle, style: const TextStyle(fontSize: 12)), - trailing: SwitchWidget( - value: appConfiguration.pipEnabled.value, - onChanged: (value) { - appConfiguration.pipEnabled.value = value; - appConfiguration.flushConfig(); - })), - ListTile( - title: Text(localizations.headerExpanded), - subtitle: Text(localizations.headerExpandedSubtitle, style: const TextStyle(fontSize: 12)), - trailing: SwitchWidget( - value: appConfiguration.headerExpanded, - onChanged: (value) { - appConfiguration.headerExpanded = value; - appConfiguration.flushConfig(); - })) - ]))); - } - - //选择语言 - void _language(BuildContext context) { - AppLocalizations localizations = AppLocalizations.of(context)!; - showDialog( - context: context, - builder: (context) { - return AlertDialog( - contentPadding: const EdgeInsets.only(left: 5, top: 5), - actionsPadding: const EdgeInsets.only(bottom: 5, right: 5), - title: Text(localizations.language, style: const TextStyle(fontSize: 16)), - content: Wrap( - children: [ - TextButton( - onPressed: () { - appConfiguration.language = null; - Navigator.of(context).pop(); - }, - child: Text(localizations.followSystem)), - const Divider(thickness: 0.5, height: 0), - TextButton( - onPressed: () { - appConfiguration.language = const Locale.fromSubtags(languageCode: 'zh'); - Navigator.of(context).pop(); - }, - child: const Text("简体中文")), - const Divider(thickness: 0.5, height: 0), - TextButton( - child: const Text("English"), - onPressed: () { - appConfiguration.language = const Locale.fromSubtags(languageCode: 'en'); - Navigator.of(context).pop(); - }), - const Divider(thickness: 0.5), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(localizations.cancel)), - ], - ); - }); - } -} - -///抓包过滤菜单 -class FilterMenu extends StatelessWidget { - final ProxyServer proxyServer; - - const FilterMenu({super.key, required this.proxyServer}); - - @override - Widget build(BuildContext context) { - AppLocalizations localizations = AppLocalizations.of(context)!; - - return Scaffold( - appBar: AppBar(title: Text(localizations.filter, style: const TextStyle(fontSize: 16)), centerTitle: true), - body: Padding( - padding: const EdgeInsets.all(5), - child: ListView(children: [ - ListTile( - title: Text(localizations.domainWhitelist), - trailing: const Icon(Icons.arrow_right), - onTap: () => navigator(context, - MobileFilterWidget(configuration: proxyServer.configuration, hostList: HostFilter.whitelist))), - ListTile( - title: Text(localizations.domainBlacklist), - trailing: const Icon(Icons.arrow_right), - onTap: () => navigator(context, - MobileFilterWidget(configuration: proxyServer.configuration, hostList: HostFilter.blacklist))), - Platform.isIOS - ? const SizedBox() - : ListTile( - title: Text(localizations.appWhitelist), - trailing: const Icon(Icons.arrow_right), - onTap: () => navigator(context, AppWhitelist(proxyServer: proxyServer))), - ]))); - } -} - -/// +号菜单 -class MoreMenu extends StatelessWidget { - final ProxyServer proxyServer; - final ValueNotifier desktop; - - const MoreMenu({super.key, required this.proxyServer, required this.desktop}); - - @override - Widget build(BuildContext context) { - AppLocalizations localizations = AppLocalizations.of(context)!; - - return PopupMenuButton( - offset: const Offset(0, 30), - child: const SizedBox(height: 38, width: 38, child: Icon(Icons.more_vert, size: 26)), - itemBuilder: (BuildContext context) { - return [ - PopupMenuItem( - height: 32, - child: ListTile( - dense: true, - title: Text(localizations.httpsProxy), - leading: Icon(Icons.https_outlined, color: proxyServer.enableSsl ? null : Colors.red), - onTap: () { - navigator(context, MobileSslWidget(proxyServer: proxyServer)); - })), - PopupMenuItem( - height: 32, - child: ListTile( - dense: true, - leading: const Icon(Icons.qr_code_scanner_outlined), - title: Text(localizations.connectRemote), - onTap: () { - Navigator.maybePop(context); - connectRemote(context); - }, - )), - PopupMenuItem( - height: 32, - child: ListTile( - dense: true, - leading: const Icon(Icons.phone_iphone_outlined), - title: Text(localizations.myQRCode), - onTap: () async { - Navigator.maybePop(context); - var ip = await localIp(); - if (context.mounted) { - connectQrCode(context, ip, proxyServer.port); - } - }, - )), - const PopupMenuDivider(height: 0), - PopupMenuItem( - height: 32, - child: ListTile( - dense: true, - leading: const Icon(Icons.highlight_outlined), - title: Text(localizations.highlight), - onTap: () { - navigator(context, const KeywordHighlight()); - }, - )), - PopupMenuItem( - height: 32, - child: ListTile( - dense: true, - leading: const Icon(Icons.share_outlined), - title: Text(localizations.viewExport), - onTap: () async { - Navigator.maybePop(context); - var name = formatDate(DateTime.now(), [m, '-', d, ' ', HH, ':', nn, ':', ss]); - MobileHomeState.requestStateKey.currentState?.export('ProxyPin$name'); - }, - )), - ]; - }, - ); - } - - void navigator(BuildContext context, Widget widget) async { - await Navigator.maybePop(context); - if (context.mounted) { - Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) => widget), - ); - } - } - - ///扫码连接 - connectRemote(BuildContext context) async { - AppLocalizations localizations = AppLocalizations.of(context)!; - - String scanRes; - if (Platform.isAndroid) { - await EasyPermission.requestPermissions([PermissionType.CAMERA]); - scanRes = await scanner.scan() ?? "-1"; - } else { - scanRes = await FlutterBarcodeScanner.scanBarcode("#ff6666", localizations.cancel, true, ScanMode.QR); - } - if (scanRes == "-1") return; - if (scanRes.startsWith("http")) { - launchUrl(Uri.parse(scanRes), mode: LaunchMode.externalApplication); - return; - } - - if (scanRes.startsWith("proxypin://connect")) { - Uri uri = Uri.parse(scanRes); - var host = uri.queryParameters['host']; - var port = uri.queryParameters['port']; - - try { - var response = await HttpClients.get("http://$host:$port/ping").timeout(const Duration(seconds: 1)); - if (response.bodyAsString == "pong") { - desktop.value = RemoteModel( - connect: true, - host: host, - port: int.parse(port!), - os: response.headers.get("os"), - hostname: response.headers.get("hostname")); - - if (context.mounted && Navigator.canPop(context)) { - FlutterToastr.show( - "${localizations.connectSuccess}${Vpn.isVpnStarted ? '' : ', ${localizations.remoteConnectSuccessTips}'}", - context, - duration: 3); - Navigator.pop(context); - } - } - } catch (e) { - print(e); - if (context.mounted) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog(content: Text(localizations.remoteConnectFail)); - }); - } - } - return; - } - if (context.mounted) { - FlutterToastr.show(localizations.invalidQRCode, context); - } - } - - ///连接二维码 - connectQrCode(BuildContext context, String host, int port) { - AppLocalizations localizations = AppLocalizations.of(context)!; - - showDialog( - context: context, - builder: (context) { - return AlertDialog( - contentPadding: const EdgeInsets.only(top: 5), - actionsPadding: const EdgeInsets.only(bottom: 5), - title: Text(localizations.remoteConnectForward, style: const TextStyle(fontSize: 16)), - content: SizedBox( - height: 240, - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - QrImageView( - backgroundColor: Colors.white, - data: "proxypin://connect?host=$host&port=${proxyServer.port}", - version: QrVersions.auto, - size: 200.0, - ), - const SizedBox(height: 20), - Text(localizations.mobileScan), - ], - )), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(localizations.cancel)), - ], - ); - }); - } -} diff --git a/lib/ui/mobile/menu/drawer.dart b/lib/ui/mobile/menu/drawer.dart new file mode 100644 index 0000000..7132caf --- /dev/null +++ b/lib/ui/mobile/menu/drawer.dart @@ -0,0 +1,158 @@ +import 'dart:io'; + +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/components/host_filter.dart'; +import 'package:network_proxy/network/components/request_block_manager.dart'; +import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/storage/histories.dart'; +import 'package:network_proxy/ui/component/toolbox.dart'; +import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/ui/configuration.dart'; +import 'package:network_proxy/ui/mobile/menu/preference.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'; +import 'package:network_proxy/ui/mobile/setting/filter.dart'; +import 'package:network_proxy/ui/mobile/setting/request_block.dart'; +import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart'; +import 'package:network_proxy/ui/mobile/setting/script.dart'; +import 'package:network_proxy/ui/mobile/setting/ssl.dart'; +import 'package:network_proxy/ui/mobile/widgets/about.dart'; +import 'package:network_proxy/utils/listenable_list.dart'; + +///左侧抽屉 +class DrawerWidget extends StatelessWidget { + final ProxyServer proxyServer; + final ListenableList container; + final HistoryTask historyTask; + + DrawerWidget({super.key, required this.proxyServer, required this.container}) + : historyTask = HistoryTask.ensureInstance(proxyServer.configuration, container); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer), + child: const Text(''), + ), + ListTile( + leading: const Icon(Icons.favorite), + title: Text(localizations.favorites), + onTap: () => navigator(context, MobileFavorites(proxyServer: proxyServer))), + ListTile( + leading: const Icon(Icons.history), + title: Text(localizations.history), + onTap: () => navigator( + context, MobileHistory(proxyServer: proxyServer, container: container, historyTask: historyTask)), + ), + const Divider(thickness: 0.3, height: 0), + ListTile( + leading: const Icon(Icons.construction), + title: Text(localizations.toolbox), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(localizations.toolbox), centerTitle: true), + body: Toolbox(proxyServer: proxyServer)); + }), + )), + ListTile( + title: Text(localizations.httpsProxy), + leading: const Icon(Icons.https), + onTap: () => navigator(context, MobileSslWidget(proxyServer: proxyServer))), + const Divider(thickness: 0.3, height: 0), + ListTile( + title: Text(localizations.filter), + leading: const Icon(Icons.filter_alt_outlined), + onTap: () => navigator(context, FilterMenu(proxyServer: proxyServer))), + ListTile( + title: Text(localizations.requestRewrite), + leading: const Icon(Icons.replay_outlined), + onTap: () async { + var requestRewrites = await RequestRewrites.instance; + if (context.mounted) { + navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites)); + } + }), + ListTile( + title: Text(localizations.requestBlock), + leading: const Icon(Icons.block_flipped), + onTap: () async { + var requestBlockManager = await RequestBlockManager.instance; + if (context.mounted) { + navigator(context, MobileRequestBlock(requestBlockManager: requestBlockManager)); + } + }), + ListTile( + title: Text(localizations.script), + leading: const Icon(Icons.code), + onTap: () => navigator(context, const MobileScript())), + ListTile( + title: Text(localizations.setting), + leading: const Icon(Icons.settings), + onTap: () => navigator( + context, + futureWidget(AppConfiguration.instance, + (appConfiguration) => SettingMenu(proxyServer: proxyServer, appConfiguration: appConfiguration)))), + ListTile( + title: Text(localizations.about), + leading: const Icon(Icons.info_outline), + onTap: () => navigator(context, const About())), + const SizedBox(height: 20) + ], + )); + } +} + +///跳转页面 +navigator(BuildContext context, Widget widget) { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return widget; + }), + ); +} + +///抓包过滤菜单 +class FilterMenu extends StatelessWidget { + final ProxyServer proxyServer; + + const FilterMenu({super.key, required this.proxyServer}); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar(title: Text(localizations.filter, style: const TextStyle(fontSize: 16)), centerTitle: true), + body: Padding( + padding: const EdgeInsets.all(5), + child: ListView(children: [ + ListTile( + title: Text(localizations.domainWhitelist), + trailing: const Icon(Icons.arrow_right), + onTap: () => navigator(context, + MobileFilterWidget(configuration: proxyServer.configuration, hostList: HostFilter.whitelist))), + ListTile( + title: Text(localizations.domainBlacklist), + trailing: const Icon(Icons.arrow_right), + onTap: () => navigator(context, + MobileFilterWidget(configuration: proxyServer.configuration, hostList: HostFilter.blacklist))), + Platform.isIOS + ? const SizedBox() + : ListTile( + title: Text(localizations.appWhitelist), + trailing: const Icon(Icons.arrow_right), + onTap: () => navigator(context, AppWhitelist(proxyServer: proxyServer))), + ]))); + } +} diff --git a/lib/ui/mobile/menu/menu.dart b/lib/ui/mobile/menu/menu.dart new file mode 100644 index 0000000..6c3cb07 --- /dev/null +++ b/lib/ui/mobile/menu/menu.dart @@ -0,0 +1,202 @@ +import 'dart:io'; + +import 'package:date_format/date_format.dart'; +import 'package:easy_permission/easy_permission.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:network_proxy/native/vpn.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/http_client.dart'; +import 'package:network_proxy/ui/mobile/mobile.dart'; +import 'package:network_proxy/ui/mobile/setting/ssl.dart'; +import 'package:network_proxy/ui/mobile/widgets/connect_remote.dart'; +import 'package:network_proxy/ui/mobile/widgets/highlight.dart'; +import 'package:network_proxy/utils/ip.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:qrscan/qrscan.dart' as scanner; +import 'package:url_launcher/url_launcher.dart'; + +/// +号菜单 +class MoreMenu extends StatelessWidget { + final ProxyServer proxyServer; + final ValueNotifier desktop; + + const MoreMenu({super.key, required this.proxyServer, required this.desktop}); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + return PopupMenuButton( + offset: const Offset(0, 30), + child: const SizedBox(height: 38, width: 38, child: Icon(Icons.more_vert, size: 26)), + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + height: 32, + child: ListTile( + dense: true, + title: Text(localizations.httpsProxy), + leading: Icon(Icons.https_outlined, color: proxyServer.enableSsl ? null : Colors.red), + onTap: () { + navigator(context, MobileSslWidget(proxyServer: proxyServer)); + })), + PopupMenuItem( + height: 32, + child: ListTile( + dense: true, + leading: const Icon(Icons.qr_code_scanner_outlined), + title: Text(localizations.connectRemote), + onTap: () { + Navigator.maybePop(context); + connectRemote(context); + }, + )), + PopupMenuItem( + height: 32, + child: ListTile( + dense: true, + leading: const Icon(Icons.phone_iphone_outlined), + title: Text(localizations.myQRCode), + onTap: () async { + Navigator.maybePop(context); + var ip = await localIp(); + if (context.mounted) { + connectQrCode(context, ip, proxyServer.port); + } + }, + )), + const PopupMenuDivider(height: 0), + PopupMenuItem( + height: 32, + child: ListTile( + dense: true, + leading: const Icon(Icons.highlight_outlined), + title: Text(localizations.highlight), + onTap: () { + navigator(context, const KeywordHighlight()); + }, + )), + PopupMenuItem( + height: 32, + child: ListTile( + dense: true, + leading: const Icon(Icons.share_outlined), + title: Text(localizations.viewExport), + onTap: () async { + Navigator.maybePop(context); + var name = formatDate(DateTime.now(), [m, '-', d, ' ', HH, ':', nn, ':', ss]); + MobileHomeState.requestStateKey.currentState?.export('ProxyPin$name'); + }, + )), + ]; + }, + ); + } + + void navigator(BuildContext context, Widget widget) async { + await Navigator.maybePop(context); + if (context.mounted) { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) => widget), + ); + } + } + + ///扫码连接 + connectRemote(BuildContext context) async { + AppLocalizations localizations = AppLocalizations.of(context)!; + + String scanRes; + if (Platform.isAndroid) { + await EasyPermission.requestPermissions([PermissionType.CAMERA]); + scanRes = await scanner.scan() ?? "-1"; + } else { + scanRes = await FlutterBarcodeScanner.scanBarcode("#ff6666", localizations.cancel, true, ScanMode.QR); + } + if (scanRes == "-1") return; + if (scanRes.startsWith("http")) { + launchUrl(Uri.parse(scanRes), mode: LaunchMode.externalApplication); + return; + } + + if (scanRes.startsWith("proxypin://connect")) { + Uri uri = Uri.parse(scanRes); + var host = uri.queryParameters['host']; + var port = uri.queryParameters['port']; + + try { + var response = await HttpClients.get("http://$host:$port/ping").timeout(const Duration(seconds: 1)); + if (response.bodyAsString == "pong") { + desktop.value = RemoteModel( + connect: true, + host: host, + port: int.parse(port!), + os: response.headers.get("os"), + hostname: response.headers.get("hostname")); + + if (context.mounted && Navigator.canPop(context)) { + FlutterToastr.show( + "${localizations.connectSuccess}${Vpn.isVpnStarted ? '' : ', ${localizations.remoteConnectSuccessTips}'}", + context, + duration: 3); + Navigator.pop(context); + } + } + } catch (e) { + print(e); + if (context.mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog(content: Text(localizations.remoteConnectFail)); + }); + } + } + return; + } + if (context.mounted) { + FlutterToastr.show(localizations.invalidQRCode, context); + } + } + + ///连接二维码 + connectQrCode(BuildContext context, String host, int port) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + contentPadding: const EdgeInsets.only(top: 5), + actionsPadding: const EdgeInsets.only(bottom: 5), + title: Text(localizations.remoteConnectForward, style: const TextStyle(fontSize: 16)), + content: SizedBox( + height: 240, + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + QrImageView( + backgroundColor: Colors.white, + data: "proxypin://connect?host=$host&port=${proxyServer.port}", + version: QrVersions.auto, + size: 200.0, + ), + const SizedBox(height: 20), + Text(localizations.mobileScan), + ], + )), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.cancel)), + ], + ); + }); + } +} diff --git a/lib/ui/mobile/menu/preference.dart b/lib/ui/mobile/menu/preference.dart new file mode 100644 index 0000000..0b730e9 --- /dev/null +++ b/lib/ui/mobile/menu/preference.dart @@ -0,0 +1,127 @@ +import 'dart:io'; + +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/ui/component/widgets.dart'; +import 'package:network_proxy/ui/configuration.dart'; +import 'package:network_proxy/ui/mobile/setting/proxy.dart'; +import 'package:network_proxy/ui/mobile/setting/theme.dart'; + +///设置 +class SettingMenu extends StatelessWidget { + final ProxyServer proxyServer; + final AppConfiguration appConfiguration; + + const SettingMenu({super.key, required this.proxyServer, required this.appConfiguration}); + + @override + Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar(title: Text(localizations.setting, style: const TextStyle(fontSize: 16)), centerTitle: true), + body: Padding( + padding: const EdgeInsets.all(5), + child: ListView(children: [ + ListTile( + title: Text(localizations.proxy), + trailing: const Icon(Icons.arrow_right), + onTap: () => Navigator.push( + context, MaterialPageRoute(builder: (context) => ProxySetting(proxyServer: proxyServer)))), + ListTile( + title: Text(localizations.language), + trailing: const Icon(Icons.arrow_right), + onTap: () => _language(context), + ), + MobileThemeSetting(appConfiguration: appConfiguration), + ListTile( + title: Text(localizations.autoStartup), //默认是否启动 + subtitle: Text(localizations.autoStartupDescribe, style: const TextStyle(fontSize: 12)), + trailing: SwitchWidget( + value: proxyServer.configuration.startup, + scale: 0.8, + onChanged: (value) { + proxyServer.configuration.startup = value; + proxyServer.configuration.flushConfig(); + })), + ListTile( + title: Text(localizations.windowMode), + subtitle: Text(localizations.windowModeSubTitle, style: const TextStyle(fontSize: 12)), + trailing: SwitchWidget( + value: appConfiguration.pipEnabled.value, + scale: 0.8, + onChanged: (value) { + appConfiguration.pipEnabled.value = value; + appConfiguration.flushConfig(); + })), + if (Platform.isAndroid) + ListTile( + title: Text(localizations.windowIcon), + subtitle: Text(localizations.windowIconDescribe, style: const TextStyle(fontSize: 12)), + trailing: SwitchWidget( + value: appConfiguration.pipIcon.value, + scale: 0.8, + onChanged: (value) { + appConfiguration.pipIcon.value = value; + appConfiguration.flushConfig(); + })), + ListTile( + title: Text(localizations.headerExpanded), + subtitle: Text(localizations.headerExpandedSubtitle, style: const TextStyle(fontSize: 12)), + trailing: SwitchWidget( + value: appConfiguration.headerExpanded, + scale: 0.8, + onChanged: (value) { + appConfiguration.headerExpanded = value; + appConfiguration.flushConfig(); + })) + ]))); + } + + //选择语言 + void _language(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + showDialog( + context: context, + builder: (context) { + return AlertDialog( + contentPadding: const EdgeInsets.only(left: 5, top: 5), + actionsPadding: const EdgeInsets.only(bottom: 5, right: 5), + title: Text(localizations.language, style: const TextStyle(fontSize: 16)), + content: Wrap( + children: [ + TextButton( + onPressed: () { + appConfiguration.language = null; + Navigator.of(context).pop(); + }, + child: Text(localizations.followSystem)), + const Divider(thickness: 0.5, height: 0), + TextButton( + onPressed: () { + appConfiguration.language = const Locale.fromSubtags(languageCode: 'zh'); + Navigator.of(context).pop(); + }, + child: const Text("简体中文")), + const Divider(thickness: 0.5, height: 0), + TextButton( + child: const Text("English"), + onPressed: () { + appConfiguration.language = const Locale.fromSubtags(languageCode: 'en'); + Navigator.of(context).pop(); + }), + const Divider(thickness: 0.5), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.cancel)), + ], + ); + }); + } +} diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 73eb57f..7caa8f3 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -18,12 +18,14 @@ import 'package:network_proxy/network/http_client.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/menu.dart'; +import 'package:network_proxy/ui/mobile/menu/drawer.dart'; +import 'package:network_proxy/ui/mobile/menu/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'; +import 'package:network_proxy/utils/lang.dart'; import 'package:network_proxy/utils/listenable_list.dart'; class MobileHomePage extends StatefulWidget { @@ -38,9 +40,6 @@ class MobileHomePage extends StatefulWidget { } } -///画中画 -final ValueNotifier pictureInPictureNotifier = ValueNotifier(false); - class MobileHomeState extends State implements EventListener, LifecycleListener { static final GlobalKey requestStateKey = GlobalKey(); static final container = ListenableList(); @@ -50,6 +49,9 @@ class MobileHomeState extends State implements EventListener, Li late ProxyServer proxyServer; + ///画中画 + // bool pictureInPicture = false; + AppLocalizations get localizations => AppLocalizations.of(context)!; @override @@ -58,7 +60,7 @@ class MobileHomeState extends State implements EventListener, Li } Future enterPictureInPicture() async { - if (Vpn.isVpnStarted && !pictureInPictureNotifier.value) { + if (Vpn.isVpnStarted) { if (desktop.value.connect || !Platform.isAndroid || !(await (AppConfiguration.instance)).pipEnabled.value) { return false; } @@ -71,26 +73,30 @@ class MobileHomeState extends State implements EventListener, Li @override onPictureInPictureModeChanged(bool isInPictureInPictureMode) async { - if (isInPictureInPictureMode && !pictureInPictureNotifier.value) { - while (Navigator.canPop(context)) { - Navigator.pop(context); - } - pictureInPictureNotifier.value = true; + if (isInPictureInPictureMode) { + Navigator.push( + context, + PageRouteBuilder( + transitionDuration: Duration.zero, + pageBuilder: (context, animation, secondaryAnimation) { + return PictureInPictureWindow(container); + })); return; } - if (!isInPictureInPictureMode && pictureInPictureNotifier.value) { + if (!isInPictureInPictureMode) { + Navigator.maybePop(context); Vpn.isRunning().then((value) { Vpn.isVpnStarted = value; - pictureInPictureNotifier.value = false; + SocketLaunch.startStatus.value = ValueWrap.of(value); }); } } @override void onRequest(Channel channel, HttpRequest request) { - PictureInPicture.addData(request.requestUrl); requestStateKey.currentState!.add(channel, request); + PictureInPicture.addData(request.requestUrl); } @override @@ -157,36 +163,25 @@ class MobileHomeState extends State implements EventListener, Li } return; } - //退出程序 SystemNavigator.pop(); }, - child: ValueListenableBuilder( - valueListenable: pictureInPictureNotifier, - builder: (context, pip, _) { - if (pip) { - return Scaffold( - body: RequestListWidget(key: requestStateKey, proxyServer: proxyServer, list: container)); - } - - return Scaffold( - floatingActionButton: PictureInPictureWindow(proxyServer), - body: Scaffold( - appBar: appBar(), - drawer: DrawerWidget(proxyServer: proxyServer, container: container), - 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, list: container)) - ]); - }), - )); - })); + child: Scaffold( + floatingActionButton: PictureInPictureIcon(proxyServer), + body: Scaffold( + appBar: appBar(), + drawer: DrawerWidget(proxyServer: proxyServer, container: container), + 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, list: container)) + ]); + }), + ))); } AppBar appBar() { @@ -208,7 +203,7 @@ class MobileHomeState extends State implements EventListener, Li child: SocketLaunch( proxyServer: proxyServer, size: 36, - startup: Vpn.isVpnStarted, + startup: proxyServer.configuration.startup, serverLaunch: false, onStart: () async { Vpn.startVpn(Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port, diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index 92c1f30..d066afa 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -12,10 +12,8 @@ import 'package:network_proxy/network/components/host_filter.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/ui/desktop/left/model/search_model.dart'; -import 'package:network_proxy/ui/mobile/mobile.dart'; import 'package:network_proxy/ui/mobile/request/request.dart'; import 'package:network_proxy/utils/har.dart'; -import 'package:network_proxy/utils/lang.dart'; import 'package:network_proxy/utils/listenable_list.dart'; import 'package:share_plus/share_plus.dart'; @@ -50,25 +48,6 @@ class RequestListState extends State { @override Widget build(BuildContext context) { - if (pictureInPictureNotifier.value) { - if (container.isEmpty) { - return Center(child: Text(localizations.emptyData, style: const TextStyle(color: Colors.grey))); - } - - return ListView.separated( - padding: const EdgeInsets.only(left: 2), - itemCount: container.length, - separatorBuilder: (context, index) => const Divider(thickness: 0.3, height: 0.5), - itemBuilder: (context, index) { - return Text.rich( - overflow: TextOverflow.ellipsis, - TextSpan( - text: container.elementAt(container.length - index - 1).requestUrl.fixAutoLines(), - style: const TextStyle(fontSize: 9)), - maxLines: 2); - }); - } - List tabs = [ Tab(child: Text(localizations.sequence)), Tab(child: Text(localizations.domainList)), @@ -92,13 +71,6 @@ class RequestListState extends State { ///添加请求 add(Channel channel, HttpRequest request) { - if (pictureInPictureNotifier.value) { - setState(() { - container.add(request); - }); - return; - } - container.add(request); requestSequenceKey.currentState?.add(request); domainListKey.currentState?.add(request); diff --git a/lib/ui/mobile/widgets/pip.dart b/lib/ui/mobile/widgets/pip.dart index 887246a..9e5d272 100644 --- a/lib/ui/mobile/widgets/pip.dart +++ b/lib/ui/mobile/widgets/pip.dart @@ -4,22 +4,78 @@ 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/network/http/http.dart'; import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/utils/ip.dart'; +import 'package:network_proxy/utils/lang.dart'; +import 'package:network_proxy/utils/listenable_list.dart'; +/// Picture in Picture Window class PictureInPictureWindow extends StatefulWidget { + final ListenableList container; + + const PictureInPictureWindow(this.container, {super.key}); + + @override + State createState() => _PictureInPictureWindowState(); +} + +class _PictureInPictureWindowState extends State { + AppLocalizations get localizations => AppLocalizations.of(context)!; + + OnchangeListEvent? changeEvent; + + @override + void initState() { + super.initState(); + changeEvent = OnchangeListEvent(() { + setState(() {}); + }); + widget.container.addListener(changeEvent!); + } + + @override + void dispose() { + widget.container.removeListener(changeEvent!); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.container.isEmpty) { + return Material(child: Center(child: Text(localizations.emptyData, style: const TextStyle(color: Colors.grey)))); + } + + return Material( + child: ListView.separated( + padding: const EdgeInsets.only(left: 2), + itemCount: widget.container.length, + separatorBuilder: (context, index) => const Divider(thickness: 0.3, height: 0.5), + itemBuilder: (context, index) { + return Text.rich( + overflow: TextOverflow.ellipsis, + TextSpan( + text: widget.container.elementAt(widget.container.length - index - 1).requestUrl.fixAutoLines(), + style: const TextStyle(fontSize: 9)), + maxLines: 2); + })); + } +} + +/// pip Icon +class PictureInPictureIcon extends StatefulWidget { final ProxyServer proxyServer; - const PictureInPictureWindow( + const PictureInPictureIcon( this.proxyServer, { super.key, }); @override - State createState() => _PictureInPictureState(); + State createState() => _PictureInPictureState(); } -class _PictureInPictureState extends State { +class _PictureInPictureState extends State { static double xPosition = -1; static double yPosition = -1; static Size? size; @@ -29,14 +85,21 @@ class _PictureInPictureState extends State { @override void initState() { super.initState(); - AppConfiguration.current?.pipEnabled.addListener(() { + if (Platform.isIOS) { + AppConfiguration.current?.pipEnabled.addListener(() { + setState(() {}); + }); + } + + AppConfiguration.current?.pipIcon.addListener(() { setState(() {}); }); } @override Widget build(BuildContext context) { - if (Platform.isAndroid || AppConfiguration.current?.pipEnabled.value == false) return const SizedBox(); + if (Platform.isIOS && AppConfiguration.current?.pipEnabled.value == false) return const SizedBox(); + if (Platform.isAndroid && AppConfiguration.current?.pipIcon.value != true) return const SizedBox(); size ??= MediaQuery.sizeOf(context); if (size == null || size!.isEmpty) { @@ -69,7 +132,7 @@ class _PictureInPictureState extends State { PictureInPicture.enterPictureInPictureMode( Platform.isAndroid ? await localIp() : "127.0.0.1", widget.proxyServer.port); }, - icon: const Icon(Icons.picture_in_picture))), + icon: const Icon(Icons.picture_in_picture_alt))), ) ]); } diff --git a/pubspec.lock b/pubspec.lock index ae303df..4438d7e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: "direct main" description: @@ -166,18 +166,18 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03 url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.1" + version: "8.0.0+1" file_selector: dependency: "direct main" description: name: file_selector - sha256: "070062b9fca7482baf60af671219086e188e50dd80497393e7d58532b56215d3" + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.2" + version: "1.0.3" file_selector_android: dependency: transitive description: @@ -222,10 +222,10 @@ packages: dependency: transitive description: name: file_selector_web - sha256: c0f025d460de3301b7bbbf837fc8d0759df85f182c635f1dd94934b4cdc92352 + sha256: "619e431b224711a3869e30dbd7d516f5f5a4f04b265013a50912f39e1abc88c8" url: "https://pub.flutter-io.cn" source: hosted - version: "0.9.3" + version: "0.9.4+1" file_selector_windows: dependency: transitive description: @@ -346,10 +346,10 @@ packages: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" http_parser: dependency: transitive description: @@ -370,10 +370,10 @@ packages: dependency: transitive description: name: js - sha256: "4186c61b32f99e60f011f7160e32c89a758ae9b1d0c6d28e2c02ef0382300e2b" + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.0" + version: "0.7.1" json_annotation: dependency: transitive description: @@ -426,10 +426,10 @@ packages: dependency: "direct main" description: name: logger - sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04" url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.2+1" + version: "2.2.0" logging: dependency: transitive description: @@ -610,18 +610,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + sha256: "05ec043470319bfbabe0adbc90d3a84cbff0426b9d9f3a6e2ad3e131fa5fa629" url: "https://pub.flutter-io.cn" source: hosted - version: "7.2.2" + version: "8.0.2" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.1" + version: "3.4.0" sky_engine: dependency: transitive description: flutter @@ -711,26 +711,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.flutter-io.cn" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.flutter-io.cn" source: hosted - version: "6.2.2" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.flutter-io.cn" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -751,18 +751,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -799,18 +799,18 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.2" + version: "0.5.1" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.flutter-io.cn" source: hosted - version: "5.2.0" + version: "5.3.0" window_manager: dependency: "direct main" description: @@ -828,5 +828,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 92f561c..107b832 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,12 +31,12 @@ dependencies: qrscan: ^0.3.3 flutter_barcode_scanner: ^2.0.0 flutter_toastr: ^1.0.3 - share_plus: ^7.2.1 + share_plus: ^8.0.2 brotli: ^0.6.0 file_selector: ^1.0.2 flutter_js: ^0.8.0 flutter_code_editor: - file_picker: ^6.1.1 + file_picker: ^8.0.0 flutter_desktop_context_menu: ^0.2.0 # video_player: # git: