/* * 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 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 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))) ]) ])))); } }