/* * 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? list; final NetworkTabController panel; const DesktopRequestListWidget({super.key, required this.proxyServer, this.list, required this.panel}); @override State createState() { return DesktopRequestListState(); } } class DesktopRequestListState extends State with AutomaticKeepAliveClientMixin { final GlobalKey requestSequenceKey = GlobalKey(); final GlobalKey domainListKey = GlobalKey(); final GlobalKey searchKey = GlobalKey(); TabController? _tabController; //请求列表容器 ListenableList 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 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 >[ _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 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 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 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? 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 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 export(String fileName) async { //获取请求 List? requests = currentView(); if (requests == null) return; _doExport(fileName, requests); } Future _doExport(String fileName, List 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? 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 }