Files
proxypin/lib/ui/mobile/menu/drawer.dart
2026-03-12 23:34:33 +08:00

401 lines
19 KiB
Dart

/*
* Copyright 2023 Hongen Wang All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:proxypin/l10n/app_localizations.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/components/host_filter.dart';
import 'package:proxypin/network/components/manager/hosts_manager.dart';
import 'package:proxypin/network/components/manager/request_block_manager.dart';
import 'package:proxypin/network/components/manager/request_breakpoint_manager.dart';
import 'package:proxypin/network/components/manager/request_rewrite_manager.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/util/system_proxy.dart';
import 'package:proxypin/storage/histories.dart';
import 'package:proxypin/ui/mobile/setting/hosts.dart';
import 'package:proxypin/ui/mobile/setting/request_breakpoint.dart';
import 'package:proxypin/ui/mobile/setting/request_map.dart';
import 'package:proxypin/ui/toolbox/toolbox.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:proxypin/ui/mobile/setting/preference.dart';
import 'package:proxypin/ui/mobile/request/favorite.dart';
import 'package:proxypin/ui/mobile/request/history.dart';
import 'package:proxypin/ui/mobile/setting/app_filter.dart';
import 'package:proxypin/ui/mobile/setting/filter.dart';
import 'package:proxypin/ui/mobile/setting/request_block.dart';
import 'package:proxypin/ui/mobile/setting/request_rewrite.dart';
import 'package:proxypin/ui/mobile/setting/request_crypto.dart';
import 'package:proxypin/ui/mobile/setting/script.dart';
import 'package:proxypin/ui/mobile/setting/ssl.dart';
import 'package:proxypin/ui/mobile/widgets/about.dart';
import 'package:proxypin/utils/listenable_list.dart';
import '../../component/proxy_port_setting.dart';
import '../../component/widgets.dart';
import '../../desktop/setting/external_proxy.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)!;
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
return Drawer(
backgroundColor: Theme.of(context).cardColor,
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer),
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
CircleAvatar(
radius: 24,
backgroundColor: Colors.white,
child: Center(
child: Image.asset(
'assets/icon_foreground.png',
width: 52,
))),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('ProxyPin', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 4),
Text(isCN ? "全平台开源免费抓包软件" : "Full platform open source free capture HTTP(S) traffic software",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall)
]),
)
])),
// Favorites & History
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: proxyServer.enableSsl ? Icon(Icons.lock_open) : 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.hosts),
leading: Icon(Icons.domain),
onTap: () async {
var hostsManager = await HostsManager.instance;
if (context.mounted) {
navigator(context, HostsPage(hostsManager: hostsManager));
}
}),
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.requestRewrite),
leading: const Icon(Icons.edit_outlined),
onTap: () async {
var requestRewrites = await RequestRewriteManager.instance;
if (context.mounted) {
navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites));
}
}),
ListTile(
title: Text(localizations.requestMap),
leading: Icon(Icons.swap_horiz_outlined),
onTap: () => navigator(context, MobileRequestMapPage())),
ListTile(
title: Text(localizations.requestCrypto),
leading: const Icon(Icons.lock_outline),
onTap: () => navigator(context, const MobileRequestCryptoPage())),
ListTile(
title: Text(localizations.script),
leading: const Icon(Icons.code),
onTap: () => navigator(context, const MobileScript())),
ListTile(
title: Text(localizations.breakpoint),
leading: const Icon(Icons.bug_report_outlined),
onTap: () async {
var manager = await RequestBreakpointManager.instance;
if (context.mounted) {
navigator(context, MobileRequestBreakpointPage(manager: manager));
}
}),
ListTile(
title: Text(localizations.setting),
leading: const Icon(Icons.settings),
onTap: () => navigator(
context,
futureWidget(
AppConfiguration.instance,
(appConfiguration) =>
_SettingPage(proxyServer: proxyServer, appConfiguration: appConfiguration)))),
ListTile(
title: Text(localizations.about),
leading: const Icon(Icons.info_outline),
onTap: () => navigator(context, const About())),
const SizedBox(height: 20)
],
));
}
}
///跳转页面
void navigator(BuildContext context, Widget widget) {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) {
return widget;
}),
);
}
class _SettingPage extends StatelessWidget {
final ProxyServer proxyServer;
final AppConfiguration appConfiguration;
const _SettingPage({required this.proxyServer, required this.appConfiguration});
@override
Widget build(BuildContext context) {
final configuration = proxyServer.configuration;
var textEditingController = TextEditingController(text: configuration.proxyPassDomains);
AppLocalizations localizations = AppLocalizations.of(context)!;
bool isEn = appConfiguration.language?.languageCode == 'en';
Widget section(List<Widget> tiles) => Card(
color: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),
borderRadius: BorderRadius.circular(10)),
child: Column(children: tiles),
);
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(42),
child: AppBar(
title: Text(localizations.setting, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w400)),
centerTitle: true,
)),
body: ListView(padding: const EdgeInsets.all(12), children: [
// Port and switches
Card(
color: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),
borderRadius: BorderRadius.circular(10)),
child: Column(children: [
PortWidget(
proxyServer: proxyServer,
title: '${localizations.proxy}${isEn ? ' ' : ''}${localizations.port}',
textStyle: const TextStyle(fontSize: 16)),
Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
if (Platform.isAndroid)
ListTile(
title: Text(localizations.systemProxy),
trailing: SwitchWidget(
value: configuration.enableSystemProxy,
scale: 0.8,
onChanged: (value) {
configuration.enableSystemProxy = value;
proxyServer.configuration.flushConfig();
})),
if (Platform.isAndroid)
Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
ListTile(
title: const Text("SOCKS5"),
trailing: SwitchWidget(
value: configuration.enableSocks5,
scale: 0.8,
onChanged: (value) {
configuration.enableSocks5 = value;
proxyServer.configuration.flushConfig();
})),
Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
ListTile(
title: Text(localizations.enabledHTTP2),
trailing: SwitchWidget(
value: configuration.enabledHttp2,
scale: 0.8,
onChanged: (value) {
configuration.enabledHttp2 = value;
proxyServer.configuration.flushConfig();
})),
Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
ListTile(
title: Text(localizations.externalProxy),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () {
showDialog(
context: context,
builder: (_) => ExternalProxyDialog(configuration: proxyServer.configuration));
}),
Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Row(children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(localizations.proxyIgnoreDomain, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 3),
Text(isEn ? "Use ';' to separate multiple entries" : "多个使用;分割",
style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
],
)),
Padding(
padding: const EdgeInsets.only(left: 35),
child: TextButton(
child: Text(localizations.reset),
onPressed: () {
textEditingController.text = SystemProxy.proxyPassDomains;
},
))
])),
const SizedBox(height: 5),
Padding(
padding: const EdgeInsets.only(left: 15, right: 5),
child: TextField(
textInputAction: TextInputAction.done,
style: const TextStyle(fontSize: 13),
controller: textEditingController,
onSubmitted: (_) {
configuration.proxyPassDomains = textEditingController.text;
proxyServer.configuration.flushConfig();
},
decoration:
const InputDecoration(contentPadding: EdgeInsets.all(10), border: OutlineInputBorder()),
maxLines: 5,
minLines: 1)),
const SizedBox(height: 10),
])),
const SizedBox(height: 12),
section([
ListTile(
title: Text(localizations.preference),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () =>
navigator(context, Preference(proxyServer: proxyServer, appConfiguration: appConfiguration))),
Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
ListTile(
title: Text(localizations.about),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () => navigator(context, const About())),
]),
const SizedBox(height: 8),
]));
}
}
///抓包过滤菜单
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(12),
child: Card(
color: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
side: BorderSide(color: Theme.of(context).dividerColor.withValues(alpha: 0.13)),
borderRadius: BorderRadius.circular(10)),
child: Column(mainAxisSize: MainAxisSize.min, children: [
ListTile(
title: Text(localizations.domainWhitelist),
trailing: const Icon(Icons.arrow_right),
onTap: () => navigator(
context,
MobileFilterWidget(
configuration: proxyServer.configuration, hostList: HostFilter.whitelist))),
Divider(height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
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()
: Column(mainAxisSize: MainAxisSize.min, children: [
Divider(
height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
ListTile(
title: Text(localizations.appWhitelist),
trailing: const Icon(Icons.arrow_right),
onTap: () => navigator(context, AppWhitelist(proxyServer: proxyServer))),
Divider(
height: 0, thickness: 0.4, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
ListTile(
title: Text(localizations.appBlacklist),
trailing: const Icon(Icons.arrow_right),
onTap: () => navigator(context, AppBlacklist(proxyServer: proxyServer)))
])
]))));
}
}