mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-20 16:15:47 +08:00
Picture in Picture Window
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
: '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)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
158
lib/ui/mobile/menu/drawer.dart
Normal file
158
lib/ui/mobile/menu/drawer.dart
Normal 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))),
|
||||
])));
|
||||
}
|
||||
}
|
||||
202
lib/ui/mobile/menu/menu.dart
Normal file
202
lib/ui/mobile/menu/menu.dart
Normal 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)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
127
lib/ui/mobile/menu/preference.dart
Normal file
127
lib/ui/mobile/menu/preference.dart
Normal 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)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user