From 258c21721e1da2843d26aef3c2e3a23553fa5f6e Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Sat, 25 Oct 2025 02:18:57 +0800 Subject: [PATCH] Mobile support report server --- ios/Runner.xcodeproj/project.pbxproj | 10 +- lib/l10n/app_en.arb | 6 + lib/l10n/app_localizations.dart | 36 +++ lib/l10n/app_localizations_en.dart | 18 ++ lib/l10n/app_localizations_zh.dart | 36 +++ lib/l10n/app_zh.arb | 6 + lib/l10n/app_zh_Hant.arb | 6 + .../manager/report_server_manager.dart | 7 - lib/ui/desktop/request/report_servers.dart | 5 +- lib/ui/mobile/menu/menu.dart | 12 + lib/ui/mobile/mobile.dart | 2 +- lib/ui/mobile/setting/report_servers.dart | 273 ++++++++++++++++++ 12 files changed, 398 insertions(+), 19 deletions(-) create mode 100644 lib/ui/mobile/setting/report_servers.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5fe4f0e..d14488e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -524,14 +524,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -583,14 +579,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5e5c62f..b2f3d74 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -105,6 +105,12 @@ "matchRule": "Match Rule", "emptyMatchAll": "Empty means match all", "newBuilt": "New", + "reportServers": "Report Servers", + "addReportServer": "Add Report Server", + "editReportServer": "Edit Report Server", + "serverUrl": "Server URL", + "compression": "Compression", + "compressionNone": "None", "newFolder": "New Folder", "enableSelect": "Enable Select", "disableSelect": "Disable Select", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7718f5b..8d7f3a3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -708,6 +708,42 @@ abstract class AppLocalizations { /// **'New'** String get newBuilt; + /// No description provided for @reportServers. + /// + /// In en, this message translates to: + /// **'Report Servers'** + String get reportServers; + + /// No description provided for @addReportServer. + /// + /// In en, this message translates to: + /// **'Add Report Server'** + String get addReportServer; + + /// No description provided for @editReportServer. + /// + /// In en, this message translates to: + /// **'Edit Report Server'** + String get editReportServer; + + /// No description provided for @serverUrl. + /// + /// In en, this message translates to: + /// **'Server URL'** + String get serverUrl; + + /// No description provided for @compression. + /// + /// In en, this message translates to: + /// **'Compression'** + String get compression; + + /// No description provided for @compressionNone. + /// + /// In en, this message translates to: + /// **'None'** + String get compressionNone; + /// No description provided for @newFolder. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6926436..b21f951 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -316,6 +316,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get newBuilt => 'New'; + @override + String get reportServers => 'Report Servers'; + + @override + String get addReportServer => 'Add Report Server'; + + @override + String get editReportServer => 'Edit Report Server'; + + @override + String get serverUrl => 'Server URL'; + + @override + String get compression => 'Compression'; + + @override + String get compressionNone => 'None'; + @override String get newFolder => 'New Folder'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index a76f329..aa4025f 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -316,6 +316,24 @@ class AppLocalizationsZh extends AppLocalizations { @override String get newBuilt => '新建'; + @override + String get reportServers => '上报服务器'; + + @override + String get addReportServer => '新增上报服务器'; + + @override + String get editReportServer => '编辑上报服务器'; + + @override + String get serverUrl => '服务器 URL'; + + @override + String get compression => '压缩'; + + @override + String get compressionNone => '无'; + @override String get newFolder => '新建文件夹'; @@ -1310,6 +1328,24 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get newBuilt => '新建'; + @override + String get reportServers => '上報伺服器'; + + @override + String get addReportServer => '新增上報伺服器'; + + @override + String get editReportServer => '編輯上報伺服器'; + + @override + String get serverUrl => '伺服器 URL'; + + @override + String get compression => '壓縮'; + + @override + String get compressionNone => '無'; + @override String get newFolder => '新建資料夾'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index ee72325..7906568 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -105,6 +105,12 @@ "matchRule": "匹配规则", "emptyMatchAll": "为空表示匹配全部", "newBuilt": "新建", + "reportServers": "上报服务器", + "addReportServer": "新增上报服务器", + "editReportServer": "编辑上报服务器", + "serverUrl": "服务器 URL", + "compression": "压缩", + "compressionNone": "无", "newFolder": "新建文件夹", "enableSelect": "启用选择", "disableSelect": "禁用选择", diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 6ab6c35..5433d51 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -103,6 +103,12 @@ "matchRule": "符合規則", "emptyMatchAll": "為空表示符合全部", "newBuilt": "新建", + "reportServers": "上報伺服器", + "addReportServer": "新增上報伺服器", + "editReportServer": "編輯上報伺服器", + "serverUrl": "伺服器 URL", + "compression": "壓縮", + "compressionNone": "無", "newFolder": "新建資料夾", "enableSelect": "啟用選擇", "disableSelect": "停用選擇", diff --git a/lib/network/components/manager/report_server_manager.dart b/lib/network/components/manager/report_server_manager.dart index d927c41..d4c032d 100644 --- a/lib/network/components/manager/report_server_manager.dart +++ b/lib/network/components/manager/report_server_manager.dart @@ -96,9 +96,6 @@ class ReportServer { /// 压缩方式:none/gzip,默认 none final String? compression; - /// 额外请求头(可选) - final Map? headers; - RegExp _urlReg; ReportServer({ @@ -107,7 +104,6 @@ class ReportServer { required this.serverUrl, this.enabled = true, this.compression, - this.headers, }) : _urlReg = RegExp(matchUrl.replaceAll("*", ".*").replaceFirst('?', '\\?')); bool match(String url) { @@ -136,19 +132,16 @@ class ReportServer { serverUrl: serverUrl ?? this.serverUrl, enabled: enabled ?? this.enabled, compression: compression ?? this.compression, - headers: headers ?? this.headers, ); } factory ReportServer.fromJson(Map json) { - final headers = json['headers']; return ReportServer( name: json['name'] ?? '', matchUrl: json['matchUrl'] ?? '', serverUrl: json['serverUrl'] ?? '', enabled: json['enabled'] ?? true, compression: (json['compression'] ?? 'none') as String, - headers: headers == null ? null : Map.from(headers as Map), ); } diff --git a/lib/ui/desktop/request/report_servers.dart b/lib/ui/desktop/request/report_servers.dart index e84e5fc..7f134ba 100644 --- a/lib/ui/desktop/request/report_servers.dart +++ b/lib/ui/desktop/request/report_servers.dart @@ -77,7 +77,7 @@ class _ReportServersPageState extends State { Widget labeled(String label, Widget field, {bool expanded = true}) => Row( children: [ - SizedBox(width: 85, child: Text(label)), + SizedBox(width: AppLocalizations.of(context)!.localeName == 'en' ? 95 : 85, child: Text(label)), const SizedBox(width: 12), expanded ? Expanded(child: field) : field, ], @@ -158,7 +158,8 @@ class _ReportServersPageState extends State { FilledButton( onPressed: () { if (!(formKey.currentState as FormState).validate()) { - FlutterToastr.show("${localizations.serverUrl} ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top); + FlutterToastr.show("${localizations.serverUrl} ${localizations.cannotBeEmpty}", context, + position: FlutterToastr.top); return; } diff --git a/lib/ui/mobile/menu/menu.dart b/lib/ui/mobile/menu/menu.dart index 53fad98..2aacb67 100644 --- a/lib/ui/mobile/menu/menu.dart +++ b/lib/ui/mobile/menu/menu.dart @@ -21,6 +21,7 @@ import 'package:proxypin/l10n/app_localizations.dart'; import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/ui/mobile/mobile.dart'; import 'package:proxypin/ui/mobile/setting/app_filter.dart'; +import 'package:proxypin/ui/mobile/setting/report_servers.dart'; import 'package:proxypin/ui/mobile/setting/ssl.dart'; import 'package:proxypin/ui/mobile/widgets/highlight.dart'; import 'package:proxypin/ui/mobile/widgets/remote_device.dart'; @@ -77,6 +78,17 @@ class MoreMenu extends StatelessWidget { navigator(context, RemoteDevicePage(proxyServer: proxyServer, remoteDevice: remoteDevice)); }, )), + PopupMenuItem( + height: 32, + child: ListTile( + dense: true, + leading: const Icon(Icons.cloud_upload_outlined), + title: Text(localizations.reportServers), + onTap: () { + Navigator.maybePop(context); + navigator(context, const ReportServersPageMobile()); + }, + )), const PopupMenuDivider(height: 0), PopupMenuItem( height: 32, diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index b49ad15..8ae3e51 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -443,7 +443,7 @@ class RequestPageState extends State { } /// 检查远程连接 - checkConnectTask(BuildContext context) async { + Future checkConnectTask(BuildContext context) async { int retry = 0; Timer.periodic(const Duration(milliseconds: 15000), (timer) async { if (remoteDevice.value.connect == false) { diff --git a/lib/ui/mobile/setting/report_servers.dart b/lib/ui/mobile/setting/report_servers.dart new file mode 100644 index 0000000..0fa4f44 --- /dev/null +++ b/lib/ui/mobile/setting/report_servers.dart @@ -0,0 +1,273 @@ +/* + * Mobile report servers page + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:proxypin/network/components/manager/report_server_manager.dart'; +import 'package:proxypin/ui/component/widgets.dart'; +import 'package:proxypin/ui/component/utils.dart'; +import '../../../l10n/app_localizations.dart'; + +class ReportServersPageMobile extends StatefulWidget { + const ReportServersPageMobile({super.key}); + + @override + State createState() => _ReportServersPageMobileState(); +} + +class _ReportServersPageMobileState extends State { + List _servers = []; + bool _loading = true; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final manager = await ReportServerManager.instance; + setState(() { + _servers = List.of(manager.servers); + _loading = false; + }); + } + + Future _showServerDialog({ReportServer? initial}) async { + // Push the edit page and return the created/edited ReportServer + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => ReportServerEditPageMobile(initial: initial), + ), + ); + + return result; + } + + Future _addServer() async { + final server = await _showServerDialog(); + if (server != null) { + final manager = await ReportServerManager.instance; + await manager.add(server); + await _load(); + } + } + + Future _editServer(int index) async { + final initial = _servers[index]; + final server = await _showServerDialog(initial: initial); + if (server != null) { + final manager = await ReportServerManager.instance; + await manager.update(index, server); + await _load(); + } + } + + Future _confirmDelete(int index) async { + showConfirmDialog(context, onConfirm: () async { + final manager = await ReportServerManager.instance; + await manager.removeAt(index); + await _load(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(localizations.reportServers, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + centerTitle: true, + actions: [ + TextButton.icon( + label: Text(localizations.add), + onPressed: _addServer, + icon: const Icon(Icons.add), + ), + ], + ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _servers.isEmpty + ? Center(child: Text(localizations.emptyData)) + : ListView.separated( + itemCount: _servers.length, + separatorBuilder: (_, __) => const Divider(height: 0, thickness: 0.3), + itemBuilder: (ctx, idx) { + final s = _servers[idx]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + leading: SizedBox( + width: 32, + child: Checkbox( + value: s.enabled, + onChanged: (v) async { + final manager = await ReportServerManager.instance; + await manager.toggleEnabled(idx, v == true); + await _load(); + })), + title: Text(s.name.isEmpty ? '-' : s.name), + subtitle: Text(s.serverUrl), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // IconButton( + // onPressed: () => _editServer(idx), icon: const Icon(Icons.edit_outlined, size: 23)), + IconButton( + onPressed: () => _confirmDelete(idx), icon: const Icon(Icons.delete_outline, size: 23)), + ], + ), + onTap: () => _editServer(idx), + ); + }, + ), + ); + } +} + +// A standalone page for adding / editing a ReportServer on mobile. +class ReportServerEditPageMobile extends StatefulWidget { + final ReportServer? initial; + + const ReportServerEditPageMobile({super.key, this.initial}); + + @override + State createState() => _ReportServerEditPageMobileState(); +} + +class _ReportServerEditPageMobileState extends State { + late TextEditingController _nameCtrl; + late TextEditingController _matchUrlCtrl; + late TextEditingController _serverUrlCtrl; + String _compression = 'none'; + bool _enabled = true; + + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + final init = widget.initial; + _nameCtrl = TextEditingController(text: init?.name ?? ''); + _matchUrlCtrl = TextEditingController(text: init?.matchUrl ?? ''); + _serverUrlCtrl = TextEditingController(text: init?.serverUrl ?? ''); + _compression = init?.compression ?? 'none'; + _enabled = init?.enabled ?? true; + } + + InputDecoration dec({String? hint}) => InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + focusedBorder: + OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)), + isDense: true, + border: const OutlineInputBorder(), + ); + + Widget labeled(String label, Widget field, {bool expanded = true}) => Row( + children: [ + SizedBox(width: AppLocalizations.of(context)!.localeName == 'en' ? 95 : 85, child: Text(label)), + const SizedBox(width: 12), + expanded ? Expanded(child: field) : field, + ], + ); + + void _onSave() { + if (!(_formKey.currentState as FormState).validate()) { + FlutterToastr.show( + "${AppLocalizations.of(context)!.serverUrl} ${AppLocalizations.of(context)!.cannotBeEmpty}", context, + position: FlutterToastr.top); + return; + } + + var serverUrl = _serverUrlCtrl.text.trim(); + if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) { + serverUrl = 'http://$serverUrl'; + } + + final server = ReportServer( + name: _nameCtrl.text.trim(), + matchUrl: _matchUrlCtrl.text.trim(), + serverUrl: serverUrl, + enabled: _enabled, + compression: _compression, + ); + + Navigator.of(context).pop(server); + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + title: Text(widget.initial == null ? localizations.addReportServer : localizations.editReportServer), + centerTitle: true, + actions: [ + TextButton(onPressed: _onSave, child: Text(localizations.save)), + ], + ), + body: Padding( + padding: const EdgeInsets.all(12.0), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + labeled('${localizations.name}: ', + TextField(controller: _nameCtrl, decoration: dec(hint: localizations.pleaseEnter))), + const SizedBox(height: 12), + labeled( + '${localizations.match} URL: ', + TextFormField( + controller: _matchUrlCtrl, + keyboardType: TextInputType.url, + validator: (v) => v?.isNotEmpty == true ? null : "", + decoration: dec(hint: 'https://example.com/api/*')), + ), + const SizedBox(height: 12), + labeled( + '${localizations.serverUrl}: ', + TextFormField( + controller: _serverUrlCtrl, + keyboardType: TextInputType.url, + validator: (v) => v?.isNotEmpty == true ? null : "", + decoration: dec(hint: 'http://example.com/report')), + ), + const SizedBox(height: 12), + labeled( + '${localizations.compression}: ', + expanded: false, + SizedBox( + width: 120, + child: DropdownButtonFormField( + initialValue: _compression, + decoration: dec(), + items: [ + DropdownMenuItem(value: 'none', child: Text(localizations.compressionNone)), + DropdownMenuItem(value: 'gzip', child: Text('GZIP')), + ], + onChanged: (v) => setState(() => _compression = v ?? 'none'), + ), + ), + ), + const SizedBox(height: 12), + labeled( + '${localizations.enable}: ', + Align( + alignment: Alignment.centerLeft, + child: SwitchWidget(value: _enabled, scale: 0.9, onChanged: (v) => setState(() => _enabled = v))), + ), + ], + ), + ), + ), + ), + ); + } +}