// Author: fruit.com.cn part of 'auth_view.dart'; class _AuthFields extends StatelessWidget { const _AuthFields({required this.state, required this.callbacks}); final _AuthCardState state; final _AuthCardCallbacks callbacks; @override Widget build(BuildContext context) { final error = state.status.fieldError; final emailError = error == AuthFieldError.emailRequired || error == AuthFieldError.emailInvalid; final passwordError = error == AuthFieldError.passwordRequired || error == AuthFieldError.passwordLength || error == AuthFieldError.passwordWeak; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _fieldLabel(state.l10n.authEmailLabel), const SizedBox(height: 8), _emailField(context, hasError: emailError), const SizedBox(height: 16), _fieldLabel(state.l10n.authPasswordLabel), const SizedBox(height: 8), _passwordField(context, hasError: passwordError), if (state.status.signUpMode) ...[ const SizedBox(height: 8), Text( state.l10n.authPasswordHint, key: const Key('auth-password-rule'), style: const TextStyle( color: AppColors.muted, fontSize: AppDimens.caption, ), ), ], ], ); } Widget _emailField(BuildContext context, {required bool hasError}) { final enabled = !state.status.busy; return ListenableBuilder( listenable: state.fields.emailFocus, builder: (context, _) => SizedBox( height: _fieldHeight, child: TextField( key: const Key('auth-email'), controller: state.fields.email, focusNode: state.fields.emailFocus, enabled: enabled, autofocus: true, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, autofillHints: const [AutofillHints.email], onChanged: callbacks.onEmailChanged, onSubmitted: (_) => state.fields.passwordFocus.requestFocus(), style: const TextStyle(color: AppColors.onPrimary, fontSize: 15), decoration: _decoration( context: context, hint: 'name@example.com', icon: Icons.mail_outline_rounded, focused: state.fields.emailFocus.hasFocus, hasError: hasError, ), ), ), ); } Widget _passwordField(BuildContext context, {required bool hasError}) { final enabled = !state.status.busy; final l10n = state.l10n; return ListenableBuilder( listenable: state.fields.passwordFocus, builder: (context, _) => SizedBox( height: _fieldHeight, child: TextField( key: const Key('auth-password'), controller: state.fields.password, focusNode: state.fields.passwordFocus, enabled: enabled, obscureText: state.status.obscurePassword, keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.done, autofillHints: const [AutofillHints.password], onChanged: callbacks.onPasswordChanged, onSubmitted: (_) => callbacks.onSubmit(), style: const TextStyle(color: AppColors.onPrimary, fontSize: 15), decoration: _decoration( context: context, hint: l10n.authPasswordHint, icon: Icons.lock_outline_rounded, focused: state.fields.passwordFocus.hasFocus, hasError: hasError, suffix: IconButton( key: const Key('auth-password-visibility'), constraints: const BoxConstraints.tightFor( width: _minTapTarget, height: _minTapTarget, ), tooltip: state.status.obscurePassword ? l10n.showPassword : l10n.hidePassword, onPressed: enabled ? callbacks.onTogglePasswordVisibility : null, icon: Icon( state.status.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: AppColors.muted, ), ), ), ), ), ); } Widget _fieldLabel(String label) => Text( label, style: const TextStyle( color: AppColors.muted, fontSize: 13, fontWeight: FontWeight.w600, ), ); InputDecoration _decoration({ required BuildContext context, required String hint, required IconData icon, required bool focused, required bool hasError, Widget? suffix, }) { final border = OutlineInputBorder( borderRadius: BorderRadius.circular(_fieldRadius), borderSide: BorderSide.none, ); final errorColor = Theme.of(context).colorScheme.error; final errorBorder = OutlineInputBorder( borderRadius: BorderRadius.circular(_fieldRadius), borderSide: BorderSide(color: errorColor, width: _fieldBorderWidth), ); return InputDecoration( hintText: hint, hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14), prefixIcon: Icon( icon, size: 21, color: focused ? AppColors.blueSoft : AppColors.muted, ), prefixIconConstraints: const BoxConstraints( minWidth: _minTapTarget, minHeight: _minTapTarget, ), suffixIcon: suffix, filled: true, fillColor: AppColors.bar, isDense: true, contentPadding: const EdgeInsets.only(right: 16), border: border, enabledBorder: border, disabledBorder: border, focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(_fieldRadius), borderSide: const BorderSide( color: AppColors.blue, width: _fieldBorderWidth, ), ), errorBorder: errorBorder, focusedErrorBorder: errorBorder, ); } } class _AuthSubmitButton extends StatelessWidget { const _AuthSubmitButton({required this.state, required this.onSubmit}); final _AuthCardState state; final VoidCallback onSubmit; @override Widget build(BuildContext context) { final status = state.status; final l10n = state.l10n; return SizedBox( width: double.infinity, height: _buttonHeight, child: FilledButton( key: const Key('auth-submit'), onPressed: status.busy ? null : onSubmit, style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return AppColors.blue.withValues(alpha: 0.5); } if (states.contains(WidgetState.pressed)) { return AppColors.bluePressed; } return AppColors.blue; }), foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), elevation: const WidgetStatePropertyAll(2), shadowColor: WidgetStatePropertyAll( AppColors.blue.withValues(alpha: 0.22), ), padding: const WidgetStatePropertyAll(EdgeInsets.zero), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(_buttonRadius), ), ), ), child: status.busy ? const SizedBox.square( dimension: 18, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.onPrimary, ), ) : Text( status.signUpMode ? l10n.signUp : l10n.signIn, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, ), ), ), ); } } class _ModeSegment extends StatelessWidget { const _ModeSegment({ required this.segmentKey, required this.label, required this.hiddenLabel, required this.selected, required this.onTap, }); final Key segmentKey; final String label; final String hiddenLabel; final bool selected; final VoidCallback? onTap; @override Widget build(BuildContext context) { return GestureDetector( key: segmentKey, behavior: HitTestBehavior.opaque, onTap: onTap, child: AnimatedContainer( duration: _segmentDuration, alignment: Alignment.center, decoration: BoxDecoration( color: selected ? AppColors.blue : Colors.transparent, borderRadius: BorderRadius.circular(18), ), child: Stack( fit: StackFit.expand, children: [ Center( child: Text( label, style: TextStyle( color: selected ? AppColors.onPrimary : AppColors.muted, fontSize: 14, fontWeight: FontWeight.w700, ), ), ), Positioned.fill( child: ExcludeSemantics( child: Opacity( opacity: 0, child: Center( child: Text( hiddenLabel, maxLines: 1, softWrap: false, style: const TextStyle(fontSize: 14), ), ), ), ), ), ], ), ), ); } } class _AuthBrand extends StatelessWidget { const _AuthBrand({required this.tagline, required this.compact}); final String tagline; final bool compact; @override Widget build(BuildContext context) { final copy = _brandCopy(context, centered: !compact); if (compact) { return Row( key: const Key('auth-brand'), children: [ _brandMark(), const SizedBox(width: 14), Expanded(child: copy), ], ); } return Column( key: const Key('auth-brand'), mainAxisSize: MainAxisSize.min, children: [_brandMark(), const SizedBox(height: 14), copy], ); } Widget _brandMark() => Container( key: const Key('auth-brand-mark'), width: _brandMarkSize, height: _brandMarkSize, decoration: BoxDecoration( 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( crossAxisAlignment: centered ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)!.appName, textAlign: centered ? TextAlign.center : TextAlign.start, style: const TextStyle( color: AppColors.onPrimary, fontSize: 26, fontWeight: FontWeight.w800, height: 1.1, ), ), const SizedBox(height: 6), Text( tagline, textAlign: centered ? TextAlign.center : TextAlign.start, style: const TextStyle( color: AppColors.blueSoft, fontSize: 14, height: 1.35, ), ), ], ); } class _AuthError extends StatelessWidget { const _AuthError({required this.message}); final String message; @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.error; return Container( key: const Key('auth-error-capsule'), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: color.withValues(alpha: _errorOpacity), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withValues(alpha: _errorBorderOpacity)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.error_outline_rounded, color: color, size: 18), const SizedBox(width: 8), Expanded( child: Text( message, key: const Key('auth-error'), style: TextStyle(color: color, fontSize: 13, height: 1.35), ), ), ], ), ); } }