From fecc5b0d270f5d635369ced3add4ea2c7a8907c4 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 19 Oct 2026 15:50:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=85=A5=E5=8F=A3=E4=B8=8E=E6=88=91=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/auth/auth_view.dart | 170 ++- lib/pages/mine/mine_view.dart | 1328 ++++++++++--------- test/account/mine_account_entry_test.dart | 2 + test/auth/auth_k21_test.dart | 3 + test/auth/auth_landing_test.dart | 65 + test/auth/auth_page_test.dart | 3 + test/auth/auth_visual_test.dart | 26 +- test/mine/mine_preview_layout_test.dart | 69 + test/notice/notices_entry_test.dart | 2 + test/watch/watch_history_provider_test.dart | 2 + 10 files changed, 1021 insertions(+), 649 deletions(-) create mode 100644 test/auth/auth_landing_test.dart create mode 100644 test/mine/mine_preview_layout_test.dart diff --git a/lib/pages/auth/auth_view.dart b/lib/pages/auth/auth_view.dart index 555d495..5ddbde0 100644 --- a/lib/pages/auth/auth_view.dart +++ b/lib/pages/auth/auth_view.dart @@ -60,6 +60,7 @@ class _AuthPageState extends State { late final FocusNode _emailFocus; late final FocusNode _passwordFocus; bool _obscurePassword = true; + bool _emailEntryOpen = false; @override void initState() { @@ -243,7 +244,13 @@ class _AuthPageState extends State { tooltip: MaterialLocalizations.of( context, ).backButtonTooltip, - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + if (_emailEntryOpen) { + setState(() => _emailEntryOpen = false); + } else { + Navigator.of(context).pop(); + } + }, icon: const Icon( Icons.arrow_back_rounded, color: AppColors.onPrimary, @@ -272,16 +279,107 @@ class _AuthPageState extends State { child: child, ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _AuthBrand(tagline: l10n.authBrandTagline, compact: compact), - SizedBox(height: compact ? _compactBrandGap : 28), - _authCard(l10n), - ], - ), + child: _emailEntryOpen + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _AuthBrand(tagline: l10n.authBrandTagline, compact: compact), + SizedBox(height: compact ? _compactBrandGap : 28), + _authCard(l10n), + ], + ) + : _landing(l10n), ); + Widget _landing(AppLocalizations l10n) => Column( + key: const Key('auth-landing'), + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + key: const Key('auth-brand-logo'), + width: 64, + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColors.blue, AppColors.secondary], + ), + ), + child: const Icon( + Icons.play_arrow_rounded, + color: AppColors.onPrimary, + size: 32, + ), + ), + ), + const SizedBox(height: 14), + Text( + l10n.fruitShortsBrand, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'Outfit', + color: AppColors.onSurface, + fontSize: 20, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 6), + Text( + l10n.authLandingSubtitle, + textAlign: TextAlign.center, + style: const TextStyle(color: Color(0xFF9A96A3), fontSize: 11), + ), + const SizedBox(height: 22), + _AuthEntryButton( + key: const Key('auth-entry-google'), + label: 'G ${l10n.authContinueGoogle}', + background: Colors.white, + foreground: const Color(0xFF1A1A1A), + leadingColor: const Color(0xFF4285F4), + onTap: () => _comingSoon(l10n), + ), + const SizedBox(height: 11), + _AuthEntryButton( + key: const Key('auth-entry-phone'), + label: l10n.authPhoneSignIn, + icon: Icons.phone_android_rounded, + background: AppColors.input, + foreground: AppColors.onSurface, + onTap: () => _comingSoon(l10n), + ), + const SizedBox(height: 11), + _AuthEntryButton( + key: const Key('auth-entry-email'), + label: l10n.authEmailSignIn, + icon: Icons.mail_outline_rounded, + background: AppColors.input, + foreground: AppColors.onSurface, + onTap: () => setState(() => _emailEntryOpen = true), + ), + const SizedBox(height: 18), + Text( + l10n.authLegal, + key: const Key('auth-legal'), + textAlign: TextAlign.center, + style: const TextStyle( + color: Color(0xFF6B6772), + fontSize: 9, + height: 1.6, + ), + ), + ], + ); + + void _comingSoon(AppLocalizations l10n) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.comingSoon))); + } + Widget _authCard(AppLocalizations l10n) => _AuthCard( state: _AuthCardState( l10n: l10n, @@ -320,3 +418,57 @@ class _AuthPageState extends State { setState(() => _logic.toggleMode()); } } + +class _AuthEntryButton extends StatelessWidget { + const _AuthEntryButton({ + super.key, + required this.label, + required this.background, + required this.foreground, + required this.onTap, + this.icon, + this.leadingColor, + }); + + final String label; + final Color background; + final Color foreground; + final VoidCallback onTap; + final IconData? icon; + final Color? leadingColor; + + @override + Widget build(BuildContext context) { + return Material( + color: background, + borderRadius: BorderRadius.circular(9), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: SizedBox( + height: 44, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) + Icon(icon, size: 16, color: leadingColor ?? foreground), + if (icon != null) const SizedBox(width: 7), + Flexible( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: foreground, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index 8091ab5..3a21c53 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -6,27 +6,28 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../core/formatting/localized_content.dart'; +import '../../core/config/locale_controller.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/interaction_controller.dart'; import '../../core/data/services/favorite_controller.dart'; +import '../../core/data/services/interaction_controller.dart'; import '../../core/formatting/error_text.dart'; -import '../../core/widgets/empty_state.dart'; -import '../../core/widgets/app_bottom_nav.dart'; -import '../../core/widgets/remote_image.dart'; +import '../../core/formatting/localized_content.dart'; import '../../core/theme/app_colors.dart'; -import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; +import '../../core/widgets/app_bottom_nav.dart'; +import '../../core/widgets/empty_state.dart'; +import '../../core/widgets/remote_image.dart'; import '../../l10n/app_localizations.dart'; -import 'mine_binding.dart'; -import 'mine_logic.dart'; -import '../watch_records/watch_records_page.dart'; +import '../account/account_entry_card.dart'; +import '../downloads/downloads_page.dart'; import '../favorites/favorites_page.dart'; import '../notices/notices_page.dart'; -import '../downloads/downloads_page.dart'; -import '../account/account_entry_card.dart'; +import '../watch_records/watch_records_page.dart'; +import 'mine_binding.dart'; +import 'mine_logic.dart'; class MyPage extends StatefulWidget { const MyPage({ @@ -117,40 +118,27 @@ class _MyPageState extends State { children: [ _topBar(l10n), Expanded( - child: FutureBuilder( - future: logic.position, - builder: (context, snapshot) { - final position = snapshot.data ?? Duration.zero; - final total = widget.duration > Duration.zero - ? widget.duration - : const Duration(seconds: 12); - final fraction = - (position.inMilliseconds / total.inMilliseconds).clamp( - 0.0, - 1.0, - ); - return ListView( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 20), - children: [ - if (widget.profile) ..._accountPage(l10n), - if (widget.profile) const SizedBox(height: 24), - _library(l10n, position, total, fraction), - if (!widget.profile) const SizedBox(height: 24), - if (!widget.profile) ..._accountPage(l10n), - const SizedBox(height: 24), - Center( - child: Text( - l10n.brandMark, - style: const TextStyle( - color: AppColors.muted, - fontSize: AppDimens.caption, - letterSpacing: 1.3, - ), - ), - ), - ], - ); - }, + child: ListView( + padding: const EdgeInsets.fromLTRB(14, 2, 14, 18), + children: [ + _userRow(l10n), + const SizedBox(height: 12), + _wallet(l10n), + if (_continueDrama != null) ...[ + const SizedBox(height: 12), + _continueCard(l10n, _continueDrama!, _continueRecord!), + ], + const SizedBox(height: 14), + _library(l10n), + const SizedBox(height: 14), + _languageRow(l10n), + if (widget.signedIn) ...[ + const SizedBox(height: 12), + const AccountEntryCard(), + ], + const SizedBox(height: 12), + _menuCard(l10n), + ], ), ), if (!widget.embedded) _bottomNav(l10n), @@ -166,124 +154,102 @@ class _MyPageState extends State { } Widget _topBar(AppLocalizations l10n) => SizedBox( - height: 54, + height: 46, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( children: [ - const Icon(Icons.play_circle_fill_rounded, color: _accent, size: 27), - const SizedBox(width: 7), + Container( + width: 24, + height: 24, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.blue, + borderRadius: BorderRadius.circular(7), + ), + child: const Icon( + Icons.play_arrow_rounded, + color: AppColors.onPrimary, + size: 16, + ), + ), + const SizedBox(width: 8), Text( - widget.profile ? l10n.myTab : l10n.myListTab, + l10n.myTab, style: const TextStyle( - fontSize: AppDimens.pageTitle, + fontFamily: 'Outfit', + color: AppColors.onSurface, + fontSize: 15, fontWeight: FontWeight.w800, ), ), const Spacer(), - IconButton( + _RoundHeaderButton( tooltip: l10n.search, - onPressed: () => logic.act( + icon: Icons.search_rounded, + onTap: () => logic.act( context, MyPageAction.discover, embedded: widget.embedded, onAction: widget.onAction, ), - icon: const Icon(Icons.search_rounded, size: 23), ), - const SizedBox(width: 6), - const CircleAvatar( - radius: 14, - backgroundColor: AppColors.raised, - child: Icon(Icons.person_rounded, color: Colors.white, size: 19), + const SizedBox(width: 7), + Container( + width: 28, + height: 28, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF6A5CFF), AppColors.blue], + ), + ), + child: const Icon( + Icons.person_rounded, + color: AppColors.onSurface, + size: 15, + ), ), ], ), ), ); - List _accountPage(AppLocalizations l10n) => [ - if (widget.signedIn) _signedInRow(l10n) else _guestRow(l10n), - if (widget.signedIn) ...[ - const SizedBox(height: 8), - _wallet(l10n), - const SizedBox(height: 4), - const AccountEntryCard(), - ], - const SizedBox(height: 4), - _menuCard(l10n), - const SizedBox(height: 8), - Text( - l10n.preferencesHeading, - style: const TextStyle( - color: AppColors.muted, - fontSize: AppDimens.caption, - fontWeight: FontWeight.w800, - letterSpacing: 1.1, - ), - ), - const SizedBox(height: 8), - ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.language_rounded, color: AppColors.blueSoft), - title: Text(l10n.appLanguage), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () => logic.showLanguage( - context, - showServerSetting: widget.debugShowSetting, - ), - ), - ]; - - Widget _guestRow(AppLocalizations l10n) { + Widget _userRow(AppLocalizations l10n) { + final user = widget.signedIn ? context.watch().user : null; + final name = widget.signedIn + ? ((user?.nickname.isNotEmpty ?? false) + ? user!.nickname + : l10n.localViewer) + : l10n.localViewer; + final subtitle = widget.signedIn + ? ((user?.uid.isNotEmpty ?? false) + ? 'ID: ${user!.uid}' + : l10n.myAccount) + : l10n.logInHint; return Row( + key: const Key('mine-user-row'), children: [ - const CircleAvatar( - radius: 28, - backgroundColor: AppColors.raised, - child: Icon(Icons.person_rounded, color: Colors.white, size: 30), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - l10n.logInHint, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF6A5CFF), AppColors.blue], + ), + ), + child: const Icon( + Icons.person_rounded, + color: AppColors.onSurface, + size: 22, ), ), - const SizedBox(width: 8), - TextButton( - onPressed: widget.onSignIn, - style: TextButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: AppColors.blue, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - shape: const StadiumBorder(), - ), - child: Text(l10n.logIn), - ), - ], - ); - } - - Widget _signedInRow(AppLocalizations l10n) { - // 显示真实登录账号(昵称 + uid);拿不到才回落到演示文案 - final user = context.watch().user; - final name = (user?.nickname.isNotEmpty ?? false) - ? user!.nickname - : l10n.mockUserName; - final uidText = (user?.uid.isNotEmpty ?? false) - ? 'ID: ${user!.uid}' - : l10n.mockUserId; - return Row( - children: [ - const CircleAvatar( - radius: 28, - backgroundColor: AppColors.raised, - child: Icon(Icons.person_rounded, color: Colors.white, size: 30), - ), - const SizedBox(width: 12), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -293,120 +259,498 @@ class _MyPageState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, + color: AppColors.onSurface, + fontSize: 15, + fontWeight: FontWeight.w800, ), ), const SizedBox(height: 2), Text( - uidText, - style: const TextStyle(color: AppColors.muted, fontSize: 12), + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 10), ), ], ), ), - TextButton( - onPressed: widget.onSignOut ?? () => _mockSnack(l10n), - style: TextButton.styleFrom( - backgroundColor: AppColors.raised, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - shape: const StadiumBorder(), + if (widget.signedIn) + TextButton( + onPressed: widget.onSignOut, + style: TextButton.styleFrom( + foregroundColor: AppColors.muted, + backgroundColor: const Color(0xFF24252A), + minimumSize: const Size(52, 30), + padding: const EdgeInsets.symmetric(horizontal: 10), + shape: const StadiumBorder(), + textStyle: const TextStyle(fontSize: 10), + ), + child: Text(l10n.signOut), + ) + else + TextButton( + key: const Key('mine-sign-in'), + onPressed: widget.onSignIn, + style: TextButton.styleFrom( + foregroundColor: AppColors.cream, + backgroundColor: const Color(0xFF24252A), + minimumSize: const Size(64, 30), + padding: const EdgeInsets.symmetric(horizontal: 10), + shape: const StadiumBorder(), + textStyle: const TextStyle(fontSize: 10), + ), + child: Text('${l10n.logIn}/${l10n.signUp}'), ), - child: Text(l10n.signOut), - ), ], ); } Widget _wallet(AppLocalizations l10n) { + return InkWell( + key: const Key('mine-wallet'), + onTap: () => _mockSnack(l10n), + borderRadius: BorderRadius.circular(13), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.goldCard, + borderRadius: BorderRadius.circular(13), + ), + child: Row( + children: [ + const Icon( + Icons.monetization_on_rounded, + color: AppColors.gold, + size: 20, + ), + const SizedBox(width: 9), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.coins, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + '0 ${l10n.coins} · ${l10n.topUpWallet}', + style: const TextStyle( + color: AppColors.accent, + fontSize: 9, + ), + ), + ], + ), + ), + const Icon( + Icons.chevron_right_rounded, + color: Color(0xFF666666), + size: 16, + ), + ], + ), + ), + ); + } + + WatchProgress? get _continueRecord { + for (final record in logic.recent) { + if (!record.finished && record.hasPosition) return record; + } + return null; + } + + Drama? get _continueDrama { + final record = _continueRecord; + if (record == null) return null; + for (final drama in [...?widget.dramas, widget.drama]) { + if (drama.id == record.dramaId) return drama; + } + return null; + } + + Widget _continueCard( + AppLocalizations l10n, + Drama drama, + WatchProgress record, + ) { return Container( - padding: const EdgeInsets.all(AppDimens.cardPadding), + key: const Key('mine-continue-watching'), + padding: const EdgeInsets.all(9), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), + borderRadius: BorderRadius.circular(12), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - Text( - l10n.myAccount, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800), + _poster(drama, width: 50, height: 64, radius: 7), + const SizedBox(width: 9), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.continueWatching, + style: const TextStyle( + color: AppColors.accent, + fontSize: 9, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + localizedDramaTitle(l10n, drama), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + '${_episodeText(drama, record)} · ' + '${_clock(record.position)} / ${_clock(record.duration)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9), + ), + ], + ), ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _accountTile( - label: l10n.coins, - value: '\$ 0', - action: l10n.topUpWallet, - color: AppColors.gold, - onPressed: () => _mockSnack(l10n), + const SizedBox(width: 8), + SizedBox( + width: 60, + height: 28, + child: FilledButton( + onPressed: () => _resume(drama), + style: FilledButton.styleFrom( + backgroundColor: AppColors.blue, + foregroundColor: AppColors.onPrimary, + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9), + ), + textStyle: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, ), ), - const SizedBox(width: 10), - Expanded( - child: _accountTile( - label: l10n.vipMembership, - value: l10n.notActive, - action: l10n.joinVip, - color: AppColors.blue, - onPressed: () => _mockSnack(l10n), - ), - ), - ], + child: Text(l10n.resume), + ), ), ], ), ); } - void _mockSnack(AppLocalizations l10n) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.mockNotice))); + Widget _library(AppLocalizations l10n) { + final tabs = [l10n.history, l10n.watchlist, l10n.likedTab]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + for (var index = 0; index < tabs.length; index++) + Expanded( + child: Semantics( + selected: logic.section == index, + button: true, + child: InkWell( + key: Key('mine-library-tab-$index'), + onTap: () => logic.selectSection(index), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + tabs[index], + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: logic.section == index + ? AppColors.blue + : const Color(0xFFE4DBDD), + fontSize: 14, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + if (logic.section == 1 && context.watch().loading) + const Center( + child: Padding( + padding: EdgeInsets.all(12), + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + else if (logic.section == 1) + ..._favoriteCards(l10n) + else if (logic.section == 2) + ..._likedCards(l10n) + else if (logic.recentLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(12), + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + else if (_recentDramas.isEmpty) + EmptyState( + key: const Key('mine-recent-empty'), + icon: Icons.history_rounded, + title: l10n.noStories, + subtitle: l10n.emptyHint, + compact: true, + ) + else + for (final drama in _recentDramas) ...[ + _historyCard(l10n, drama), + const SizedBox(height: 8), + ], + ], + ); } - Widget _accountTile({ - required String label, - required String value, - required String action, - required Color color, - required VoidCallback onPressed, - }) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.ink, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - border: Border.all(color: color.withValues(alpha: 0.45)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: TextStyle(color: color, fontSize: 12)), - const SizedBox(height: 6), - Text( - value, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800), - ), - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: onPressed, - style: FilledButton.styleFrom( - backgroundColor: color, - foregroundColor: color == AppColors.blue - ? Colors.white - : AppColors.onGold, - shape: const StadiumBorder(), + List _favoriteCards(AppLocalizations l10n) { + final favorites = context.watch(); + if (favorites.error != null && favorites.cards.isEmpty) { + return [ + EmptyState( + icon: Icons.cloud_off_rounded, + title: errorText(l10n, favorites.error, l10n.favoritesError), + actionLabel: l10n.retry, + onAction: () => unawaited(favorites.load()), + compact: true, + ), + ]; + } + if (favorites.cards.isEmpty) { + return [ + EmptyState( + key: const Key('mine-saved-empty'), + icon: Icons.bookmark_border_rounded, + title: l10n.noSavedShows, + subtitle: l10n.emptyHint, + compact: true, + ), + ]; + } + return [ + for (final drama in favorites.cards) ...[ + _smallDramaCard(l10n, drama), + const SizedBox(height: 8), + ], + ]; + } + + List _likedCards(AppLocalizations l10n) { + final interactions = context.watch(); + final dramas = [ + ...?widget.dramas, + widget.drama, + ].where((drama) => interactions.isLiked(drama.id)).toList(); + if (dramas.isEmpty) { + return [ + EmptyState( + key: const Key('mine-saved-empty'), + icon: Icons.favorite_border_rounded, + title: l10n.noSavedShows, + subtitle: l10n.emptyHint, + compact: true, + ), + ]; + } + return [ + for (final drama in dramas) ...[ + _smallDramaCard(l10n, drama), + const SizedBox(height: 8), + ], + ]; + } + + Widget _historyCard(AppLocalizations l10n, Drama drama) { + final record = _recordFor(drama.id); + return InkWell( + key: Key('mine-history-card-${drama.id}'), + onTap: () => _resume(drama), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + _poster(drama, width: 48, height: 60, radius: 7), + const SizedBox(width: 9), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizedDramaTitle(l10n, drama), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 3), + Text( + record == null + ? l10n.episodeCount(drama.episodeCount) + : '${_episodeText(drama, record)} · ' + '${_clock(record.position)} / ${_clock(record.duration)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xFFBBBBBB), + fontSize: 9, + ), + ), + ], ), - child: Text(action, maxLines: 1, overflow: TextOverflow.ellipsis), ), - ), - ], + Container( + width: 26, + height: 26, + decoration: const BoxDecoration( + color: Color(0xFF313137), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.play_arrow_rounded, + color: AppColors.blue, + size: 15, + ), + ), + ], + ), + ), + ); + } + + Widget _smallDramaCard(AppLocalizations l10n, Drama drama) { + return InkWell( + onTap: () { + if (widget.onDramaSelected != null) { + widget.onDramaSelected!(drama); + } else { + _resume(drama); + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + _poster(drama, width: 48, height: 60, radius: 7), + const SizedBox(width: 9), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizedDramaTitle(l10n, drama), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 3), + Text( + l10n.episodeCount(drama.episodeCount), + style: const TextStyle( + color: Color(0xFFBBBBBB), + fontSize: 9, + ), + ), + ], + ), + ), + const Icon( + Icons.chevron_right_rounded, + color: AppColors.muted, + size: 16, + ), + ], + ), + ), + ); + } + + Widget _languageRow(AppLocalizations l10n) { + final locale = context.watch(); + final code = locale.locale?.languageCode ?? 'id'; + final value = switch (code) { + 'zh' => l10n.simplifiedChinese, + 'en' => l10n.english, + _ => l10n.bahasaIndonesia, + }; + return InkWell( + key: const Key('mine-language'), + onTap: () => logic.showLanguage( + context, + showServerSetting: widget.debugShowSetting, + ), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon( + Icons.language_rounded, + color: AppColors.muted, + size: 14, + ), + const SizedBox(width: 9), + Expanded( + child: Text( + l10n.appLanguage, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 11, + ), + ), + ), + Text( + value, + style: const TextStyle(color: AppColors.accent, fontSize: 10), + ), + const SizedBox(width: 3), + const Icon( + Icons.chevron_right_rounded, + color: AppColors.accent, + size: 16, + ), + ], + ), ), ); } @@ -414,97 +758,42 @@ class _MyPageState extends State { Widget _menuCard(AppLocalizations l10n) { return Material( color: AppColors.card, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(12), clipBehavior: Clip.antiAlias, child: Column( children: [ - ListTile( - dense: true, - visualDensity: VisualDensity.compact, - minTileHeight: AppDimens.tapTarget, - leading: const Icon( - Icons.card_giftcard_rounded, - color: AppColors.blueSoft, - ), - title: Text(l10n.earnRewards), - trailing: Text( - l10n.freeBonus, - style: const TextStyle(color: AppColors.blueSoft, fontSize: 12), - ), - onTap: () => _mockSnack(l10n), - ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - dense: true, - visualDensity: VisualDensity.compact, - minTileHeight: AppDimens.tapTarget, + _menuTile( + icon: Icons.notifications_none_rounded, + title: l10n.notices, key: const Key('mine-notices'), - leading: const Icon( - Icons.notifications_none_rounded, - color: AppColors.blueSoft, - ), - title: Text(l10n.notices), - trailing: const Icon(Icons.chevron_right_rounded), onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const NoticesPage()), ), ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - dense: true, - visualDensity: VisualDensity.compact, - minTileHeight: AppDimens.tapTarget, - leading: const Icon( - Icons.history_rounded, - color: AppColors.blueSoft, - ), - title: Text(l10n.watchHistory), - trailing: const Icon(Icons.chevron_right_rounded), + _menuTile( + icon: Icons.history_rounded, + title: l10n.watchHistory, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const WatchRecordsPage()), ), ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - dense: true, - visualDensity: VisualDensity.compact, - minTileHeight: AppDimens.tapTarget, - leading: const Icon( - Icons.favorite_outline_rounded, - color: AppColors.blueSoft, - ), - title: Text(l10n.myFavorites), - trailing: const Icon(Icons.chevron_right_rounded), + _menuTile( + icon: Icons.favorite_outline_rounded, + title: l10n.myFavorites, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const FavoritesPage()), ), ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - dense: true, - visualDensity: VisualDensity.compact, - minTileHeight: AppDimens.tapTarget, - leading: const Icon( - Icons.download_outlined, - color: AppColors.blueSoft, - ), - title: Text(l10n.downloads), - trailing: const Icon(Icons.chevron_right_rounded), + _menuTile( + icon: Icons.download_outlined, + title: l10n.downloads, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const DownloadsPage()), ), ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - dense: true, - visualDensity: VisualDensity.compact, - minTileHeight: AppDimens.tapTarget, - leading: const Icon( - Icons.settings_rounded, - color: AppColors.blueSoft, - ), - title: Text(l10n.settings), - trailing: const Icon(Icons.chevron_right_rounded), + _menuTile( + icon: Icons.settings_rounded, + title: l10n.settings, onTap: () => logic.showLanguage( context, showServerSetting: widget.debugShowSetting, @@ -515,346 +804,30 @@ class _MyPageState extends State { ); } - Widget _library( - AppLocalizations l10n, - Duration position, - Duration total, - double fraction, - ) { - final tabs = [l10n.history, l10n.watchlist, l10n.likedTab]; - final filters = [l10n.all, l10n.inProgress, l10n.completed]; - final hasHistory = - logic.filter == 0 || - (logic.filter == 1 && fraction < 0.99) || - (logic.filter == 2 && fraction >= 0.99); - final interactions = context.watch(); - // 收藏以服务端为准;剧集点赞暂按当前会话状态过滤 feed。 - final favorites = context.watch(); - final isWatchlist = logic.section == 1; - final markedDramas = isWatchlist - ? favorites.cards - : (widget.dramas ?? [widget.drama]) - .where((drama) => interactions.isLiked(drama.id)) - .toList(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (var index = 0; index < tabs.length; index++) ...[ - Semantics( - selected: logic.section == index, - button: true, - child: InkWell( - key: Key('mine-library-tab-$index'), - onTap: () { - logic.selectSection(index); - // 打开收藏页签时以服务端为准刷新一次(多设备/多入口都能对齐) - if (index == 1) unawaited(favorites.load()); - }, - borderRadius: BorderRadius.circular(AppDimens.pillRadius), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - decoration: BoxDecoration( - color: logic.section == index - ? AppColors.blue - : AppColors.raised, - borderRadius: BorderRadius.circular( - AppDimens.pillRadius, - ), - ), - child: Text( - tabs[index], - style: const TextStyle( - color: Colors.white, - fontSize: AppDimens.body, - fontWeight: FontWeight.w700, - ), - ), - ), - ), - ), - if (index < tabs.length - 1) const SizedBox(width: 18), - ], - ], - ), - ), - const SizedBox(height: 17), - if (logic.section == 0) - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (var index = 0; index < filters.length; index++) - Padding( - padding: const EdgeInsets.only(right: 7), - child: ChoiceChip( - label: Text(filters[index]), - selected: logic.filter == index, - onSelected: (_) => logic.selectFilter(index), - showCheckmark: false, - materialTapTargetSize: MaterialTapTargetSize.padded, - backgroundColor: AppColors.raised, - selectedColor: AppColors.blue, - labelStyle: TextStyle( - color: logic.filter == index - ? Colors.white - : AppColors.muted, - fontSize: AppDimens.caption, - fontWeight: FontWeight.w700, - ), - side: BorderSide.none, - ), - ), - ], - ), - ), - if (logic.section == 0) const SizedBox(height: 11), - if (logic.section == 0 && hasHistory) - InkWell( - onTap: () => logic.act( - context, - MyPageAction.resume, - embedded: widget.embedded, - onAction: widget.onAction, - ), - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - child: Container( - padding: const EdgeInsets.all(AppDimens.innerPadding), - decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - ), - child: Row( - children: [ - _poster(width: 74, height: 103), - const SizedBox(width: 11), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - localizedDramaTitle(l10n, widget.drama), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: AppDimens.listTitle, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 5), - Text( - l10n.dramaTag, - style: const TextStyle( - color: AppColors.cream, - fontSize: AppDimens.caption, - ), - ), - const SizedBox(height: 32), - Text( - '${l10n.episodeLabel(1)} · ${_clock(position)} / ${_clock(total)}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.blueSoft, - fontSize: AppDimens.caption, - ), - ), - ], - ), - ), - const CircleAvatar( - radius: 15, - backgroundColor: AppColors.raised, - child: Icon( - Icons.play_arrow_rounded, - color: _accent, - size: 20, - ), - ), - ], - ), - ), - ) - else if (isWatchlist && favorites.loading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 22), - child: Center( - child: SizedBox( - width: 22, - height: 22, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), - ) - else if (isWatchlist && favorites.error != null) - EmptyState( - icon: Icons.cloud_off_rounded, - title: errorText(l10n, favorites.error, l10n.favoritesError), - actionLabel: l10n.retry, - onAction: () => unawaited(favorites.load()), - compact: true, - ) - else if (logic.section != 0 && markedDramas.isNotEmpty) - for (final drama in markedDramas) ...[ - _markedDramaCard(drama, l10n), - const SizedBox(height: 9), - ] - else - EmptyState( - key: const Key('mine-saved-empty'), - icon: Icons.bookmark_border_rounded, - title: l10n.noSavedShows, - subtitle: l10n.emptyHint, - ), - // 最近观看(服务端 /app/watch/recent):只渲染能在已加载目录里匹配到的剧; - // 匹配不到的(例如不在当前首页分区里)先不显示,K2 的记录页会做独立请求。 - if (logic.section == 0) ...[ - const SizedBox(height: 18), - Text( - l10n.history, - style: const TextStyle( - color: Colors.white, - fontSize: AppDimens.body, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 9), - if (logic.positionFailed) - Row( - children: [ - Expanded( - child: Text( - l10n.progressError, - style: const TextStyle(color: AppColors.cream), - ), - ), - TextButton( - onPressed: logic.refreshPosition, - child: Text(l10n.retry), - ), - ], - ) - else if (logic.recentLoading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), - ) - else if (logic.recentError != null) - Row( - children: [ - Expanded( - child: Text( - errorText(l10n, logic.recentError, l10n.progressError), - style: const TextStyle(color: AppColors.cream), - ), - ), - TextButton( - onPressed: () => unawaited(logic.refreshRecent()), - child: Text(l10n.retry), - ), - ], - ) - else if (_recentDramas.isEmpty) - EmptyState( - key: const Key('mine-recent-empty'), - icon: Icons.history_rounded, - title: l10n.noStories, - subtitle: l10n.emptyHint, - ) - else - for (final drama in _recentDramas) ...[ - _markedDramaCard(drama, l10n), - const SizedBox(height: 9), - ], - ], - ], + Widget _menuTile({ + required IconData icon, + required String title, + required VoidCallback onTap, + Key? key, + }) { + return ListTile( + key: key, + dense: true, + minTileHeight: 44, + leading: Icon(icon, color: AppColors.secondary, size: 17), + title: Text( + title, + style: const TextStyle(color: AppColors.onSurface, fontSize: 11), + ), + trailing: const Icon( + Icons.chevron_right_rounded, + color: AppColors.muted, + size: 16, + ), + onTap: onTap, ); } - /// 服务端最近观看 → 已加载目录里的卡片(按 id 匹配,去重,当前剧也算一条)。 - List get _recentDramas { - final pool = [...?widget.dramas, widget.drama]; - final result = []; - for (final item in logic.recent) { - for (final drama in pool) { - if (drama.id == item.dramaId && - !result.any((picked) => picked.id == drama.id)) { - result.add(drama); - } - } - } - return result; - } - - Widget _markedDramaCard(Drama drama, AppLocalizations l10n) => InkWell( - onTap: () { - if (widget.onDramaSelected != null) { - widget.onDramaSelected!(drama); - } else if (drama.id == widget.drama.id) { - logic.act( - context, - MyPageAction.resume, - embedded: widget.embedded, - onAction: widget.onAction, - ); - } - }, - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - child: Container( - padding: const EdgeInsets.all(AppDimens.innerPadding), - decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - ), - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - child: RemoteImage(drama.posterUrl, width: 74, height: 103), - ), - const SizedBox(width: 11), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - localizedDramaTitle(l10n, drama), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: AppDimens.listTitle, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 7), - Text( - l10n.episodeCount(drama.episodeCount), - style: const TextStyle( - color: AppColors.cream, - fontSize: AppDimens.caption, - ), - ), - ], - ), - ), - const Icon(Icons.chevron_right_rounded, color: AppColors.cream), - ], - ), - ), - ); - Widget _bottomNav(AppLocalizations l10n) => AppBottomNav( items: [ AppBottomNavItem( @@ -892,15 +865,100 @@ class _MyPageState extends State { ], ); - Widget _poster({required double width, required double height}) => ClipRRect( - borderRadius: BorderRadius.circular(7), - child: RemoteImage(widget.drama.posterUrl, width: width, height: height), + List get _recentDramas { + final pool = [...?widget.dramas, widget.drama]; + final result = []; + for (final item in logic.recent) { + for (final drama in pool) { + if (drama.id == item.dramaId && + !result.any((picked) => picked.id == drama.id)) { + result.add(drama); + } + } + } + return result; + } + + WatchProgress? _recordFor(String dramaId) { + for (final record in logic.recent) { + if (record.dramaId == dramaId) return record; + } + return null; + } + + String _episodeText(Drama drama, WatchProgress record) { + for (final episode in drama.episodes) { + if (episode.id == record.episodeId) { + return AppLocalizations.of( + context, + )!.episodeOfTotal(episode.number, drama.episodeCount); + } + } + return AppLocalizations.of(context)!.episodeCount(drama.episodeCount); + } + + void _resume(Drama drama) { + if (widget.onDramaSelected != null) { + widget.onDramaSelected!(drama); + } else { + logic.act( + context, + MyPageAction.resume, + embedded: widget.embedded, + onAction: widget.onAction, + ); + } + } + + void _mockSnack(AppLocalizations l10n) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.mockNotice))); + } + + Widget _poster( + Drama drama, { + required double width, + required double height, + required double radius, + }) => ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: RemoteImage(drama.posterUrl, width: width, height: height), ); + + String _clock(Duration duration) { + final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } } -String _clock(Duration duration) { - final seconds = duration.inSeconds; - return '${(seconds ~/ 60).toString().padLeft(2, '0')}:${(seconds % 60).toString().padLeft(2, '0')}'; -} +class _RoundHeaderButton extends StatelessWidget { + const _RoundHeaderButton({ + required this.tooltip, + required this.icon, + required this.onTap, + }); -const _accent = AppColors.blueSoft; + final String tooltip; + final IconData icon; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 28, + height: 28, + child: IconButton( + tooltip: tooltip, + onPressed: onTap, + padding: EdgeInsets.zero, + style: IconButton.styleFrom( + backgroundColor: const Color(0xFF2C2D33), + foregroundColor: AppColors.onSurface, + ), + icon: Icon(icon, size: 14), + ), + ); + } +} diff --git a/test/account/mine_account_entry_test.dart b/test/account/mine_account_entry_test.dart index 7097af0..d0612ca 100644 --- a/test/account/mine_account_entry_test.dart +++ b/test/account/mine_account_entry_test.dart @@ -46,6 +46,8 @@ void main() { await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); + await tester.drag(find.byType(ListView), const Offset(0, -520)); + await tester.pumpAndSettle(); expect(find.byKey(const Key('mine-account-card')), findsOneWidget); expect(find.byKey(const Key('mine-account-profile')), findsOneWidget); diff --git a/test/auth/auth_k21_test.dart b/test/auth/auth_k21_test.dart index 14fefef..6312819 100644 --- a/test/auth/auth_k21_test.dart +++ b/test/auth/auth_k21_test.dart @@ -82,4 +82,7 @@ Future _pumpAuth(WidgetTester tester) async { ), ); await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('auth-entry-email'))); + await tester.tap(find.byKey(const Key('auth-entry-email'))); + await tester.pumpAndSettle(); } diff --git a/test/auth/auth_landing_test.dart b/test/auth/auth_landing_test.dart new file mode 100644 index 0000000..af8b6d7 --- /dev/null +++ b/test/auth/auth_landing_test.dart @@ -0,0 +1,65 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.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/data/remote/api_client.dart'; +import 'package:fruitshorts/core/data/remote/auth_api.dart'; +import 'package:fruitshorts/core/data/services/auth_controller.dart'; +import 'package:fruitshorts/core/data/services/auth_session_store.dart'; +import 'package:fruitshorts/core/platform/haptics.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/auth/auth_view.dart'; + +import '../support/fake_haptics.dart'; +import '../support/fake_http_adapter.dart'; + +void main() { + testWidgets('should_show_three_login_entries_and_open_email_form', ( + tester, + ) async { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + final auth = AuthController( + AuthApi( + ApiClient( + baseUrl: 'http://127.0.0.1:1', + adapter: FakeHttpAdapter((_) async => FakeHttpResponse('{}', 500)), + ), + ), + AuthSessionStore(), + ); + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: auth), + Provider.value(value: FakeHaptics()), + ], + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const AuthPage(), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('auth-entry-google')), findsOneWidget); + expect(find.byKey(const Key('auth-entry-phone')), findsOneWidget); + expect(find.byKey(const Key('auth-entry-email')), findsOneWidget); + expect(find.byKey(const Key('auth-legal')), findsOneWidget); + + await tester.tap(find.byKey(const Key('auth-entry-google'))); + await tester.pumpAndSettle(); + expect(find.text('Coming soon'), findsOneWidget); + + await tester.tap(find.byKey(const Key('auth-entry-email'))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('auth-email')), findsOneWidget); + expect(find.byKey(const Key('auth-password')), findsOneWidget); + }); +} diff --git a/test/auth/auth_page_test.dart b/test/auth/auth_page_test.dart index ffa24e0..3a5851c 100644 --- a/test/auth/auth_page_test.dart +++ b/test/auth/auth_page_test.dart @@ -59,6 +59,9 @@ void main() { ), ); await tester.pumpAndSettle(); + await tester.ensureVisible(find.byKey(const Key('auth-entry-email'))); + await tester.tap(find.byKey(const Key('auth-entry-email'))); + await tester.pumpAndSettle(); } testWidgets('should_show_email_error_and_stay_on_page_when_form_is_empty', ( diff --git a/test/auth/auth_visual_test.dart b/test/auth/auth_visual_test.dart index 5f5e052..10c94f4 100644 --- a/test/auth/auth_visual_test.dart +++ b/test/auth/auth_visual_test.dart @@ -29,6 +29,7 @@ void main() { WidgetTester tester, { AuthApi? api, FakeHaptics? haptics, + bool openEmail = true, }) async { final auth = AuthController( api ?? @@ -57,20 +58,32 @@ void main() { ), ); await tester.pump(const Duration(milliseconds: 220)); + if (openEmail) { + await tester.ensureVisible(find.byKey(const Key('auth-entry-email'))); + await tester.tap(find.byKey(const Key('auth-entry-email'))); + await tester.pumpAndSettle(); + } } testWidgets( 'should_render_branded_dark_layout_with_approved_tokens_when_page_opens', (tester) async { - await pumpAuthPage(tester); + await pumpAuthPage(tester, openEmail: false); - expect(find.byKey(const Key('auth-brand')), findsOneWidget); + expect(find.byKey(const Key('auth-landing')), findsOneWidget); expect(find.text('FruitShorts'), findsOneWidget); - expect(find.text('Short dramas made for Indonesia'), findsOneWidget); expect( - tester.getSize(find.byKey(const Key('auth-brand-mark'))), - const Size(48, 48), + find.text('Sign in to sync progress, favorites and coins'), + findsOneWidget, ); + expect( + tester.getSize(find.byKey(const Key('auth-brand-logo'))), + const Size(64, 64), + ); + expect(find.byKey(const Key('auth-entry-google')), findsOneWidget); + expect(find.byKey(const Key('auth-entry-phone')), findsOneWidget); + expect(find.byKey(const Key('auth-entry-email')), findsOneWidget); + expect(find.byKey(const Key('auth-legal')), findsOneWidget); final glow = tester.widget( find.byKey(const Key('auth-background-glow')), @@ -81,6 +94,9 @@ void main() { expect(color.a, lessThanOrEqualTo(0.18)); } + await tester.tap(find.byKey(const Key('auth-entry-email'))); + await tester.pumpAndSettle(); + final card = tester.widget(find.byKey(const Key('auth-card'))); final cardDecoration = card.decoration! as BoxDecoration; expect(cardDecoration.color, AppColors.card); diff --git a/test/mine/mine_preview_layout_test.dart b/test/mine/mine_preview_layout_test.dart new file mode 100644 index 0000000..cb9d013 --- /dev/null +++ b/test/mine/mine_preview_layout_test.dart @@ -0,0 +1,69 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; + +import 'package:fruitshorts/app.dart'; +import 'package:fruitshorts/core/data/models/watch_progress.dart'; +import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; +import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; +import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; + +import '../support/fake_catalog.dart'; +import '../support/fake_category.dart'; +import '../support/fake_haptics.dart'; +import '../support/fake_interaction_repositories.dart'; +import '../support/fake_notice_repository.dart'; +import '../support/widget_player.dart'; + +void main() { + testWidgets('should_render_me_user_wallet_continue_and_language_rows', ( + tester, + ) async { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + final catalog = FakeCatalogRepository(); + await tester.pumpWidget( + LumaDramaApp( + playerFactory: createWidgetPlayer, + catalogRepository: catalog, + categoryRepository: FakeCategoryRepository(catalog: catalog), + watchHistoryRepository: InMemoryWatchHistoryRepository( + seed: const [ + WatchProgress( + dramaId: 'city-lights-demo', + episodeId: 'city-lights-demo-1', + position: Duration(minutes: 2), + duration: Duration(minutes: 5), + ), + ], + ), + favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), + playbackRepository: FakePlaybackRepository(), + commentRepository: FakeCommentRepository(), + interactionRepository: FakeInteractionRepository(), + noticeRepository: FakeNoticeRepository(), + haptics: FakeHaptics(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tab-my'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('mine-user-row')), findsOneWidget); + expect(find.byKey(const Key('mine-wallet')), findsOneWidget); + expect(find.byKey(const Key('mine-continue-watching')), findsOneWidget); + expect(find.byKey(const Key('mine-language')), findsOneWidget); + expect(find.byKey(const Key('mine-library-tab-0')), findsOneWidget); + expect(find.byKey(const Key('mine-library-tab-1')), findsOneWidget); + expect(find.byKey(const Key('mine-library-tab-2')), findsOneWidget); + expect(tester.takeException(), isNull); + }); +} diff --git a/test/notice/notices_entry_test.dart b/test/notice/notices_entry_test.dart index db6d08b..1767c7f 100644 --- a/test/notice/notices_entry_test.dart +++ b/test/notice/notices_entry_test.dart @@ -46,6 +46,8 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); + await tester.drag(find.byType(ListView), const Offset(0, -520)); + await tester.pumpAndSettle(); expect(find.byKey(const Key('mine-notices')), findsOneWidget); await tester.tap(find.byKey(const Key('mine-notices'))); diff --git a/test/watch/watch_history_provider_test.dart b/test/watch/watch_history_provider_test.dart index 932728f..2b10336 100644 --- a/test/watch/watch_history_provider_test.dart +++ b/test/watch/watch_history_provider_test.dart @@ -42,6 +42,8 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); + await tester.drag(find.byType(ListView), const Offset(0, -520)); + await tester.pumpAndSettle(); await tester.tap(find.text('Watch History')); await tester.pumpAndSettle();