Files
ReelShort/lib/pages/auth/auth_form_widgets.dart
T

403 lines
12 KiB
Dart

// 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: 6),
_emailField(context, hasError: emailError),
const SizedBox(height: 14),
_fieldLabel(state.l10n.authPasswordLabel),
const SizedBox(height: 6),
_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.onSurface, fontSize: 15),
cursorColor: AppColors.blue,
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.onSurface, fontSize: 15),
cursorColor: AppColors.blue,
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: 12,
fontWeight: FontWeight.w700,
),
);
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: const BorderSide(
color: AppColors.border,
width: _fieldBorderWidth,
),
);
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.subtle, fontSize: 14),
prefixIcon: Icon(
icon,
size: 21,
color: focused ? AppColors.blue : AppColors.muted,
),
prefixIconConstraints: const BoxConstraints(
minWidth: _minTapTarget,
minHeight: _minTapTarget,
),
suffixIcon: suffix,
filled: true,
fillColor: AppColors.card,
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: 15,
fontWeight: FontWeight.w800,
),
),
),
);
}
}
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 : AppColors.transparent,
borderRadius: BorderRadius.circular(18),
),
child: Stack(
fit: StackFit.expand,
children: [
Center(
child: Text(
label,
style: TextStyle(
color: selected ? AppColors.onPrimary : AppColors.onSurface,
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() => _brandMarkWidget(
key: const Key('auth-brand-mark'),
size: compact ? _compactBrandMarkSize : _brandMarkSize,
radius: compact ? _compactBrandMarkRadius : _brandMarkRadius,
);
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.onSurface,
fontSize: 24,
fontWeight: FontWeight.w900,
height: 1.1,
),
),
const SizedBox(height: 6),
Text(
tagline,
textAlign: centered ? TextAlign.center : TextAlign.start,
style: const TextStyle(
color: AppColors.muted,
fontSize: 13,
height: 1.35,
),
),
],
);
}
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: 12, height: 1.35),
),
),
],
),
);
}
}