diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index 00d554c..056754a 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -4,18 +4,16 @@ import 'package:flutter/foundation.dart'; /// 客户端与后端约定的运行时配置。 /// -/// 接口地址的优先级:`--dart-define=API_BASE_URL=...` > 按运行环境判断的默认值。 +/// 接口地址优先级:运行时设置 > `--dart-define=API_BASE_URL=...` > 平台默认值。 /// 开发机的局域网地址会随网络变化,换网络后改 [devHostBaseUrl] 即可。 class AppConfig { const AppConfig._(); - /// 开发机的局域网地址。真机上的 `127.0.0.1` 指向手机自己,只能走这个地址。 + /// 真机默认使用的开发机局域网地址。 /// /// 这个地址跟着电脑所在的网络走,换网络后要用 - /// `ipconfig getifaddr en0` 重新确认并改这里; - /// 也可以换成 Bonjour 名字 `http://.local:8081`(本机是 - /// `jiangdeMacBook-Air.local`),换网络不用改,但部分办公网会拦组播解析。 - static const String devHostBaseUrl = 'http://jiangdeMacBook-Air.local:8081'; + /// `ipconfig getifaddr en0` 重新确认。默认值只改这一行。 + static const String devHostBaseUrl = 'http://10.157.242.1:8081'; static const String _loopbackBaseUrl = 'http://127.0.0.1:8081'; diff --git a/lib/core/widgets/server_endpoint_dialog.dart b/lib/core/widgets/server_endpoint_dialog.dart new file mode 100644 index 0000000..66d6e9d --- /dev/null +++ b/lib/core/widgets/server_endpoint_dialog.dart @@ -0,0 +1,128 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../../l10n/app_localizations.dart'; +import '../config/app_config.dart'; +import '../config/server_endpoint.dart'; +import '../theme/app_colors.dart'; + +/// Opens the debug-only runtime server address editor. +Future showServerEndpointDialog( + BuildContext context, + AppLocalizations l10n, +) async { + final changed = await showDialog( + context: context, + builder: (_) => _ServerEndpointDialog(l10n: l10n), + ); + if (changed == true && context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.restartToApply))); + return true; + } + return false; +} + +class _ServerEndpointDialog extends StatefulWidget { + const _ServerEndpointDialog({required this.l10n}); + + final AppLocalizations l10n; + + @override + State<_ServerEndpointDialog> createState() => _ServerEndpointDialogState(); +} + +class _ServerEndpointDialogState extends State<_ServerEndpointDialog> { + late final TextEditingController _controller = TextEditingController( + text: AppConfig.apiBaseUrl, + ); + String? _error; + bool _saving = false; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _save() async { + final normalized = ServerEndpoint.normalize(_controller.text); + if (normalized == null) { + setState(() => _error = widget.l10n.invalidServerAddress); + return; + } + setState(() { + _saving = true; + _error = null; + }); + final saved = await ServerEndpoint.save(normalized); + if (!mounted) return; + if (!saved) { + setState(() { + _saving = false; + _error = widget.l10n.invalidServerAddress; + }); + return; + } + Navigator.pop(context, true); + } + + Future _restoreDefault() async { + setState(() => _saving = true); + await ServerEndpoint.clear(); + if (mounted) Navigator.pop(context, true); + } + + @override + Widget build(BuildContext context) { + final l10n = widget.l10n; + return AlertDialog( + title: Text(l10n.serverAddress), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + key: const Key('server-endpoint-input'), + controller: _controller, + enabled: !_saving, + keyboardType: TextInputType.url, + autocorrect: false, + decoration: InputDecoration( + hintText: l10n.serverAddressHint, + errorText: _error, + errorMaxLines: 2, + ), + ), + const SizedBox(height: 10), + Text( + l10n.restartToApply, + key: const Key('server-endpoint-restart-hint'), + style: const TextStyle(color: AppColors.muted, fontSize: 12), + ), + const SizedBox(height: 4), + TextButton( + key: const Key('server-endpoint-restore'), + onPressed: _saving ? null : _restoreDefault, + child: Text(l10n.restoreDefault), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _saving ? null : () => Navigator.pop(context, false), + child: Text(l10n.cancel), + ), + FilledButton( + key: const Key('server-endpoint-save'), + onPressed: _saving ? null : _save, + child: Text(l10n.saveAddress), + ), + ], + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 18ebcfb..fd92349 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -263,6 +263,7 @@ "watchHistory": "Watch History", "settings": "Settings", "serverAddress": "Server address", + "serverSettings": "Server settings", "serverAddressHint": "http(s)://host:port", "saveAddress": "Save address", "restartToApply": "Restart the app to apply.", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index ce7eea5..8b5f4eb 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -263,6 +263,7 @@ "watchHistory": "Riwayat Tontonan", "settings": "Pengaturan", "serverAddress": "Alamat server", + "serverSettings": "Pengaturan server", "serverAddressHint": "http(s)://host:port", "saveAddress": "Simpan alamat", "restartToApply": "Mulai ulang aplikasi untuk menerapkan.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index aeed5a1..66fe23d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1234,6 +1234,12 @@ abstract class AppLocalizations { /// **'Server address'** String get serverAddress; + /// No description provided for @serverSettings. + /// + /// In en, this message translates to: + /// **'Server settings'** + String get serverSettings; + /// No description provided for @serverAddressHint. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 40e3466..af33553 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -604,6 +604,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get serverAddress => 'Server address'; + @override + String get serverSettings => 'Server settings'; + @override String get serverAddressHint => 'http(s)://host:port'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 9063d41..f969a29 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -606,6 +606,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get serverAddress => 'Alamat server'; + @override + String get serverSettings => 'Pengaturan server'; + @override String get serverAddressHint => 'http(s)://host:port'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8bd6d57..3430dc1 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -595,6 +595,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get serverAddress => '服务器地址'; + @override + String get serverSettings => '服务器设置'; + @override String get serverAddressHint => 'http(s)://主机:端口'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 625b639..058edff 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -225,6 +225,7 @@ "watchHistory": "观看历史", "settings": "设置", "serverAddress": "服务器地址", + "serverSettings": "服务器设置", "serverAddressHint": "http(s)://主机:端口", "saveAddress": "保存地址", "restartToApply": "重启 App 生效", diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 51c15a2..28a67fe 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -15,6 +15,7 @@ import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/empty_state.dart'; +import '../../core/widgets/server_endpoint_dialog.dart'; import '../../l10n/app_localizations.dart'; import '../discover/discover_view.dart'; import '../mine/mine_view.dart'; @@ -50,28 +51,9 @@ class HomePage extends StatelessWidget { if (catalog.error != null) { return Scaffold( backgroundColor: Colors.black, - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.error_outline_rounded, - color: Colors.white70, - size: 40, - ), - const SizedBox(height: 12), - Text(l10n.catalogError), - const SizedBox(height: 12), - FilledButton(onPressed: catalog.load, child: Text(l10n.retry)), - if (kDebugMode) ...[ - const SizedBox(height: 16), - Text( - AppConfig.apiBaseUrl, - style: const TextStyle(fontSize: 12, color: Colors.white54), - ), - ], - ], - ), + body: HomeFailureView( + onRetry: catalog.load, + showDebugTools: kDebugMode, ), ); } @@ -96,6 +78,53 @@ class HomePage extends StatelessWidget { } } +/// Home catalog failure with optional debug-only endpoint controls. +class HomeFailureView extends StatelessWidget { + const HomeFailureView({ + super.key, + required this.onRetry, + this.showDebugTools = kDebugMode, + }); + + final VoidCallback onRetry; + final bool showDebugTools; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.error_outline_rounded, + color: Colors.white70, + size: 40, + ), + const SizedBox(height: 12), + Text(l10n.catalogError), + const SizedBox(height: 12), + FilledButton(onPressed: onRetry, child: Text(l10n.retry)), + if (showDebugTools) ...[ + const SizedBox(height: 16), + Text( + AppConfig.apiBaseUrl, + style: const TextStyle(fontSize: 12, color: Colors.white54), + ), + const SizedBox(height: 8), + OutlinedButton.icon( + key: const Key('home-server-settings'), + onPressed: () => showServerEndpointDialog(context, l10n), + icon: const Icon(Icons.dns_outlined), + label: Text(l10n.serverSettings), + ), + ], + ], + ), + ); + } +} + /// The three primary destinations share a single route and keep their state. class _HomeTabs extends StatefulWidget { const _HomeTabs({super.key, required this.dramas}); diff --git a/lib/pages/mine/mine_logic.dart b/lib/pages/mine/mine_logic.dart index b2e2f9d..553d35d 100644 --- a/lib/pages/mine/mine_logic.dart +++ b/lib/pages/mine/mine_logic.dart @@ -6,11 +6,10 @@ import 'package:provider/provider.dart'; import '../../core/config/app_config.dart'; import '../../core/config/locale_controller.dart'; -import '../../core/config/server_endpoint.dart'; import '../../core/data/models/drama.dart'; import '../../core/data/models/watch_progress.dart'; import '../../core/data/repositories/watch_history_repository.dart'; -import '../../core/theme/app_colors.dart'; +import '../../core/widgets/server_endpoint_dialog.dart'; import '../../l10n/app_localizations.dart'; enum MyPageAction { home, discover, resume } @@ -169,17 +168,7 @@ class MineLogic extends ChangeNotifier { Future _editServerEndpoint( BuildContext context, AppLocalizations l10n, - ) async { - final changed = await showDialog( - context: context, - builder: (_) => _ServerEndpointDialog(l10n: l10n), - ); - if (changed == true && context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.restartToApply))); - } - } + ) => showServerEndpointDialog(context, l10n); @override void dispose() { @@ -187,105 +176,3 @@ class MineLogic extends ChangeNotifier { super.dispose(); } } - -class _ServerEndpointDialog extends StatefulWidget { - const _ServerEndpointDialog({required this.l10n}); - - final AppLocalizations l10n; - - @override - State<_ServerEndpointDialog> createState() => _ServerEndpointDialogState(); -} - -class _ServerEndpointDialogState extends State<_ServerEndpointDialog> { - late final TextEditingController _controller = TextEditingController( - text: AppConfig.apiBaseUrl, - ); - String? _error; - bool _saving = false; - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - Future _save() async { - final normalized = ServerEndpoint.normalize(_controller.text); - if (normalized == null) { - setState(() => _error = widget.l10n.invalidServerAddress); - return; - } - setState(() { - _saving = true; - _error = null; - }); - final saved = await ServerEndpoint.save(normalized); - if (!mounted) return; - if (!saved) { - setState(() { - _saving = false; - _error = widget.l10n.invalidServerAddress; - }); - return; - } - Navigator.pop(context, true); - } - - Future _restoreDefault() async { - setState(() => _saving = true); - await ServerEndpoint.clear(); - if (mounted) Navigator.pop(context, true); - } - - @override - Widget build(BuildContext context) { - final l10n = widget.l10n; - return AlertDialog( - title: Text(l10n.serverAddress), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - key: const Key('server-endpoint-input'), - controller: _controller, - enabled: !_saving, - keyboardType: TextInputType.url, - autocorrect: false, - decoration: InputDecoration( - hintText: l10n.serverAddressHint, - errorText: _error, - errorMaxLines: 2, - ), - ), - const SizedBox(height: 10), - Text( - l10n.restartToApply, - key: const Key('server-endpoint-restart-hint'), - style: const TextStyle(color: AppColors.muted, fontSize: 12), - ), - const SizedBox(height: 4), - TextButton( - key: const Key('server-endpoint-restore'), - onPressed: _saving ? null : _restoreDefault, - child: Text(l10n.restoreDefault), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: _saving ? null : () => Navigator.pop(context, false), - child: Text(l10n.cancel), - ), - FilledButton( - key: const Key('server-endpoint-save'), - onPressed: _saving ? null : _save, - child: Text(l10n.saveAddress), - ), - ], - ); - } -} diff --git a/test/config/server_endpoint_test.dart b/test/config/server_endpoint_test.dart index 974b42b..2918105 100644 --- a/test/config/server_endpoint_test.dart +++ b/test/config/server_endpoint_test.dart @@ -56,6 +56,10 @@ void main() { ); }); + test('should_use_the_configured_lan_address_for_device_default', () { + expect(AppConfig.devHostBaseUrl, 'http://10.157.242.1:8081'); + }); + test('should_apply_stored_url_only_when_debug_is_enabled', () async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.withData({ diff --git a/test/home/home_server_setting_test.dart b/test/home/home_server_setting_test.dart new file mode 100644 index 0000000..239da4f --- /dev/null +++ b/test/home/home_server_setting_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; + +import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/home/home_view.dart'; + +void main() { + setUp(() { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + }); + + testWidgets( + 'should_show_server_settings_and_open_dialog_when_debug_tools_are_enabled', + (tester) async { + await _pumpFailureView(tester, showDebugTools: true); + + final button = find.byKey(const Key('home-server-settings')); + expect(button, findsOneWidget); + expect(find.text('Server settings'), findsOneWidget); + + await tester.tap(button); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('server-endpoint-input')), findsOneWidget); + expect(find.text('Restart the app to apply.'), findsOneWidget); + }, + ); + + testWidgets('should_hide_server_settings_when_debug_tools_are_disabled', ( + tester, + ) async { + await _pumpFailureView(tester, showDebugTools: false); + + expect(find.byKey(const Key('home-server-settings')), findsNothing); + expect(find.text('Server settings'), findsNothing); + }); +} + +Future _pumpFailureView( + WidgetTester tester, { + required bool showDebugTools, +}) async { + await tester.pumpWidget( + MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: HomeFailureView(onRetry: () {}, showDebugTools: showDebugTools), + ), + ), + ); + await tester.pumpAndSettle(); +}