diff --git a/android/app/src/main/kotlin/com/stormg/lumadrama/MainActivity.kt b/android/app/src/main/kotlin/com/stormg/lumadrama/MainActivity.kt index 71a3a09..b423dd4 100644 --- a/android/app/src/main/kotlin/com/stormg/lumadrama/MainActivity.kt +++ b/android/app/src/main/kotlin/com/stormg/lumadrama/MainActivity.kt @@ -4,6 +4,7 @@ package com.stormg.lumadrama import android.content.Intent import android.app.PictureInPictureParams +import android.content.Context import android.content.res.Configuration import android.graphics.Bitmap import android.media.MediaMetadataRetriever @@ -13,6 +14,7 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.provider.Settings +import android.telephony.TelephonyManager import android.util.Rational import android.content.pm.PackageManager import android.view.WindowManager @@ -22,6 +24,7 @@ import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import java.io.ByteArrayOutputStream +import java.util.Locale import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean @@ -69,16 +72,19 @@ class MainActivity : FlutterActivity() { // 设备标识:SSAID 免权限,卸载重装不变,避免客户端自己生成的 id 每次重装都变 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.stormg.lumadrama/device") .setMethodCallHandler { call, result -> - if (call.method != "ssaid") { - result.notImplemented() - return@setMethodCallHandler + when (call.method) { + "ssaid" -> { + val value = try { + Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) + } catch (_: Exception) { + null + } + result.success(value) + } + // 设备区域:注册时作为服务端 IP 解析失败后的兜底 + "region" -> result.success(currentRegion()) + else -> result.notImplemented() } - val value = try { - Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) - } catch (_: Exception) { - null - } - result.success(value) } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.stormg.lumadrama/share") .setMethodCallHandler { call, result -> @@ -171,6 +177,28 @@ class MainActivity : FlutterActivity() { ?.bottom ?: 0 + /** 网络运营商国家优先,其次 SIM 卡,最后系统语言;都拿不到返回空串。 */ + private fun currentRegion(): String { + return try { + val telephony = getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + val localeCountry = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resources.configuration.locales.get(0).country + } else { + @Suppress("DEPRECATION") + resources.configuration.locale.country + } + val candidates = listOfNotNull( + telephony?.networkCountryIso, + telephony?.simCountryIso, + localeCountry + ) + candidates.firstOrNull { it.isNotBlank() }?.uppercase(Locale.ROOT) ?: "" + } catch (_: Exception) { + // 个别 ROM 读运营商信息会抛 SecurityException,别让注册流程卡住 + "" + } + } + private fun emitImeInset(value: Int) { if (value == imeInset) return imeInset = value diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 43a3513..6b22270 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -146,17 +146,25 @@ final class RunnerFlutterViewController: FlutterViewController { } /// 设备标识:IDFV 免权限,卸载本 App 不变,避免客户端自生成的 id 每次重装都变。 + /// 同通道顺带提供设备区域(ISO-2),注册时作为服务端 IP 解析失败后的兜底。 private func registerDeviceIdChannel(_ engineBridge: FlutterImplicitEngineBridge) { let channel = FlutterMethodChannel( name: "com.stormg.lumadrama/device", binaryMessenger: engineBridge.applicationRegistrar.messenger() ) channel.setMethodCallHandler { call, result in - guard call.method == "idfv" else { + switch call.method { + case "idfv": + result(UIDevice.current.identifierForVendor?.uuidString) + case "region": + if #available(iOS 16.0, *) { + result(Locale.current.region?.identifier) + } else { + result(Locale.current.regionCode) + } + default: result(FlutterMethodNotImplemented) - return } - result(UIDevice.current.identifierForVendor?.uuidString) } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 80b4716..1e27100 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -48,7 +48,7 @@ NSIncludesSubdomains - 10.43.47.1 + 192.168.31.243 NSExceptionAllowsInsecureHTTPLoads diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index d9ed463..391fcaa 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -14,7 +14,7 @@ class AppConfig { /// 开发调试默认服务地址,当前指向本机局域网 IP。 /// /// 换网络时只改这一行,地址不要带结尾斜杠。 - static const String devHostBaseUrl = 'http://10.43.47.1:8081'; + static const String devHostBaseUrl = 'http://192.168.31.243:8081'; /// 本机后端地址:通过 `adb reverse tcp:8081 tcp:8081` 映射到开发机, /// 探测可达时 App 直连本机服务;不可达时回落到公网地址。 diff --git a/lib/core/data/remote/auth_api.dart b/lib/core/data/remote/auth_api.dart index b452aec..603e181 100644 --- a/lib/core/data/remote/auth_api.dart +++ b/lib/core/data/remote/auth_api.dart @@ -56,6 +56,7 @@ class AuthApi { required String target, required String code, required String deviceId, + String countryCode = '', String platform = '', String osVersion = '', String appVersion = '', @@ -66,6 +67,7 @@ class AuthApi { 'targetType': 'PHONE', 'target': target, 'code': code, + 'countryCode': countryCode, 'deviceId': deviceId, 'platform': platform, 'osVersion': osVersion, @@ -79,6 +81,7 @@ class AuthApi { required String email, required String password, required String deviceId, + String countryCode = '', String platform = '', String osVersion = '', String appVersion = '', @@ -89,7 +92,7 @@ class AuthApi { 'targetType': 'EMAIL', 'target': email, 'password': password, - 'countryCode': 'ID', + 'countryCode': countryCode, 'deviceId': deviceId, 'platform': platform, 'osVersion': osVersion, diff --git a/lib/core/data/services/auth_controller.dart b/lib/core/data/services/auth_controller.dart index 7364098..12ed578 100644 --- a/lib/core/data/services/auth_controller.dart +++ b/lib/core/data/services/auth_controller.dart @@ -48,6 +48,7 @@ class AuthController extends ChangeNotifier { email: email, password: password, deviceId: await _store.deviceId(), + countryCode: await _store.deviceRegion(), platform: _platform, osVersion: _osVersion, ); @@ -95,6 +96,7 @@ class AuthController extends ChangeNotifier { target: target, code: code, deviceId: await _store.deviceId(), + countryCode: await _store.deviceRegion(), platform: _platform, osVersion: _osVersion, ); diff --git a/lib/core/data/services/auth_session_store.dart b/lib/core/data/services/auth_session_store.dart index cf178b7..26174e8 100644 --- a/lib/core/data/services/auth_session_store.dart +++ b/lib/core/data/services/auth_session_store.dart @@ -30,6 +30,7 @@ class AuthSessionStore { static const _kDeviceId = 'device.id'; String? _cachedDeviceId; + String? _cachedRegion; Future deviceId() async { if (_cachedDeviceId != null) return _cachedDeviceId!; @@ -39,6 +40,14 @@ class AuthSessionStore { return value; } + /// 设备区域(ISO-2),取不到返回空串;只在注册时作为服务端 IP 解析失败后的兜底。 + Future deviceRegion() async { + if (_cachedRegion != null) return _cachedRegion!; + final value = await _deviceIdProvider.deviceRegion() ?? ''; + _cachedRegion = value; + return value; + } + /// 系统标识取不到时的兜底:本地 UUIDv4,生成一次后固定下来。 Future _localDeviceId() async { final stored = await _preferences.getString(_kDeviceId) ?? ''; diff --git a/lib/core/data/services/device_id_provider.dart b/lib/core/data/services/device_id_provider.dart index ab702db..34f790c 100644 --- a/lib/core/data/services/device_id_provider.dart +++ b/lib/core/data/services/device_id_provider.dart @@ -33,4 +33,18 @@ class DeviceIdProvider { } return null; } + + /// 设备所在区域(ISO-2),仅用于服务端 IP 解析失败时兜底注册国家。 + /// Android 依次取网络运营商 / SIM 卡 / 系统语言的国家,iOS 取系统 region。 + Future deviceRegion() async { + if (kIsWeb) return null; + try { + final region = await _channel.invokeMethod('region'); + final value = region?.trim().toUpperCase() ?? ''; + if (value.length == 2) return value; + } catch (_) { + // 原生通道不可用(widget 测试)时返回 null,由服务端兜底 + } + return null; + } } diff --git a/lib/core/formatting/account_display.dart b/lib/core/formatting/account_display.dart new file mode 100644 index 0000000..0a4a410 --- /dev/null +++ b/lib/core/formatting/account_display.dart @@ -0,0 +1,239 @@ +// Author: fruit.com.cn + +/// 账号资料页只读字段的展示格式化。 +/// +/// 后端存的是 ISO 国家码、BCP-47 语言标签和 IANA 时区(`ID` / `id-ID` / `Asia/Jakarta`), +/// 直接展示对用户没有意义,这里统一转成当前界面语言的名称。 +/// 国家与城市名只维护中文与英文两套:界面语言为印尼语时回退英文名, +/// 语言名与账号状态则三种界面语言都有。 +library; + +const _countryNamesZh = { + 'ID': '印度尼西亚', + 'MY': '马来西亚', + 'SG': '新加坡', + 'TH': '泰国', + 'VN': '越南', + 'PH': '菲律宾', + 'KH': '柬埔寨', + 'MM': '缅甸', + 'LA': '老挝', + 'BN': '文莱', + 'CN': '中国', + 'TW': '中国台湾', + 'HK': '中国香港', + 'MO': '中国澳门', + 'JP': '日本', + 'KR': '韩国', + 'IN': '印度', + 'PK': '巴基斯坦', + 'BD': '孟加拉国', + 'LK': '斯里兰卡', + 'NP': '尼泊尔', + 'US': '美国', + 'CA': '加拿大', + 'MX': '墨西哥', + 'BR': '巴西', + 'AR': '阿根廷', + 'CL': '智利', + 'CO': '哥伦比亚', + 'PE': '秘鲁', + 'VE': '委内瑞拉', + 'EC': '厄瓜多尔', + 'GT': '危地马拉', + 'DO': '多米尼加', + 'GB': '英国', + 'DE': '德国', + 'FR': '法国', + 'ES': '西班牙', + 'IT': '意大利', + 'NL': '荷兰', + 'PL': '波兰', + 'RU': '俄罗斯', + 'UA': '乌克兰', + 'TR': '土耳其', + 'SA': '沙特阿拉伯', + 'AE': '阿联酋', + 'IL': '以色列', + 'EG': '埃及', + 'NG': '尼日利亚', + 'ZA': '南非', + 'KE': '肯尼亚', + 'MA': '摩洛哥', + 'AU': '澳大利亚', + 'NZ': '新西兰', +}; + +const _countryNamesEn = { + 'ID': 'Indonesia', + 'MY': 'Malaysia', + 'SG': 'Singapore', + 'TH': 'Thailand', + 'VN': 'Vietnam', + 'PH': 'Philippines', + 'KH': 'Cambodia', + 'MM': 'Myanmar', + 'LA': 'Laos', + 'BN': 'Brunei', + 'CN': 'China', + 'TW': 'Taiwan', + 'HK': 'Hong Kong', + 'MO': 'Macau', + 'JP': 'Japan', + 'KR': 'South Korea', + 'IN': 'India', + 'PK': 'Pakistan', + 'BD': 'Bangladesh', + 'LK': 'Sri Lanka', + 'NP': 'Nepal', + 'US': 'United States', + 'CA': 'Canada', + 'MX': 'Mexico', + 'BR': 'Brazil', + 'AR': 'Argentina', + 'CL': 'Chile', + 'CO': 'Colombia', + 'PE': 'Peru', + 'VE': 'Venezuela', + 'EC': 'Ecuador', + 'GT': 'Guatemala', + 'DO': 'Dominican Republic', + 'GB': 'United Kingdom', + 'DE': 'Germany', + 'FR': 'France', + 'ES': 'Spain', + 'IT': 'Italy', + 'NL': 'Netherlands', + 'PL': 'Poland', + 'RU': 'Russia', + 'UA': 'Ukraine', + 'TR': 'Turkey', + 'SA': 'Saudi Arabia', + 'AE': 'United Arab Emirates', + 'IL': 'Israel', + 'EG': 'Egypt', + 'NG': 'Nigeria', + 'ZA': 'South Africa', + 'KE': 'Kenya', + 'MA': 'Morocco', + 'AU': 'Australia', + 'NZ': 'New Zealand', +}; + +const _timezoneNamesZh = { + 'Asia/Jakarta': '雅加达', + 'Asia/Kuala_Lumpur': '吉隆坡', + 'Asia/Singapore': '新加坡', + 'Asia/Bangkok': '曼谷', + 'Asia/Ho_Chi_Minh': '胡志明市', + 'Asia/Manila': '马尼拉', + 'Asia/Phnom_Penh': '金边', + 'Asia/Yangon': '仰光', + 'Asia/Vientiane': '万象', + 'Asia/Brunei': '斯里巴加湾市', + 'Asia/Shanghai': '上海', + 'Asia/Taipei': '台北', + 'Asia/Hong_Kong': '香港', + 'Asia/Macau': '澳门', + 'Asia/Tokyo': '东京', + 'Asia/Seoul': '首尔', + 'Asia/Kolkata': '加尔各答', + 'Asia/Karachi': '卡拉奇', + 'Asia/Dhaka': '达卡', + 'Asia/Colombo': '科伦坡', + 'Asia/Kathmandu': '加德满都', + 'America/New_York': '纽约', + 'America/Toronto': '多伦多', + 'America/Mexico_City': '墨西哥城', + 'America/Sao_Paulo': '圣保罗', + 'America/Argentina/Buenos_Aires': '布宜诺斯艾利斯', + 'America/Santiago': '圣地亚哥', + 'America/Bogota': '波哥大', + 'America/Lima': '利马', + 'America/Caracas': '加拉加斯', + 'America/Guayaquil': '瓜亚基尔', + 'America/Guatemala': '危地马拉城', + 'America/Santo_Domingo': '圣多明各', + 'Europe/London': '伦敦', + 'Europe/Berlin': '柏林', + 'Europe/Paris': '巴黎', + 'Europe/Madrid': '马德里', + 'Europe/Rome': '罗马', + 'Europe/Amsterdam': '阿姆斯特丹', + 'Europe/Warsaw': '华沙', + 'Europe/Moscow': '莫斯科', + 'Europe/Kyiv': '基辅', + 'Europe/Istanbul': '伊斯坦布尔', + 'Asia/Riyadh': '利雅得', + 'Asia/Dubai': '迪拜', + 'Asia/Jerusalem': '耶路撒冷', + 'Africa/Cairo': '开罗', + 'Africa/Lagos': '拉各斯', + 'Africa/Johannesburg': '约翰内斯堡', + 'Africa/Nairobi': '内罗毕', + 'Africa/Casablanca': '卡萨布兰卡', + 'Australia/Sydney': '悉尼', + 'Pacific/Auckland': '奥克兰', +}; + +const _languageNames = { + 'id': {'zh': '印度尼西亚语', 'en': 'Indonesian', 'id': 'Bahasa Indonesia'}, + 'en': {'zh': '英语', 'en': 'English', 'id': 'Inggris'}, + 'zh': {'zh': '中文', 'en': 'Chinese', 'id': 'Mandarin'}, +}; + +const _statusNames = { + 'normal': {'zh': '正常', 'en': 'Active', 'id': 'Aktif'}, + 'limited': {'zh': '受限', 'en': 'Limited', 'id': 'Dibatasi'}, + 'banned': {'zh': '已封禁', 'en': 'Banned', 'id': 'Diblokir'}, + 'deactivated': {'zh': '已注销', 'en': 'Deactivated', 'id': 'Dinonaktifkan'}, +}; + +String _pick(Map names, String localeName) => + names[localeName] ?? names['en']!; + +/// 国家码 → 国家名;未收录的码原样返回。 +String countryName(String code, String localeName) { + final key = code.trim().toUpperCase(); + if (key.isEmpty) return ''; + final names = localeName == 'zh' ? _countryNamesZh : _countryNamesEn; + return names[key] ?? key; +} + +/// IANA 时区 → 城市名;中文界面查表,其余语言取时区里的城市段。 +String timezoneName(String timezone, String localeName) { + final value = timezone.trim(); + if (value.isEmpty) return ''; + if (localeName == 'zh') { + final name = _timezoneNamesZh[value]; + if (name != null) return name; + } + final city = value.split('/').last.replaceAll('_', ' '); + return city.isEmpty ? value : city; +} + +/// BCP-47 语言标签 → 语言名,按主语言归类(`zh-TW` 也算中文)。 +String languageName(String language, String localeName) { + final value = language.trim(); + if (value.isEmpty) return ''; + final primary = value.split(RegExp('[-_]')).first.toLowerCase(); + final names = _languageNames[primary]; + return names == null ? value : _pick(names, localeName); +} + +/// 账号状态 → 展示文案;未知状态原样返回。 +String accountStatusText(String status, String localeName) { + final value = status.trim().toLowerCase(); + final names = _statusNames[value]; + return names == null ? status : _pick(names, localeName); +} + +/// VIP 等级:0 表示未开通,避免用户看到一个孤零零的 0。 +String vipLevelText(int level, String localeName) { + if (level > 0) return 'VIP $level'; + return switch (localeName) { + 'zh' => '未开通', + 'id' => 'Belum aktif', + _ => 'Not subscribed', + }; +} diff --git a/lib/pages/account/account_profile_page.dart b/lib/pages/account/account_profile_page.dart index 87b15d4..d9994a8 100644 --- a/lib/pages/account/account_profile_page.dart +++ b/lib/pages/account/account_profile_page.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import '../../core/data/models/account_profile.dart'; import '../../core/data/repositories/account_repository.dart'; import '../../core/data/services/auth_controller.dart'; +import '../../core/formatting/account_display.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; @@ -225,6 +226,7 @@ class _AccountProfilePageState extends State { ); Widget _readOnlyCard(AccountProfile profile, AppLocalizations l10n) { + final localeName = Localizations.localeOf(context).languageCode; return _AccountCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -238,13 +240,34 @@ class _AccountProfilePageState extends State { ), const SizedBox(height: 10), _info(l10n.accountMaskedAccount, profile.maskedTarget), - _info(l10n.accountCountry, profile.countryCode), + _info( + l10n.accountCountry, + countryName(profile.countryCode, localeName), + ), _info(l10n.accountCurrency, profile.currency), - _info(l10n.accountStatus, profile.status), - _info(l10n.accountLanguage, profile.language), - _info(l10n.accountTimezone, profile.timezone), - _info(l10n.accountRegion, profile.regionCode), - _info(l10n.accountVipLevel, '${profile.vipLevel}'), + _info( + l10n.accountStatus, + accountStatusText(profile.status, localeName), + ), + _info( + l10n.accountLanguage, + languageName(profile.language, localeName), + ), + _info( + l10n.accountTimezone, + timezoneName(profile.timezone, localeName), + ), + // region 与 country 后端写的是同一个值,重复时不再单列一行 + if (profile.regionCode.isNotEmpty && + profile.regionCode != profile.countryCode) + _info( + l10n.accountRegion, + countryName(profile.regionCode, localeName), + ), + _info( + l10n.accountVipLevel, + vipLevelText(profile.vipLevel, localeName), + ), if (profile.createdAt != null) _info( l10n.accountCreatedAt, @@ -275,7 +298,7 @@ class _AccountProfilePageState extends State { ), Flexible( child: Text( - value.isEmpty ? '-' : value, + value.isEmpty ? '—' : value, textAlign: TextAlign.right, style: const TextStyle(fontSize: AppDimens.body), ), diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index 3c57d6c..18ab58d 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -6,8 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import '../../core/data/models/drama.dart'; +import '../../core/data/models/watch_progress.dart'; import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/services/auth_controller.dart'; +import '../../core/data/services/catalog_controller.dart'; import '../../core/data/services/favorite_controller.dart'; import '../../core/data/services/locale_controller.dart'; import '../../core/data/services/wallet_controller.dart'; @@ -22,6 +25,7 @@ import '../downloads/downloads_page.dart'; import '../account/account_profile_page.dart'; import '../filter/filter_page.dart'; import '../notices/notices_page.dart'; +import '../player/open_player.dart'; import '../settings/settings_page.dart'; import '../store/vip_page.dart'; import '../wallet/wallet_page.dart'; @@ -222,6 +226,7 @@ class _MyPageState extends State { const SizedBox(height: AppDimens.mineSectionGap), MineContinueWatching( records: continueWatching, + onOpenRecord: _openContinueRecord, onOpenAll: _openHistoryTab, ), ], @@ -294,6 +299,36 @@ class _MyPageState extends State { unawaited(_openHistoryPage()); } + /// 继续观看卡片:补齐剧集后直接进全屏播放页,播放器自己会定位到记录的秒数。 + Future _openContinueRecord(WatchProgress record) async { + final catalog = context.read(); + // 续看记录只有剧的概要信息,先拼一个占位对象让目录去取完整详情 + final summary = Drama( + id: record.dramaId, + title: record.dramaTitle ?? '', + synopsis: '', + posterUrl: record.dramaCover ?? '', + ); + Drama drama; + try { + drama = await catalog.requireDetail(summary); + } catch (_) { + if (!mounted) return; + final l10n = AppLocalizations.of(context)!; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.detailLoadFailed))); + return; + } + if (!mounted) return; + await playDramaFromCatalog( + context, + drama, + episodeId: record.episodeId.isEmpty ? null : record.episodeId, + ); + if (mounted) unawaited(logic.refreshRecent()); + } + Future _openHistoryPage() async { await _push(const WatchRecordsPage()); if (mounted) unawaited(logic.refreshRecent()); diff --git a/lib/pages/mine/widgets/mine_continue_watching.dart b/lib/pages/mine/widgets/mine_continue_watching.dart index ac06b13..b6c2c06 100644 --- a/lib/pages/mine/widgets/mine_continue_watching.dart +++ b/lib/pages/mine/widgets/mine_continue_watching.dart @@ -12,10 +12,14 @@ class MineContinueWatching extends StatelessWidget { const MineContinueWatching({ super.key, required this.records, + required this.onOpenRecord, required this.onOpenAll, }); final List records; + + /// 单张卡片:直接进入该剧的播放页,并定位到记录的集。 + final ValueChanged onOpenRecord; final VoidCallback onOpenAll; @override @@ -94,7 +98,11 @@ class MineContinueWatching extends StatelessWidget { ); } - Widget _card(AppLocalizations l10n, WatchProgress record, String languageCode) { + Widget _card( + AppLocalizations l10n, + WatchProgress record, + String languageCode, + ) { final title = record.localizedTitle(languageCode).trim(); final episode = record.episodeNumber; final hasProgress = record.duration > Duration.zero; @@ -103,7 +111,7 @@ class MineContinueWatching extends StatelessWidget { child: InkWell( key: Key('mine-continue-${record.dramaId}'), borderRadius: BorderRadius.circular(AppDimens.homeCardRadius), - onTap: onOpenAll, + onTap: () => onOpenRecord(record), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/test/account/account_display_test.dart b/test/account/account_display_test.dart new file mode 100644 index 0000000..2b95e4b --- /dev/null +++ b/test/account/account_display_test.dart @@ -0,0 +1,40 @@ +// Author: fruit.com.cn + +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/formatting/account_display.dart'; + +void main() { + test('should_localize_country_name', () { + expect(countryName('ID', 'zh'), '印度尼西亚'); + expect(countryName('us', 'en'), 'United States'); + expect(countryName('TH', 'id'), 'Thailand'); + // 未收录的码原样返回,避免展示成空白 + expect(countryName('ZZ', 'zh'), 'ZZ'); + expect(countryName('', 'zh'), ''); + }); + + test('should_localize_timezone_to_city', () { + expect(timezoneName('Asia/Jakarta', 'zh'), '雅加达'); + expect(timezoneName('Asia/Jakarta', 'en'), 'Jakarta'); + expect( + timezoneName('America/Argentina/Buenos_Aires', 'en'), + 'Buenos Aires', + ); + expect(timezoneName('', 'zh'), ''); + }); + + test('should_localize_language_by_primary_tag', () { + expect(languageName('id-ID', 'zh'), '印度尼西亚语'); + expect(languageName('zh-TW', 'en'), 'Chinese'); + expect(languageName('en-US', 'id'), 'Inggris'); + expect(languageName('', 'zh'), ''); + }); + + test('should_localize_status_and_vip_level', () { + expect(accountStatusText('normal', 'zh'), '正常'); + expect(accountStatusText('banned', 'id'), 'Diblokir'); + expect(accountStatusText('weird', 'zh'), 'weird'); + expect(vipLevelText(0, 'zh'), '未开通'); + expect(vipLevelText(2, 'zh'), 'VIP 2'); + }); +} diff --git a/test/core/device_id_test.dart b/test/core/device_id_test.dart index 1ef86dc..699b1b0 100644 --- a/test/core/device_id_test.dart +++ b/test/core/device_id_test.dart @@ -8,9 +8,10 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p /// 可控的系统设备标识来源。 class _FakeDeviceIdProvider implements DeviceIdProvider { - _FakeDeviceIdProvider(this.value); + _FakeDeviceIdProvider(this.value, {this.region}); final String? value; + final String? region; int calls = 0; @override @@ -18,6 +19,9 @@ class _FakeDeviceIdProvider implements DeviceIdProvider { calls++; return value; } + + @override + Future deviceRegion() async => region; } void main() { @@ -62,4 +66,16 @@ void main() { expect(provider.calls, 1); }); + + test('should_return_device_region_or_empty_string', () async { + final store = AuthSessionStore( + deviceIdProvider: _FakeDeviceIdProvider(null, region: 'TH'), + ); + final missing = AuthSessionStore( + deviceIdProvider: _FakeDeviceIdProvider(null), + ); + + expect(await store.deviceRegion(), 'TH'); + expect(await missing.deviceRegion(), ''); + }); } diff --git a/test/mine/mine_preview_layout_test.dart b/test/mine/mine_preview_layout_test.dart index 2c9cfcc..7e8fe57 100644 --- a/test/mine/mine_preview_layout_test.dart +++ b/test/mine/mine_preview_layout_test.dart @@ -437,6 +437,24 @@ void main() { expect(find.byKey(const Key('following-history-grid')), findsOneWidget); }); + testWidgets('should_open_player_when_tapping_continue_watching_card', ( + tester, + ) async { + await _pumpApp( + tester, + signedIn: true, + history: InMemoryWatchHistoryRepository(seed: [_recent]), + ); + await _openMine(tester); + + await tester.tap(find.byKey(const Key('mine-continue-city-lights-demo'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('player-scaffold')), findsOneWidget); + // 继续观看直接进播放页,不应该先落到历史列表 + expect(find.byKey(const Key('following-page')), findsNothing); + }); + testWidgets('should_open_vip_from_mine_without_floating_player', ( tester, ) async {