Mobile support report server

This commit is contained in:
wanghongenpin
2025-10-25 02:18:57 +08:00
parent aa49bb6226
commit 258c21721e
12 changed files with 398 additions and 19 deletions

View File

@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 55; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -524,14 +524,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -583,14 +579,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";

View File

@@ -105,6 +105,12 @@
"matchRule": "Match Rule", "matchRule": "Match Rule",
"emptyMatchAll": "Empty means match all", "emptyMatchAll": "Empty means match all",
"newBuilt": "New", "newBuilt": "New",
"reportServers": "Report Servers",
"addReportServer": "Add Report Server",
"editReportServer": "Edit Report Server",
"serverUrl": "Server URL",
"compression": "Compression",
"compressionNone": "None",
"newFolder": "New Folder", "newFolder": "New Folder",
"enableSelect": "Enable Select", "enableSelect": "Enable Select",
"disableSelect": "Disable Select", "disableSelect": "Disable Select",

View File

@@ -708,6 +708,42 @@ abstract class AppLocalizations {
/// **'New'** /// **'New'**
String get newBuilt; 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. /// No description provided for @newFolder.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -316,6 +316,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get newBuilt => 'New'; 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 @override
String get newFolder => 'New Folder'; String get newFolder => 'New Folder';

View File

@@ -316,6 +316,24 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get newBuilt => '新建'; 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 @override
String get newFolder => '新建文件夹'; String get newFolder => '新建文件夹';
@@ -1310,6 +1328,24 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override @override
String get newBuilt => '新建'; 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 @override
String get newFolder => '新建資料夾'; String get newFolder => '新建資料夾';

View File

@@ -105,6 +105,12 @@
"matchRule": "匹配规则", "matchRule": "匹配规则",
"emptyMatchAll": "为空表示匹配全部", "emptyMatchAll": "为空表示匹配全部",
"newBuilt": "新建", "newBuilt": "新建",
"reportServers": "上报服务器",
"addReportServer": "新增上报服务器",
"editReportServer": "编辑上报服务器",
"serverUrl": "服务器 URL",
"compression": "压缩",
"compressionNone": "无",
"newFolder": "新建文件夹", "newFolder": "新建文件夹",
"enableSelect": "启用选择", "enableSelect": "启用选择",
"disableSelect": "禁用选择", "disableSelect": "禁用选择",

View File

@@ -103,6 +103,12 @@
"matchRule": "符合規則", "matchRule": "符合規則",
"emptyMatchAll": "為空表示符合全部", "emptyMatchAll": "為空表示符合全部",
"newBuilt": "新建", "newBuilt": "新建",
"reportServers": "上報伺服器",
"addReportServer": "新增上報伺服器",
"editReportServer": "編輯上報伺服器",
"serverUrl": "伺服器 URL",
"compression": "壓縮",
"compressionNone": "無",
"newFolder": "新建資料夾", "newFolder": "新建資料夾",
"enableSelect": "啟用選擇", "enableSelect": "啟用選擇",
"disableSelect": "停用選擇", "disableSelect": "停用選擇",

View File

@@ -96,9 +96,6 @@ class ReportServer {
/// 压缩方式none/gzip默认 none /// 压缩方式none/gzip默认 none
final String? compression; final String? compression;
/// 额外请求头(可选)
final Map<String, String>? headers;
RegExp _urlReg; RegExp _urlReg;
ReportServer({ ReportServer({
@@ -107,7 +104,6 @@ class ReportServer {
required this.serverUrl, required this.serverUrl,
this.enabled = true, this.enabled = true,
this.compression, this.compression,
this.headers,
}) : _urlReg = RegExp(matchUrl.replaceAll("*", ".*").replaceFirst('?', '\\?')); }) : _urlReg = RegExp(matchUrl.replaceAll("*", ".*").replaceFirst('?', '\\?'));
bool match(String url) { bool match(String url) {
@@ -136,19 +132,16 @@ class ReportServer {
serverUrl: serverUrl ?? this.serverUrl, serverUrl: serverUrl ?? this.serverUrl,
enabled: enabled ?? this.enabled, enabled: enabled ?? this.enabled,
compression: compression ?? this.compression, compression: compression ?? this.compression,
headers: headers ?? this.headers,
); );
} }
factory ReportServer.fromJson(Map<String, dynamic> json) { factory ReportServer.fromJson(Map<String, dynamic> json) {
final headers = json['headers'];
return ReportServer( return ReportServer(
name: json['name'] ?? '', name: json['name'] ?? '',
matchUrl: json['matchUrl'] ?? '', matchUrl: json['matchUrl'] ?? '',
serverUrl: json['serverUrl'] ?? '', serverUrl: json['serverUrl'] ?? '',
enabled: json['enabled'] ?? true, enabled: json['enabled'] ?? true,
compression: (json['compression'] ?? 'none') as String, compression: (json['compression'] ?? 'none') as String,
headers: headers == null ? null : Map<String, String>.from(headers as Map),
); );
} }

View File

@@ -77,7 +77,7 @@ class _ReportServersPageState extends State<ReportServersPage> {
Widget labeled(String label, Widget field, {bool expanded = true}) => Row( Widget labeled(String label, Widget field, {bool expanded = true}) => Row(
children: [ children: [
SizedBox(width: 85, child: Text(label)), SizedBox(width: AppLocalizations.of(context)!.localeName == 'en' ? 95 : 85, child: Text(label)),
const SizedBox(width: 12), const SizedBox(width: 12),
expanded ? Expanded(child: field) : field, expanded ? Expanded(child: field) : field,
], ],
@@ -158,7 +158,8 @@ class _ReportServersPageState extends State<ReportServersPage> {
FilledButton( FilledButton(
onPressed: () { onPressed: () {
if (!(formKey.currentState as FormState).validate()) { 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; return;
} }

View File

@@ -21,6 +21,7 @@ import 'package:proxypin/l10n/app_localizations.dart';
import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/ui/mobile/mobile.dart'; import 'package:proxypin/ui/mobile/mobile.dart';
import 'package:proxypin/ui/mobile/setting/app_filter.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/setting/ssl.dart';
import 'package:proxypin/ui/mobile/widgets/highlight.dart'; import 'package:proxypin/ui/mobile/widgets/highlight.dart';
import 'package:proxypin/ui/mobile/widgets/remote_device.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)); 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), const PopupMenuDivider(height: 0),
PopupMenuItem( PopupMenuItem(
height: 32, height: 32,

View File

@@ -443,7 +443,7 @@ class RequestPageState extends State<RequestPage> {
} }
/// 检查远程连接 /// 检查远程连接
checkConnectTask(BuildContext context) async { Future<void> checkConnectTask(BuildContext context) async {
int retry = 0; int retry = 0;
Timer.periodic(const Duration(milliseconds: 15000), (timer) async { Timer.periodic(const Duration(milliseconds: 15000), (timer) async {
if (remoteDevice.value.connect == false) { if (remoteDevice.value.connect == false) {

View File

@@ -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<ReportServersPageMobile> createState() => _ReportServersPageMobileState();
}
class _ReportServersPageMobileState extends State<ReportServersPageMobile> {
List<ReportServer> _servers = [];
bool _loading = true;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final manager = await ReportServerManager.instance;
setState(() {
_servers = List.of(manager.servers);
_loading = false;
});
}
Future<ReportServer?> _showServerDialog({ReportServer? initial}) async {
// Push the edit page and return the created/edited ReportServer
final result = await Navigator.of(context).push<ReportServer>(
MaterialPageRoute(
builder: (ctx) => ReportServerEditPageMobile(initial: initial),
),
);
return result;
}
Future<void> _addServer() async {
final server = await _showServerDialog();
if (server != null) {
final manager = await ReportServerManager.instance;
await manager.add(server);
await _load();
}
}
Future<void> _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<void> _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<ReportServerEditPageMobile> createState() => _ReportServerEditPageMobileState();
}
class _ReportServerEditPageMobileState extends State<ReportServerEditPageMobile> {
late TextEditingController _nameCtrl;
late TextEditingController _matchUrlCtrl;
late TextEditingController _serverUrlCtrl;
String _compression = 'none';
bool _enabled = true;
final _formKey = GlobalKey<FormState>();
@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<String>(
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))),
),
],
),
),
),
),
);
}
}