merge: 合并 K41-A 登录页重设计
This commit is contained in:
commit
7285a1cd4e
5 files changed
+233
-141
No files matched your search
@@ -22,11 +22,11 @@ class _AuthFields extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_fieldLabel(state.l10n.authEmailLabel),
|
_fieldLabel(state.l10n.authEmailLabel),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
_emailField(context, hasError: emailError),
|
_emailField(context, hasError: emailError),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 14),
|
||||||
_fieldLabel(state.l10n.authPasswordLabel),
|
_fieldLabel(state.l10n.authPasswordLabel),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
_passwordField(context, hasError: passwordError),
|
_passwordField(context, hasError: passwordError),
|
||||||
if (state.status.signUpMode) ...[
|
if (state.status.signUpMode) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -60,7 +60,8 @@ class _AuthFields extends StatelessWidget {
|
|||||||
autofillHints: const [AutofillHints.email],
|
autofillHints: const [AutofillHints.email],
|
||||||
onChanged: callbacks.onEmailChanged,
|
onChanged: callbacks.onEmailChanged,
|
||||||
onSubmitted: (_) => state.fields.passwordFocus.requestFocus(),
|
onSubmitted: (_) => state.fields.passwordFocus.requestFocus(),
|
||||||
style: const TextStyle(color: AppColors.onPrimary, fontSize: 15),
|
style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
|
||||||
|
cursorColor: AppColors.blue,
|
||||||
decoration: _decoration(
|
decoration: _decoration(
|
||||||
context: context,
|
context: context,
|
||||||
hint: 'name@example.com',
|
hint: 'name@example.com',
|
||||||
@@ -91,7 +92,8 @@ class _AuthFields extends StatelessWidget {
|
|||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
onChanged: callbacks.onPasswordChanged,
|
onChanged: callbacks.onPasswordChanged,
|
||||||
onSubmitted: (_) => callbacks.onSubmit(),
|
onSubmitted: (_) => callbacks.onSubmit(),
|
||||||
style: const TextStyle(color: AppColors.onPrimary, fontSize: 15),
|
style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
|
||||||
|
cursorColor: AppColors.blue,
|
||||||
decoration: _decoration(
|
decoration: _decoration(
|
||||||
context: context,
|
context: context,
|
||||||
hint: l10n.authPasswordHint,
|
hint: l10n.authPasswordHint,
|
||||||
@@ -125,8 +127,8 @@ class _AuthFields extends StatelessWidget {
|
|||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.muted,
|
color: AppColors.muted,
|
||||||
fontSize: 13,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -140,7 +142,10 @@ class _AuthFields extends StatelessWidget {
|
|||||||
}) {
|
}) {
|
||||||
final border = OutlineInputBorder(
|
final border = OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(_fieldRadius),
|
borderRadius: BorderRadius.circular(_fieldRadius),
|
||||||
borderSide: BorderSide.none,
|
borderSide: const BorderSide(
|
||||||
|
color: AppColors.border,
|
||||||
|
width: _fieldBorderWidth,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final errorColor = Theme.of(context).colorScheme.error;
|
final errorColor = Theme.of(context).colorScheme.error;
|
||||||
final errorBorder = OutlineInputBorder(
|
final errorBorder = OutlineInputBorder(
|
||||||
@@ -149,11 +154,11 @@ class _AuthFields extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14),
|
hintStyle: const TextStyle(color: AppColors.subtle, fontSize: 14),
|
||||||
prefixIcon: Icon(
|
prefixIcon: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 21,
|
size: 21,
|
||||||
color: focused ? AppColors.blueSoft : AppColors.muted,
|
color: focused ? AppColors.blue : AppColors.muted,
|
||||||
),
|
),
|
||||||
prefixIconConstraints: const BoxConstraints(
|
prefixIconConstraints: const BoxConstraints(
|
||||||
minWidth: _minTapTarget,
|
minWidth: _minTapTarget,
|
||||||
@@ -161,7 +166,7 @@ class _AuthFields extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
suffixIcon: suffix,
|
suffixIcon: suffix,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.bar,
|
fillColor: AppColors.card,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: const EdgeInsets.only(right: 16),
|
contentPadding: const EdgeInsets.only(right: 16),
|
||||||
border: border,
|
border: border,
|
||||||
@@ -229,8 +234,8 @@ class _AuthSubmitButton extends StatelessWidget {
|
|||||||
: Text(
|
: Text(
|
||||||
status.signUpMode ? l10n.signUp : l10n.signIn,
|
status.signUpMode ? l10n.signUp : l10n.signIn,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w800,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -273,7 +278,7 @@ class _ModeSegment extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selected ? AppColors.onPrimary : AppColors.muted,
|
color: selected ? AppColors.onPrimary : AppColors.onSurface,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
@@ -327,29 +332,11 @@ class _AuthBrand extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _brandMark() => Container(
|
Widget _brandMark() => _brandMarkWidget(
|
||||||
key: const Key('auth-brand-mark'),
|
key: const Key('auth-brand-mark'),
|
||||||
width: _brandMarkSize,
|
size: compact ? _compactBrandMarkSize : _brandMarkSize,
|
||||||
height: _brandMarkSize,
|
radius: compact ? _compactBrandMarkRadius : _brandMarkRadius,
|
||||||
decoration: BoxDecoration(
|
iconSize: compact ? _compactBrandIconSize : _brandIconSize,
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.blue.withValues(alpha: 0.32),
|
|
||||||
blurRadius: 24,
|
|
||||||
offset: const Offset(0, 10),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/icon/splash_mark.png',
|
|
||||||
width: _brandMarkSize,
|
|
||||||
height: _brandMarkSize,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _brandCopy(BuildContext context, {required bool centered}) => Column(
|
Widget _brandCopy(BuildContext context, {required bool centered}) => Column(
|
||||||
@@ -361,9 +348,9 @@ class _AuthBrand extends StatelessWidget {
|
|||||||
AppLocalizations.of(context)!.appName,
|
AppLocalizations.of(context)!.appName,
|
||||||
textAlign: centered ? TextAlign.center : TextAlign.start,
|
textAlign: centered ? TextAlign.center : TextAlign.start,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.onPrimary,
|
color: AppColors.onSurface,
|
||||||
fontSize: 26,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w900,
|
||||||
height: 1.1,
|
height: 1.1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -372,8 +359,8 @@ class _AuthBrand extends StatelessWidget {
|
|||||||
tagline,
|
tagline,
|
||||||
textAlign: centered ? TextAlign.center : TextAlign.start,
|
textAlign: centered ? TextAlign.center : TextAlign.start,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.blueSoft,
|
color: AppColors.muted,
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
height: 1.35,
|
height: 1.35,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -406,7 +393,7 @@ class _AuthError extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
message,
|
message,
|
||||||
key: const Key('auth-error'),
|
key: const Key('auth-error'),
|
||||||
style: TextStyle(color: color, fontSize: 13, height: 1.35),
|
style: TextStyle(color: color, fontSize: 12, height: 1.35),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
+103
-71
@@ -11,6 +11,7 @@ import '../../core/platform/haptics.dart';
|
|||||||
import '../../core/theme/app_colors.dart';
|
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/subpage_header.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import 'auth_logic.dart';
|
import 'auth_logic.dart';
|
||||||
import 'phone_auth_sheet.dart';
|
import 'phone_auth_sheet.dart';
|
||||||
@@ -19,18 +20,21 @@ part 'auth_widgets.dart';
|
|||||||
part 'auth_form_widgets.dart';
|
part 'auth_form_widgets.dart';
|
||||||
|
|
||||||
const double _minTapTarget = 44;
|
const double _minTapTarget = 44;
|
||||||
const double _brandMarkSize = 48;
|
const double _brandMarkSize = 72;
|
||||||
|
const double _compactBrandMarkSize = 48;
|
||||||
|
const double _brandMarkRadius = 20;
|
||||||
|
const double _compactBrandMarkRadius = 14;
|
||||||
|
const double _brandIconSize = 36;
|
||||||
|
const double _compactBrandIconSize = 26;
|
||||||
const double _contentMaxWidth = 440;
|
const double _contentMaxWidth = 440;
|
||||||
// Auth keeps its approved 20px card silhouette.
|
// Auth keeps its approved 20px card silhouette.
|
||||||
const double _cardRadius = AppDimens.authCardRadius;
|
const double _cardRadius = AppDimens.authCardRadius;
|
||||||
const double _cardPadding = AppDimens.cardPadding;
|
const double _cardPadding = 20;
|
||||||
const double _cardBorderWidth = 1;
|
|
||||||
const double _fieldHeight = 52;
|
const double _fieldHeight = 52;
|
||||||
const double _fieldRadius = 14;
|
const double _fieldRadius = 14;
|
||||||
const double _fieldBorderWidth = 1.5;
|
const double _fieldBorderWidth = 1;
|
||||||
const double _buttonHeight = 52;
|
const double _buttonHeight = 50;
|
||||||
const double _buttonRadius = 26;
|
const double _buttonRadius = 25;
|
||||||
const double _glowOpacity = 0.18;
|
|
||||||
const double _errorOpacity = 0.10;
|
const double _errorOpacity = 0.10;
|
||||||
const double _errorBorderOpacity = 0.28;
|
const double _errorBorderOpacity = 0.28;
|
||||||
const double _enterSlideDistance = 16;
|
const double _enterSlideDistance = 16;
|
||||||
@@ -39,11 +43,48 @@ const double _bottomContentPadding = 28;
|
|||||||
const double _compactTopContentPadding = 48;
|
const double _compactTopContentPadding = 48;
|
||||||
const double _compactHeightBreakpoint = 720;
|
const double _compactHeightBreakpoint = 720;
|
||||||
const double _compactBrandGap = 16;
|
const double _compactBrandGap = 16;
|
||||||
const double _warmGlowSize = 260;
|
const double _glowSize = 280;
|
||||||
const double _warmGlowOpacity = 0.06;
|
const double _warmGlowOpacity = 0.18;
|
||||||
|
const double _entryButtonHeight = 50;
|
||||||
|
const double _entryButtonRadius = 25;
|
||||||
|
const double _entryButtonFontSize = 15;
|
||||||
|
const double _entryIconSize = 20;
|
||||||
const Duration _enterDuration = Duration(milliseconds: 220);
|
const Duration _enterDuration = Duration(milliseconds: 220);
|
||||||
const Duration _segmentDuration = Duration(milliseconds: 160);
|
const Duration _segmentDuration = Duration(milliseconds: 160);
|
||||||
|
|
||||||
|
Widget _brandMarkWidget({
|
||||||
|
Key? key,
|
||||||
|
required double size,
|
||||||
|
required double radius,
|
||||||
|
required double iconSize,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
key: key,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(radius),
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [AppColors.blue, AppColors.secondary],
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColors.blue.withValues(alpha: 0.24),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.play_arrow_rounded,
|
||||||
|
color: AppColors.onPrimary,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 登录 / 注册页:邮箱 + 密码。
|
/// 登录 / 注册页:邮箱 + 密码。
|
||||||
class AuthPage extends StatefulWidget {
|
class AuthPage extends StatefulWidget {
|
||||||
const AuthPage({super.key, this.signUpMode = false});
|
const AuthPage({super.key, this.signUpMode = false});
|
||||||
@@ -147,34 +188,39 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
backgroundColor: AppColors.ink,
|
backgroundColor: AppColors.ink,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
const Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: DecoratedBox(
|
||||||
|
key: Key('auth-page-background'),
|
||||||
|
decoration: BoxDecoration(gradient: AppTheme.pageGradient),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: -90,
|
||||||
left: 0,
|
right: -70,
|
||||||
right: 0,
|
width: _glowSize,
|
||||||
height: 420,
|
height: _glowSize,
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Container(
|
child: Container(
|
||||||
key: const Key('auth-background-glow'),
|
key: const Key('auth-background-glow'),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
shape: BoxShape.circle,
|
||||||
begin: Alignment.topCenter,
|
gradient: RadialGradient(
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
colors: [
|
||||||
AppColors.blue.withValues(alpha: _glowOpacity),
|
AppColors.surfaceTop,
|
||||||
AppColors.blue.withValues(alpha: 0.05),
|
AppColors.surfaceTop.withValues(alpha: 0),
|
||||||
AppColors.ink.withValues(alpha: 0),
|
|
||||||
],
|
],
|
||||||
stops: const [0, 0.55, 1],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 72,
|
left: -110,
|
||||||
right: -80,
|
bottom: -90,
|
||||||
width: _warmGlowSize,
|
width: _glowSize,
|
||||||
height: _warmGlowSize,
|
height: _glowSize,
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Container(
|
child: Container(
|
||||||
key: const Key('auth-background-warm-glow'),
|
key: const Key('auth-background-warm-glow'),
|
||||||
@@ -239,23 +285,16 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 4, top: 4),
|
padding: const EdgeInsets.only(left: 14, top: 8),
|
||||||
child: IconButton(
|
child: AppSubpageBackButton(
|
||||||
key: const Key('auth-back'),
|
backKey: const Key('auth-back'),
|
||||||
tooltip: MaterialLocalizations.of(
|
onBack: () {
|
||||||
context,
|
|
||||||
).backButtonTooltip,
|
|
||||||
onPressed: () {
|
|
||||||
if (_emailEntryOpen) {
|
if (_emailEntryOpen) {
|
||||||
setState(() => _emailEntryOpen = false);
|
setState(() => _emailEntryOpen = false);
|
||||||
} else {
|
} else {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back_rounded,
|
|
||||||
color: AppColors.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -298,58 +337,44 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: _brandMarkWidget(
|
||||||
key: const Key('auth-brand-logo'),
|
key: const Key('auth-brand-logo'),
|
||||||
width: 64,
|
size: _brandMarkSize,
|
||||||
height: 64,
|
radius: _brandMarkRadius,
|
||||||
decoration: BoxDecoration(
|
iconSize: _brandIconSize,
|
||||||
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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
l10n.fruitShortsBrand,
|
l10n.fruitShortsBrand,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'Outfit',
|
fontFamily: 'Outfit',
|
||||||
color: AppColors.onSurface,
|
color: AppColors.onSurface,
|
||||||
fontSize: 20,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
l10n.authLandingSubtitle,
|
l10n.authLandingSubtitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(color: AppColors.subtle, fontSize: 11),
|
style: const TextStyle(color: AppColors.muted, fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 22),
|
const SizedBox(height: 28),
|
||||||
_AuthEntryButton(
|
_AuthEntryButton(
|
||||||
key: const Key('auth-entry-phone'),
|
key: const Key('auth-entry-phone'),
|
||||||
label: l10n.authPhoneSignIn,
|
label: l10n.authPhoneSignIn,
|
||||||
icon: Icons.phone_android_rounded,
|
icon: Icons.phone_android_rounded,
|
||||||
background: AppColors.input,
|
primary: true,
|
||||||
foreground: AppColors.onSurface,
|
|
||||||
onTap: _openPhoneSheet,
|
onTap: _openPhoneSheet,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 11),
|
const SizedBox(height: 12),
|
||||||
_AuthEntryButton(
|
_AuthEntryButton(
|
||||||
key: const Key('auth-entry-email'),
|
key: const Key('auth-entry-email'),
|
||||||
label: l10n.authEmailSignIn,
|
label: l10n.authEmailSignIn,
|
||||||
icon: Icons.mail_outline_rounded,
|
icon: Icons.mail_outline_rounded,
|
||||||
background: AppColors.input,
|
primary: false,
|
||||||
foreground: AppColors.onSurface,
|
|
||||||
onTap: () => setState(() => _emailEntryOpen = true),
|
onTap: () => setState(() => _emailEntryOpen = true),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
@@ -417,33 +442,40 @@ class _AuthEntryButton extends StatelessWidget {
|
|||||||
const _AuthEntryButton({
|
const _AuthEntryButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.background,
|
required this.primary,
|
||||||
required this.foreground,
|
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.icon,
|
this.icon,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final Color background;
|
final bool primary;
|
||||||
final Color foreground;
|
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final background = primary ? AppColors.blue : AppColors.card;
|
||||||
|
final foreground = primary ? AppColors.onPrimary : AppColors.onSurface;
|
||||||
return Material(
|
return Material(
|
||||||
color: background,
|
color: background,
|
||||||
borderRadius: BorderRadius.circular(9),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(_entryButtonRadius),
|
||||||
|
side: primary
|
||||||
|
? BorderSide.none
|
||||||
|
: const BorderSide(color: AppColors.border),
|
||||||
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(_entryButtonRadius),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 44,
|
height: _entryButtonHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (icon != null) Icon(icon, size: 16, color: foreground),
|
if (icon != null)
|
||||||
if (icon != null) const SizedBox(width: 7),
|
Icon(icon, size: _entryIconSize, color: foreground),
|
||||||
|
if (icon != null) const SizedBox(width: 8),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
@@ -451,8 +483,8 @@ class _AuthEntryButton extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: foreground,
|
color: foreground,
|
||||||
fontSize: 13,
|
fontSize: _entryButtonFontSize,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w800,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -76,10 +76,7 @@ class _AuthCard extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.card,
|
color: AppColors.card,
|
||||||
borderRadius: BorderRadius.circular(_cardRadius),
|
borderRadius: BorderRadius.circular(_cardRadius),
|
||||||
border: Border.all(
|
boxShadow: const [AppTheme.cardShadow],
|
||||||
color: AppColors.outlineVariantDark,
|
|
||||||
width: _cardBorderWidth,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -94,7 +91,7 @@ class _AuthCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
|
status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.onPrimary,
|
color: AppColors.onSurface,
|
||||||
fontSize: 25,
|
fontSize: 25,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
@@ -149,12 +146,11 @@ class _AuthModeSwitch extends StatelessWidget {
|
|||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: _minTapTarget,
|
height: 36,
|
||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.bar,
|
color: AppColors.raised,
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(18),
|
||||||
border: Border.all(color: AppColors.outlineVariantDark),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ void main() {
|
|||||||
final errorBorder = email.decoration!.errorBorder! as OutlineInputBorder;
|
final errorBorder = email.decoration!.errorBorder! as OutlineInputBorder;
|
||||||
final scheme = Theme.of(tester.element(emailFinder)).colorScheme;
|
final scheme = Theme.of(tester.element(emailFinder)).colorScheme;
|
||||||
expect(errorBorder.borderSide.color, scheme.error);
|
expect(errorBorder.borderSide.color, scheme.error);
|
||||||
expect(errorBorder.borderSide.width, 1.5);
|
expect(errorBorder.borderSide.width, 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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/theme/app_theme.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';
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ void main() {
|
|||||||
AuthApi? api,
|
AuthApi? api,
|
||||||
FakeHaptics? haptics,
|
FakeHaptics? haptics,
|
||||||
bool openEmail = true,
|
bool openEmail = true,
|
||||||
|
bool asRoute = false,
|
||||||
}) async {
|
}) async {
|
||||||
final auth = AuthController(
|
final auth = AuthController(
|
||||||
api ??
|
api ??
|
||||||
@@ -53,11 +55,15 @@ void main() {
|
|||||||
locale: const Locale('en'),
|
locale: const Locale('en'),
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
home: const AuthPage(),
|
home: asRoute ? const _AuthRouteHost() : const AuthPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.pump(const Duration(milliseconds: 220));
|
await tester.pump(const Duration(milliseconds: 220));
|
||||||
|
if (asRoute) {
|
||||||
|
await tester.tap(find.byKey(const Key('auth-open')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
if (openEmail) {
|
if (openEmail) {
|
||||||
await tester.ensureVisible(find.byKey(const Key('auth-entry-email')));
|
await tester.ensureVisible(find.byKey(const Key('auth-entry-email')));
|
||||||
await tester.tap(find.byKey(const Key('auth-entry-email')));
|
await tester.tap(find.byKey(const Key('auth-entry-email')));
|
||||||
@@ -78,21 +84,46 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
tester.getSize(find.byKey(const Key('auth-brand-logo'))),
|
tester.getSize(find.byKey(const Key('auth-brand-logo'))),
|
||||||
const Size(64, 64),
|
const Size(72, 72),
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('auth-entry-google')), findsNothing);
|
expect(find.byKey(const Key('auth-entry-google')), findsNothing);
|
||||||
expect(find.byKey(const Key('auth-entry-phone')), 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-entry-email')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('auth-legal')), findsOneWidget);
|
expect(find.byKey(const Key('auth-legal')), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.getSize(find.byKey(const Key('auth-entry-phone'))).height,
|
||||||
|
50,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getSize(find.byKey(const Key('auth-entry-email'))).height,
|
||||||
|
50,
|
||||||
|
);
|
||||||
|
final phoneButton = tester.widget<Material>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('auth-entry-phone')),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(phoneButton.color, AppColors.blue);
|
||||||
|
final emailButton = tester.widget<Material>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('auth-entry-email')),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(emailButton.color, AppColors.card);
|
||||||
|
expect(
|
||||||
|
(emailButton.shape! as RoundedRectangleBorder).side.color,
|
||||||
|
AppColors.border,
|
||||||
|
);
|
||||||
|
|
||||||
final glow = tester.widget<Container>(
|
final glow = tester.widget<Container>(
|
||||||
find.byKey(const Key('auth-background-glow')),
|
find.byKey(const Key('auth-background-glow')),
|
||||||
);
|
);
|
||||||
final glowDecoration = glow.decoration! as BoxDecoration;
|
final glowDecoration = glow.decoration! as BoxDecoration;
|
||||||
final gradient = glowDecoration.gradient! as LinearGradient;
|
final gradient = glowDecoration.gradient! as RadialGradient;
|
||||||
for (final color in gradient.colors) {
|
expect(gradient.colors.first, AppColors.surfaceTop);
|
||||||
expect(color.a, lessThanOrEqualTo(0.18));
|
expect(gradient.colors.last.a, 0);
|
||||||
}
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('auth-entry-email')));
|
await tester.tap(find.byKey(const Key('auth-entry-email')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -101,19 +132,28 @@ void main() {
|
|||||||
final cardDecoration = card.decoration! as BoxDecoration;
|
final cardDecoration = card.decoration! as BoxDecoration;
|
||||||
expect(cardDecoration.color, AppColors.card);
|
expect(cardDecoration.color, AppColors.card);
|
||||||
expect(cardDecoration.borderRadius, BorderRadius.circular(20));
|
expect(cardDecoration.borderRadius, BorderRadius.circular(20));
|
||||||
expect(
|
expect(cardDecoration.boxShadow, contains(AppTheme.cardShadow));
|
||||||
(cardDecoration.border! as Border).top.color,
|
expect(card.padding, const EdgeInsets.all(20));
|
||||||
AppColors.outlineVariantDark,
|
|
||||||
);
|
|
||||||
expect((cardDecoration.border! as Border).top.width, 1);
|
|
||||||
|
|
||||||
final email = tester.widget<TextField>(
|
final email = tester.widget<TextField>(
|
||||||
find.byKey(const Key('auth-email')),
|
find.byKey(const Key('auth-email')),
|
||||||
);
|
);
|
||||||
expect(email.decoration?.fillColor, AppColors.bar);
|
expect(email.decoration?.fillColor, AppColors.card);
|
||||||
|
expect(email.style?.color, AppColors.onSurface);
|
||||||
|
expect(email.style?.fontSize, 15);
|
||||||
|
expect(email.cursorColor, AppColors.blue);
|
||||||
expect(tester.getSize(find.byKey(const Key('auth-email'))).height, 52);
|
expect(tester.getSize(find.byKey(const Key('auth-email'))).height, 52);
|
||||||
final emailBorder = email.decoration!.border! as OutlineInputBorder;
|
final emailBorder =
|
||||||
|
email.decoration!.enabledBorder! as OutlineInputBorder;
|
||||||
expect(emailBorder.borderRadius, BorderRadius.circular(14));
|
expect(emailBorder.borderRadius, BorderRadius.circular(14));
|
||||||
|
expect(emailBorder.borderSide.color, AppColors.border);
|
||||||
|
expect(emailBorder.borderSide.width, 1);
|
||||||
|
final password = tester.widget<TextField>(
|
||||||
|
find.byKey(const Key('auth-password')),
|
||||||
|
);
|
||||||
|
expect(password.style?.color, AppColors.onSurface);
|
||||||
|
expect(password.style?.fontSize, 15);
|
||||||
|
expect(password.cursorColor, AppColors.blue);
|
||||||
|
|
||||||
final submit = tester.widget<FilledButton>(
|
final submit = tester.widget<FilledButton>(
|
||||||
find.byKey(const Key('auth-submit')),
|
find.byKey(const Key('auth-submit')),
|
||||||
@@ -123,11 +163,11 @@ void main() {
|
|||||||
submit.style!.backgroundColor!.resolve(const {WidgetState.pressed}),
|
submit.style!.backgroundColor!.resolve(const {WidgetState.pressed}),
|
||||||
AppColors.bluePressed,
|
AppColors.bluePressed,
|
||||||
);
|
);
|
||||||
expect(tester.getSize(find.byKey(const Key('auth-submit'))).height, 52);
|
expect(tester.getSize(find.byKey(const Key('auth-submit'))).height, 50);
|
||||||
expect(
|
expect(
|
||||||
(submit.style!.shape!.resolve(const {})! as RoundedRectangleBorder)
|
(submit.style!.shape!.resolve(const {})! as RoundedRectangleBorder)
|
||||||
.borderRadius,
|
.borderRadius,
|
||||||
BorderRadius.circular(26),
|
BorderRadius.circular(25),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.text('Sign In'), findsWidgets);
|
expect(find.text('Sign In'), findsWidgets);
|
||||||
@@ -139,6 +179,23 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('should_render_40px_back_button_when_page_is_pushed', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await pumpAuthPage(tester, openEmail: false, asRoute: true);
|
||||||
|
|
||||||
|
final back = find.byKey(const Key('auth-back'));
|
||||||
|
expect(tester.getSize(back), const Size(40, 40));
|
||||||
|
expect(
|
||||||
|
tester
|
||||||
|
.widget<Material>(
|
||||||
|
find.descendant(of: back, matching: find.byType(Material)),
|
||||||
|
)
|
||||||
|
.color,
|
||||||
|
AppColors.card,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_fade_and_slide_content_in_within_220ms', (tester) async {
|
testWidgets('should_fade_and_slide_content_in_within_220ms', (tester) async {
|
||||||
await pumpAuthPage(tester);
|
await pumpAuthPage(tester);
|
||||||
|
|
||||||
@@ -198,10 +255,10 @@ void main() {
|
|||||||
final focusedBorder =
|
final focusedBorder =
|
||||||
email.decoration!.focusedBorder! as OutlineInputBorder;
|
email.decoration!.focusedBorder! as OutlineInputBorder;
|
||||||
expect(focusedBorder.borderSide.color, AppColors.blue);
|
expect(focusedBorder.borderSide.color, AppColors.blue);
|
||||||
expect(focusedBorder.borderSide.width, 1.5);
|
expect(focusedBorder.borderSide.width, 1);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Icon>(find.byIcon(Icons.mail_outline_rounded)).color,
|
tester.widget<Icon>(find.byIcon(Icons.mail_outline_rounded)).color,
|
||||||
AppColors.blueSoft,
|
AppColors.blue,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('auth-password-visibility')));
|
await tester.tap(find.byKey(const Key('auth-password-visibility')));
|
||||||
@@ -280,6 +337,7 @@ void main() {
|
|||||||
final error = tester.widget<Text>(errorFinder);
|
final error = tester.widget<Text>(errorFinder);
|
||||||
final scheme = Theme.of(tester.element(errorFinder)).colorScheme;
|
final scheme = Theme.of(tester.element(errorFinder)).colorScheme;
|
||||||
expect(error.style?.color, scheme.error);
|
expect(error.style?.color, scheme.error);
|
||||||
|
expect(error.style?.fontSize, 12);
|
||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: capsuleFinder,
|
of: capsuleFinder,
|
||||||
@@ -325,3 +383,22 @@ class _PendingAuthApi extends AuthApi {
|
|||||||
throw const ApiException(500, 'pending failure');
|
throw const ApiException(500, 'pending failure');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AuthRouteHost extends StatelessWidget {
|
||||||
|
const _AuthRouteHost();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: TextButton(
|
||||||
|
key: const Key('auth-open'),
|
||||||
|
onPressed: () => Navigator.of(
|
||||||
|
context,
|
||||||
|
).push<void>(MaterialPageRoute(builder: (_) => const AuthPage())),
|
||||||
|
child: const Text('Open auth'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user