Files
proxypin/lib/ui/desktop/request/list.dart

410 lines
15 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:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:proxypin/l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/ui/component/multi_select_controller.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/component/widgets.dart';
import 'package:proxypin/ui/content/panel.dart';
import 'package:proxypin/ui/desktop/request/request_sequence.dart';
import 'package:proxypin/ui/desktop/request/request.dart';
import 'package:proxypin/ui/desktop/request/search.dart';
import 'package:proxypin/ui/component/selection_action_bar.dart';
import 'package:proxypin/utils/har.dart';
import 'package:proxypin/utils/lang.dart';
import 'package:proxypin/utils/listenable_list.dart';
import '../../component/model/search_model.dart';
import 'domains.dart';
import 'package:proxypin/ui/desktop/request/report_servers.dart';
/// @author wanghongen
class DesktopRequestListWidget extends StatefulWidget {
final ProxyServer proxyServer;
final ListenableList<HttpRequest>? list;
final NetworkTabController panel;
const DesktopRequestListWidget({super.key, required this.proxyServer, this.list, required this.panel});
@override
State<StatefulWidget> createState() {
return DesktopRequestListState();
}
}
class DesktopRequestListState extends State<DesktopRequestListWidget> with AutomaticKeepAliveClientMixin {
final GlobalKey<RequestSequenceState> requestSequenceKey = GlobalKey<RequestSequenceState>();
final GlobalKey<DomainWidgetState> domainListKey = GlobalKey<DomainWidgetState>();
final GlobalKey<SearchState> searchKey = GlobalKey<SearchState>();
TabController? _tabController;
//请求列表容器
ListenableList<HttpRequest> container = ListenableList();
bool sortDesc = true;
// 选择控制器
final MultiSelectController selectionController = MultiSelectController();
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
void initState() {
super.initState();
if (widget.list != null) {
container = widget.list!;
}
}
bool get isSelectionMode => selectionController.isSelectionMode;
@override
bool get wantKeepAlive => true;
@override
void dispose() {
RequestWidget.removeAutoReadByIds(container.map((request) => request.requestId));
selectionController.clear();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
List<Tab> tabs = [
Tab(child: Text(localizations.domainList, style: const TextStyle(fontSize: 13))),
Tab(child: Text(localizations.sequence, style: const TextStyle(fontSize: 13))),
];
return FocusableActionDetector(
autofocus: true,
shortcuts: {
SingleActivator(LogicalKeyboardKey.escape): const _ClearSelectionIntent(),
},
actions: {
_ClearSelectionIntent: CallbackAction<_ClearSelectionIntent>(onInvoke: (intent) {
if (_isTextInputFocused()) {
return null;
}
if (isSelectionMode) {
selectionController.clear();
}
return null;
}),
},
child: DefaultTabController(
length: tabs.length,
child: Builder(builder: (tabContext) {
_tabController = DefaultTabController.of(tabContext);
return Scaffold(
appBar: AppBar(
toolbarHeight: 40,
title: SizedBox(height: 40, child: TabBar(tabs: tabs, dividerColor: Colors.transparent)),
automaticallyImplyLeading: false,
actions: [popupMenus()],
),
bottomNavigationBar: Search(key: searchKey, onSearch: search),
body: Padding(
padding: const EdgeInsets.only(right: 5),
child: Column(children: [
Obx(() => selectionController.selectionMode.value
? SelectionActionBar(
selectionController: selectionController,
onRepeat: repeatSelected,
onExport: exportSelected,
onDelete: deleteSelected)
: SizedBox()),
Expanded(
child: TabBarView(physics: const NeverScrollableScrollPhysics(), children: [
DomainList(
key: domainListKey,
list: container,
panel: widget.panel,
proxyServer: widget.proxyServer,
selectionController: selectionController,
selectionHandlers: RequestSelectionHandlers(
onRangeSelection: rangeSelectRequest,
onDeleteSelected: deleteSelected,
onRepeatSelected: repeatSelected,
onExportSelected: exportSelected,
),
onRemove: domainListRemove,
),
RequestSequence(
key: requestSequenceKey,
container: container,
proxyServer: widget.proxyServer,
selectionController: selectionController,
selectionHandlers: RequestSelectionHandlers(
onRangeSelection: rangeSelectRequest,
onDeleteSelected: deleteSelected,
onRepeatSelected: repeatSelected,
onExportSelected: exportSelected,
),
onRemove: sequenceRemove,
),
])),
])));
})));
}
bool _isTextInputFocused() {
return FocusManager.instance.primaryFocus?.context?.widget is EditableText;
}
Widget popupMenus() {
return PopupMenuButton<_RequestListMenuAction>(
offset: const Offset(0, 32),
icon: const Icon(Icons.more_vert_outlined, size: 20),
onSelected: _onMenuSelected,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<_RequestListMenuAction>>[
_menuItem(_RequestListMenuAction.search,
icon: const Icon(Icons.search, size: 17), text: localizations.search),
_menuItem(_RequestListMenuAction.export,
icon: const Icon(Icons.share, size: 16), text: localizations.viewExport),
_menuItem(_RequestListMenuAction.repeat,
icon: const Icon(Icons.repeat, size: 16), text: localizations.repeatAllRequests),
_menuItem(_RequestListMenuAction.select,
icon: const Icon(Icons.checklist_outlined, size: 16), text: localizations.selectAction),
_menuItem(_RequestListMenuAction.sort,
icon: const Icon(Icons.sort, size: 16),
text: sortDesc ? localizations.timeAsc : localizations.timeDesc),
_menuItem(_RequestListMenuAction.report,
icon: const Icon(Icons.cloud_upload_outlined, size: 16), text: localizations.reportServers),
];
});
}
PopupMenuEntry<_RequestListMenuAction> _menuItem(_RequestListMenuAction value,
{required Icon icon, required String text}) {
return CustomPopupMenuItem<_RequestListMenuAction>(
value: value, height: 37, child: IconText(icon: icon, text: text, textStyle: const TextStyle(fontSize: 13)));
}
void _onMenuSelected(_RequestListMenuAction action) {
switch (action) {
case _RequestListMenuAction.search:
searchKey.currentState?.searchDialog();
break;
case _RequestListMenuAction.export:
export('ProxyPin_${DateTime.now().dateFormat()}.har');
break;
case _RequestListMenuAction.repeat:
repeatAllRequests();
break;
case _RequestListMenuAction.select:
selectionController.toggleSelectionMode();
break;
case _RequestListMenuAction.sort:
setState(() {
sortDesc = !sortDesc;
});
requestSequenceKey.currentState?.sort(sortDesc);
domainListKey.currentState?.sort(sortDesc);
break;
case _RequestListMenuAction.report:
showReportServersDialog(context);
break;
}
}
///添加请求
void add(Channel channel, HttpRequest request) {
container.add(request);
domainListKey.currentState?.add(channel, request);
requestSequenceKey.currentState?.add(request);
}
///添加响应
void addResponse(ChannelContext channelContext, HttpResponse response) {
domainListKey.currentState?.addResponse(channelContext, response);
requestSequenceKey.currentState?.addResponse(response);
}
void remove(List<HttpRequest> list) {
container.removeWhere((element) => list.contains(element));
domainListKey.currentState?.remove(list);
requestSequenceKey.currentState?.remove(list);
RequestWidget.removeAutoReadByIds(list.map((request) => request.requestId));
}
///移除
void domainListRemove(List<HttpRequest> list) {
container.removeWhere((element) => list.contains(element));
requestSequenceKey.currentState?.remove(list);
RequestWidget.removeAutoReadByIds(list.map((request) => request.requestId));
selectionController.prune(container.map((request) => request.requestId));
}
///全部请求删除
void sequenceRemove(List<HttpRequest> list) {
container.removeWhere((element) => list.contains(element));
domainListKey.currentState?.remove(list);
RequestWidget.removeAutoReadByIds(list.map((request) => request.requestId));
selectionController.prune(container.map((request) => request.requestId));
}
void search(SearchModel searchModel) {
domainListKey.currentState?.search(searchModel);
requestSequenceKey.currentState?.search(searchModel);
}
List<HttpRequest>? currentView() {
return domainListKey.currentState?.currentView();
}
///清理
void clean() {
setState(() {
RequestWidget.removeAutoReadByIds(container.map((request) => request.requestId));
container.clear();
domainListKey.currentState?.clean();
requestSequenceKey.currentState?.clean();
widget.panel.change(null, null);
selectionController.clear();
});
}
void cleanupEarlyData(int retain) {
var list = container.source;
if (list.length <= retain) {
return;
}
var removeRange = container.removeRange(0, list.length - retain);
domainListKey.currentState?.clean();
requestSequenceKey.currentState?.clean();
RequestWidget.removeAutoReadByIds(removeRange.map((request) => request.requestId));
selectionController.prune(container.map((request) => request.requestId));
}
void deleteSelected() {
final selectedRequests = domainListKey.currentState?.selectedRequests();
if (selectedRequests == null || selectedRequests.isEmpty) {
return;
}
showConfirmDialog(context, content: '${localizations.delete} ${selectedRequests.length} ${localizations.request}?',
onConfirm: () {
setState(() {
remove(selectedRequests);
selectionController.clear();
});
if (mounted) {
FlutterToastr.show(localizations.deleteSuccess, context);
}
});
}
void rangeSelectRequest(HttpRequest request) {
switch (_tabController?.index) {
case 1:
requestSequenceKey.currentState?.selectRange(request);
break;
case 0:
default:
domainListKey.currentState?.selectRange(request);
break;
}
}
void repeatSelected() {
final selectedRequests = domainListKey.currentState?.selectedRequests();
_repeatRequests(selectedRequests);
selectionController.clear();
}
Future<void> exportSelected() async {
final selectedRequests = domainListKey.currentState?.selectedRequests();
if (selectedRequests == null || selectedRequests.isEmpty) {
return;
}
final fileName = 'ProxyPin_selected_${DateTime.now().dateFormat()}.har';
_doExport(fileName, selectedRequests);
selectionController.clear();
}
///导出
Future<void> export(String fileName) async {
//
List<HttpRequest>? requests = currentView();
if (requests == null) return;
_doExport(fileName, requests);
}
Future<void> _doExport(String fileName, List<HttpRequest> requests) async {
var path = await FilePicker.platform.saveFile(fileName: fileName);
if (path == null) {
return;
}
var file = await File(path).create();
await Har.writeFile(requests, file, title: fileName);
if (mounted) FlutterToastr.show(AppLocalizations.of(context)!.exportSuccess, context);
}
///重发所有请求
void repeatAllRequests() async {
var requests = currentView();
_repeatRequests(requests);
}
void _repeatRequests(List<HttpRequest>? requests) async {
if (requests == null) return;
var localizations = AppLocalizations.of(context);
final proxyServer = widget.proxyServer;
for (var request in requests) {
var httpRequest = request.copy(uri: request.requestUrl);
var proxyInfo = proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", proxyServer.port) : null;
try {
await HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3));
if (mounted) {
FlutterToastr.show(localizations!.reSendRequest, rootNavigator: true, context);
}
} catch (e) {
if (mounted) {
FlutterToastr.show('${localizations!.fail} $e', rootNavigator: true, context);
}
}
}
}
}
class _ClearSelectionIntent extends Intent {
const _ClearSelectionIntent();
}
enum _RequestListMenuAction { search, export, repeat, select, sort, report }