Picture in Picture Window

This commit is contained in:
wanghongenpin
2024-03-25 12:45:38 +08:00
parent f8119b2dca
commit 9900ee34e5
17 changed files with 675 additions and 572 deletions

View File

@@ -31,7 +31,10 @@ class AppConfiguration {
/// 是否启用画中画
ValueNotifier<bool> pipEnabled = ValueNotifier(true);
///
/// 显示画中画图标
ValueNotifier<bool> 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
};

View File

@@ -142,7 +142,7 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> 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<DesktopHomePage> implements EventL
'2. 关键词匹配高亮;\n'
'3. 脚本批量操作和导入导出;\n'
'4. 脚本支持日志查看通过console.log()输出;\n'
'5. 设置增加自动开启抓包;\n'
: 'TipsBy 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)));
});
}

View File

@@ -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(

View File

@@ -207,10 +207,10 @@ class _SslState extends State<SslWidget> {
length: 2,
child: Scaffold(
appBar: TabBar(
tabs: [
tabs: <Widget>[
Tab(text: localizations.androidRoot),
Tab(text: localizations.androidUserCA),
] as List<Widget>),
]),
body: Padding(
padding: const EdgeInsets.all(10),
child: TabBarView(children: [

View File

@@ -50,6 +50,9 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
windowManager.setPreventClose(true);
}
SocketLaunch.startStatus.addListener(() {
if (SocketLaunch.startStatus.value.get() == started) {
return;
}
setState(() {
started = SocketLaunch.startStatus.value.get() ?? started;
});

View File

@@ -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<HttpRequest> 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<RemoteModel> 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 <PopupMenuEntry>[
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)),
],
);
});
}
}

View File

@@ -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<HttpRequest> 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))),
])));
}
}

View File

@@ -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<RemoteModel> 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 <PopupMenuEntry>[
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)),
],
);
});
}
}

View File

@@ -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)),
],
);
});
}
}

View File

@@ -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<bool> pictureInPictureNotifier = ValueNotifier(false);
class MobileHomeState extends State<MobileHomePage> implements EventListener, LifecycleListener {
static final GlobalKey<RequestListState> requestStateKey = GlobalKey<RequestListState>();
static final container = ListenableList<HttpRequest>();
@@ -50,6 +49,9 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
late ProxyServer proxyServer;
///画中画
// bool pictureInPicture = false;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
@@ -58,7 +60,7 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
}
Future<bool> 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<MobileHomePage> 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<MobileHomePage> implements EventListener, Li
}
return;
}
//退出程序
SystemNavigator.pop();
},
child: ValueListenableBuilder<bool>(
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<MobileHomePage> 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,

View File

@@ -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<RequestListWidget> {
@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<Tab> tabs = [
Tab(child: Text(localizations.sequence)),
Tab(child: Text(localizations.domainList)),
@@ -92,13 +71,6 @@ class RequestListState extends State<RequestListWidget> {
///添加请求
add(Channel channel, HttpRequest request) {
if (pictureInPictureNotifier.value) {
setState(() {
container.add(request);
});
return;
}
container.add(request);
requestSequenceKey.currentState?.add(request);
domainListKey.currentState?.add(request);

View File

@@ -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<HttpRequest> container;
const PictureInPictureWindow(this.container, {super.key});
@override
State<PictureInPictureWindow> createState() => _PictureInPictureWindowState();
}
class _PictureInPictureWindowState extends State<PictureInPictureWindow> {
AppLocalizations get localizations => AppLocalizations.of(context)!;
OnchangeListEvent<HttpRequest>? 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<PictureInPictureWindow> createState() => _PictureInPictureState();
State<PictureInPictureIcon> createState() => _PictureInPictureState();
}
class _PictureInPictureState extends State<PictureInPictureWindow> {
class _PictureInPictureState extends State<PictureInPictureIcon> {
static double xPosition = -1;
static double yPosition = -1;
static Size? size;
@@ -29,14 +85,21 @@ class _PictureInPictureState extends State<PictureInPictureWindow> {
@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<PictureInPictureWindow> {
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))),
)
]);
}