From 905d8932bd93054a89db252e906c61d8e2631d9f Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Wed, 25 Feb 2026 16:41:12 +0800 Subject: [PATCH] Add breakpoint export and import (#669)(#660)(#386) --- lib/l10n/app_en.arb | 1 + lib/l10n/app_localizations.dart | 6 ++ lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_zh.arb | 1 + .../desktop/setting/request_breakpoint.dart | 81 ++++++++++++++++++- lib/ui/mobile/setting/request_breakpoint.dart | 40 ++++++++- 7 files changed, 131 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0e1c0e3..ab5b441 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -62,6 +62,7 @@ "importFailed": "Import failed", "export": "Export", "exportSuccess": "Export successful", + "exportFailed": "Export failed", "deleteSuccess": "Delete successful", "send": "Send", "fail": "fail", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 588c079..777b2ea 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -456,6 +456,12 @@ abstract class AppLocalizations { /// **'Export successful'** String get exportSuccess; + /// No description provided for @exportFailed. + /// + /// In en, this message translates to: + /// **'Export failed'** + String get exportFailed; + /// No description provided for @deleteSuccess. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 8088987..022a4d9 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -188,6 +188,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get exportSuccess => 'Export successful'; + @override + String get exportFailed => 'Export failed'; + @override String get deleteSuccess => 'Delete successful'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 5fdf125..4612a28 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -188,6 +188,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get exportSuccess => '导出成功'; + @override + String get exportFailed => '导出失败'; + @override String get deleteSuccess => '删除成功'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b6f70fe..9b25072 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -63,6 +63,7 @@ "importFailed": "导入失败", "export": "导出", "exportSuccess": "导出成功", + "exportFailed": "导出失败", "deleteSuccess": "删除成功", "send": "发送", "fail": "失败", diff --git a/lib/ui/desktop/setting/request_breakpoint.dart b/lib/ui/desktop/setting/request_breakpoint.dart index 9ce9f50..b41ecf8 100644 --- a/lib/ui/desktop/setting/request_breakpoint.dart +++ b/lib/ui/desktop/setting/request_breakpoint.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -41,6 +45,60 @@ class _RequestBreakpointPageState extends State { await _refreshConfig(); } + Future _import() async { + + String? path; + if (Platform.isMacOS) { + path = await DesktopMultiWindow.invokeMethod(0, "pickFiles", { + "allowedExtensions": ['json'] + }); + if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show(); + } else { + FilePickerResult? result = + await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']); + path = result?.files.single.path; + } + if (path == null) return; + File file = File(path); + try { + String content = await file.readAsString(); + List list = jsonDecode(content); + var rules = list.map((e) => RequestBreakpointRule.fromJson(e)).toList(); + for (var rule in rules) { + manager?.list.add(rule); + } + await _save(); + setState(() { + this.rules = manager!.list; + }); + + if (mounted) CustomToast.success(localizations.importSuccess).show(context); + } catch (e) { + if (mounted) CustomToast.error(localizations.importFailed).show(context); + } + } + + Future _export(List exportRules) async { + if (exportRules.isEmpty) return; + + String? outputFile; + if (Platform.isMacOS) { + outputFile = await DesktopMultiWindow.invokeMethod(0, "saveFile", {"fileName": 'request_breakpoint_rules.json'}); + if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show(); + } else { + outputFile = await FilePicker.platform.saveFile(fileName: 'request_breakpoint_rules.json'); + } + if (outputFile == null) return; + File file = File(outputFile); + try { + var json = exportRules.map((e) => e.toJson()).toList(); + await file.writeAsString(jsonEncode(json)); + if (mounted) CustomToast.success(localizations.exportSuccess).show(context); + } catch (e) { + if (mounted) CustomToast.error(localizations.exportFailed).show(context); + } + } + @override void initState() { super.initState(); @@ -114,7 +172,12 @@ class _RequestBreakpointPageState extends State { Expanded( child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( - icon: const Icon(Icons.add, size: 18), label: Text(localizations.add), onPressed: _editRule) + icon: const Icon(Icons.add, size: 18), label: Text(localizations.add), onPressed: _editRule), + const SizedBox(width: 5), + TextButton.icon( + icon: const Icon(Icons.input_rounded, size: 18), + onPressed: _import, + label: Text(localizations.import)), ])), const SizedBox(width: 15) ]), @@ -266,6 +329,22 @@ class _RequestBreakpointPageState extends State { } }, ), + PopupMenuItem( + height: 32, + child: Text(localizations.export), + onTap: () async { + if (selected.isEmpty) return; + var list = selected.toList(); + List exportRules = []; + for (var i in list) { + exportRules.add(rules[i]); + } + await _export(exportRules); + setState(() { + selected.clear(); + }); + }, + ), PopupMenuItem( height: 32, child: Text(localizations.delete), diff --git a/lib/ui/mobile/setting/request_breakpoint.dart b/lib/ui/mobile/setting/request_breakpoint.dart index 7dc0f9c..d63bb6a 100644 --- a/lib/ui/mobile/setting/request_breakpoint.dart +++ b/lib/ui/mobile/setting/request_breakpoint.dart @@ -1,5 +1,6 @@ import 'dart:collection'; import 'dart:convert'; +import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -65,7 +66,7 @@ class _RequestBreakpointPageState extends State { child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ SizedBox( - width: isEN ? 280 : 250, + width: isEN ? 230 : 160, child: ListTile( title: Text("${localizations.enable} ${localizations.breakpoint}"), contentPadding: const EdgeInsets.only(left: 2), @@ -82,8 +83,15 @@ class _RequestBreakpointPageState extends State { 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), + IconButton( + icon: Icon(Icons.add, size: 22, color: Theme.of(context).colorScheme.primary), + onPressed: _editRule, + tooltip: localizations.add), + const SizedBox(width: 5), + IconButton( + icon: Icon(Icons.input_rounded, size: 22, color: Theme.of(context).colorScheme.primary), + onPressed: _import, + tooltip: localizations.import), ])), const SizedBox(width: 15) ]), @@ -195,6 +203,32 @@ class _RequestBreakpointPageState extends State { } } + Future _import() async { + try { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + if (result == null || result.files.isEmpty) return; + File file = File(result.files.single.path!); + String content = await file.readAsString(); + List list = jsonDecode(content); + var newRules = list.map((e) => RequestBreakpointRule.fromJson(e)).toList(); + for (var rule in newRules) { + manager?.list.add(rule); + } + await _save(); + setState(() { + rules = manager!.list; + }); + + if (mounted) FlutterToastr.show(localizations.importSuccess, context); + } catch (e) { + logger.e('Import failed', error: e); + if (mounted) FlutterToastr.show(localizations.importFailed, context); + } + } + Stack _buildSelectionFooter() { final l10n = localizations; return Stack(children: [