feat(flutter): 增加 debug 服务地址编辑器

This commit is contained in:
Zhou Weidong committed 2026-10-20 18:23:44 +08:00
1 parent eab5d3e230
commit c259b0b543
9 files changed
+298 -5

No files matched your search

+6 -1
View File
@@ -80,7 +80,12 @@ class AppConfig {
Future<bool> Function(String baseUrl)? probe,
String? fallbackBaseUrl,
}) async {
for (final candidate in _probeCandidates) {
final candidates = <String>[
if (_runtimeBaseUrl.isNotEmpty) _runtimeBaseUrl,
for (final candidate in _probeCandidates)
if (candidate != _runtimeBaseUrl) candidate,
];
for (final candidate in candidates) {
final result = probe == null
? await _probeBaseUrl(candidate)
: await _runInjectedProbe(candidate, probe);
+1
View File
@@ -69,6 +69,7 @@ abstract final class ServerEndpoint {
preferenceKey,
normalized,
);
AppConfig.applyRuntimeBaseUrl(normalized);
return true;
} catch (_) {
return false;
+8 -1
View File
@@ -14,9 +14,10 @@ const TextStyle _debugStyle = TextStyle(
/// 仅 debug 展示当前服务地址与目录请求的原始错误。
class CatalogDebugInfo extends StatelessWidget {
const CatalogDebugInfo({super.key, required this.error});
const CatalogDebugInfo({super.key, required this.error, this.onEditEndpoint});
final String? error;
final VoidCallback? onEditEndpoint;
@override
Widget build(BuildContext context) {
@@ -41,6 +42,12 @@ class CatalogDebugInfo extends StatelessWidget {
textAlign: TextAlign.center,
style: _debugStyle,
),
if (onEditEndpoint != null)
TextButton(
key: const Key('catalog-debug-endpoint-edit'),
onPressed: onEditEndpoint,
child: const Text('设置服务地址'),
),
],
);
}
@@ -0,0 +1,126 @@
// Author: fruit.com.cn
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../config/app_config.dart';
import '../config/server_endpoint.dart';
const String _title = '设置服务地址';
const String _save = '保存';
const String _cancel = '取消';
const String _invalid = '请输入合法地址,例如 http://192.168.1.10:8081';
const String _saveFailed = '服务地址保存失败';
const String _localNetworkHint = '手机需与电脑同一 Wi-Fi,并在 iOS 设置-隐私与安全性-本地网络 中允许本应用';
/// Debug 下编辑服务地址,保存后重新探测并加载目录。
Future<void> showCatalogEndpointEditor(
BuildContext context, {
required Future<void> Function() reload,
Future<bool> Function(String value)? save,
Future<void> Function()? probe,
}) async {
if (!kDebugMode) return;
final value = await showDialog<String>(
context: context,
builder: (_) => _CatalogEndpointDialog(initialValue: AppConfig.apiBaseUrl),
);
if (value == null) return;
final saveEndpoint = save ?? (input) => ServerEndpoint.save(input);
if (!await saveEndpoint(value)) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text(_saveFailed)));
}
return;
}
await (probe ?? () => AppConfig.probeAndApplyBaseUrl())();
await reload();
}
class _CatalogEndpointDialog extends StatefulWidget {
const _CatalogEndpointDialog({required this.initialValue});
final String initialValue;
@override
State<_CatalogEndpointDialog> createState() => _CatalogEndpointDialogState();
}
class _CatalogEndpointDialogState extends State<_CatalogEndpointDialog> {
late final TextEditingController _controller = TextEditingController(
text: widget.initialValue,
);
String? _error;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _submit() {
final normalized = ServerEndpoint.normalize(_controller.text);
if (normalized == null) {
setState(() => _error = _invalid);
return;
}
Navigator.of(context).pop(normalized);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text(_title),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
key: const Key('catalog-debug-endpoint-input'),
controller: _controller,
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
autocorrect: false,
onSubmitted: (_) => _submit(),
decoration: const InputDecoration(
labelText: '服务地址',
hintText: 'http://IP:端口',
),
),
const SizedBox(height: 10),
const Text(
_localNetworkHint,
key: Key('catalog-debug-endpoint-hint'),
style: TextStyle(fontSize: 12, color: Colors.grey),
),
if (_error != null) ...[
const SizedBox(height: 8),
Text(
_error!,
key: const Key('catalog-debug-endpoint-error'),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.error,
),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(_cancel),
),
FilledButton(
key: const Key('catalog-debug-endpoint-save'),
onPressed: _submit,
child: const Text(_save),
),
],
);
}
}
+10 -1
View File
@@ -18,6 +18,7 @@ import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_bottom_nav.dart';
import '../../core/widgets/app_pull_to_refresh.dart';
import '../../core/widgets/catalog_debug_info.dart';
import '../../core/widgets/catalog_endpoint_editor.dart';
import '../../core/widgets/empty_state.dart';
import '../../l10n/app_localizations.dart';
import '../filter/filter_page.dart';
@@ -258,7 +259,11 @@ class _DiscoverPageState extends State<DiscoverPage> {
actionLabel: l10n.retry,
onAction: () => unawaited(_retryCategories(categories)),
footer: kDebugMode
? CatalogDebugInfo(error: categories.debugError ?? error)
? CatalogDebugInfo(
error: categories.debugError ?? error,
onEditEndpoint: () =>
unawaited(_editCategoriesEndpoint(categories)),
)
: null,
),
),
@@ -518,6 +523,10 @@ class _DiscoverPageState extends State<DiscoverPage> {
return retryCatalogLoad(reload: categories.load);
}
Future<void> _editCategoriesEndpoint(CategoryController categories) {
return showCatalogEndpointEditor(context, reload: categories.load);
}
void _act(DiscoverPageAction action) {
logic.act(
context,
+16 -2
View File
@@ -16,6 +16,7 @@ import '../../core/platform/haptics.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_bottom_nav.dart';
import '../../core/widgets/catalog_debug_info.dart';
import '../../core/widgets/catalog_endpoint_editor.dart';
import '../../core/widgets/empty_state.dart';
import '../../l10n/app_localizations.dart';
import '../discover/discover_types.dart';
@@ -79,6 +80,10 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
return retryCatalogLoad(reload: catalog.load);
}
Future<void> _editHomeEndpoint(CatalogController catalog) {
return showCatalogEndpointEditor(context, reload: catalog.load);
}
@override
Widget build(BuildContext context) {
final catalog = context.watch<CatalogController>();
@@ -107,6 +112,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
body: HomeFailureView(
error: catalog.debugError ?? catalog.error,
onRetry: () => unawaited(_retryHome(catalog)),
onEditEndpoint: () => unawaited(_editHomeEndpoint(catalog)),
),
);
}
@@ -134,10 +140,16 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
/// 首页目录加载失败与重试入口。
class HomeFailureView extends StatelessWidget {
const HomeFailureView({super.key, this.error, required this.onRetry});
const HomeFailureView({
super.key,
this.error,
required this.onRetry,
this.onEditEndpoint,
});
final String? error;
final VoidCallback onRetry;
final VoidCallback? onEditEndpoint;
@override
Widget build(BuildContext context) {
@@ -147,7 +159,9 @@ class HomeFailureView extends StatelessWidget {
title: l10n.catalogError,
actionLabel: l10n.retry,
onAction: onRetry,
footer: kDebugMode ? CatalogDebugInfo(error: error) : null,
footer: kDebugMode
? CatalogDebugInfo(error: error, onEditEndpoint: onEditEndpoint)
: null,
);
}
}