mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-30 17:05:49 +08:00
This commit is contained in:
@@ -152,6 +152,7 @@
|
||||
"domainListSubtitle": "Last Request Time: {time}, Count: {count}",
|
||||
|
||||
"selectAction": "Select action",
|
||||
"select": "Select",
|
||||
"copy": "Copy",
|
||||
"copyHost": "Copy Host",
|
||||
"copyUrl": "Copy URL",
|
||||
|
||||
@@ -972,6 +972,12 @@ abstract class AppLocalizations {
|
||||
/// **'Select action'**
|
||||
String get selectAction;
|
||||
|
||||
/// No description provided for @select.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select'**
|
||||
String get select;
|
||||
|
||||
/// No description provided for @copy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -452,6 +452,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get selectAction => 'Select action';
|
||||
|
||||
@override
|
||||
String get select => 'Select';
|
||||
|
||||
@override
|
||||
String get copy => 'Copy';
|
||||
|
||||
|
||||
@@ -450,6 +450,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get selectAction => '选择操作';
|
||||
|
||||
@override
|
||||
String get select => '选择';
|
||||
|
||||
@override
|
||||
String get copy => '复制';
|
||||
|
||||
@@ -1541,6 +1544,9 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
|
||||
@override
|
||||
String get selectAction => '選擇操作';
|
||||
|
||||
@override
|
||||
String get select => '選擇';
|
||||
|
||||
@override
|
||||
String get copy => '複製';
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"domainListSubtitle": "最后请求时间: {time}, 次数: {count}",
|
||||
|
||||
"selectAction": "选择操作",
|
||||
"select": "选择",
|
||||
"copy": "复制",
|
||||
"copyHost": "复制域名",
|
||||
"copyUrl": "复制URL",
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"deleteWhitelist": "刪除代理白名單",
|
||||
"domainListSubtitle": "最後請求時間: {time}, 次數: {count}",
|
||||
"selectAction": "選擇操作",
|
||||
"select": "選擇",
|
||||
"copy": "複製",
|
||||
"copyHost": "複製網域名稱",
|
||||
"copyUrl": "複製URL",
|
||||
|
||||
@@ -22,6 +22,10 @@ class MultiSelectController {
|
||||
selectionMode.value = false;
|
||||
}
|
||||
|
||||
void remove(String requestId) {
|
||||
selectedIds.remove(requestId);
|
||||
}
|
||||
|
||||
void enterSelectionMode([String? requestId]) {
|
||||
selectionMode.value = true;
|
||||
if (requestId != null) {
|
||||
|
||||
@@ -340,6 +340,7 @@ class DesktopRequestListState extends State<DesktopRequestListWidget> with Autom
|
||||
void repeatSelected() {
|
||||
final selectedRequests = domainListKey.currentState?.selectedRequests();
|
||||
_repeatRequests(selectedRequests);
|
||||
selectionController.clear();
|
||||
}
|
||||
|
||||
Future<void> exportSelected() async {
|
||||
@@ -350,6 +351,7 @@ class DesktopRequestListState extends State<DesktopRequestListWidget> with Autom
|
||||
|
||||
final fileName = 'ProxyPin_selected_${DateTime.now().dateFormat()}.har';
|
||||
_doExport(fileName, selectedRequests);
|
||||
selectionController.clear();
|
||||
}
|
||||
|
||||
///导出
|
||||
|
||||
@@ -240,7 +240,7 @@ class _RequestWidgetState extends State<RequestWidget> {
|
||||
_menuAction(localizations.favorite, _RequestMenuAction.favorite),
|
||||
MenuItem(label: localizations.highlight, type: 'submenu', submenu: highlightMenu()),
|
||||
MenuItem.separator(),
|
||||
_menuAction(localizations.selectAction, _RequestMenuAction.select),
|
||||
_menuAction(localizations.select, _RequestMenuAction.select),
|
||||
MenuItem.separator(),
|
||||
_menuAction(localizations.delete, _RequestMenuAction.delete),
|
||||
]);
|
||||
|
||||
@@ -124,6 +124,17 @@ class MoreMenu extends StatelessWidget {
|
||||
MobileApp.requestStateKey.currentState?.export(context, 'ProxyPin$name');
|
||||
},
|
||||
)),
|
||||
PopupMenuItem(
|
||||
height: 32,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.checklist_rtl_outlined),
|
||||
title: Text(localizations.selectAction),
|
||||
onTap: () async {
|
||||
await Navigator.maybePop(context);
|
||||
MobileApp.multiSelectController.toggleSelectionMode();
|
||||
},
|
||||
)),
|
||||
PopupMenuItem(
|
||||
height: 32,
|
||||
child: ListTile(
|
||||
|
||||
@@ -35,6 +35,7 @@ import 'package:proxypin/network/http/websocket.dart';
|
||||
import 'package:proxypin/network/http/http_client.dart';
|
||||
import 'package:proxypin/storage/histories.dart';
|
||||
import 'package:proxypin/ui/component/memory_cleanup.dart';
|
||||
import 'package:proxypin/ui/component/multi_select_controller.dart';
|
||||
import 'package:proxypin/ui/toolbox/toolbox.dart';
|
||||
import 'package:proxypin/ui/configuration.dart';
|
||||
import 'package:proxypin/ui/content/panel.dart';
|
||||
@@ -79,6 +80,8 @@ class MobileApp {
|
||||
|
||||
///请求列表容器
|
||||
static final container = ListenableList<HttpRequest>();
|
||||
|
||||
static final multiSelectController = MultiSelectController();
|
||||
}
|
||||
|
||||
class MobileHomeState extends State<MobileHomePage> implements EventListener, LifecycleListener {
|
||||
@@ -434,7 +437,10 @@ class RequestPageState extends State<RequestPage> {
|
||||
value.connect ? remoteConnect(value) : const SizedBox(),
|
||||
Expanded(
|
||||
child: RequestListWidget(
|
||||
key: MobileApp.requestStateKey, proxyServer: proxyServer, list: MobileApp.container))
|
||||
key: MobileApp.requestStateKey,
|
||||
proxyServer: proxyServer,
|
||||
list: MobileApp.container,
|
||||
selectionController: MobileApp.multiSelectController))
|
||||
]);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -29,6 +29,7 @@ import 'package:proxypin/network/components/host_filter.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/widgets.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request_sequence.dart';
|
||||
import 'package:proxypin/utils/har.dart';
|
||||
@@ -238,22 +239,24 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(view.elementAt(index).domain, style: const TextStyle(fontSize: 16))),
|
||||
body: RequestSequence(
|
||||
key: requestSequenceKey,
|
||||
displayDomain: false,
|
||||
container: ListenableList(sortDesc ? list : list?.reversed.toList()),
|
||||
sortDesc: sortDesc,
|
||||
onRemove: widget.onRemove,
|
||||
proxyServer: widget.proxyServer));
|
||||
key: requestSequenceKey,
|
||||
displayDomain: false,
|
||||
container: ListenableList(sortDesc ? list : list?.reversed.toList()),
|
||||
sortDesc: sortDesc,
|
||||
onRemove: widget.onRemove,
|
||||
proxyServer: widget.proxyServer,
|
||||
selectionController: MultiSelectController(),
|
||||
));
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
void scrollToTop() {
|
||||
_scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
///菜单
|
||||
menu(int index) {
|
||||
void menu(int index) {
|
||||
var hostAndPort = view.elementAt(index);
|
||||
|
||||
showModalBottomSheet(
|
||||
|
||||
@@ -31,6 +31,7 @@ import 'package:proxypin/network/http/http_client.dart';
|
||||
import 'package:proxypin/network/util/logger.dart';
|
||||
import 'package:proxypin/storage/histories.dart';
|
||||
import 'package:proxypin/ui/component/history_cache_time.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/mobile/request/list.dart';
|
||||
@@ -412,11 +413,13 @@ class HistoryRecord extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HistoryRecordState extends State<HistoryRecord> {
|
||||
GlobalKey<RequestListState> requestStateKey = GlobalKey<RequestListState>();
|
||||
final GlobalKey<RequestListState> requestStateKey = GlobalKey<RequestListState>();
|
||||
|
||||
///搜索key
|
||||
final GlobalKey<MobileSearchState> searchStateKey = GlobalKey<MobileSearchState>();
|
||||
|
||||
final MultiSelectController multiSelectController = MultiSelectController();
|
||||
|
||||
var searchEnabled = ValueNotifier(false);
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
@@ -424,6 +427,7 @@ class _HistoryRecordState extends State<HistoryRecord> {
|
||||
@override
|
||||
void dispose() {
|
||||
searchEnabled.dispose();
|
||||
multiSelectController.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -462,6 +466,16 @@ class _HistoryRecordState extends State<HistoryRecord> {
|
||||
PopupMenuItem(
|
||||
onTap: () => export(context),
|
||||
child: IconText(icon: const Icon(Icons.share), text: localizations.viewExport)),
|
||||
PopupMenuItem(
|
||||
height: 32,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.checklist_rtl_outlined),
|
||||
title: Text(localizations.selectAction),
|
||||
onTap: () async {
|
||||
await Navigator.maybePop(context);
|
||||
multiSelectController.toggleSelectionMode();
|
||||
})),
|
||||
PopupMenuItem(
|
||||
onTap: () async {
|
||||
var requests = requestStateKey.currentState?.currentView();
|
||||
@@ -476,10 +490,15 @@ class _HistoryRecordState extends State<HistoryRecord> {
|
||||
],
|
||||
)),
|
||||
body: futureWidget(
|
||||
loading: true,
|
||||
HistoryStorage.instance.then((storage) => storage.getRequests(widget.history)),
|
||||
(data) =>
|
||||
RequestListWidget(proxyServer: widget.proxyServer, list: ListenableList(data), key: requestStateKey)));
|
||||
loading: true,
|
||||
HistoryStorage.instance.then((storage) => storage.getRequests(widget.history)),
|
||||
(data) => RequestListWidget(
|
||||
proxyServer: widget.proxyServer,
|
||||
list: ListenableList(data),
|
||||
key: requestStateKey,
|
||||
selectionController: multiSelectController,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
//导出har
|
||||
|
||||
@@ -22,6 +22,7 @@ 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/http/http.dart';
|
||||
import 'package:proxypin/ui/component/multi_select_controller.dart';
|
||||
import 'package:proxypin/ui/mobile/request/domians.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request_sequence.dart';
|
||||
@@ -37,8 +38,9 @@ import '../../component/model/search_model.dart';
|
||||
class RequestListWidget extends StatefulWidget {
|
||||
final ProxyServer proxyServer;
|
||||
final ListenableList<HttpRequest>? list;
|
||||
final MultiSelectController selectionController;
|
||||
|
||||
const RequestListWidget({super.key, required this.proxyServer, this.list});
|
||||
const RequestListWidget({super.key, required this.proxyServer, this.list, required this.selectionController});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -91,7 +93,8 @@ class RequestListState extends State<RequestListWidget> {
|
||||
key: requestSequenceKey,
|
||||
container: container,
|
||||
proxyServer: widget.proxyServer,
|
||||
onRemove: sequenceRemove),
|
||||
onRemove: sequenceRemove,
|
||||
selectionController: widget.selectionController),
|
||||
DomainList(
|
||||
key: domainListKey, list: container, proxyServer: widget.proxyServer, onRemove: domainListRemove),
|
||||
],
|
||||
@@ -182,7 +185,6 @@ class RequestListState extends State<RequestListWidget> {
|
||||
requestSequenceKey.currentState?.sort(sortDesc);
|
||||
domainListKey.currentState?.sort(sortDesc);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DoubleClickHandle {
|
||||
|
||||
@@ -29,10 +29,12 @@ import 'package:proxypin/network/http/http.dart';
|
||||
import 'package:proxypin/network/http/http_client.dart';
|
||||
import 'package:proxypin/network/util/cache.dart';
|
||||
import 'package:proxypin/storage/favorites.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/configuration.dart';
|
||||
import 'package:proxypin/ui/content/panel.dart';
|
||||
import 'package:proxypin/ui/desktop/request/request.dart';
|
||||
import 'package:proxypin/ui/mobile/request/repeat.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request_editor.dart';
|
||||
import 'package:proxypin/ui/mobile/setting/request_rewrite.dart';
|
||||
@@ -50,14 +52,19 @@ class RequestRow extends StatefulWidget {
|
||||
final ProxyServer proxyServer;
|
||||
final bool displayDomain;
|
||||
final Function(HttpRequest)? onRemove;
|
||||
final MultiSelectController selectionController;
|
||||
final RequestSelectionHandlers selectionHandlers;
|
||||
|
||||
const RequestRow(
|
||||
{super.key,
|
||||
required this.request,
|
||||
required this.proxyServer,
|
||||
this.displayDomain = true,
|
||||
this.onRemove,
|
||||
required this.index});
|
||||
const RequestRow({
|
||||
super.key,
|
||||
required this.request,
|
||||
required this.proxyServer,
|
||||
this.displayDomain = true,
|
||||
this.onRemove,
|
||||
required this.selectionController,
|
||||
required this.index,
|
||||
required this.selectionHandlers,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -134,10 +141,11 @@ class RequestRowState extends State<RequestRow> {
|
||||
child: ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
minLeadingWidth: 5,
|
||||
selected: selected,
|
||||
selected: selected ||
|
||||
(widget.selectionController.isSelectionMode && widget.selectionController.contains(request.requestId)),
|
||||
textColor: highlightColor,
|
||||
selectedColor: highlightColor,
|
||||
leading: appIcon(),
|
||||
leading: rowLeading(),
|
||||
title: Text(title.fixAutoLines(),
|
||||
overflow: TextOverflow.ellipsis, maxLines: 2, style: const TextStyle(fontSize: 14)),
|
||||
subtitle: Text.rich(
|
||||
@@ -150,6 +158,11 @@ class RequestRowState extends State<RequestRow> {
|
||||
contentPadding:
|
||||
Platform.isIOS ? const EdgeInsets.symmetric(horizontal: 8) : const EdgeInsets.only(left: 3, right: 5),
|
||||
onTap: () {
|
||||
if (widget.selectionController.isSelectionMode) {
|
||||
widget.selectionController.toggle(request.requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppConfiguration.current?.autoReadEnabled == true) {
|
||||
if (markAutoRead(request.requestId)) {
|
||||
setState(() {});
|
||||
@@ -167,6 +180,23 @@ class RequestRowState extends State<RequestRow> {
|
||||
));
|
||||
}
|
||||
|
||||
Widget? rowLeading() {
|
||||
var icon = appIcon();
|
||||
if (!widget.selectionController.isSelectionMode) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
bool isSelected = widget.selectionController.contains(request.requestId);
|
||||
var checkbox = Icon(isSelected ? Icons.check_box_outlined : Icons.check_box_outline_blank_outlined,
|
||||
size: 20, color: isSelected ? Theme.of(context).colorScheme.primary : null);
|
||||
|
||||
if (icon == null) {
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: [checkbox, const SizedBox(width: 6), icon]);
|
||||
}
|
||||
|
||||
Widget? appIcon() {
|
||||
if (Platform.isIOS) {
|
||||
return null;
|
||||
@@ -201,6 +231,7 @@ class RequestRowState extends State<RequestRow> {
|
||||
var globalPosition = details.globalPosition;
|
||||
MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||
var position = RelativeRect.fromLTRB(globalPosition.dx, globalPosition.dy, globalPosition.dx, globalPosition.dy);
|
||||
final selectionMode = widget.selectionController.isSelectionMode;
|
||||
// Trigger haptic feedback
|
||||
if (Platform.isAndroid) HapticFeedback.mediumImpact();
|
||||
|
||||
@@ -209,145 +240,189 @@ class RequestRowState extends State<RequestRow> {
|
||||
constraints: BoxConstraints(maxWidth: mediaQuery.size.width * 0.88),
|
||||
position: position,
|
||||
items: [
|
||||
//复制url
|
||||
PopupMenuContainer(
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 5),
|
||||
child: Text(localizations.selectAction, style: Theme.of(context).textTheme.bodyLarge)),
|
||||
),
|
||||
//copy
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: request.requestUrl)).then((value) {
|
||||
FlutterToastr.show(localizations.copied, getContext());
|
||||
Navigator.maybePop(getContext());
|
||||
});
|
||||
},
|
||||
label: localizations.copyUrl,
|
||||
icon: Icons.link,
|
||||
iconSize: 22),
|
||||
right: itemButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: curlRequest(request))).then((value) {
|
||||
FlutterToastr.show(localizations.copied, getContext());
|
||||
Navigator.maybePop(getContext());
|
||||
});
|
||||
},
|
||||
label: localizations.copyCurl,
|
||||
icon: Icons.code),
|
||||
),
|
||||
//repeat
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
onRepeat(request);
|
||||
Navigator.maybePop(getContext());
|
||||
},
|
||||
label: localizations.repeat,
|
||||
icon: Icons.repeat_one),
|
||||
right: itemButton(
|
||||
onPressed: () => showCustomRepeat(request), label: localizations.customRepeat, icon: Icons.repeat),
|
||||
),
|
||||
//favorite and edit
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
FavoriteStorage.addFavorite(widget.request);
|
||||
FlutterToastr.show(localizations.addSuccess, availableContext);
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.favorite,
|
||||
icon: Icons.favorite_outline),
|
||||
right: itemButton(
|
||||
onPressed: () async {
|
||||
await Navigator.maybePop(availableContext);
|
||||
children: selectionMode
|
||||
? [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 5),
|
||||
child: Text(localizations.selectAction, style: Theme.of(context).textTheme.bodyLarge)),
|
||||
),
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
widget.selectionHandlers.onExportSelected?.call();
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.export,
|
||||
icon: Icons.checklist_rtl_outlined),
|
||||
right: itemButton(
|
||||
onPressed: () {
|
||||
widget.selectionHandlers.onRepeatSelected?.call();
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.repeat,
|
||||
icon: Icons.delete_outline),
|
||||
),
|
||||
SizedBox(height: 1),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
itemButton(
|
||||
onPressed: () {
|
||||
widget.selectionHandlers.onDeleteSelected?.call();
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.delete,
|
||||
icon: Icons.delete_outline),
|
||||
SizedBox(width: 15),
|
||||
]),
|
||||
]
|
||||
: [
|
||||
//复制url
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 5),
|
||||
child: Text(localizations.selectAction, style: Theme.of(context).textTheme.bodyLarge)),
|
||||
),
|
||||
//copy
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: request.requestUrl)).then((value) {
|
||||
FlutterToastr.show(localizations.copied, getContext());
|
||||
Navigator.maybePop(getContext());
|
||||
});
|
||||
},
|
||||
label: localizations.copyUrl,
|
||||
icon: Icons.link,
|
||||
iconSize: 22),
|
||||
right: itemButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: curlRequest(request))).then((value) {
|
||||
FlutterToastr.show(localizations.copied, getContext());
|
||||
Navigator.maybePop(getContext());
|
||||
});
|
||||
},
|
||||
label: localizations.copyCurl,
|
||||
icon: Icons.code),
|
||||
),
|
||||
//repeat
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
onRepeat(request);
|
||||
Navigator.maybePop(getContext());
|
||||
},
|
||||
label: localizations.repeat,
|
||||
icon: Icons.repeat_one),
|
||||
right: itemButton(
|
||||
onPressed: () => showCustomRepeat(request),
|
||||
label: localizations.customRepeat,
|
||||
icon: Icons.repeat),
|
||||
),
|
||||
//favorite and edit
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
FavoriteStorage.addFavorite(widget.request);
|
||||
FlutterToastr.show(localizations.addSuccess, availableContext);
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.favorite,
|
||||
icon: Icons.favorite_outline),
|
||||
right: itemButton(
|
||||
onPressed: () async {
|
||||
await Navigator.maybePop(availableContext);
|
||||
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
MobileRequestEditor(request: widget.request, proxyServer: widget.proxyServer));
|
||||
Navigator.push(getContext(), pageRoute);
|
||||
},
|
||||
label: localizations.editRequest,
|
||||
icon: Icons.replay_outlined),
|
||||
),
|
||||
//script and rewrite
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () async {
|
||||
Navigator.maybePop(availableContext);
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
MobileRequestEditor(request: widget.request, proxyServer: widget.proxyServer));
|
||||
Navigator.push(getContext(), pageRoute);
|
||||
},
|
||||
label: localizations.editRequest,
|
||||
icon: Icons.replay_outlined),
|
||||
),
|
||||
//script and rewrite
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () async {
|
||||
Navigator.maybePop(availableContext);
|
||||
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
var url = request.domainPath;
|
||||
var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.urls.contains(url));
|
||||
String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);
|
||||
var scriptManager = await ScriptManager.instance;
|
||||
var url = request.domainPath;
|
||||
var scriptItem = scriptManager.list.firstWhereOrNull((it) => it.urls.contains(url));
|
||||
String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);
|
||||
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (context) => ScriptEdit(
|
||||
scriptItem: scriptItem,
|
||||
script: script,
|
||||
urls: scriptItem?.urls ?? [url],
|
||||
title: request.hostAndPort?.host));
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (context) => ScriptEdit(
|
||||
scriptItem: scriptItem,
|
||||
script: script,
|
||||
urls: scriptItem?.urls ?? [url],
|
||||
title: request.hostAndPort?.host));
|
||||
|
||||
Navigator.push(getContext(), pageRoute);
|
||||
},
|
||||
label: localizations.script,
|
||||
icon: Icons.javascript_outlined),
|
||||
right: itemButton(
|
||||
onPressed: () async {
|
||||
Navigator.maybePop(availableContext);
|
||||
bool isRequest = response == null;
|
||||
var requestRewrites = await RequestRewriteManager.instance;
|
||||
Navigator.push(getContext(), pageRoute);
|
||||
},
|
||||
label: localizations.script,
|
||||
icon: Icons.javascript_outlined),
|
||||
right: itemButton(
|
||||
onPressed: () async {
|
||||
Navigator.maybePop(availableContext);
|
||||
bool isRequest = response == null;
|
||||
var requestRewrites = await RequestRewriteManager.instance;
|
||||
|
||||
var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace;
|
||||
var rule = requestRewrites.getRequestRewriteRule(request, ruleType);
|
||||
var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace;
|
||||
var rule = requestRewrites.getRequestRewriteRule(request, ruleType);
|
||||
|
||||
var rewriteItems = await requestRewrites.getRewriteItems(rule);
|
||||
var rewriteItems = await requestRewrites.getRewriteItems(rule);
|
||||
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request));
|
||||
var context = availableContext;
|
||||
if (context.mounted) Navigator.push(context, pageRoute);
|
||||
},
|
||||
label: localizations.requestRewrite,
|
||||
icon: Icons.edit_outlined),
|
||||
),
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
highlightColor = Theme.of(availableContext).colorScheme.primary;
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.highlight,
|
||||
icon: Icons.highlight_outlined),
|
||||
right: itemButton(
|
||||
onPressed: () {
|
||||
AppConfiguration.current?.autoReadEnabled = !AppConfiguration.current!.autoReadEnabled;
|
||||
highlightColor = Colors.grey;
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.autoRead,
|
||||
icon: AppConfiguration.current?.autoReadEnabled == true
|
||||
? Icons.check_box_outlined
|
||||
: Icons.check_box_outline_blank_outlined),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
itemButton(
|
||||
onPressed: () {
|
||||
widget.onRemove?.call(request);
|
||||
FlutterToastr.show(localizations.deleteSuccess, availableContext);
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.delete,
|
||||
icon: Icons.delete_outline),
|
||||
SizedBox(width: 15),
|
||||
]),
|
||||
],
|
||||
var pageRoute = MaterialPageRoute(
|
||||
builder: (_) => RewriteRule(rule: rule, items: rewriteItems, request: request));
|
||||
var context = availableContext;
|
||||
if (context.mounted) Navigator.push(context, pageRoute);
|
||||
},
|
||||
label: localizations.requestRewrite,
|
||||
icon: Icons.edit_outlined),
|
||||
),
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
highlightColor = Theme.of(availableContext).colorScheme.primary;
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.highlight,
|
||||
icon: Icons.highlight_outlined),
|
||||
right: itemButton(
|
||||
onPressed: () {
|
||||
AppConfiguration.current?.autoReadEnabled = !AppConfiguration.current!.autoReadEnabled;
|
||||
highlightColor = Colors.grey;
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.autoRead,
|
||||
icon: AppConfiguration.current?.autoReadEnabled == true
|
||||
? Icons.check_box_outlined
|
||||
: Icons.check_box_outline_blank_outlined),
|
||||
),
|
||||
SizedBox(height: 1),
|
||||
menuItem(
|
||||
left: itemButton(
|
||||
onPressed: () {
|
||||
widget.selectionController.toggle(request.requestId);
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.select,
|
||||
icon: Icons.checklist_rtl_outlined),
|
||||
right: itemButton(
|
||||
onPressed: () {
|
||||
widget.onRemove?.call(request);
|
||||
FlutterToastr.show(localizations.deleteSuccess, availableContext);
|
||||
Navigator.maybePop(availableContext);
|
||||
},
|
||||
label: localizations.delete,
|
||||
icon: Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
)),
|
||||
]).then((value) {
|
||||
selected = false;
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:proxypin/l10n/app_localizations.dart';
|
||||
import 'package:proxypin/network/bin/server.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/selection_action_bar.dart';
|
||||
import 'package:proxypin/ui/component/utils.dart';
|
||||
import 'package:proxypin/ui/desktop/request/request.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request.dart';
|
||||
import 'package:proxypin/utils/har.dart';
|
||||
import 'package:proxypin/utils/keyword_highlight.dart';
|
||||
import 'package:proxypin/utils/listenable_list.dart';
|
||||
|
||||
import '../../../network/channel/host_port.dart' show ProxyInfo;
|
||||
import '../../../utils/lang.dart';
|
||||
import '../../component/model/search_model.dart';
|
||||
|
||||
///请求序列 列表
|
||||
@@ -17,6 +30,7 @@ class RequestSequence extends StatefulWidget {
|
||||
final bool displayDomain;
|
||||
final bool? sortDesc;
|
||||
final Function(List<HttpRequest>)? onRemove;
|
||||
final MultiSelectController selectionController;
|
||||
|
||||
const RequestSequence(
|
||||
{super.key,
|
||||
@@ -24,7 +38,8 @@ class RequestSequence extends StatefulWidget {
|
||||
required this.proxyServer,
|
||||
this.displayDomain = true,
|
||||
this.onRemove,
|
||||
this.sortDesc});
|
||||
this.sortDesc,
|
||||
required this.selectionController});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -35,6 +50,7 @@ class RequestSequence extends StatefulWidget {
|
||||
class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliveClientMixin {
|
||||
///请求id和对应的row的映射
|
||||
Map<String, GlobalKey<RequestRowState>> indexes = HashMap();
|
||||
late final MultiSelectListener<String> selectionListener;
|
||||
|
||||
///显示的请求列表 最新的在前面
|
||||
Queue<HttpRequest> view = Queue();
|
||||
@@ -48,11 +64,23 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
//关键词高亮监听
|
||||
late VoidCallback highlightListener;
|
||||
|
||||
MultiSelectController get selectionController => widget.selectionController;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
void initState() {
|
||||
super.initState();
|
||||
sortDesc = widget.sortDesc ?? true;
|
||||
view.addAll(widget.container.source.reversed);
|
||||
selectionListener = MultiSelectListener((items) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
widget.selectionController.selectedIds.addListener(selectionListener);
|
||||
highlightListener = () {
|
||||
//回调时机在高亮设置页面dispose之后。所以需要在下一帧刷新,否则会报错
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
@@ -64,6 +92,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.selectionController.selectedIds.removeListener(selectionListener);
|
||||
KeywordHighlights.removeListener(highlightListener);
|
||||
super.dispose();
|
||||
}
|
||||
@@ -103,6 +132,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
}
|
||||
|
||||
void clean() {
|
||||
widget.selectionController.clear();
|
||||
setState(() {
|
||||
view.clear();
|
||||
indexes.clear();
|
||||
@@ -119,6 +149,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
indexes.remove(requestId);
|
||||
}
|
||||
});
|
||||
selectionController.prune(view.map((request) => request.requestId));
|
||||
}
|
||||
|
||||
///过滤
|
||||
@@ -129,6 +160,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
} else {
|
||||
view = Queue.of(widget.container.where((it) => searchModel.filter(it, it.response)).toList().reversed);
|
||||
}
|
||||
selectionController.prune(view.map((request) => request.requestId));
|
||||
changeState();
|
||||
}
|
||||
|
||||
@@ -136,7 +168,37 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
return view;
|
||||
}
|
||||
|
||||
changeState() {
|
||||
void deleteSelected() {
|
||||
final selected = selectedRequests();
|
||||
if (selected.isEmpty) {
|
||||
return;
|
||||
}
|
||||
showConfirmDialog(context, content: '${localizations.delete} ${selected.length} ${localizations.request}?',
|
||||
onConfirm: () {
|
||||
final removedRequestIds = selected.map((request) => request.requestId).toSet();
|
||||
setState(() {
|
||||
view.removeWhere((request) => removedRequestIds.contains(request.requestId));
|
||||
indexes.removeWhere((requestId, _) => removedRequestIds.contains(requestId));
|
||||
selectionController.clear();
|
||||
widget.onRemove?.call(selected);
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
FlutterToastr.show(localizations.deleteSuccess, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<HttpRequest> selectedRequests() {
|
||||
final selectedIds = selectionController.selectedIds.toSet();
|
||||
if (selectedIds.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return view.where((request) => selectedIds.contains(request.requestId)).toList();
|
||||
}
|
||||
|
||||
void changeState() {
|
||||
//防止频繁刷新
|
||||
if (!changing) {
|
||||
changing = true;
|
||||
@@ -155,37 +217,57 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return Scrollbar(
|
||||
controller: PrimaryScrollController.maybeOf(context),
|
||||
child: ListView.separated(
|
||||
controller: PrimaryScrollController.maybeOf(context),
|
||||
cacheExtent: 1000,
|
||||
separatorBuilder: (context, index) =>
|
||||
Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor),
|
||||
itemCount: view.length,
|
||||
itemBuilder: (context, index) {
|
||||
final requestId = view.elementAt(index).requestId;
|
||||
return Obx(() {
|
||||
final selectionMode = selectionController.isSelectionMode;
|
||||
|
||||
final key = GlobalKey<RequestRowState>();
|
||||
indexes[requestId] = key;
|
||||
return Column(children: [
|
||||
if (selectionMode)
|
||||
SelectionActionBar(
|
||||
selectionController: selectionController,
|
||||
onRepeat: repeatSelected,
|
||||
onExport: exportSelected,
|
||||
onDelete: deleteSelected),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: PrimaryScrollController.maybeOf(context),
|
||||
child: ListView.separated(
|
||||
controller: PrimaryScrollController.maybeOf(context),
|
||||
cacheExtent: 1000,
|
||||
separatorBuilder: (context, index) =>
|
||||
Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor),
|
||||
itemCount: view.length,
|
||||
itemBuilder: (context, index) {
|
||||
final request = view.elementAt(index);
|
||||
final requestId = request.requestId;
|
||||
|
||||
return RequestRow(
|
||||
index: sortDesc ? view.length - index : index,
|
||||
key: key,
|
||||
request: view.elementAt(index),
|
||||
proxyServer: widget.proxyServer,
|
||||
displayDomain: widget.displayDomain,
|
||||
onRemove: (request) {
|
||||
setState(() {
|
||||
view.remove(request);
|
||||
indexes.remove(requestId);
|
||||
});
|
||||
widget.onRemove?.call([request]);
|
||||
});
|
||||
}));
|
||||
final key = GlobalKey<RequestRowState>();
|
||||
indexes[requestId] = key;
|
||||
|
||||
return RequestRow(
|
||||
index: sortDesc ? view.length - index : index,
|
||||
key: key,
|
||||
request: request,
|
||||
proxyServer: widget.proxyServer,
|
||||
displayDomain: widget.displayDomain,
|
||||
selectionController: selectionController,
|
||||
selectionHandlers: RequestSelectionHandlers(
|
||||
onDeleteSelected: deleteSelected,
|
||||
onExportSelected: exportSelected,
|
||||
onRepeatSelected: repeatSelected),
|
||||
onRemove: (item) {
|
||||
setState(() {
|
||||
view.remove(item);
|
||||
indexes.remove(requestId);
|
||||
});
|
||||
selectionController.remove(request.requestId);
|
||||
widget.onRemove?.call([item]);
|
||||
});
|
||||
})))
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
void scrollToTop() {
|
||||
PrimaryScrollController.maybeOf(context)
|
||||
?.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.ease);
|
||||
}
|
||||
@@ -201,4 +283,53 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
view = Queue.of(view.toList().reversed);
|
||||
});
|
||||
}
|
||||
|
||||
void exportSelected() {
|
||||
final selected = selectedRequests();
|
||||
if (selected.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_doExport('ProxyPin_selected_${DateTime.now().dateFormat()}.har', selected);
|
||||
}
|
||||
|
||||
void repeatSelected() {
|
||||
final selected = selectedRequests();
|
||||
if (selected.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_repeatRequests(selected);
|
||||
}
|
||||
|
||||
Future<void> _doExport(String fileName, List<HttpRequest> requests) async {
|
||||
var json = await Har.writeJson(requests, title: fileName);
|
||||
final path = await FilePicker.platform.saveFile(fileName: fileName, bytes: utf8.encode(json));
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
selectionController.clear();
|
||||
if (mounted) {
|
||||
FlutterToastr.show(localizations.exportSuccess, context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _repeatRequests(List<HttpRequest> requests) async {
|
||||
final proxyServer = widget.proxyServer;
|
||||
selectionController.clear();
|
||||
for (final request in requests) {
|
||||
final httpRequest = request.copy(uri: request.requestUrl);
|
||||
final 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user