feat(ui): 新增登录入口与我的页预览

This commit is contained in:
Zhou Weidong committed 2026-10-19 15:50:15 +08:00
1 parent 21d457fd98
commit fecc5b0d27
10 files changed
+1021 -649

No files matched your search

+161 -9
View File
@@ -60,6 +60,7 @@ class _AuthPageState extends State<AuthPage> {
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<AuthPage> {
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<AuthPage> {
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<AuthPage> {
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,
),
),
),
],
),
),
),
);
}
}
File diff suppressed because it is too large. Load diff
@@ -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);
+3
View File
@@ -82,4 +82,7 @@ Future<void> _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();
}
+65
View File
@@ -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<AuthController>.value(value: auth),
Provider<Haptics>.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);
});
}
+3
View File
@@ -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', (
+21 -5
View File
@@ -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<Container>(
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<Container>(find.byKey(const Key('auth-card')));
final cardDecoration = card.decoration! as BoxDecoration;
expect(cardDecoration.color, AppColors.card);
+69
View File
@@ -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);
});
}
+2
View File
@@ -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')));
@@ -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();