feat(auth): 登录页改为 ymb 品牌并支持后台配置背景图
This commit is contained in:
1 parent
22cc669bbe
commit
e7399fa3fd
24 files changed
+378
-188
No files matched your search
@@ -81,12 +81,15 @@ Android 模拟器访问宿主机时,同样在调试条里填 `http://10.0.2.2:
|
|||||||
|
|
||||||
### iOS
|
### iOS
|
||||||
|
|
||||||
手机与 Mac 需在同一网络,首次启动允许“本地网络”权限:
|
iOS 模拟器直接访问 Mac 本机服务,默认地址为 `http://127.0.0.1:8081`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter run -d <udid>
|
flutter run -d <simulator-udid>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
iPhone 真机不能用 `127.0.0.1` 访问 Mac;真机联调时需让手机与 Mac 位于同一网络,
|
||||||
|
并通过调试入口临时填写 Mac 的局域网地址。
|
||||||
|
|
||||||
### 服务地址配置
|
### 服务地址配置
|
||||||
|
|
||||||
地址是代码里的一个开关,**不做任何网络探测**,也不用传构建参数。两套环境各占
|
地址是代码里的一个开关,**不做任何网络探测**,也不用传构建参数。两套环境各占
|
||||||
@@ -94,7 +97,7 @@ flutter run -d <udid>
|
|||||||
|
|
||||||
| 文件 | 用途 | 默认值 |
|
| 文件 | 用途 | 默认值 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `lib/core/config/dev_config.dart` | 开发 / 联调 | `http://192.168.31.243:8081` |
|
| `lib/core/config/dev_config.dart` | 开发 / 联调 | `http://127.0.0.1:8081` |
|
||||||
| `lib/core/config/prod_config.dart` | 生产 | `http://39.162.50.99:8081` |
|
| `lib/core/config/prod_config.dart` | 生产 | `http://39.162.50.99:8081` |
|
||||||
|
|
||||||
切换环境只改 `lib/core/config/app_config.dart` 里的一个布尔值:
|
切换环境只改 `lib/core/config/app_config.dart` 里的一个布尔值:
|
||||||
@@ -103,7 +106,7 @@ flutter run -d <udid>
|
|||||||
static const bool useDevConfig = true; // false 即用生产地址
|
static const bool useDevConfig = true; // false 即用生产地址
|
||||||
```
|
```
|
||||||
|
|
||||||
换 Wi-Fi 或换网段时只改 `dev_config.dart` 的 `baseUrl`。打生产包前把
|
真机或 Android 模拟器联调时,通过调试入口覆盖地址,不修改默认开发配置。打生产包前把
|
||||||
`useDevConfig` 改成 `false`;release 构建若仍为 `true` 会在启动时直接报错,
|
`useDevConfig` 改成 `false`;release 构建若仍为 `true` 会在启动时直接报错,
|
||||||
避免线上包连到开发机。
|
避免线上包连到开发机。
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'core/data/remote/catalog_api.dart';
|
|||||||
import 'core/data/remote/category_api.dart';
|
import 'core/data/remote/category_api.dart';
|
||||||
import 'core/data/remote/comment_api.dart';
|
import 'core/data/remote/comment_api.dart';
|
||||||
import 'core/data/remote/commerce_api.dart';
|
import 'core/data/remote/commerce_api.dart';
|
||||||
|
import 'core/data/remote/config_api.dart';
|
||||||
import 'core/data/remote/device_profile.dart';
|
import 'core/data/remote/device_profile.dart';
|
||||||
import 'core/data/remote/download_api.dart';
|
import 'core/data/remote/download_api.dart';
|
||||||
import 'core/data/remote/interaction_api.dart';
|
import 'core/data/remote/interaction_api.dart';
|
||||||
@@ -35,6 +36,7 @@ import 'core/data/repositories/playlist_repository.dart';
|
|||||||
import 'core/data/repositories/search_repository.dart';
|
import 'core/data/repositories/search_repository.dart';
|
||||||
import 'core/data/repositories/store_repository.dart';
|
import 'core/data/repositories/store_repository.dart';
|
||||||
import 'core/data/repositories/watch_history_repository.dart';
|
import 'core/data/repositories/watch_history_repository.dart';
|
||||||
|
import 'core/data/services/app_config_controller.dart';
|
||||||
import 'core/data/services/auth_controller.dart';
|
import 'core/data/services/auth_controller.dart';
|
||||||
import 'core/data/services/auth_session_store.dart';
|
import 'core/data/services/auth_session_store.dart';
|
||||||
import 'core/data/services/catalog_controller.dart';
|
import 'core/data/services/catalog_controller.dart';
|
||||||
@@ -208,6 +210,11 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
|||||||
|
|
||||||
late final LocaleController _localeController = LocaleController();
|
late final LocaleController _localeController = LocaleController();
|
||||||
|
|
||||||
|
/// 后台下发的启动配置(登录页背景等);拉不到就用本地默认。
|
||||||
|
late final AppConfigController _appConfigController = AppConfigController(
|
||||||
|
ConfigApi(_apiClient),
|
||||||
|
);
|
||||||
|
|
||||||
/// 登录期内共享的「已解锁收费集」:播放页进出、feed 与全屏切换都要沿用。
|
/// 登录期内共享的「已解锁收费集」:播放页进出、feed 与全屏切换都要沿用。
|
||||||
late final UnlockedEpisodeRegistry _unlockedEpisodes =
|
late final UnlockedEpisodeRegistry _unlockedEpisodes =
|
||||||
UnlockedEpisodeRegistry();
|
UnlockedEpisodeRegistry();
|
||||||
@@ -218,6 +225,7 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
|||||||
// 恢复本地令牌与设备号;未登录也能正常浏览,不阻塞首屏
|
// 恢复本地令牌与设备号;未登录也能正常浏览,不阻塞首屏
|
||||||
_auth.load();
|
_auth.load();
|
||||||
unawaited(_localeController.load());
|
unawaited(_localeController.load());
|
||||||
|
unawaited(_appConfigController.load());
|
||||||
// 登录/登出时同步收藏状态:收藏属于账号,不能跨账号串联
|
// 登录/登出时同步收藏状态:收藏属于账号,不能跨账号串联
|
||||||
_auth.addListener(_onAuthChanged);
|
_auth.addListener(_onAuthChanged);
|
||||||
unawaited(_favoriteController.load());
|
unawaited(_favoriteController.load());
|
||||||
@@ -252,6 +260,7 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
|||||||
_walletController.dispose();
|
_walletController.dispose();
|
||||||
_interactionController.dispose();
|
_interactionController.dispose();
|
||||||
_localeController.dispose();
|
_localeController.dispose();
|
||||||
|
_appConfigController.dispose();
|
||||||
_apiClient.close();
|
_apiClient.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -304,6 +313,7 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
|||||||
ChangeNotifierProvider.value(value: _walletController),
|
ChangeNotifierProvider.value(value: _walletController),
|
||||||
ChangeNotifierProvider.value(value: _interactionController),
|
ChangeNotifierProvider.value(value: _interactionController),
|
||||||
ChangeNotifierProvider.value(value: _localeController),
|
ChangeNotifierProvider.value(value: _localeController),
|
||||||
|
ChangeNotifierProvider.value(value: _appConfigController),
|
||||||
ChangeNotifierProvider.value(value: _auth),
|
ChangeNotifierProvider.value(value: _auth),
|
||||||
],
|
],
|
||||||
child: Consumer<LocaleController>(
|
child: Consumer<LocaleController>(
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
/// 开发环境配置。
|
/// 开发环境配置。
|
||||||
///
|
///
|
||||||
/// 本机联调用:换 Wi-Fi、换网段后只改这里的 [baseUrl],地址不要带结尾斜杠。
|
/// 本机联调用:模拟器与桌面端可直接用回环地址,但 iOS/Android 真机必须用
|
||||||
|
/// 开发机的局域网 IP(真机上的 127.0.0.1 指向手机自己)。地址不要带结尾斜杠。
|
||||||
abstract final class DevConfig {
|
abstract final class DevConfig {
|
||||||
/// 开发机后端地址。
|
/// 开发机后端地址(Mac 当前 Wi-Fi 地址,换网络后需要同步修改)。
|
||||||
static const String baseUrl = 'http://192.168.31.243:8081';
|
static const String baseUrl = 'http://192.168.31.243:8081';
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'api_client.dart';
|
||||||
|
|
||||||
|
/// 启动配置:`GET /app/config`(匿名,后台只下发白名单键)。
|
||||||
|
class ConfigApi {
|
||||||
|
const ConfigApi(this._client);
|
||||||
|
|
||||||
|
final ApiClient _client;
|
||||||
|
|
||||||
|
/// 白名单配置表;结构异常时返回空表,调用方按本地默认处理。
|
||||||
|
Future<Map<String, dynamic>> appConfig() async {
|
||||||
|
final data = await _client.get('/app/config');
|
||||||
|
return data is Map<String, dynamic> ? data : const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../remote/config_api.dart';
|
||||||
|
|
||||||
|
/// 后台下发的启动配置。
|
||||||
|
///
|
||||||
|
/// 失败一律静默:拿不到就用 App 内置默认,不阻塞启动、不弹错。
|
||||||
|
class AppConfigController extends ChangeNotifier {
|
||||||
|
AppConfigController(this._api);
|
||||||
|
|
||||||
|
final ConfigApi _api;
|
||||||
|
|
||||||
|
String _loginBackgroundUrl = '';
|
||||||
|
|
||||||
|
/// 登录页整屏背景图;空串表示用本地默认图。
|
||||||
|
String get loginBackgroundUrl => _loginBackgroundUrl;
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
try {
|
||||||
|
final config = await _api.appConfig();
|
||||||
|
final url = (config['loginBackgroundUrl'] as String? ?? '').trim();
|
||||||
|
if (url == _loginBackgroundUrl) return;
|
||||||
|
_loginBackgroundUrl = url;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('[config] load failed: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ class RemoteImage extends StatelessWidget {
|
|||||||
this.fallbackIconKey,
|
this.fallbackIconKey,
|
||||||
this.fallbackAspectRatio,
|
this.fallbackAspectRatio,
|
||||||
this.fallbackColor = AppColors.raised,
|
this.fallbackColor = AppColors.raised,
|
||||||
|
this.fallbackWidget,
|
||||||
this.decodeWidth,
|
this.decodeWidth,
|
||||||
this.decodeHeight,
|
this.decodeHeight,
|
||||||
});
|
});
|
||||||
@@ -47,6 +48,9 @@ class RemoteImage extends StatelessWidget {
|
|||||||
final Key? fallbackIconKey;
|
final Key? fallbackIconKey;
|
||||||
final double? fallbackAspectRatio;
|
final double? fallbackAspectRatio;
|
||||||
final Color fallbackColor;
|
final Color fallbackColor;
|
||||||
|
|
||||||
|
/// 自定义兜底:地址为空、加载中、加载失败时都显示它(优先于 [fallbackColor])。
|
||||||
|
final Widget? fallbackWidget;
|
||||||
final int? decodeWidth;
|
final int? decodeWidth;
|
||||||
final int? decodeHeight;
|
final int? decodeHeight;
|
||||||
|
|
||||||
@@ -77,9 +81,9 @@ class RemoteImage extends StatelessWidget {
|
|||||||
// cover/contain 本身会保持宽高比,此时若同时强制 cacheWidth 和
|
// cover/contain 本身会保持宽高比,此时若同时强制 cacheWidth 和
|
||||||
// cacheHeight,解码会把竖版图强行拉成横版像素。只在显式指定了
|
// cacheHeight,解码会把竖版图强行拉成横版像素。只在显式指定了
|
||||||
// decodeHeight、或 fit 不保持宽高比时才限定解码高度。
|
// decodeHeight、或 fit 不保持宽高比时才限定解码高度。
|
||||||
final keepsAspectRatio =
|
final keepsAspectRatio = fit == BoxFit.cover || fit == BoxFit.contain;
|
||||||
fit == BoxFit.cover || fit == BoxFit.contain;
|
final cacheHeight =
|
||||||
final cacheHeight = decodeHeight ??
|
decodeHeight ??
|
||||||
(keepsAspectRatio || inferredHeight == null
|
(keepsAspectRatio || inferredHeight == null
|
||||||
? null
|
? null
|
||||||
: resolveImageDecodePixels(inferredHeight, pixelRatio));
|
: resolveImageDecodePixels(inferredHeight, pixelRatio));
|
||||||
@@ -106,6 +110,8 @@ class RemoteImage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _fallback({required bool brand}) {
|
Widget _fallback({required bool brand}) {
|
||||||
|
final custom = fallbackWidget;
|
||||||
|
if (custom != null) return custom;
|
||||||
final mark = brand
|
final mark = brand
|
||||||
? LayoutBuilder(
|
? LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
|||||||
+6
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
"appName": "FruitShorts",
|
"appName": "ymb",
|
||||||
"demo": "DEMO",
|
"demo": "DEMO",
|
||||||
"homeHeadline": "Watch now",
|
"homeHeadline": "Watch now",
|
||||||
"homeSubtitle": "Free short drama preview.",
|
"homeSubtitle": "Free short drama preview.",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"searchCancel": "Cancel",
|
"searchCancel": "Cancel",
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"recommended": "Recommended",
|
"recommended": "Recommended",
|
||||||
"fruitShortsBrand": "FruitShorts",
|
"fruitShortsBrand": "ymb",
|
||||||
"comingSoon": "Coming soon",
|
"comingSoon": "Coming soon",
|
||||||
"authLegal": "By continuing, you agree to the Terms of Service and Privacy Policy.",
|
"authLegal": "By continuing, you agree to the Terms of Service and Privacy Policy.",
|
||||||
"authLegalLead": "By continuing, you agree to the",
|
"authLegalLead": "By continuing, you agree to the",
|
||||||
@@ -408,8 +408,8 @@
|
|||||||
"genreDramaMystery": "Drama · Mystery",
|
"genreDramaMystery": "Drama · Mystery",
|
||||||
"continueWatchingHeading": "CONTINUE WATCHING",
|
"continueWatchingHeading": "CONTINUE WATCHING",
|
||||||
"preferencesHeading": "PREFERENCES",
|
"preferencesHeading": "PREFERENCES",
|
||||||
"brandMark": "FRUITSHORTS",
|
"brandMark": "YMB",
|
||||||
"shareFooter": "FruitShorts",
|
"shareFooter": "ymb",
|
||||||
"shortsTab": "Shorts",
|
"shortsTab": "Shorts",
|
||||||
"myListTab": "My List",
|
"myListTab": "My List",
|
||||||
"watchFull": "Watch Full",
|
"watchFull": "Watch Full",
|
||||||
@@ -789,7 +789,7 @@
|
|||||||
"offlineDownloads": "Downloads",
|
"offlineDownloads": "Downloads",
|
||||||
"watchPreferences": "Watch preferences",
|
"watchPreferences": "Watch preferences",
|
||||||
"contactSupport": "Contact support",
|
"contactSupport": "Contact support",
|
||||||
"vipBrandName": "FruitShorts VIP",
|
"vipBrandName": "VIP",
|
||||||
"vipOpenNow": "Get VIP",
|
"vipOpenNow": "Get VIP",
|
||||||
"vipBenefitFree": "Free full access",
|
"vipBenefitFree": "Free full access",
|
||||||
"vipBenefitNoAds": "Ad-free viewing",
|
"vipBenefitNoAds": "Ad-free viewing",
|
||||||
@@ -851,6 +851,7 @@
|
|||||||
"dataUnavailable": "—",
|
"dataUnavailable": "—",
|
||||||
"commonQuestions": "Common questions",
|
"commonQuestions": "Common questions",
|
||||||
"aboutUs": "About us",
|
"aboutUs": "About us",
|
||||||
|
"aboutBrandName": "Your mini business",
|
||||||
"aboutDescription": "A focused, immersive place to watch short dramas.",
|
"aboutDescription": "A focused, immersive place to watch short dramas.",
|
||||||
"versionLabel": "Version {version}",
|
"versionLabel": "Version {version}",
|
||||||
"@versionLabel": {
|
"@versionLabel": {
|
||||||
|
|||||||
+6
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "id",
|
"@@locale": "id",
|
||||||
"appName": "FruitShorts",
|
"appName": "ymb",
|
||||||
"demo": "Demo",
|
"demo": "Demo",
|
||||||
"homeHeadline": "Tonton sekarang",
|
"homeHeadline": "Tonton sekarang",
|
||||||
"homeSubtitle": "Pratinjau drama pendek gratis.",
|
"homeSubtitle": "Pratinjau drama pendek gratis.",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"searchCancel": "Batal",
|
"searchCancel": "Batal",
|
||||||
"featured": "Unggulan",
|
"featured": "Unggulan",
|
||||||
"recommended": "Rekomendasi",
|
"recommended": "Rekomendasi",
|
||||||
"fruitShortsBrand": "FruitShorts",
|
"fruitShortsBrand": "ymb",
|
||||||
"comingSoon": "Segera hadir",
|
"comingSoon": "Segera hadir",
|
||||||
"authLegal": "Dengan melanjutkan, Anda menyetujui Ketentuan Layanan dan Kebijakan Privasi.",
|
"authLegal": "Dengan melanjutkan, Anda menyetujui Ketentuan Layanan dan Kebijakan Privasi.",
|
||||||
"authLegalLead": "Dengan melanjutkan, Anda menyetujui",
|
"authLegalLead": "Dengan melanjutkan, Anda menyetujui",
|
||||||
@@ -387,8 +387,8 @@
|
|||||||
"genreDramaMystery": "Drama · Misteri",
|
"genreDramaMystery": "Drama · Misteri",
|
||||||
"continueWatchingHeading": "LANJUT TONTON",
|
"continueWatchingHeading": "LANJUT TONTON",
|
||||||
"preferencesHeading": "PREFERENSI",
|
"preferencesHeading": "PREFERENSI",
|
||||||
"brandMark": "FRUITSHORTS",
|
"brandMark": "YMB",
|
||||||
"shareFooter": "FruitShorts",
|
"shareFooter": "ymb",
|
||||||
"shortsTab": "Drama Pendek",
|
"shortsTab": "Drama Pendek",
|
||||||
"myListTab": "Daftar Saya",
|
"myListTab": "Daftar Saya",
|
||||||
"watchFull": "Tonton Penuh",
|
"watchFull": "Tonton Penuh",
|
||||||
@@ -751,7 +751,7 @@
|
|||||||
"offlineDownloads": "Unduhan",
|
"offlineDownloads": "Unduhan",
|
||||||
"watchPreferences": "Preferensi tontonan",
|
"watchPreferences": "Preferensi tontonan",
|
||||||
"contactSupport": "Hubungi dukungan",
|
"contactSupport": "Hubungi dukungan",
|
||||||
"vipBrandName": "FruitShorts VIP",
|
"vipBrandName": "VIP",
|
||||||
"vipOpenNow": "Buka VIP",
|
"vipOpenNow": "Buka VIP",
|
||||||
"vipBenefitFree": "Nonton semua gratis",
|
"vipBenefitFree": "Nonton semua gratis",
|
||||||
"vipBenefitNoAds": "Bebas iklan",
|
"vipBenefitNoAds": "Bebas iklan",
|
||||||
@@ -813,6 +813,7 @@
|
|||||||
"dataUnavailable": "—",
|
"dataUnavailable": "—",
|
||||||
"commonQuestions": "Pertanyaan umum",
|
"commonQuestions": "Pertanyaan umum",
|
||||||
"aboutUs": "Tentang kami",
|
"aboutUs": "Tentang kami",
|
||||||
|
"aboutBrandName": "Your mini business",
|
||||||
"aboutDescription": "Tempat fokus dan imersif untuk menonton drama pendek.",
|
"aboutDescription": "Tempat fokus dan imersif untuk menonton drama pendek.",
|
||||||
"versionLabel": "Versi {version}",
|
"versionLabel": "Versi {version}",
|
||||||
"@versionLabel": {
|
"@versionLabel": {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @appName.
|
/// No description provided for @appName.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'FruitShorts'**
|
/// **'ymb'**
|
||||||
String get appName;
|
String get appName;
|
||||||
|
|
||||||
/// No description provided for @demo.
|
/// No description provided for @demo.
|
||||||
@@ -217,7 +217,7 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @fruitShortsBrand.
|
/// No description provided for @fruitShortsBrand.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'FruitShorts'**
|
/// **'ymb'**
|
||||||
String get fruitShortsBrand;
|
String get fruitShortsBrand;
|
||||||
|
|
||||||
/// No description provided for @comingSoon.
|
/// No description provided for @comingSoon.
|
||||||
@@ -1627,13 +1627,13 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @brandMark.
|
/// No description provided for @brandMark.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'FRUITSHORTS'**
|
/// **'YMB'**
|
||||||
String get brandMark;
|
String get brandMark;
|
||||||
|
|
||||||
/// No description provided for @shareFooter.
|
/// No description provided for @shareFooter.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'FruitShorts'**
|
/// **'ymb'**
|
||||||
String get shareFooter;
|
String get shareFooter;
|
||||||
|
|
||||||
/// No description provided for @shortsTab.
|
/// No description provided for @shortsTab.
|
||||||
@@ -3439,7 +3439,7 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @vipBrandName.
|
/// No description provided for @vipBrandName.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'FruitShorts VIP'**
|
/// **'VIP'**
|
||||||
String get vipBrandName;
|
String get vipBrandName;
|
||||||
|
|
||||||
/// No description provided for @vipOpenNow.
|
/// No description provided for @vipOpenNow.
|
||||||
@@ -3592,6 +3592,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'About us'**
|
/// **'About us'**
|
||||||
String get aboutUs;
|
String get aboutUs;
|
||||||
|
|
||||||
|
/// No description provided for @aboutBrandName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your mini business'**
|
||||||
|
String get aboutBrandName;
|
||||||
|
|
||||||
/// No description provided for @aboutDescription.
|
/// No description provided for @aboutDescription.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appName => 'FruitShorts';
|
String get appName => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get demo => 'DEMO';
|
String get demo => 'DEMO';
|
||||||
@@ -67,7 +67,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get recommended => 'Recommended';
|
String get recommended => 'Recommended';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fruitShortsBrand => 'FruitShorts';
|
String get fruitShortsBrand => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get comingSoon => 'Coming soon';
|
String get comingSoon => 'Coming soon';
|
||||||
@@ -831,10 +831,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get preferencesHeading => 'PREFERENCES';
|
String get preferencesHeading => 'PREFERENCES';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get brandMark => 'FRUITSHORTS';
|
String get brandMark => 'YMB';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get shareFooter => 'FruitShorts';
|
String get shareFooter => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get shortsTab => 'Shorts';
|
String get shortsTab => 'Shorts';
|
||||||
@@ -1795,7 +1795,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get contactSupport => 'Contact support';
|
String get contactSupport => 'Contact support';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get vipBrandName => 'FruitShorts VIP';
|
String get vipBrandName => 'VIP';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get vipOpenNow => 'Get VIP';
|
String get vipOpenNow => 'Get VIP';
|
||||||
@@ -1886,6 +1886,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get aboutUs => 'About us';
|
String get aboutUs => 'About us';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get aboutBrandName => 'Your mini business';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get aboutDescription =>
|
String get aboutDescription =>
|
||||||
'A focused, immersive place to watch short dramas.';
|
'A focused, immersive place to watch short dramas.';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
AppLocalizationsId([String locale = 'id']) : super(locale);
|
AppLocalizationsId([String locale = 'id']) : super(locale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appName => 'FruitShorts';
|
String get appName => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get demo => 'Demo';
|
String get demo => 'Demo';
|
||||||
@@ -67,7 +67,7 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
String get recommended => 'Rekomendasi';
|
String get recommended => 'Rekomendasi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fruitShortsBrand => 'FruitShorts';
|
String get fruitShortsBrand => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get comingSoon => 'Segera hadir';
|
String get comingSoon => 'Segera hadir';
|
||||||
@@ -834,10 +834,10 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
String get preferencesHeading => 'PREFERENSI';
|
String get preferencesHeading => 'PREFERENSI';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get brandMark => 'FRUITSHORTS';
|
String get brandMark => 'YMB';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get shareFooter => 'FruitShorts';
|
String get shareFooter => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get shortsTab => 'Drama Pendek';
|
String get shortsTab => 'Drama Pendek';
|
||||||
@@ -1797,7 +1797,7 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
String get contactSupport => 'Hubungi dukungan';
|
String get contactSupport => 'Hubungi dukungan';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get vipBrandName => 'FruitShorts VIP';
|
String get vipBrandName => 'VIP';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get vipOpenNow => 'Buka VIP';
|
String get vipOpenNow => 'Buka VIP';
|
||||||
@@ -1888,6 +1888,9 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get aboutUs => 'Tentang kami';
|
String get aboutUs => 'Tentang kami';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get aboutBrandName => 'Your mini business';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get aboutDescription =>
|
String get aboutDescription =>
|
||||||
'Tempat fokus dan imersif untuk menonton drama pendek.';
|
'Tempat fokus dan imersif untuk menonton drama pendek.';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appName => 'FruitShorts';
|
String get appName => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get demo => '演示';
|
String get demo => '演示';
|
||||||
@@ -66,7 +66,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get recommended => '推荐';
|
String get recommended => '推荐';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fruitShortsBrand => 'FruitShorts';
|
String get fruitShortsBrand => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get comingSoon => '即将上线';
|
String get comingSoon => '即将上线';
|
||||||
@@ -821,10 +821,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get preferencesHeading => '偏好设置';
|
String get preferencesHeading => '偏好设置';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get brandMark => 'FRUITSHORTS';
|
String get brandMark => 'YMB';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get shareFooter => 'FruitShorts';
|
String get shareFooter => 'ymb';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get shortsTab => '短剧';
|
String get shortsTab => '短剧';
|
||||||
@@ -1770,7 +1770,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get contactSupport => '联系客服';
|
String get contactSupport => '联系客服';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get vipBrandName => 'FruitShorts VIP';
|
String get vipBrandName => 'VIP';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get vipOpenNow => '立即开通';
|
String get vipOpenNow => '立即开通';
|
||||||
@@ -1861,6 +1861,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get aboutUs => '关于我们';
|
String get aboutUs => '关于我们';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get aboutBrandName => 'Your mini business';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get aboutDescription => '专注于短剧观看与沉浸式追剧体验。';
|
String get aboutDescription => '专注于短剧观看与沉浸式追剧体验。';
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "zh",
|
"@@locale": "zh",
|
||||||
"appName": "FruitShorts",
|
"appName": "ymb",
|
||||||
"demo": "演示",
|
"demo": "演示",
|
||||||
"homeHeadline": "即刻观看",
|
"homeHeadline": "即刻观看",
|
||||||
"homeSubtitle": "免费短剧预览。",
|
"homeSubtitle": "免费短剧预览。",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"searchCancel": "取消",
|
"searchCancel": "取消",
|
||||||
"featured": "精选",
|
"featured": "精选",
|
||||||
"recommended": "推荐",
|
"recommended": "推荐",
|
||||||
"fruitShortsBrand": "FruitShorts",
|
"fruitShortsBrand": "ymb",
|
||||||
"comingSoon": "即将上线",
|
"comingSoon": "即将上线",
|
||||||
"authLegal": "继续即代表同意《用户协议》与《隐私政策》",
|
"authLegal": "继续即代表同意《用户协议》与《隐私政策》",
|
||||||
"authLegalLead": "继续即代表同意",
|
"authLegalLead": "继续即代表同意",
|
||||||
@@ -349,8 +349,8 @@
|
|||||||
"genreDramaMystery": "剧情 · 悬疑",
|
"genreDramaMystery": "剧情 · 悬疑",
|
||||||
"continueWatchingHeading": "继续观看",
|
"continueWatchingHeading": "继续观看",
|
||||||
"preferencesHeading": "偏好设置",
|
"preferencesHeading": "偏好设置",
|
||||||
"brandMark": "FRUITSHORTS",
|
"brandMark": "YMB",
|
||||||
"shareFooter": "FruitShorts",
|
"shareFooter": "ymb",
|
||||||
"shortsTab": "短剧",
|
"shortsTab": "短剧",
|
||||||
"myListTab": "片单",
|
"myListTab": "片单",
|
||||||
"watchFull": "看全集",
|
"watchFull": "看全集",
|
||||||
@@ -713,7 +713,7 @@
|
|||||||
"offlineDownloads": "离线缓存",
|
"offlineDownloads": "离线缓存",
|
||||||
"watchPreferences": "观看偏好",
|
"watchPreferences": "观看偏好",
|
||||||
"contactSupport": "联系客服",
|
"contactSupport": "联系客服",
|
||||||
"vipBrandName": "FruitShorts VIP",
|
"vipBrandName": "VIP",
|
||||||
"vipOpenNow": "立即开通",
|
"vipOpenNow": "立即开通",
|
||||||
"vipBenefitFree": "全剧免费看",
|
"vipBenefitFree": "全剧免费看",
|
||||||
"vipBenefitNoAds": "免广告打扰",
|
"vipBenefitNoAds": "免广告打扰",
|
||||||
@@ -775,6 +775,7 @@
|
|||||||
"dataUnavailable": "—",
|
"dataUnavailable": "—",
|
||||||
"commonQuestions": "常见问题",
|
"commonQuestions": "常见问题",
|
||||||
"aboutUs": "关于我们",
|
"aboutUs": "关于我们",
|
||||||
|
"aboutBrandName": "Your mini business",
|
||||||
"aboutDescription": "专注于短剧观看与沉浸式追剧体验。",
|
"aboutDescription": "专注于短剧观看与沉浸式追剧体验。",
|
||||||
"versionLabel": "版本 {version}",
|
"versionLabel": "版本 {version}",
|
||||||
"@versionLabel": {
|
"@versionLabel": {
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ class _AuthBrand extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final copy = _brandCopy(context, centered: !compact);
|
final copy = _brandCopy(centered: !compact);
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return Row(
|
return Row(
|
||||||
key: const Key('auth-brand'),
|
key: const Key('auth-brand'),
|
||||||
@@ -324,32 +324,14 @@ class _AuthBrand extends StatelessWidget {
|
|||||||
radius: compact ? _compactBrandMarkRadius : _brandMarkRadius,
|
radius: compact ? _compactBrandMarkRadius : _brandMarkRadius,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _brandCopy(BuildContext context, {required bool centered}) => Column(
|
Widget _brandCopy({required bool centered}) => Text(
|
||||||
crossAxisAlignment: centered
|
tagline,
|
||||||
? CrossAxisAlignment.center
|
textAlign: centered ? TextAlign.center : TextAlign.start,
|
||||||
: CrossAxisAlignment.start,
|
style: const TextStyle(
|
||||||
children: [
|
color: AppColors.muted,
|
||||||
Text(
|
fontSize: 13,
|
||||||
AppLocalizations.of(context)!.appName,
|
height: 1.35,
|
||||||
textAlign: centered ? TextAlign.center : TextAlign.start,
|
),
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.onSurface,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
height: 1.1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
tagline,
|
|
||||||
textAlign: centered ? TextAlign.center : TextAlign.start,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.muted,
|
|
||||||
fontSize: 13,
|
|
||||||
height: 1.35,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../../core/data/remote/api_client.dart';
|
import '../../core/data/remote/api_client.dart';
|
||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
|
import '../../core/data/services/app_config_controller.dart';
|
||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/platform/haptics.dart';
|
import '../../core/platform/haptics.dart';
|
||||||
import '../../core/platform/keyboard_inset.dart';
|
import '../../core/platform/keyboard_inset.dart';
|
||||||
@@ -14,6 +15,7 @@ import '../../core/theme/app_colors.dart';
|
|||||||
import '../../core/theme/app_dimens.dart';
|
import '../../core/theme/app_dimens.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../core/widgets/centered_toast.dart';
|
import '../../core/widgets/centered_toast.dart';
|
||||||
|
import '../../core/widgets/remote_image.dart';
|
||||||
import '../../core/widgets/subpage_header.dart';
|
import '../../core/widgets/subpage_header.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import 'auth_legal_links.dart';
|
import 'auth_legal_links.dart';
|
||||||
@@ -28,7 +30,7 @@ const double _brandMarkSize = 88;
|
|||||||
const double _compactBrandMarkSize = 48;
|
const double _compactBrandMarkSize = 48;
|
||||||
const double _brandMarkRadius = 26;
|
const double _brandMarkRadius = 26;
|
||||||
const double _compactBrandMarkRadius = 14;
|
const double _compactBrandMarkRadius = 14;
|
||||||
const String _appLogoAsset = 'assets/icon/fruitshorts.png';
|
const String _appLogoAsset = 'assets/icon/ymb.png';
|
||||||
const double _contentMaxWidth = 440;
|
const double _contentMaxWidth = 440;
|
||||||
const double _fieldHeight = 52;
|
const double _fieldHeight = 52;
|
||||||
const double _fieldRadius = 12;
|
const double _fieldRadius = 12;
|
||||||
@@ -276,9 +278,7 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 全屏海报背景:压暗保证文字可读,顶部再叠品牌红光。
|
// 全屏海报背景:压暗保证文字可读,顶部再叠品牌红光。
|
||||||
Positioned.fill(
|
Positioned.fill(child: IgnorePointer(child: _AuthPosterBackdrop())),
|
||||||
child: IgnorePointer(child: _AuthPosterBackdrop()),
|
|
||||||
),
|
|
||||||
const Positioned.fill(
|
const Positioned.fill(
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
@@ -396,80 +396,67 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
AppLocalizations l10n, {
|
AppLocalizations l10n, {
|
||||||
required bool compact,
|
required bool compact,
|
||||||
required double minHeight,
|
required double minHeight,
|
||||||
}) =>
|
}) => TweenAnimationBuilder<double>(
|
||||||
TweenAnimationBuilder<double>(
|
key: const Key('auth-enter-animation'),
|
||||||
key: const Key('auth-enter-animation'),
|
tween: Tween(begin: 0, end: 1),
|
||||||
tween: Tween(begin: 0, end: 1),
|
duration: _enterDuration,
|
||||||
duration: _enterDuration,
|
curve: Curves.easeOutCubic,
|
||||||
curve: Curves.easeOutCubic,
|
builder: (context, value, child) => Opacity(
|
||||||
builder: (context, value, child) => Opacity(
|
opacity: value,
|
||||||
opacity: value,
|
child: Transform.translate(
|
||||||
child: Transform.translate(
|
offset: Offset(0, _enterSlideDistance * (1 - value)),
|
||||||
offset: Offset(0, _enterSlideDistance * (1 - value)),
|
child: child,
|
||||||
child: child,
|
),
|
||||||
),
|
),
|
||||||
),
|
child: _emailEntryOpen
|
||||||
child: _emailEntryOpen
|
? Column(
|
||||||
? Column(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
children: [
|
||||||
children: [
|
_AuthBrand(tagline: l10n.authBrandTagline, compact: compact),
|
||||||
_AuthBrand(tagline: l10n.authBrandTagline, compact: compact),
|
SizedBox(height: compact ? _compactBrandGap : 28),
|
||||||
SizedBox(height: compact ? _compactBrandGap : 28),
|
_authCard(l10n),
|
||||||
_authCard(l10n),
|
],
|
||||||
],
|
)
|
||||||
)
|
: _landing(l10n, minHeight: minHeight),
|
||||||
: _landing(l10n, minHeight: minHeight),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
/// 落地页内容需要有限高度才能用 Spacer 三段分割,这里显式钉住可用高度。
|
/// 落地页内容需要有限高度才能用 Spacer 三段分割,这里显式钉住可用高度。
|
||||||
Widget _landing(AppLocalizations l10n, {required double minHeight}) =>
|
Widget _landing(AppLocalizations l10n, {required double minHeight}) =>
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: minHeight,
|
height: minHeight,
|
||||||
child: Column(
|
child: Column(
|
||||||
key: const Key('auth-landing'),
|
key: const Key('auth-landing'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Spacer(flex: 5),
|
const Spacer(flex: 5),
|
||||||
Center(
|
Center(
|
||||||
child: _brandMarkWidget(
|
child: _brandMarkWidget(
|
||||||
key: const Key('auth-brand-logo'),
|
key: const Key('auth-brand-logo'),
|
||||||
size: _brandMarkSize,
|
size: _brandMarkSize,
|
||||||
radius: _brandMarkRadius,
|
radius: _brandMarkRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(flex: 2),
|
||||||
|
_AuthEntryButton(
|
||||||
|
key: const Key('auth-entry-phone'),
|
||||||
|
label: l10n.authPhoneSignIn,
|
||||||
|
icon: Icons.phone_android_rounded,
|
||||||
|
primary: true,
|
||||||
|
onTap: _openPhoneSheet,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_AuthEntryButton(
|
||||||
|
key: const Key('auth-entry-email'),
|
||||||
|
label: l10n.authEmailSignIn,
|
||||||
|
icon: Icons.mail_outline_rounded,
|
||||||
|
primary: false,
|
||||||
|
onTap: () => setState(() => _emailEntryOpen = true),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
const Opacity(opacity: 0.75, child: AuthLegalLinks()),
|
||||||
|
const Spacer(flex: 6),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Text(
|
|
||||||
l10n.fruitShortsBrand,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'Outfit',
|
|
||||||
color: AppColors.onSurface,
|
|
||||||
fontSize: 26,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
letterSpacing: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(flex: 3),
|
|
||||||
_AuthEntryButton(
|
|
||||||
key: const Key('auth-entry-phone'),
|
|
||||||
label: l10n.authPhoneSignIn,
|
|
||||||
icon: Icons.phone_android_rounded,
|
|
||||||
primary: true,
|
|
||||||
onTap: _openPhoneSheet,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_AuthEntryButton(
|
|
||||||
key: const Key('auth-entry-email'),
|
|
||||||
label: l10n.authEmailSignIn,
|
|
||||||
icon: Icons.mail_outline_rounded,
|
|
||||||
primary: false,
|
|
||||||
onTap: () => setState(() => _emailEntryOpen = true),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
const Opacity(opacity: 0.75, child: AuthLegalLinks()),
|
|
||||||
const Spacer(flex: 6),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _openPhoneSheet() async {
|
Future<void> _openPhoneSheet() async {
|
||||||
@@ -578,16 +565,26 @@ class _AuthEntryButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 登录页全屏海报背景(单张,无拼缝)。
|
/// 登录页全屏海报背景(单张,无拼缝)。
|
||||||
|
///
|
||||||
|
/// 优先用后台下发的 `loginBackgroundUrl`(后台未配置或加载失败时回落到本地默认图)。
|
||||||
class _AuthPosterBackdrop extends StatelessWidget {
|
class _AuthPosterBackdrop extends StatelessWidget {
|
||||||
const _AuthPosterBackdrop();
|
const _AuthPosterBackdrop();
|
||||||
|
|
||||||
|
static const String _defaultAsset = 'assets/auth/bg_3.jpg';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Image.asset(
|
final remoteUrl = context.watch<AppConfigController>().loginBackgroundUrl;
|
||||||
|
return RemoteImage(
|
||||||
key: const Key('auth-poster-backdrop'),
|
key: const Key('auth-poster-backdrop'),
|
||||||
'assets/auth/bg_3.jpg',
|
remoteUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.center,
|
fallbackWidget: Image.asset(
|
||||||
|
_defaultAsset,
|
||||||
|
key: const Key('auth-poster-default'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ class MineAboutPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
l10n.appName,
|
l10n.aboutBrandName,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Outfit',
|
fontFamily: 'Outfit',
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
|
|||||||
|
|
||||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
||||||
|
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
||||||
import 'package:fruitshorts/core/platform/haptics.dart';
|
import 'package:fruitshorts/core/platform/haptics.dart';
|
||||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||||
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
||||||
|
|
||||||
|
import '../support/fake_app_config.dart';
|
||||||
import '../support/fake_haptics.dart';
|
import '../support/fake_haptics.dart';
|
||||||
import '../support/fake_http_adapter.dart';
|
import '../support/fake_http_adapter.dart';
|
||||||
|
|
||||||
@@ -71,6 +73,9 @@ Future<void> _pumpAuth(WidgetTester tester) async {
|
|||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider<AuthController>.value(value: auth),
|
ChangeNotifierProvider<AuthController>.value(value: auth),
|
||||||
|
ChangeNotifierProvider<AppConfigController>.value(
|
||||||
|
value: await fakeAppConfigController(),
|
||||||
|
),
|
||||||
Provider<Haptics>.value(value: FakeHaptics()),
|
Provider<Haptics>.value(value: FakeHaptics()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
|
|||||||
|
|
||||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
||||||
|
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
||||||
import 'package:fruitshorts/core/platform/haptics.dart';
|
import 'package:fruitshorts/core/platform/haptics.dart';
|
||||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||||
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
||||||
|
|
||||||
|
import '../support/fake_app_config.dart';
|
||||||
import '../support/fake_haptics.dart';
|
import '../support/fake_haptics.dart';
|
||||||
import '../support/fake_http_adapter.dart';
|
import '../support/fake_http_adapter.dart';
|
||||||
|
|
||||||
@@ -36,6 +38,9 @@ void main() {
|
|||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider<AuthController>.value(value: auth),
|
ChangeNotifierProvider<AuthController>.value(value: auth),
|
||||||
|
ChangeNotifierProvider<AppConfigController>.value(
|
||||||
|
value: await fakeAppConfigController(),
|
||||||
|
),
|
||||||
Provider<Haptics>.value(value: FakeHaptics()),
|
Provider<Haptics>.value(value: FakeHaptics()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
|
|||||||
import 'package:fruitshorts/core/data/models/auth_session.dart';
|
import 'package:fruitshorts/core/data/models/auth_session.dart';
|
||||||
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
||||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
|
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
||||||
import 'package:fruitshorts/core/platform/haptics.dart';
|
import 'package:fruitshorts/core/platform/haptics.dart';
|
||||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||||
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
||||||
|
|
||||||
|
import '../support/fake_app_config.dart';
|
||||||
import '../support/fake_http_adapter.dart';
|
import '../support/fake_http_adapter.dart';
|
||||||
import '../support/fake_haptics.dart';
|
import '../support/fake_haptics.dart';
|
||||||
|
|
||||||
@@ -29,17 +31,17 @@ void main() {
|
|||||||
// 不打桩这个通道调用会一直等不到回复(提交按钮转圈、pumpAndSettle 超时)。
|
// 不打桩这个通道调用会一直等不到回复(提交按钮转圈、pumpAndSettle 超时)。
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(
|
.setMockMethodCallHandler(
|
||||||
const MethodChannel('com.stormg.lumadrama/device'),
|
const MethodChannel('com.stormg.lumadrama/device'),
|
||||||
(call) async => call.method == 'region' ? 'ID' : null,
|
(call) async => call.method == 'region' ? 'ID' : null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(
|
.setMockMethodCallHandler(
|
||||||
const MethodChannel('com.stormg.lumadrama/device'),
|
const MethodChannel('com.stormg.lumadrama/device'),
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> pumpAuthPage(
|
Future<void> pumpAuthPage(
|
||||||
@@ -65,6 +67,9 @@ void main() {
|
|||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider<AuthController>.value(value: auth),
|
ChangeNotifierProvider<AuthController>.value(value: auth),
|
||||||
|
ChangeNotifierProvider<AppConfigController>.value(
|
||||||
|
value: await fakeAppConfigController(),
|
||||||
|
),
|
||||||
Provider<Haptics>.value(value: haptics ?? FakeHaptics()),
|
Provider<Haptics>.value(value: haptics ?? FakeHaptics()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
@@ -212,7 +217,7 @@ void main() {
|
|||||||
) async {
|
) async {
|
||||||
await pumpAuthPage(tester);
|
await pumpAuthPage(tester);
|
||||||
|
|
||||||
expect(find.text('FruitShorts'), findsOneWidget);
|
expect(find.text('ymb'), findsNothing);
|
||||||
expect(find.text('Short dramas made for Indonesia'), findsOneWidget);
|
expect(find.text('Short dramas made for Indonesia'), findsOneWidget);
|
||||||
final email = tester.widget<TextField>(find.byKey(const Key('auth-email')));
|
final email = tester.widget<TextField>(find.byKey(const Key('auth-email')));
|
||||||
final password = tester.widget<TextField>(
|
final password = tester.widget<TextField>(
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
|
|||||||
import 'package:fruitshorts/core/data/models/auth_session.dart';
|
import 'package:fruitshorts/core/data/models/auth_session.dart';
|
||||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
import 'package:fruitshorts/core/data/remote/auth_api.dart';
|
||||||
|
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
||||||
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
|
||||||
import 'package:fruitshorts/core/platform/haptics.dart';
|
import 'package:fruitshorts/core/platform/haptics.dart';
|
||||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||||
|
import 'package:fruitshorts/core/widgets/remote_image.dart';
|
||||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||||
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
||||||
|
|
||||||
|
import '../support/fake_app_config.dart';
|
||||||
import '../support/fake_haptics.dart';
|
import '../support/fake_haptics.dart';
|
||||||
import '../support/fake_http_adapter.dart';
|
import '../support/fake_http_adapter.dart';
|
||||||
|
|
||||||
@@ -29,6 +32,7 @@ void main() {
|
|||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
AuthApi? api,
|
AuthApi? api,
|
||||||
FakeHaptics? haptics,
|
FakeHaptics? haptics,
|
||||||
|
String loginBackgroundUrl = '',
|
||||||
bool openEmail = true,
|
bool openEmail = true,
|
||||||
bool asRoute = false,
|
bool asRoute = false,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -44,10 +48,14 @@ void main() {
|
|||||||
),
|
),
|
||||||
AuthSessionStore(),
|
AuthSessionStore(),
|
||||||
);
|
);
|
||||||
|
final appConfig = await fakeAppConfigController(
|
||||||
|
loginBackgroundUrl: loginBackgroundUrl,
|
||||||
|
);
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider<AuthController>.value(value: auth),
|
ChangeNotifierProvider<AuthController>.value(value: auth),
|
||||||
|
ChangeNotifierProvider<AppConfigController>.value(value: appConfig),
|
||||||
Provider<Haptics>.value(value: haptics ?? FakeHaptics()),
|
Provider<Haptics>.value(value: haptics ?? FakeHaptics()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
@@ -76,11 +84,8 @@ void main() {
|
|||||||
await pumpAuthPage(tester, openEmail: false);
|
await pumpAuthPage(tester, openEmail: false);
|
||||||
|
|
||||||
expect(find.byKey(const Key('auth-landing')), findsOneWidget);
|
expect(find.byKey(const Key('auth-landing')), findsOneWidget);
|
||||||
expect(find.text('FruitShorts'), findsOneWidget);
|
// 落地页只留图标,不再显示品牌文字。
|
||||||
final brand = tester.widget<Text>(find.text('FruitShorts'));
|
expect(find.text('ymb'), findsNothing);
|
||||||
expect(brand.style?.fontSize, 26);
|
|
||||||
expect(brand.style?.fontWeight, FontWeight.w700);
|
|
||||||
expect(brand.style?.letterSpacing, 2);
|
|
||||||
expect(
|
expect(
|
||||||
tester.getSize(find.byKey(const Key('auth-brand-logo'))),
|
tester.getSize(find.byKey(const Key('auth-brand-logo'))),
|
||||||
const Size(88, 88),
|
const Size(88, 88),
|
||||||
@@ -136,10 +141,7 @@ void main() {
|
|||||||
final email = tester.widget<TextField>(
|
final email = tester.widget<TextField>(
|
||||||
find.byKey(const Key('auth-email')),
|
find.byKey(const Key('auth-email')),
|
||||||
);
|
);
|
||||||
expect(
|
expect(email.decoration?.fillColor, Colors.white.withValues(alpha: 0.05));
|
||||||
email.decoration?.fillColor,
|
|
||||||
Colors.white.withValues(alpha: 0.05),
|
|
||||||
);
|
|
||||||
expect(email.style?.color, AppColors.onSurface);
|
expect(email.style?.color, AppColors.onSurface);
|
||||||
expect(email.style?.fontSize, 15);
|
expect(email.style?.fontSize, 15);
|
||||||
expect(email.cursorColor, AppColors.brandRed);
|
expect(email.cursorColor, AppColors.brandRed);
|
||||||
@@ -182,37 +184,63 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('should_use_backend_login_background_when_configured', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await pumpAuthPage(
|
||||||
|
tester,
|
||||||
|
openEmail: false,
|
||||||
|
loginBackgroundUrl: '/files/login_bg/bg.jpg',
|
||||||
|
);
|
||||||
|
|
||||||
|
final backdrop = tester.widget<RemoteImage>(
|
||||||
|
find.byKey(const Key('auth-poster-backdrop')),
|
||||||
|
);
|
||||||
|
expect(backdrop.path, '/files/login_bg/bg.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'should_show_app_logo_lower_on_the_page_when_landing_opens',
|
'should_fall_back_to_bundled_login_background_when_not_configured',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
tester.view.physicalSize = const Size(390, 844);
|
|
||||||
tester.view.devicePixelRatio = 1;
|
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
|
||||||
await pumpAuthPage(tester, openEmail: false);
|
await pumpAuthPage(tester, openEmail: false);
|
||||||
|
|
||||||
final image = tester.widget<Image>(
|
final backdrop = tester.widget<RemoteImage>(
|
||||||
find.descendant(
|
find.byKey(const Key('auth-poster-backdrop')),
|
||||||
of: find.byKey(const Key('auth-brand-logo')),
|
|
||||||
matching: find.byType(Image),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
expect(backdrop.path, isEmpty);
|
||||||
|
// 兜底图是我们塞给 RemoteImage 的 fallbackWidget,用 auth-poster-default 定位。
|
||||||
expect(
|
expect(
|
||||||
(image.image as AssetImage).assetName,
|
find.byKey(const Key('auth-poster-default')),
|
||||||
'assets/icon/fruitshorts.png',
|
findsOneWidget,
|
||||||
);
|
|
||||||
// 新版落地页用三等分 Spacer 排版,Logo 居中且落在两个入口按钮上方。
|
|
||||||
final logo = find.byKey(const Key('auth-brand-logo'));
|
|
||||||
expect(tester.getCenter(logo).dx, closeTo(195, 0.1));
|
|
||||||
expect(
|
|
||||||
tester.getBottomLeft(logo).dy,
|
|
||||||
lessThan(
|
|
||||||
tester.getTopLeft(find.byKey(const Key('auth-entry-phone'))).dy,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('should_show_app_logo_lower_on_the_page_when_landing_opens', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
await pumpAuthPage(tester, openEmail: false);
|
||||||
|
|
||||||
|
final image = tester.widget<Image>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('auth-brand-logo')),
|
||||||
|
matching: find.byType(Image),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect((image.image as AssetImage).assetName, 'assets/icon/ymb.png');
|
||||||
|
// 新版落地页用三等分 Spacer 排版,Logo 居中且落在两个入口按钮上方。
|
||||||
|
final logo = find.byKey(const Key('auth-brand-logo'));
|
||||||
|
expect(tester.getCenter(logo).dx, closeTo(195, 0.1));
|
||||||
|
expect(
|
||||||
|
tester.getBottomLeft(logo).dy,
|
||||||
|
lessThan(tester.getTopLeft(find.byKey(const Key('auth-entry-phone'))).dy),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_render_40px_back_button_when_page_is_pushed', (
|
testWidgets('should_render_40px_back_button_when_page_is_pushed', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -335,9 +363,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<GestureDetector>(
|
.widget<GestureDetector>(find.byKey(const Key('auth-mode-sign-up')))
|
||||||
find.byKey(const Key('auth-mode-sign-up')),
|
|
||||||
)
|
|
||||||
.onTap,
|
.onTap,
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ void main() {
|
|||||||
final prod = Uri.parse(ProdConfig.baseUrl);
|
final prod = Uri.parse(ProdConfig.baseUrl);
|
||||||
|
|
||||||
expect(dev.scheme, 'http');
|
expect(dev.scheme, 'http');
|
||||||
|
expect(dev.host, isNot(prod.host));
|
||||||
expect(dev.port, 8081);
|
expect(dev.port, 8081);
|
||||||
expect(dev.path, isEmpty);
|
expect(dev.path, isEmpty);
|
||||||
expect(prod.scheme, 'http');
|
expect(prod.scheme, 'http');
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
|
import 'package:fruitshorts/core/data/remote/config_api.dart';
|
||||||
|
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
|
||||||
|
|
||||||
|
import '../support/fake_http_adapter.dart';
|
||||||
|
|
||||||
|
AppConfigController _controller(FakeHttpAdapter adapter) => AppConfigController(
|
||||||
|
ConfigApi(ApiClient(baseUrl: 'http://127.0.0.1:1', adapter: adapter)),
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('should_use_backend_url_when_login_background_is_configured', () async {
|
||||||
|
final controller = _controller(
|
||||||
|
FakeHttpAdapter(
|
||||||
|
(_) async => FakeHttpResponse(
|
||||||
|
'{"code":0,"data":{"loginBackgroundUrl":"/files/login_bg/bg.jpg"}}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(controller.loginBackgroundUrl, '/files/login_bg/bg.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_treat_blank_value_as_not_configured', () async {
|
||||||
|
final controller = _controller(
|
||||||
|
FakeHttpAdapter(
|
||||||
|
(_) async =>
|
||||||
|
FakeHttpResponse('{"code":0,"data":{"loginBackgroundUrl":" "}}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(controller.loginBackgroundUrl, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_keep_local_default_when_config_request_fails', () async {
|
||||||
|
var notified = 0;
|
||||||
|
final controller = _controller(
|
||||||
|
FakeHttpAdapter((_) async => FakeHttpResponse('{}', 500)),
|
||||||
|
)..addListener(() => notified++);
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(controller.loginBackgroundUrl, isEmpty);
|
||||||
|
expect(notified, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
|
import 'package:fruitshorts/core/data/remote/config_api.dart';
|
||||||
|
import 'package:fruitshorts/core/data/services/app_config_controller.dart';
|
||||||
|
|
||||||
|
/// 直接 pump 登录页的测试需要 App 配置 Provider。
|
||||||
|
///
|
||||||
|
/// 只用内存里的结论、不发真实请求:widget 测试的 fake-async 下等待 dio
|
||||||
|
/// 的 HTTP 请求会挂住,所以这里换掉 [ConfigApi] 的实现。
|
||||||
|
Future<AppConfigController> fakeAppConfigController({
|
||||||
|
String loginBackgroundUrl = '',
|
||||||
|
}) async {
|
||||||
|
final controller = AppConfigController(
|
||||||
|
_FakeConfigApi(loginBackgroundUrl),
|
||||||
|
);
|
||||||
|
await controller.load();
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeConfigApi extends ConfigApi {
|
||||||
|
_FakeConfigApi(this.loginBackgroundUrl)
|
||||||
|
: super(ApiClient(baseUrl: 'http://127.0.0.1:1'));
|
||||||
|
|
||||||
|
final String loginBackgroundUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, dynamic>> appConfig() async => {
|
||||||
|
'loginBackgroundUrl': loginBackgroundUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -138,7 +138,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('tab-mine')));
|
await tester.tap(find.byKey(const Key('tab-mine')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Vicky sheng'), findsOneWidget);
|
expect(find.text('Vicky sheng'), findsOneWidget);
|
||||||
expect(find.text('FruitShorts VIP'), findsOneWidget);
|
expect(find.text('VIP'), findsOneWidget);
|
||||||
expect(find.byKey(const Key('mine-language')), findsOneWidget);
|
expect(find.byKey(const Key('mine-language')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('mine-wallet')), findsOneWidget);
|
expect(find.byKey(const Key('mine-wallet')), findsOneWidget);
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('player-rail-share')));
|
await tester.tap(find.byKey(const Key('player-rail-share')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(sharedText, contains('City Lights & Velvet Nights'));
|
expect(sharedText, contains('City Lights & Velvet Nights'));
|
||||||
expect(sharedText, contains('FruitShorts'));
|
expect(sharedText, contains('ymb'));
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user