mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-15 04:23:17 +08:00
This commit is contained in:
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -174,6 +174,8 @@ enum Operation {
|
||||
}
|
||||
|
||||
class MultiWindow {
|
||||
static Function(String widgetName, Map<String, dynamic>? args)? onOpenWindow;
|
||||
|
||||
/// 刷新请求重写
|
||||
static Future<void> invokeRefreshRewrite(Operation operation,
|
||||
{int? index, RequestRewriteRule? rule, List<RewriteItem>? items, bool? enabled}) async {
|
||||
@@ -188,6 +190,11 @@ class MultiWindow {
|
||||
|
||||
static Future<WindowController> openWindow(String title, String widgetName,
|
||||
{Size size = const Size(800, 680), Map<String, dynamic>? args}) async {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
onOpenWindow?.call(widgetName, args);
|
||||
return WindowController.fromWindowId(0); // Dummy controller
|
||||
}
|
||||
|
||||
var ratio = 1.0;
|
||||
if (Platform.isWindows) {
|
||||
ratio = WindowManager.instance.getDevicePixelRatio();
|
||||
|
||||
@@ -221,7 +221,7 @@ class _RequestBreakpointPageState extends State<RequestBreakpointPage> {
|
||||
child: Row(children: [
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(rule.name?.isNotEmpty == true ? rule.name! : rule.url,
|
||||
child: Text(rule.name ?? "",
|
||||
overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
SizedBox(
|
||||
|
||||
69
lib/ui/mobile/debug/breakpoint_executor.dart
Normal file
69
lib/ui/mobile/debug/breakpoint_executor.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:proxypin/network/bin/server.dart';
|
||||
import 'package:proxypin/network/http/http.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request_editor.dart';
|
||||
import 'package:proxypin/ui/mobile/request/request_editor_source.dart';
|
||||
|
||||
class BreakpointExecutor extends StatefulWidget {
|
||||
final HttpRequest request;
|
||||
final HttpResponse? response;
|
||||
final String requestId;
|
||||
|
||||
// false: intercept request, true: intercept response
|
||||
final bool isResponse;
|
||||
|
||||
const BreakpointExecutor({
|
||||
super.key,
|
||||
required this.request,
|
||||
this.response,
|
||||
required this.requestId,
|
||||
required this.isResponse,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BreakpointExecutor> createState() => _BreakpointExecutorState();
|
||||
}
|
||||
|
||||
class _BreakpointExecutorState extends State<BreakpointExecutor> {
|
||||
late HttpRequest request;
|
||||
late HttpResponse? response;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
request = widget.request;
|
||||
response = widget.response;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isResponse) {
|
||||
return _buildResponseBody();
|
||||
}
|
||||
|
||||
return MobileRequestEditor(
|
||||
request: request,
|
||||
proxyServer: ProxyServer.current,
|
||||
source: RequestEditorSource.breakpointRequest,
|
||||
onExecuteRequest: (newRequest) async {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context, newRequest);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResponseBody() {
|
||||
return MobileRequestEditor(
|
||||
request: request,
|
||||
response: response,
|
||||
proxyServer: ProxyServer.current,
|
||||
source: RequestEditorSource.breakpointResponse,
|
||||
onExecuteResponse: (newResponse) async {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context, newResponse);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import 'package:proxypin/ui/mobile/setting/request_rewrite.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/ui/mobile/setting/request_breakpoint.dart';
|
||||
|
||||
import '../../component/widgets.dart';
|
||||
import '../setting/proxy.dart';
|
||||
@@ -158,6 +159,12 @@ class _ConfigPageState extends State<ConfigPage> {
|
||||
leading: Icon(Icons.javascript_outlined, color: color),
|
||||
trailing: arrow,
|
||||
onTap: () => navigator(context, const MobileScript())),
|
||||
Divider(height: 0, thickness: 0.3, color: Theme.of(context).dividerColor.withValues(alpha: 0.22)),
|
||||
ListTile(
|
||||
title: Text(localizations.breakpoint),
|
||||
leading: Icon(Icons.bug_report_outlined, color: color),
|
||||
trailing: arrow,
|
||||
onTap: () => navigator(context, const MobileRequestBreakpointPage())),
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -50,6 +51,8 @@ import 'package:proxypin/utils/listenable_list.dart';
|
||||
import 'package:proxypin/utils/navigator.dart';
|
||||
|
||||
import '../app_update/app_update_repository.dart';
|
||||
import 'package:proxypin/ui/component/multi_window.dart';
|
||||
import 'package:proxypin/ui/mobile/debug/breakpoint_executor.dart';
|
||||
|
||||
///移动端首页
|
||||
///@author wanghongen
|
||||
@@ -124,6 +127,26 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
|
||||
} else if (Platform.isAndroid) {
|
||||
AppUpdateRepository.checkUpdate(context);
|
||||
}
|
||||
|
||||
// Handle breakpoint window on mobile
|
||||
MultiWindow.onOpenWindow = (widgetName, args) async {
|
||||
if (widgetName == 'BreakpointExecutor' && args != null) {
|
||||
if (!mounted) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BreakpointExecutor(
|
||||
requestId: args['requestId'],
|
||||
request: HttpRequest.fromJson(jsonDecode(jsonEncode(args['request']))),
|
||||
response: args['response'] == null
|
||||
? null
|
||||
: HttpResponse.fromJson(jsonDecode(jsonEncode(args['response']))),
|
||||
isResponse: args['type'] == 'response',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -30,14 +30,29 @@ import 'package:proxypin/ui/content/body.dart';
|
||||
import 'package:proxypin/utils/curl.dart';
|
||||
import 'package:proxypin/utils/lang.dart';
|
||||
|
||||
import 'package:proxypin/ui/mobile/request/request_editor_source.dart';
|
||||
|
||||
import '../../component/http_method_popup.dart';
|
||||
|
||||
|
||||
/// @author wanghongen
|
||||
class MobileRequestEditor extends StatefulWidget {
|
||||
final HttpRequest? request;
|
||||
final ProxyServer? proxyServer;
|
||||
final RequestEditorSource source;
|
||||
final Function(HttpRequest request)? onExecuteRequest;
|
||||
final Function(HttpResponse response)? onExecuteResponse;
|
||||
final HttpResponse? response;
|
||||
|
||||
const MobileRequestEditor({super.key, this.request, required this.proxyServer});
|
||||
const MobileRequestEditor({
|
||||
super.key,
|
||||
this.request,
|
||||
this.response,
|
||||
required this.proxyServer,
|
||||
this.source = RequestEditorSource.editor,
|
||||
this.onExecuteRequest,
|
||||
this.onExecuteResponse,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -79,6 +94,7 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
|
||||
tabController = TabController(length: tabs.length, vsync: this);
|
||||
request = widget.request;
|
||||
response = widget.response;
|
||||
if (widget.request == null) {
|
||||
curlParse();
|
||||
}
|
||||
@@ -134,6 +150,14 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
];
|
||||
}
|
||||
|
||||
var buttonText = localizations.send;
|
||||
IconData icon = Icons.send;
|
||||
if (widget.source == RequestEditorSource.breakpointRequest ||
|
||||
widget.source == RequestEditorSource.breakpointResponse) {
|
||||
buttonText = "Execute";
|
||||
icon = Icons.play_arrow;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.httpRequest, style: const TextStyle(fontSize: 16)),
|
||||
@@ -143,7 +167,16 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(localizations.cancel, style: Theme.of(context).textTheme.bodyMedium)),
|
||||
actions: [
|
||||
TextButton.icon(icon: const Icon(Icons.send), label: Text(localizations.send), onPressed: sendRequest)
|
||||
TextButton.icon(
|
||||
icon: Icon(icon),
|
||||
label: Text(buttonText),
|
||||
onPressed: () {
|
||||
if (widget.source == RequestEditorSource.editor) {
|
||||
sendRequest();
|
||||
} else {
|
||||
executeBreakpoint();
|
||||
}
|
||||
})
|
||||
],
|
||||
bottom: TabBar(controller: tabController, tabs: tabs)),
|
||||
body: GestureDetector(
|
||||
@@ -156,6 +189,7 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
message: request,
|
||||
key: requestKey,
|
||||
urlQueryNotifier: _queryNotifier,
|
||||
readOnly: widget.source == RequestEditorSource.breakpointResponse,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: responseChange,
|
||||
@@ -176,7 +210,7 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
style: TextStyle(
|
||||
color: response?.status.isSuccessful() == true ? Colors.blue : Colors.red))
|
||||
]),
|
||||
readOnly: true,
|
||||
readOnly: widget.source == RequestEditorSource.breakpointRequest,
|
||||
message: response);
|
||||
}),
|
||||
],
|
||||
@@ -215,6 +249,32 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
|
||||
tabController.animateTo(1);
|
||||
}
|
||||
|
||||
void executeBreakpoint() {
|
||||
if (widget.source == RequestEditorSource.breakpointRequest) {
|
||||
var currentState = requestLineKey.currentState!;
|
||||
var headers = requestKey.currentState?.getHeaders();
|
||||
var requestBody = requestKey.currentState?.getBody();
|
||||
String url = currentState.requestUrl.text;
|
||||
|
||||
HttpRequest newRequest = request!.copy(uri: url);
|
||||
newRequest.method = currentState.requestMethod;
|
||||
newRequest.headers.clear();
|
||||
newRequest.headers.addAll(headers);
|
||||
newRequest.body = requestBody == null ? null : utf8.encode(requestBody);
|
||||
widget.onExecuteRequest?.call(newRequest);
|
||||
} else if (widget.source == RequestEditorSource.breakpointResponse) {
|
||||
var headers = responseKey.currentState?.getHeaders();
|
||||
var responseBody = responseKey.currentState?.getBody();
|
||||
|
||||
if (response == null) return;
|
||||
HttpResponse newResponse = response!.copy();
|
||||
newResponse.headers.clear();
|
||||
newResponse.headers.addAll(headers);
|
||||
newResponse.body = responseBody == null ? null : utf8.encode(responseBody);
|
||||
widget.onExecuteResponse?.call(newResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef ParamCallback = void Function(String param);
|
||||
|
||||
6
lib/ui/mobile/request/request_editor_source.dart
Normal file
6
lib/ui/mobile/request/request_editor_source.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
enum RequestEditorSource {
|
||||
editor,
|
||||
breakpointRequest,
|
||||
breakpointResponse,
|
||||
}
|
||||
|
||||
464
lib/ui/mobile/setting/request_breakpoint.dart
Normal file
464
lib/ui/mobile/setting/request_breakpoint.dart
Normal file
@@ -0,0 +1,464 @@
|
||||
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:proxypin/l10n/app_localizations.dart';
|
||||
import 'package:proxypin/network/components/manager/request_breakpoint_manager.dart';
|
||||
import 'package:proxypin/network/http/http.dart';
|
||||
import 'package:proxypin/network/util/logger.dart';
|
||||
import 'package:proxypin/ui/component/widgets.dart';
|
||||
|
||||
import '../../component/http_method_popup.dart';
|
||||
|
||||
class MobileRequestBreakpointPage extends StatefulWidget {
|
||||
const MobileRequestBreakpointPage({super.key});
|
||||
|
||||
@override
|
||||
State<MobileRequestBreakpointPage> createState() => _RequestBreakpointPageState();
|
||||
}
|
||||
|
||||
class _RequestBreakpointPageState extends State<MobileRequestBreakpointPage> {
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
List<RequestBreakpointRule> rules = [];
|
||||
bool enabled = false;
|
||||
RequestBreakpointManager? manager;
|
||||
|
||||
bool selectionMode = false;
|
||||
final Set<int> selected = HashSet<int>();
|
||||
|
||||
Future<void> _save() async {
|
||||
await manager?.save();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
RequestBreakpointManager.instance.then((value) {
|
||||
manager = value;
|
||||
setState(() {
|
||||
enabled = value.enabled;
|
||||
rules = value.list;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isEN = Localizations.localeOf(context).languageCode == 'en';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).dialogTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.breakpoint, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
toolbarHeight: 36,
|
||||
centerTitle: true),
|
||||
body: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 15, right: 10),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
SizedBox(
|
||||
width: isEN ? 280 : 250,
|
||||
child: ListTile(
|
||||
title: Text("${localizations.enable} ${localizations.breakpoint}"),
|
||||
contentPadding: const EdgeInsets.only(left: 2),
|
||||
trailing: SwitchWidget(
|
||||
value: enabled,
|
||||
scale: 0.8,
|
||||
onChanged: (val) async {
|
||||
manager?.enabled = val;
|
||||
await _save();
|
||||
setState(() {
|
||||
enabled = val;
|
||||
});
|
||||
}))),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18), label: Text(localizations.add), onPressed: _editRule),
|
||||
])),
|
||||
const SizedBox(width: 15)
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(child: _buildList()),
|
||||
if (selectionMode) _buildSelectionFooter(),
|
||||
]))));
|
||||
}
|
||||
|
||||
Widget _buildList() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.grey.withValues(alpha: 0.2))),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5, bottom: 5),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [
|
||||
Container(width: 65, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)),
|
||||
SizedBox(width: 45, child: Text(localizations.enable, textAlign: TextAlign.center)),
|
||||
const VerticalDivider(width: 10),
|
||||
Expanded(child: Text("URL", textAlign: TextAlign.center)),
|
||||
SizedBox(width: 100, child: Text(localizations.breakpoint, textAlign: TextAlign.center)),
|
||||
]),
|
||||
),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: rules.length,
|
||||
itemBuilder: (context, index) => _buildRow(index),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(int index) {
|
||||
var primaryColor = Theme.of(context).colorScheme.primary;
|
||||
var rule = rules[index];
|
||||
|
||||
return InkWell(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
hoverColor: primaryColor.withValues(alpha: 0.3),
|
||||
onLongPress: () => _showRuleActions(index),
|
||||
onTap: () {
|
||||
if (selectionMode) {
|
||||
setState(() {
|
||||
if (!selected.add(index)) {
|
||||
selected.remove(index);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
_editRule(rule: rule);
|
||||
},
|
||||
child: Container(
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withValues(alpha: 0.5)
|
||||
: index.isEven
|
||||
? Colors.grey.withValues(alpha: 0.1)
|
||||
: null,
|
||||
height: 32,
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Row(children: [
|
||||
SizedBox(
|
||||
width: 65,
|
||||
child: Text(rule.name ?? "",
|
||||
overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 45,
|
||||
child: SwitchWidget(
|
||||
scale: 0.65,
|
||||
value: rule.enabled,
|
||||
onChanged: (val) async {
|
||||
rule.enabled = val;
|
||||
await _save();
|
||||
})),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(rule.url, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center)),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
"${rule.interceptRequest ? localizations.request : ""}${rule.interceptRequest && rule.interceptResponse ? "/" : ""}${rule.interceptResponse ? localizations.response : ""}",
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _export(RequestBreakpointManager? manager, {List<int>? indexes}) async {
|
||||
try {
|
||||
if (manager == null || manager.list.isEmpty) return;
|
||||
final rules = manager.list;
|
||||
final keys = (indexes == null || indexes.isEmpty)
|
||||
? List<int>.generate(rules.length, (i) => i)
|
||||
: (indexes.toList()..sort());
|
||||
final data = keys.map((i) => rules[i].toJson()).toList();
|
||||
var bytes = utf8.encode(jsonEncode(data));
|
||||
final path = await FilePicker.platform.saveFile(fileName: 'request_breakpoints.json', bytes: bytes);
|
||||
if (path == null) return;
|
||||
if (mounted) FlutterToastr.show(localizations.exportSuccess, context);
|
||||
} catch (e) {
|
||||
logger.e('导出失败', error: e);
|
||||
if (mounted) FlutterToastr.show('Export failed: $e', context);
|
||||
}
|
||||
}
|
||||
|
||||
Stack _buildSelectionFooter() {
|
||||
final l10n = localizations;
|
||||
return Stack(children: [
|
||||
Container(
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.grey.withValues(alpha: 0.2)))),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
onPressed: () {},
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
TextButton.icon(
|
||||
onPressed: selected.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
// export selected only
|
||||
final m = await RequestBreakpointManager.instance;
|
||||
await _export(m, indexes: selected.toList());
|
||||
setState(() {
|
||||
selected.clear();
|
||||
selectionMode = false;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.share, size: 18),
|
||||
label: Text(l10n.export, style: const TextStyle(fontSize: 14))),
|
||||
TextButton.icon(
|
||||
onPressed: selected.isEmpty ? null : () => _removeSelected(),
|
||||
icon: const Icon(Icons.delete, size: 18),
|
||||
label: Text(l10n.delete, style: const TextStyle(fontSize: 14))),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
selectionMode = false;
|
||||
selected.clear();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.cancel, size: 18),
|
||||
label: Text(l10n.cancel, style: const TextStyle(fontSize: 14))),
|
||||
]))))
|
||||
]);
|
||||
}
|
||||
|
||||
void _showRuleActions(int index) {
|
||||
final l10n = localizations;
|
||||
setState(() {
|
||||
selected.add(index);
|
||||
});
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),
|
||||
context: context,
|
||||
enableDrag: true,
|
||||
builder: (ctx) {
|
||||
return Wrap(children: [
|
||||
BottomSheetItem(
|
||||
text: l10n.multiple,
|
||||
onPressed: () {
|
||||
setState(() => selectionMode = true);
|
||||
}),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
BottomSheetItem(
|
||||
text: l10n.edit,
|
||||
onPressed: () {
|
||||
_editRule(rule: rules[index]);
|
||||
}),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
BottomSheetItem(text: l10n.export, onPressed: () => _export(manager, indexes: [index])),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
BottomSheetItem(
|
||||
text: rules[index].enabled ? l10n.disabled : l10n.enable,
|
||||
onPressed: () {
|
||||
rules[index].enabled = !rules[index].enabled;
|
||||
setState(() {});
|
||||
_save();
|
||||
}),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
BottomSheetItem(
|
||||
text: l10n.delete,
|
||||
onPressed: () {
|
||||
_removeRule(index);
|
||||
}),
|
||||
Container(color: Theme.of(ctx).hoverColor, height: 8),
|
||||
TextButton(
|
||||
child: Container(
|
||||
height: 45,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: Text(l10n.cancel, textAlign: TextAlign.center)),
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
}),
|
||||
]);
|
||||
}).then((value) {
|
||||
if (selectionMode) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
selected.remove(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _removeRule(int index) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(localizations.deleteHeaderConfirm, style: const TextStyle(fontSize: 18)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
rules.removeAt(index);
|
||||
});
|
||||
await _save();
|
||||
if (context.mounted) Navigator.pop(ctx);
|
||||
},
|
||||
child: Text(localizations.delete)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _removeSelected() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(localizations.deleteHeaderConfirm, style: const TextStyle(fontSize: 18)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(localizations.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var list = selected.toList();
|
||||
list.sort((a, b) => b.compareTo(a));
|
||||
for (var i in list) {
|
||||
rules.removeAt(i);
|
||||
}
|
||||
setState(() {
|
||||
selected.clear();
|
||||
selectionMode = false;
|
||||
});
|
||||
await _save();
|
||||
if (context.mounted) Navigator.pop(ctx);
|
||||
},
|
||||
child: Text(localizations.delete)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _editRule({RequestBreakpointRule? rule}) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MobileBreakpointRuleEditor(rule: rule),
|
||||
),
|
||||
).then((value) async {
|
||||
if (value != null && value is RequestBreakpointRule) {
|
||||
setState(() {
|
||||
if (rule == null) {
|
||||
rules.add(value);
|
||||
}
|
||||
});
|
||||
await _save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class MobileBreakpointRuleEditor extends StatefulWidget {
|
||||
final RequestBreakpointRule? rule;
|
||||
|
||||
const MobileBreakpointRuleEditor({super.key, this.rule});
|
||||
|
||||
@override
|
||||
State<MobileBreakpointRuleEditor> createState() => _MobileBreakpointRuleEditorState();
|
||||
}
|
||||
|
||||
class _MobileBreakpointRuleEditorState extends State<MobileBreakpointRuleEditor> {
|
||||
late RequestBreakpointRule rule;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
late TextEditingController nameInput;
|
||||
late TextEditingController urlInput;
|
||||
|
||||
// Local state for methods to avoid modifying rule in-place before save
|
||||
HttpMethod? _method;
|
||||
bool _interceptRequest = true;
|
||||
bool _interceptResponse = true;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
rule = widget.rule ?? RequestBreakpointRule(url: '');
|
||||
nameInput = TextEditingController(text: rule.name);
|
||||
urlInput = TextEditingController(text: rule.url);
|
||||
_method = rule.method;
|
||||
_interceptRequest = rule.interceptRequest;
|
||||
_interceptResponse = rule.interceptResponse;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.rule == null
|
||||
? "${localizations.add} ${localizations.breakpointRule}"
|
||||
: "${localizations.edit} ${localizations.breakpointRule}",
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) {
|
||||
return;
|
||||
}
|
||||
rule.name = nameInput.text;
|
||||
rule.url = urlInput.text;
|
||||
rule.method = _method;
|
||||
rule.interceptRequest = _interceptRequest;
|
||||
rule.interceptResponse = _interceptResponse;
|
||||
rule.enabled = true;
|
||||
Navigator.pop(context, rule);
|
||||
},
|
||||
child: Text(localizations.save))
|
||||
]),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(children: [
|
||||
TextFormField(
|
||||
controller: nameInput,
|
||||
decoration: InputDecoration(labelText: localizations.name, border: const OutlineInputBorder()),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
TextFormField(
|
||||
controller: urlInput,
|
||||
validator: (val) => val?.isNotEmpty == true ? null : localizations.cannotBeEmpty,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'URL',
|
||||
hintText: 'https://www.example.com/api/*',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
child: MethodPopupMenu(value: _method, onChanged: (val) => setState(() => _method = val)))),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
SwitchListTile(
|
||||
title: Text(localizations.request),
|
||||
value: _interceptRequest,
|
||||
onChanged: (val) => setState(() => _interceptRequest = val)),
|
||||
SwitchListTile(
|
||||
title: Text(localizations.response),
|
||||
value: _interceptResponse,
|
||||
onChanged: (val) => setState(() => _interceptResponse = val)),
|
||||
]))));
|
||||
}
|
||||
}
|
||||
@@ -264,11 +264,9 @@ class _MobileRequestCryptoPageState extends State<MobileRequestCryptoPage> {
|
||||
setState(() => selectionMode = true);
|
||||
}),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit_outlined),
|
||||
title: Text(l10n.edit),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
BottomSheetItem(
|
||||
text: l10n.edit,
|
||||
onPressed: () {
|
||||
_editRule(manager, index);
|
||||
}),
|
||||
const Divider(thickness: 0.5, height: 5),
|
||||
@@ -286,7 +284,6 @@ class _MobileRequestCryptoPageState extends State<MobileRequestCryptoPage> {
|
||||
BottomSheetItem(
|
||||
text: l10n.delete,
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
_removeRule(manager, index);
|
||||
}),
|
||||
Container(color: Theme.of(ctx).hoverColor, height: 8),
|
||||
@@ -367,9 +364,9 @@ class _MobileRequestCryptoPageState extends State<MobileRequestCryptoPage> {
|
||||
? List<int>.generate(manager.rules.length, (i) => i)
|
||||
: (indexes.toList()..sort());
|
||||
final data = keys.map((i) => manager.rules[i].toJson()).toList();
|
||||
final path = await FilePicker.platform.saveFile(fileName: 'request_crypto.json');
|
||||
var bytes = utf8.encode(jsonEncode(data));
|
||||
final path = await FilePicker.platform.saveFile(fileName: 'request_crypto.json', bytes: bytes);
|
||||
if (path == null) return;
|
||||
await File(path).writeAsString(jsonEncode(data));
|
||||
if (mounted) FlutterToastr.show(localizations.exportSuccess, context);
|
||||
} catch (e) {
|
||||
logger.e('导出失败', error: e);
|
||||
|
||||
Reference in New Issue
Block a user