diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 1dc6cf7..391a902 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -20,7 +20,5 @@
????
CFBundleVersion
1.0
- MinimumOSVersion
- 13.0
diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart
index 8ce421c..df60ef8 100644
--- a/lib/ui/component/multi_window.dart
+++ b/lib/ui/component/multi_window.dart
@@ -174,6 +174,8 @@ enum Operation {
}
class MultiWindow {
+ static Function(String widgetName, Map? args)? onOpenWindow;
+
/// 刷新请求重写
static Future invokeRefreshRewrite(Operation operation,
{int? index, RequestRewriteRule? rule, List? items, bool? enabled}) async {
@@ -188,6 +190,11 @@ class MultiWindow {
static Future openWindow(String title, String widgetName,
{Size size = const Size(800, 680), Map? 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();
diff --git a/lib/ui/desktop/setting/request_breakpoint.dart b/lib/ui/desktop/setting/request_breakpoint.dart
index a9b93ff..9ce9f50 100644
--- a/lib/ui/desktop/setting/request_breakpoint.dart
+++ b/lib/ui/desktop/setting/request_breakpoint.dart
@@ -221,7 +221,7 @@ class _RequestBreakpointPageState extends State {
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(
diff --git a/lib/ui/mobile/debug/breakpoint_executor.dart b/lib/ui/mobile/debug/breakpoint_executor.dart
new file mode 100644
index 0000000..b5ba289
--- /dev/null
+++ b/lib/ui/mobile/debug/breakpoint_executor.dart
@@ -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 createState() => _BreakpointExecutorState();
+}
+
+class _BreakpointExecutorState extends State {
+ 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);
+ }
+ },
+ );
+ }
+}
diff --git a/lib/ui/mobile/menu/bottom_navigation.dart b/lib/ui/mobile/menu/bottom_navigation.dart
index 75261dc..6e16eb6 100644
--- a/lib/ui/mobile/menu/bottom_navigation.dart
+++ b/lib/ui/mobile/menu/bottom_navigation.dart
@@ -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 {
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)
],
diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart
index b1a5818..90b3084 100644
--- a/lib/ui/mobile/mobile.dart
+++ b/lib/ui/mobile/mobile.dart
@@ -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 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
diff --git a/lib/ui/mobile/request/request_editor.dart b/lib/ui/mobile/request/request_editor.dart
index 6fdef36..7f43cca 100644
--- a/lib/ui/mobile/request/request_editor.dart
+++ b/lib/ui/mobile/request/request_editor.dart
@@ -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 createState() {
@@ -79,6 +94,7 @@ class RequestEditorState extends State 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 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 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 with SingleTickerPro
message: request,
key: requestKey,
urlQueryNotifier: _queryNotifier,
+ readOnly: widget.source == RequestEditorSource.breakpointResponse,
),
ValueListenableBuilder(
valueListenable: responseChange,
@@ -176,7 +210,7 @@ class RequestEditorState extends State 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 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);
diff --git a/lib/ui/mobile/request/request_editor_source.dart b/lib/ui/mobile/request/request_editor_source.dart
new file mode 100644
index 0000000..5183288
--- /dev/null
+++ b/lib/ui/mobile/request/request_editor_source.dart
@@ -0,0 +1,6 @@
+enum RequestEditorSource {
+ editor,
+ breakpointRequest,
+ breakpointResponse,
+}
+
diff --git a/lib/ui/mobile/setting/request_breakpoint.dart b/lib/ui/mobile/setting/request_breakpoint.dart
new file mode 100644
index 0000000..7dc0f9c
--- /dev/null
+++ b/lib/ui/mobile/setting/request_breakpoint.dart
@@ -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 createState() => _RequestBreakpointPageState();
+}
+
+class _RequestBreakpointPageState extends State {
+ AppLocalizations get localizations => AppLocalizations.of(context)!;
+ List rules = [];
+ bool enabled = false;
+ RequestBreakpointManager? manager;
+
+ bool selectionMode = false;
+ final Set selected = HashSet();
+
+ Future _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 _export(RequestBreakpointManager? manager, {List? indexes}) async {
+ try {
+ if (manager == null || manager.list.isEmpty) return;
+ final rules = manager.list;
+ final keys = (indexes == null || indexes.isEmpty)
+ ? List.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 _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 _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 createState() => _MobileBreakpointRuleEditorState();
+}
+
+class _MobileBreakpointRuleEditorState extends State {
+ late RequestBreakpointRule rule;
+ final _formKey = GlobalKey();
+
+ 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)),
+ ]))));
+ }
+}
diff --git a/lib/ui/mobile/setting/request_crypto.dart b/lib/ui/mobile/setting/request_crypto.dart
index f65148b..6329bf7 100644
--- a/lib/ui/mobile/setting/request_crypto.dart
+++ b/lib/ui/mobile/setting/request_crypto.dart
@@ -264,11 +264,9 @@ class _MobileRequestCryptoPageState extends State {
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 {
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 {
? List.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);