feat(#98): améliorer le login avec autofill natif et navigation clavier
Active l’autofill navigateur/OS sur le formulaire de connexion et complète l’accessibilité clavier (Tab jusqu’au bouton, Entrée sur le mot de passe) sans stockage local custom des identifiants. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
80d69a5463
commit
a4e6cfc50e
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -20,7 +21,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@ -63,6 +64,11 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
return null;
|
||||
}
|
||||
|
||||
void _handlePasswordSubmitted(String _) {
|
||||
if (_isLoading) return;
|
||||
_handleLogin();
|
||||
}
|
||||
|
||||
/// Gère la connexion de l'utilisateur
|
||||
Future<void> _handleLogin() async {
|
||||
// Réinitialiser le message d'erreur
|
||||
@ -90,7 +96,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
// Vérifier si l'utilisateur doit changer son mot de passe
|
||||
if (user.changementMdpObligatoire) {
|
||||
if (!mounted) return;
|
||||
|
||||
|
||||
// Afficher la modale de changement de mot de passe (non-dismissible)
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
@ -106,6 +112,9 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Laisse au navigateur/OS la possibilité de mémoriser les identifiants.
|
||||
TextInput.finishAutofillContext(shouldSave: true);
|
||||
|
||||
// Rediriger selon le rôle de l'utilisateur
|
||||
_redirectUserByRole(user.role);
|
||||
} catch (e) {
|
||||
@ -152,47 +161,49 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
final w = constraints.maxWidth;
|
||||
final h = constraints.maxHeight;
|
||||
return FutureBuilder(
|
||||
future: _getImageDimensions(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
future: _getImageDimensions(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final imageDimensions = snapshot.data!;
|
||||
final imageHeight = h;
|
||||
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
|
||||
final remainingWidth = w - imageWidth;
|
||||
final leftMargin = remainingWidth / 4;
|
||||
final imageDimensions = snapshot.data!;
|
||||
final imageHeight = h;
|
||||
final imageWidth = imageHeight *
|
||||
(imageDimensions.width / imageDimensions.height);
|
||||
final remainingWidth = w - imageWidth;
|
||||
final leftMargin = remainingWidth / 4;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Fond en papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
return Stack(
|
||||
children: [
|
||||
// Fond en papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
// Image principale
|
||||
Positioned(
|
||||
left: leftMargin,
|
||||
top: 0,
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
child: Image.asset(
|
||||
'assets/images/river_logo_desktop.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
// Image principale
|
||||
Positioned(
|
||||
left: leftMargin,
|
||||
top: 0,
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
child: Image.asset(
|
||||
'assets/images/river_logo_desktop.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
// Formulaire dans le cadran en bas à droite
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: w * 0.6, // 60% de la largeur de l'écran
|
||||
height: h * 0.5, // 50% de la hauteur de l'écran
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
||||
),
|
||||
// Formulaire dans le cadran en bas à droite
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: w * 0.6, // 60% de la largeur de l'écran
|
||||
height: h * 0.5, // 50% de la hauteur de l'écran
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
||||
child: AutofillGroup(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
@ -207,6 +218,12 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
controller: _emailController,
|
||||
labelText: 'Email',
|
||||
hintText: 'Votre adresse email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
AutofillHints.email,
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: _validateEmail,
|
||||
style: CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 53,
|
||||
@ -220,6 +237,12 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
labelText: 'Mot de passe',
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
autofillHints: const [
|
||||
AutofillHints.password
|
||||
],
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted:
|
||||
_handlePasswordSubmitted,
|
||||
validator: _validatePassword,
|
||||
style: CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 53,
|
||||
@ -229,7 +252,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
|
||||
// Message d'erreur
|
||||
if (_errorMessage != null)
|
||||
Container(
|
||||
@ -242,7 +265,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red[700], size: 20),
|
||||
Icon(Icons.error_outline,
|
||||
color: Colors.red[700], size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@ -256,7 +280,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Bouton centré
|
||||
Center(
|
||||
child: _isLoading
|
||||
@ -309,67 +333,68 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Pied de page (Wrap pour éviter overflow sur petite largeur)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_FooterLink(
|
||||
text: 'Contact support',
|
||||
onTap: () async {
|
||||
final Uri emailLaunchUri = Uri(
|
||||
scheme: 'mailto',
|
||||
path: 'support@supernounou.local',
|
||||
);
|
||||
if (await canLaunchUrl(emailLaunchUri)) {
|
||||
await launchUrl(emailLaunchUri);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Impossible d\'ouvrir le client mail',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
),
|
||||
// Pied de page (Wrap pour éviter overflow sur petite largeur)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_FooterLink(
|
||||
text: 'Contact support',
|
||||
onTap: () async {
|
||||
final Uri emailLaunchUri = Uri(
|
||||
scheme: 'mailto',
|
||||
path: 'support@supernounou.local',
|
||||
);
|
||||
if (await canLaunchUrl(emailLaunchUri)) {
|
||||
await launchUrl(emailLaunchUri);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Impossible d\'ouvrir le client mail',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Signaler un bug',
|
||||
onTap: () {
|
||||
_showBugReportDialog(context);
|
||||
},
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Mentions légales',
|
||||
onTap: () {
|
||||
context.go('/legal');
|
||||
},
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Politique de confidentialité',
|
||||
onTap: () {
|
||||
context.go('/privacy');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Signaler un bug',
|
||||
onTap: () {
|
||||
_showBugReportDialog(context);
|
||||
},
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Mentions légales',
|
||||
onTap: () {
|
||||
context.go('/legal');
|
||||
},
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Politique de confidentialité',
|
||||
onTap: () {
|
||||
context.go('/privacy');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -378,6 +403,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
/// Dimensions de river_logo_mobile.png (à mettre à jour si l'asset change).
|
||||
static const int _riverLogoMobileWidth = 600;
|
||||
static const int _riverLogoMobileHeight = 1080;
|
||||
|
||||
/// Fraction de la hauteur de l'image où se termine visuellement le slogan (0 = haut, 1 = bas).
|
||||
static const double _sloganEndFraction = 0.42;
|
||||
static const double _gapBelowSlogan = 12.0;
|
||||
@ -388,7 +414,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
final h = constraints.maxHeight;
|
||||
final w = constraints.maxWidth;
|
||||
final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth;
|
||||
final formTop = w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan;
|
||||
final formTop =
|
||||
w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
@ -428,95 +455,115 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 20),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppTextField(
|
||||
controller: _emailController,
|
||||
labelText: 'Email',
|
||||
showLabel: false,
|
||||
hintText: 'Votre adresse email',
|
||||
validator: _validateEmail,
|
||||
style: CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomAppTextField(
|
||||
controller: _passwordController,
|
||||
labelText: 'Mot de passe',
|
||||
showLabel: false,
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
validator: _validatePassword,
|
||||
style: CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.red.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red.shade700, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700),
|
||||
),
|
||||
child: AutofillGroup(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppTextField(
|
||||
controller: _emailController,
|
||||
labelText: 'Email',
|
||||
showLabel: false,
|
||||
hintText: 'Votre adresse email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [
|
||||
AutofillHints.username,
|
||||
AutofillHints.email,
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: _validateEmail,
|
||||
style: CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomAppTextField(
|
||||
controller: _passwordController,
|
||||
labelText: 'Mot de passe',
|
||||
showLabel: false,
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
autofillHints: const [
|
||||
AutofillHints.password
|
||||
],
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: _handlePasswordSubmitted,
|
||||
validator: _validatePassword,
|
||||
style: CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.red.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline,
|
||||
color: Colors.red.shade700,
|
||||
size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 12,
|
||||
color: Colors.red.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
_isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: ImageButton(
|
||||
bg: 'assets/images/bg_green.png',
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
text: 'Se connecter',
|
||||
textColor: const Color(0xFF2D6A4F),
|
||||
onPressed: _handleLogin,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () {/* TODO */},
|
||||
child: Text(
|
||||
'Mot de passe oublié ?',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 14,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
context.go('/register-choice'),
|
||||
child: Text(
|
||||
'Créer un compte',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 16,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
_isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: ImageButton(
|
||||
bg: 'assets/images/bg_green.png',
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
text: 'Se connecter',
|
||||
textColor: const Color(0xFF2D6A4F),
|
||||
onPressed: _handleLogin,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () { /* TODO */ },
|
||||
child: Text(
|
||||
'Mot de passe oublié ?',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 14,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/register-choice'),
|
||||
child: Text(
|
||||
'Créer un compte',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 16,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -533,12 +580,17 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
text: 'Contact support',
|
||||
fontSize: 11,
|
||||
onTap: () async {
|
||||
final uri = Uri(scheme: 'mailto', path: 'support@supernounou.local');
|
||||
final uri = Uri(
|
||||
scheme: 'mailto',
|
||||
path: 'support@supernounou.local');
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Impossible d\'ouvrir le client mail', style: GoogleFonts.merienda())),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Impossible d\'ouvrir le client mail',
|
||||
style: GoogleFonts.merienda())),
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -707,4 +759,4 @@ class _FooterLink extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ enum CustomAppTextFieldStyle {
|
||||
|
||||
class CustomAppTextField extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
final String labelText;
|
||||
final String hintText;
|
||||
final double fieldWidth;
|
||||
@ -26,10 +27,14 @@ class CustomAppTextField extends StatefulWidget {
|
||||
final double labelFontSize;
|
||||
final double inputFontSize;
|
||||
final bool showLabel;
|
||||
final Iterable<String>? autofillHints;
|
||||
final TextInputAction? textInputAction;
|
||||
final ValueChanged<String>? onFieldSubmitted;
|
||||
|
||||
const CustomAppTextField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.focusNode,
|
||||
required this.labelText,
|
||||
this.showLabel = true,
|
||||
this.hintText = '',
|
||||
@ -46,6 +51,9 @@ class CustomAppTextField extends StatefulWidget {
|
||||
this.suffixIcon,
|
||||
this.labelFontSize = 18.0,
|
||||
this.inputFontSize = 18.0,
|
||||
this.autofillHints,
|
||||
this.textInputAction,
|
||||
this.onFieldSubmitted,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -68,7 +76,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double fontHeightMultiplier = 1.2;
|
||||
const double internalVerticalPadding = 16.0;
|
||||
const double internalVerticalPadding = 16.0;
|
||||
final double dynamicFieldHeight = widget.fieldHeight;
|
||||
|
||||
return Column(
|
||||
@ -90,7 +98,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||||
width: widget.fieldWidth,
|
||||
height: dynamicFieldHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
@ -99,40 +107,49 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
|
||||
child: TextFormField(
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
obscureText: widget.obscureText,
|
||||
keyboardType: widget.keyboardType,
|
||||
autofillHints: widget.autofillHints,
|
||||
textInputAction: widget.textInputAction,
|
||||
onFieldSubmitted: widget.onFieldSubmitted,
|
||||
enabled: widget.enabled,
|
||||
readOnly: widget.readOnly,
|
||||
onTap: widget.onTap,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: widget.inputFontSize,
|
||||
color: widget.enabled ? Colors.black87 : Colors.grey
|
||||
),
|
||||
fontSize: widget.inputFontSize,
|
||||
color: widget.enabled ? Colors.black87 : Colors.grey),
|
||||
validator: widget.validator ??
|
||||
(value) {
|
||||
if (!widget.enabled || widget.readOnly) return null;
|
||||
if (widget.isRequired && (value == null || value.isEmpty)) {
|
||||
return 'Ce champ est obligatoire';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
if (!widget.enabled || widget.readOnly) return null;
|
||||
if (widget.isRequired &&
|
||||
(value == null || value.isEmpty)) {
|
||||
return 'Ce champ est obligatoire';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle: GoogleFonts.merienda(fontSize: widget.inputFontSize, color: Colors.black54.withOpacity(0.7)),
|
||||
hintStyle: GoogleFonts.merienda(
|
||||
fontSize: widget.inputFontSize,
|
||||
color: Colors.black54.withOpacity(0.7)),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 0.0),
|
||||
child: Icon(widget.suffixIcon, color: Colors.black54, size: widget.inputFontSize * 1.1),
|
||||
child: Icon(widget.suffixIcon,
|
||||
color: Colors.black54,
|
||||
size: widget.inputFontSize * 1.1),
|
||||
)
|
||||
: null,
|
||||
isDense: true,
|
||||
),
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -141,4 +158,4 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,26 +23,36 @@ class ImageButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(bg),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: text,
|
||||
child: TextButton(
|
||||
onPressed: onPressed,
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
shape:
|
||||
const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: GoogleFonts.merienda(
|
||||
color: textColor,
|
||||
fontSize: fontSize, // Utilisation du paramètre
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(bg),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: GoogleFonts.merienda(
|
||||
color: textColor,
|
||||
fontSize: fontSize, // Utilisation du paramètre
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -50,4 +60,4 @@ class ImageButton extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user