merge: 合并 K13 开发服务器地址设置
This commit is contained in:
commit
f95325d581
14 files changed
+590
-27
No files matched your search
@@ -15,18 +15,28 @@ class AppConfig {
|
||||
/// `ipconfig getifaddr en0` 重新确认并改这里;
|
||||
/// 也可以换成 Bonjour 名字 `http://<LocalHostName>.local:8081`(本机是
|
||||
/// `jiangdeMacBook-Air.local`),换网络不用改,但部分办公网会拦组播解析。
|
||||
static const String devHostBaseUrl =
|
||||
'http://jiangdeMacBook-Air.local:8081';
|
||||
static const String devHostBaseUrl = 'http://jiangdeMacBook-Air.local:8081';
|
||||
|
||||
static const String _loopbackBaseUrl = 'http://127.0.0.1:8081';
|
||||
|
||||
static const String _overrideBaseUrl = String.fromEnvironment('API_BASE_URL');
|
||||
|
||||
static String get apiBaseUrl =>
|
||||
_overrideBaseUrl.isNotEmpty ? _overrideBaseUrl : _defaultBaseUrl;
|
||||
static String _runtimeBaseUrl = '';
|
||||
|
||||
static final String _defaultBaseUrl =
|
||||
_runsOnDevMachine ? _loopbackBaseUrl : devHostBaseUrl;
|
||||
/// Build-time server URL supplied through `--dart-define=API_BASE_URL`.
|
||||
static String get compiledBaseUrl => _overrideBaseUrl;
|
||||
|
||||
/// Existing platform-aware fallback used when no runtime or build override exists.
|
||||
static String get defaultBaseUrl => _defaultBaseUrl;
|
||||
|
||||
/// Effective server URL, including the debug-only runtime override.
|
||||
static String get apiBaseUrl => _runtimeBaseUrl.isNotEmpty
|
||||
? _runtimeBaseUrl
|
||||
: (compiledBaseUrl.isNotEmpty ? compiledBaseUrl : defaultBaseUrl);
|
||||
|
||||
static final String _defaultBaseUrl = _runsOnDevMachine
|
||||
? _loopbackBaseUrl
|
||||
: devHostBaseUrl;
|
||||
|
||||
/// 模拟器、桌面端与开发机同机,环回地址可用;iOS 真机不满足。
|
||||
/// Android 没有等价的探测方式,统一按真机处理(模拟器用
|
||||
@@ -39,6 +49,16 @@ class AppConfig {
|
||||
(defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
Platform.environment.containsKey('SIMULATOR_DEVICE_NAME'));
|
||||
|
||||
/// Applies an already normalized runtime URL. Passing null clears it.
|
||||
static void applyRuntimeBaseUrl(String? value) {
|
||||
_runtimeBaseUrl = value ?? '';
|
||||
}
|
||||
|
||||
/// Clears the in-memory runtime override, mainly for tests and restarts.
|
||||
static void clearRuntimeBaseUrl() {
|
||||
_runtimeBaseUrl = '';
|
||||
}
|
||||
|
||||
static const Duration requestTimeout = Duration(seconds: 8);
|
||||
|
||||
/// 后端返回的是相对路径(/files/...),这里补上服务地址。
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'app_config.dart';
|
||||
|
||||
/// Debug-only runtime server endpoint persisted outside build-time defines.
|
||||
abstract final class ServerEndpoint {
|
||||
/// Preference key used for the runtime server address.
|
||||
static const String preferenceKey = 'dev.api_base_url';
|
||||
|
||||
/// Returns a normalized http(s) URL, or null for invalid input.
|
||||
static String? normalize(String? raw) {
|
||||
final value = raw?.trim() ?? '';
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null ||
|
||||
(uri.scheme != 'http' && uri.scheme != 'https') ||
|
||||
uri.host.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return value.replaceFirst(RegExp(r'/+$'), '');
|
||||
}
|
||||
|
||||
/// Resolves runtime > dart-define > default using normalized valid values.
|
||||
static String resolve({
|
||||
required String? stored,
|
||||
required String compiled,
|
||||
required String fallback,
|
||||
}) {
|
||||
return normalize(stored) ??
|
||||
normalize(compiled) ??
|
||||
normalize(fallback) ??
|
||||
fallback;
|
||||
}
|
||||
|
||||
/// Loads the persisted override before the app creates its ApiClient.
|
||||
///
|
||||
/// In release builds this returns immediately without reading preferences.
|
||||
static Future<void> load({
|
||||
bool debug = kDebugMode,
|
||||
SharedPreferencesAsync? preferences,
|
||||
}) async {
|
||||
if (!debug) return;
|
||||
try {
|
||||
final prefs = preferences ?? SharedPreferencesAsync();
|
||||
final stored = await prefs.getString(preferenceKey);
|
||||
AppConfig.applyRuntimeBaseUrl(
|
||||
resolve(
|
||||
stored: stored,
|
||||
compiled: AppConfig.compiledBaseUrl,
|
||||
fallback: AppConfig.defaultBaseUrl,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
AppConfig.clearRuntimeBaseUrl();
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists a normalized runtime endpoint.
|
||||
static Future<bool> save(
|
||||
String raw, {
|
||||
SharedPreferencesAsync? preferences,
|
||||
}) async {
|
||||
final normalized = normalize(raw);
|
||||
if (normalized == null) return false;
|
||||
try {
|
||||
await (preferences ?? SharedPreferencesAsync()).setString(
|
||||
preferenceKey,
|
||||
normalized,
|
||||
);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the persisted runtime endpoint.
|
||||
static Future<void> clear({SharedPreferencesAsync? preferences}) async {
|
||||
try {
|
||||
await (preferences ?? SharedPreferencesAsync()).remove(preferenceKey);
|
||||
} catch (_) {
|
||||
// Clearing an optional developer override is best effort.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,6 +262,12 @@
|
||||
"freeBonus": "Get Free Bonus",
|
||||
"watchHistory": "Watch History",
|
||||
"settings": "Settings",
|
||||
"serverAddress": "Server address",
|
||||
"serverAddressHint": "http(s)://host:port",
|
||||
"saveAddress": "Save address",
|
||||
"restartToApply": "Restart the app to apply.",
|
||||
"restoreDefault": "Restore default",
|
||||
"invalidServerAddress": "Enter a valid http(s) server address.",
|
||||
"signOut": "Sign Out",
|
||||
"mockUserName": "Vicky sheng",
|
||||
"mockUserId": "ID: M1CZWVYE",
|
||||
|
||||
@@ -262,6 +262,12 @@
|
||||
"freeBonus": "Bonus Gratis",
|
||||
"watchHistory": "Riwayat Tontonan",
|
||||
"settings": "Pengaturan",
|
||||
"serverAddress": "Alamat server",
|
||||
"serverAddressHint": "http(s)://host:port",
|
||||
"saveAddress": "Simpan alamat",
|
||||
"restartToApply": "Mulai ulang aplikasi untuk menerapkan.",
|
||||
"restoreDefault": "Pulihkan default",
|
||||
"invalidServerAddress": "Masukkan alamat server http(s) yang valid.",
|
||||
"signOut": "Keluar",
|
||||
"mockUserName": "Vicky sheng",
|
||||
"mockUserId": "ID: M1CZWVYE",
|
||||
|
||||
@@ -1228,6 +1228,42 @@ abstract class AppLocalizations {
|
||||
/// **'Settings'**
|
||||
String get settings;
|
||||
|
||||
/// No description provided for @serverAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Server address'**
|
||||
String get serverAddress;
|
||||
|
||||
/// No description provided for @serverAddressHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'http(s)://host:port'**
|
||||
String get serverAddressHint;
|
||||
|
||||
/// No description provided for @saveAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save address'**
|
||||
String get saveAddress;
|
||||
|
||||
/// No description provided for @restartToApply.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Restart the app to apply.'**
|
||||
String get restartToApply;
|
||||
|
||||
/// No description provided for @restoreDefault.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Restore default'**
|
||||
String get restoreDefault;
|
||||
|
||||
/// No description provided for @invalidServerAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter a valid http(s) server address.'**
|
||||
String get invalidServerAddress;
|
||||
|
||||
/// No description provided for @signOut.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -601,6 +601,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get serverAddress => 'Server address';
|
||||
|
||||
@override
|
||||
String get serverAddressHint => 'http(s)://host:port';
|
||||
|
||||
@override
|
||||
String get saveAddress => 'Save address';
|
||||
|
||||
@override
|
||||
String get restartToApply => 'Restart the app to apply.';
|
||||
|
||||
@override
|
||||
String get restoreDefault => 'Restore default';
|
||||
|
||||
@override
|
||||
String get invalidServerAddress => 'Enter a valid http(s) server address.';
|
||||
|
||||
@override
|
||||
String get signOut => 'Sign Out';
|
||||
|
||||
|
||||
@@ -603,6 +603,25 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get settings => 'Pengaturan';
|
||||
|
||||
@override
|
||||
String get serverAddress => 'Alamat server';
|
||||
|
||||
@override
|
||||
String get serverAddressHint => 'http(s)://host:port';
|
||||
|
||||
@override
|
||||
String get saveAddress => 'Simpan alamat';
|
||||
|
||||
@override
|
||||
String get restartToApply => 'Mulai ulang aplikasi untuk menerapkan.';
|
||||
|
||||
@override
|
||||
String get restoreDefault => 'Pulihkan default';
|
||||
|
||||
@override
|
||||
String get invalidServerAddress =>
|
||||
'Masukkan alamat server http(s) yang valid.';
|
||||
|
||||
@override
|
||||
String get signOut => 'Keluar';
|
||||
|
||||
|
||||
@@ -592,6 +592,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings => '设置';
|
||||
|
||||
@override
|
||||
String get serverAddress => '服务器地址';
|
||||
|
||||
@override
|
||||
String get serverAddressHint => 'http(s)://主机:端口';
|
||||
|
||||
@override
|
||||
String get saveAddress => '保存地址';
|
||||
|
||||
@override
|
||||
String get restartToApply => '重启 App 生效';
|
||||
|
||||
@override
|
||||
String get restoreDefault => '恢复默认';
|
||||
|
||||
@override
|
||||
String get invalidServerAddress => '请输入有效的 http(s) 服务器地址。';
|
||||
|
||||
@override
|
||||
String get signOut => '退出登录';
|
||||
|
||||
|
||||
@@ -224,6 +224,12 @@
|
||||
"freeBonus": "领免费奖励",
|
||||
"watchHistory": "观看历史",
|
||||
"settings": "设置",
|
||||
"serverAddress": "服务器地址",
|
||||
"serverAddressHint": "http(s)://主机:端口",
|
||||
"saveAddress": "保存地址",
|
||||
"restartToApply": "重启 App 生效",
|
||||
"restoreDefault": "恢复默认",
|
||||
"invalidServerAddress": "请输入有效的 http(s) 服务器地址。",
|
||||
"signOut": "退出登录",
|
||||
"mockUserName": "Vicky sheng",
|
||||
"mockUserId": "ID: M1CZWVYE",
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'app.dart';
|
||||
import 'core/config/server_endpoint.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
await ServerEndpoint.load();
|
||||
runApp(const LumaDramaApp());
|
||||
}
|
||||
+162
-19
@@ -1,12 +1,16 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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 '../../l10n/app_localizations.dart';
|
||||
|
||||
enum MyPageAction { home, discover, resume }
|
||||
@@ -110,39 +114,178 @@ class MineLogic extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showLanguage(BuildContext context) async {
|
||||
Future<void> showLanguage(
|
||||
BuildContext context, {
|
||||
bool showServerSetting = kDebugMode,
|
||||
}) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: const Color(0xFF1C1D22),
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(title: Text(l10n.language)),
|
||||
for (final (code, label) in [
|
||||
('en', l10n.english),
|
||||
('zh', l10n.simplifiedChinese),
|
||||
('id', l10n.bahasaIndonesia),
|
||||
])
|
||||
ListTile(
|
||||
title: Text(label),
|
||||
onTap: () {
|
||||
context.read<LocaleController>().select(code);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.sizeOf(sheetContext).height * 0.85,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(title: Text(l10n.language)),
|
||||
for (final (code, label) in [
|
||||
('en', l10n.english),
|
||||
('zh', l10n.simplifiedChinese),
|
||||
('id', l10n.bahasaIndonesia),
|
||||
])
|
||||
ListTile(
|
||||
title: Text(label),
|
||||
onTap: () {
|
||||
context.read<LocaleController>().select(code);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
if (showServerSetting) ...[
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
key: const Key('server-endpoint-setting'),
|
||||
leading: const Icon(Icons.dns_outlined),
|
||||
title: Text(l10n.serverAddress),
|
||||
subtitle: Text(AppConfig.apiBaseUrl),
|
||||
trailing: const Icon(Icons.edit_outlined),
|
||||
onTap: () => _editServerEndpoint(context, l10n),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _editServerEndpoint(
|
||||
BuildContext context,
|
||||
AppLocalizations l10n,
|
||||
) async {
|
||||
final changed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => _ServerEndpointDialog(l10n: l10n),
|
||||
);
|
||||
if (changed == true && context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.restartToApply)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
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<void> _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<void> _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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -41,6 +42,7 @@ class MyPage extends StatefulWidget {
|
||||
this.onSignOut,
|
||||
this.dramas,
|
||||
this.onDramaSelected,
|
||||
this.debugShowSetting = kDebugMode,
|
||||
});
|
||||
|
||||
final Drama drama;
|
||||
@@ -57,6 +59,7 @@ class MyPage extends StatefulWidget {
|
||||
final VoidCallback? onSignOut;
|
||||
final List<Drama>? dramas;
|
||||
final ValueChanged<Drama>? onDramaSelected;
|
||||
final bool debugShowSetting;
|
||||
|
||||
@override
|
||||
State<MyPage> createState() => _MyPageState();
|
||||
@@ -214,7 +217,10 @@ class _MyPageState extends State<MyPage> {
|
||||
leading: const Icon(Icons.language_rounded, color: AppColors.blueSoft),
|
||||
title: Text(l10n.appLanguage),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () => logic.showLanguage(context),
|
||||
onTap: () => logic.showLanguage(
|
||||
context,
|
||||
showServerSetting: widget.debugShowSetting,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -471,7 +477,10 @@ class _MyPageState extends State<MyPage> {
|
||||
),
|
||||
title: Text(l10n.settings),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () => logic.showLanguage(context),
|
||||
onTap: () => logic.showLanguage(
|
||||
context,
|
||||
showServerSetting: widget.debugShowSetting,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.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/core/config/app_config.dart';
|
||||
import 'package:fruitshorts/core/config/server_endpoint.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
AppConfig.clearRuntimeBaseUrl();
|
||||
});
|
||||
|
||||
tearDown(AppConfig.clearRuntimeBaseUrl);
|
||||
|
||||
test('should_normalize_http_urls_and_reject_invalid_values', () {
|
||||
expect(
|
||||
ServerEndpoint.normalize(' https://192.168.1.9:8081/// '),
|
||||
'https://192.168.1.9:8081',
|
||||
);
|
||||
expect(
|
||||
ServerEndpoint.normalize('http://example.test:8081/path/'),
|
||||
'http://example.test:8081/path',
|
||||
);
|
||||
expect(ServerEndpoint.normalize('ftp://example.test'), isNull);
|
||||
expect(ServerEndpoint.normalize('example.test:8081'), isNull);
|
||||
expect(ServerEndpoint.normalize('http://'), isNull);
|
||||
});
|
||||
|
||||
test('should_prioritize_runtime_over_dart_define_over_default', () {
|
||||
expect(
|
||||
ServerEndpoint.resolve(
|
||||
stored: ' http://runtime.test:8081/ ',
|
||||
compiled: 'http://define.test:8081',
|
||||
fallback: 'http://default.test:8081',
|
||||
),
|
||||
'http://runtime.test:8081',
|
||||
);
|
||||
expect(
|
||||
ServerEndpoint.resolve(
|
||||
stored: 'ftp://invalid',
|
||||
compiled: 'http://define.test:8081/',
|
||||
fallback: 'http://default.test:8081',
|
||||
),
|
||||
'http://define.test:8081',
|
||||
);
|
||||
expect(
|
||||
ServerEndpoint.resolve(
|
||||
stored: null,
|
||||
compiled: '',
|
||||
fallback: 'http://default.test:8081/',
|
||||
),
|
||||
'http://default.test:8081',
|
||||
);
|
||||
});
|
||||
|
||||
test('should_apply_stored_url_only_when_debug_is_enabled', () async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.withData({
|
||||
ServerEndpoint.preferenceKey: ' http://192.168.31.187:8081/ ',
|
||||
});
|
||||
final original = AppConfig.apiBaseUrl;
|
||||
|
||||
await ServerEndpoint.load(debug: false);
|
||||
expect(AppConfig.apiBaseUrl, original);
|
||||
|
||||
await ServerEndpoint.load(debug: true);
|
||||
expect(AppConfig.apiBaseUrl, 'http://192.168.31.187:8081');
|
||||
});
|
||||
|
||||
test('should_save_normalized_url_and_clear_override', () async {
|
||||
expect(await ServerEndpoint.save('ftp://invalid'), isFalse);
|
||||
expect(
|
||||
await SharedPreferencesAsync().getString(ServerEndpoint.preferenceKey),
|
||||
isNull,
|
||||
);
|
||||
|
||||
expect(await ServerEndpoint.save(' http://192.168.31.187:8081/ '), isTrue);
|
||||
expect(
|
||||
await SharedPreferencesAsync().getString(ServerEndpoint.preferenceKey),
|
||||
'http://192.168.31.187:8081',
|
||||
);
|
||||
|
||||
await ServerEndpoint.clear();
|
||||
expect(
|
||||
await SharedPreferencesAsync().getString(ServerEndpoint.preferenceKey),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.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/core/config/locale_controller.dart';
|
||||
import 'package:fruitshorts/core/config/server_endpoint.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/mine/mine_logic.dart';
|
||||
|
||||
const _drama = Drama(
|
||||
id: 'drama-1',
|
||||
title: 'The Signal',
|
||||
synopsis: 'synopsis',
|
||||
posterUrl: '/files/1.jpg',
|
||||
);
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
});
|
||||
|
||||
testWidgets('should_show_server_address_when_debug_entry_is_enabled', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpSettings(tester, showServerSetting: true);
|
||||
expect(find.byKey(const Key('server-endpoint-setting')), findsOneWidget);
|
||||
expect(find.text('Server address'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_hide_server_address_when_debug_entry_is_disabled', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpSettings(tester, showServerSetting: false);
|
||||
expect(find.byKey(const Key('server-endpoint-setting')), findsNothing);
|
||||
expect(find.text('Server address'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_reject_invalid_server_address_without_saving', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpSettings(tester, showServerSetting: true);
|
||||
await tester.tap(find.byKey(const Key('server-endpoint-setting')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Restart the app to apply.'), findsOneWidget);
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('server-endpoint-input')),
|
||||
'ftp://invalid',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('server-endpoint-save')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Enter a valid http(s) server address.'), findsOneWidget);
|
||||
expect(
|
||||
await SharedPreferencesAsync().getString(ServerEndpoint.preferenceKey),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpSettings(
|
||||
WidgetTester tester, {
|
||||
required bool showServerSetting,
|
||||
}) async {
|
||||
final logic = MineLogic(
|
||||
history: InMemoryWatchHistoryRepository(),
|
||||
drama: _drama,
|
||||
initialSection: 0,
|
||||
);
|
||||
addTearDown(logic.dispose);
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => LocaleController(),
|
||||
child: MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
key: const Key('open-settings'),
|
||||
onPressed: () => logic.showLanguage(
|
||||
context,
|
||||
showServerSetting: showServerSetting,
|
||||
),
|
||||
child: const Text('Open settings'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('open-settings')));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
Reference in new issue
Block a user