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 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@ -63,6 +64,11 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handlePasswordSubmitted(String _) {
|
||||||
|
if (_isLoading) return;
|
||||||
|
_handleLogin();
|
||||||
|
}
|
||||||
|
|
||||||
/// Gère la connexion de l'utilisateur
|
/// Gère la connexion de l'utilisateur
|
||||||
Future<void> _handleLogin() async {
|
Future<void> _handleLogin() async {
|
||||||
// Réinitialiser le message d'erreur
|
// Réinitialiser le message d'erreur
|
||||||
@ -106,6 +112,9 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
if (!mounted) return;
|
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
|
// Rediriger selon le rôle de l'utilisateur
|
||||||
_redirectUserByRole(user.role);
|
_redirectUserByRole(user.role);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -152,47 +161,49 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
final w = constraints.maxWidth;
|
final w = constraints.maxWidth;
|
||||||
final h = constraints.maxHeight;
|
final h = constraints.maxHeight;
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: _getImageDimensions(),
|
future: _getImageDimensions(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
final imageDimensions = snapshot.data!;
|
final imageDimensions = snapshot.data!;
|
||||||
final imageHeight = h;
|
final imageHeight = h;
|
||||||
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
|
final imageWidth = imageHeight *
|
||||||
final remainingWidth = w - imageWidth;
|
(imageDimensions.width / imageDimensions.height);
|
||||||
final leftMargin = remainingWidth / 4;
|
final remainingWidth = w - imageWidth;
|
||||||
|
final leftMargin = remainingWidth / 4;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Fond en papier
|
// Fond en papier
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/paper2.png',
|
'assets/images/paper2.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
repeat: ImageRepeat.repeat,
|
repeat: ImageRepeat.repeat,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// Image principale
|
),
|
||||||
Positioned(
|
// Image principale
|
||||||
left: leftMargin,
|
Positioned(
|
||||||
top: 0,
|
left: leftMargin,
|
||||||
height: imageHeight,
|
top: 0,
|
||||||
width: imageWidth,
|
height: imageHeight,
|
||||||
child: Image.asset(
|
width: imageWidth,
|
||||||
'assets/images/river_logo_desktop.png',
|
child: Image.asset(
|
||||||
fit: BoxFit.contain,
|
'assets/images/river_logo_desktop.png',
|
||||||
),
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
// Formulaire dans le cadran en bas à droite
|
),
|
||||||
Positioned(
|
// Formulaire dans le cadran en bas à droite
|
||||||
right: 0,
|
Positioned(
|
||||||
bottom: 0,
|
right: 0,
|
||||||
width: w * 0.6, // 60% de la largeur de l'écran
|
bottom: 0,
|
||||||
height: h * 0.5, // 50% de la hauteur de l'écran
|
width: w * 0.6, // 60% de la largeur de l'écran
|
||||||
child: Padding(
|
height: h * 0.5, // 50% de la hauteur de l'écran
|
||||||
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
||||||
|
child: AutofillGroup(
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -207,6 +218,12 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
hintText: 'Votre adresse email',
|
hintText: 'Votre adresse email',
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
autofillHints: const [
|
||||||
|
AutofillHints.username,
|
||||||
|
AutofillHints.email,
|
||||||
|
],
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
validator: _validateEmail,
|
validator: _validateEmail,
|
||||||
style: CustomAppTextFieldStyle.lavande,
|
style: CustomAppTextFieldStyle.lavande,
|
||||||
fieldHeight: 53,
|
fieldHeight: 53,
|
||||||
@ -220,6 +237,12 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
labelText: 'Mot de passe',
|
labelText: 'Mot de passe',
|
||||||
hintText: 'Votre mot de passe',
|
hintText: 'Votre mot de passe',
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
|
autofillHints: const [
|
||||||
|
AutofillHints.password
|
||||||
|
],
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onFieldSubmitted:
|
||||||
|
_handlePasswordSubmitted,
|
||||||
validator: _validatePassword,
|
validator: _validatePassword,
|
||||||
style: CustomAppTextFieldStyle.jaune,
|
style: CustomAppTextFieldStyle.jaune,
|
||||||
fieldHeight: 53,
|
fieldHeight: 53,
|
||||||
@ -242,7 +265,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
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),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -309,67 +333,68 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Pied de page (Wrap pour éviter overflow sur petite largeur)
|
),
|
||||||
Positioned(
|
// Pied de page (Wrap pour éviter overflow sur petite largeur)
|
||||||
left: 0,
|
Positioned(
|
||||||
right: 0,
|
left: 0,
|
||||||
bottom: 0,
|
right: 0,
|
||||||
child: Container(
|
bottom: 0,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
color: Colors.transparent,
|
decoration: const BoxDecoration(
|
||||||
),
|
color: Colors.transparent,
|
||||||
child: Wrap(
|
),
|
||||||
alignment: WrapAlignment.center,
|
child: Wrap(
|
||||||
runSpacing: 8,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
runSpacing: 8,
|
||||||
_FooterLink(
|
children: [
|
||||||
text: 'Contact support',
|
_FooterLink(
|
||||||
onTap: () async {
|
text: 'Contact support',
|
||||||
final Uri emailLaunchUri = Uri(
|
onTap: () async {
|
||||||
scheme: 'mailto',
|
final Uri emailLaunchUri = Uri(
|
||||||
path: 'support@supernounou.local',
|
scheme: 'mailto',
|
||||||
);
|
path: 'support@supernounou.local',
|
||||||
if (await canLaunchUrl(emailLaunchUri)) {
|
);
|
||||||
await launchUrl(emailLaunchUri);
|
if (await canLaunchUrl(emailLaunchUri)) {
|
||||||
} else {
|
await launchUrl(emailLaunchUri);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
} else {
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(
|
SnackBar(
|
||||||
'Impossible d\'ouvrir le client mail',
|
content: Text(
|
||||||
style: GoogleFonts.merienda(),
|
'Impossible d\'ouvrir le client mail',
|
||||||
),
|
style: GoogleFonts.merienda(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
},
|
}
|
||||||
),
|
},
|
||||||
_FooterLink(
|
),
|
||||||
text: 'Signaler un bug',
|
_FooterLink(
|
||||||
onTap: () {
|
text: 'Signaler un bug',
|
||||||
_showBugReportDialog(context);
|
onTap: () {
|
||||||
},
|
_showBugReportDialog(context);
|
||||||
),
|
},
|
||||||
_FooterLink(
|
),
|
||||||
text: 'Mentions légales',
|
_FooterLink(
|
||||||
onTap: () {
|
text: 'Mentions légales',
|
||||||
context.go('/legal');
|
onTap: () {
|
||||||
},
|
context.go('/legal');
|
||||||
),
|
},
|
||||||
_FooterLink(
|
),
|
||||||
text: 'Politique de confidentialité',
|
_FooterLink(
|
||||||
onTap: () {
|
text: 'Politique de confidentialité',
|
||||||
context.go('/privacy');
|
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).
|
/// Dimensions de river_logo_mobile.png (à mettre à jour si l'asset change).
|
||||||
static const int _riverLogoMobileWidth = 600;
|
static const int _riverLogoMobileWidth = 600;
|
||||||
static const int _riverLogoMobileHeight = 1080;
|
static const int _riverLogoMobileHeight = 1080;
|
||||||
|
|
||||||
/// Fraction de la hauteur de l'image où se termine visuellement le slogan (0 = haut, 1 = bas).
|
/// 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 _sloganEndFraction = 0.42;
|
||||||
static const double _gapBelowSlogan = 12.0;
|
static const double _gapBelowSlogan = 12.0;
|
||||||
@ -388,7 +414,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
final h = constraints.maxHeight;
|
final h = constraints.maxHeight;
|
||||||
final w = constraints.maxWidth;
|
final w = constraints.maxWidth;
|
||||||
final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth;
|
final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth;
|
||||||
final formTop = w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan;
|
final formTop =
|
||||||
|
w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan;
|
||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
@ -428,95 +455,115 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24, vertical: 20),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 400),
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
child: Form(
|
child: AutofillGroup(
|
||||||
key: _formKey,
|
child: Form(
|
||||||
child: Column(
|
key: _formKey,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
const SizedBox(height: 16),
|
children: [
|
||||||
CustomAppTextField(
|
const SizedBox(height: 16),
|
||||||
controller: _emailController,
|
CustomAppTextField(
|
||||||
labelText: 'Email',
|
controller: _emailController,
|
||||||
showLabel: false,
|
labelText: 'Email',
|
||||||
hintText: 'Votre adresse email',
|
showLabel: false,
|
||||||
validator: _validateEmail,
|
hintText: 'Votre adresse email',
|
||||||
style: CustomAppTextFieldStyle.lavande,
|
keyboardType: TextInputType.emailAddress,
|
||||||
fieldHeight: 48,
|
autofillHints: const [
|
||||||
fieldWidth: double.infinity,
|
AutofillHints.username,
|
||||||
),
|
AutofillHints.email,
|
||||||
const SizedBox(height: 12),
|
],
|
||||||
CustomAppTextField(
|
textInputAction: TextInputAction.next,
|
||||||
controller: _passwordController,
|
validator: _validateEmail,
|
||||||
labelText: 'Mot de passe',
|
style: CustomAppTextFieldStyle.lavande,
|
||||||
showLabel: false,
|
fieldHeight: 48,
|
||||||
hintText: 'Votre mot de passe',
|
fieldWidth: double.infinity,
|
||||||
obscureText: true,
|
),
|
||||||
validator: _validatePassword,
|
const SizedBox(height: 12),
|
||||||
style: CustomAppTextFieldStyle.jaune,
|
CustomAppTextField(
|
||||||
fieldHeight: 48,
|
controller: _passwordController,
|
||||||
fieldWidth: double.infinity,
|
labelText: 'Mot de passe',
|
||||||
),
|
showLabel: false,
|
||||||
if (_errorMessage != null) ...[
|
hintText: 'Votre mot de passe',
|
||||||
const SizedBox(height: 12),
|
obscureText: true,
|
||||||
Container(
|
autofillHints: const [
|
||||||
padding: const EdgeInsets.all(12),
|
AutofillHints.password
|
||||||
decoration: BoxDecoration(
|
],
|
||||||
color: Colors.red.shade50,
|
textInputAction: TextInputAction.done,
|
||||||
borderRadius: BorderRadius.circular(10),
|
onFieldSubmitted: _handlePasswordSubmitted,
|
||||||
border: Border.all(color: Colors.red.shade300),
|
validator: _validatePassword,
|
||||||
),
|
style: CustomAppTextFieldStyle.jaune,
|
||||||
child: Row(
|
fieldHeight: 48,
|
||||||
children: [
|
fieldWidth: double.infinity,
|
||||||
Icon(Icons.error_outline, color: Colors.red.shade700, size: 20),
|
),
|
||||||
const SizedBox(width: 10),
|
if (_errorMessage != null) ...[
|
||||||
Expanded(
|
const SizedBox(height: 12),
|
||||||
child: Text(
|
Container(
|
||||||
_errorMessage!,
|
padding: const EdgeInsets.all(12),
|
||||||
style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700),
|
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',
|
text: 'Contact support',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final uri = Uri(scheme: 'mailto', path: 'support@supernounou.local');
|
final uri = Uri(
|
||||||
|
scheme: 'mailto',
|
||||||
|
path: 'support@supernounou.local');
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri);
|
await launchUrl(uri);
|
||||||
} else if (context.mounted) {
|
} else if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,6 +10,7 @@ enum CustomAppTextFieldStyle {
|
|||||||
|
|
||||||
class CustomAppTextField extends StatefulWidget {
|
class CustomAppTextField extends StatefulWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
|
final FocusNode? focusNode;
|
||||||
final String labelText;
|
final String labelText;
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final double fieldWidth;
|
final double fieldWidth;
|
||||||
@ -26,10 +27,14 @@ class CustomAppTextField extends StatefulWidget {
|
|||||||
final double labelFontSize;
|
final double labelFontSize;
|
||||||
final double inputFontSize;
|
final double inputFontSize;
|
||||||
final bool showLabel;
|
final bool showLabel;
|
||||||
|
final Iterable<String>? autofillHints;
|
||||||
|
final TextInputAction? textInputAction;
|
||||||
|
final ValueChanged<String>? onFieldSubmitted;
|
||||||
|
|
||||||
const CustomAppTextField({
|
const CustomAppTextField({
|
||||||
super.key,
|
super.key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
this.focusNode,
|
||||||
required this.labelText,
|
required this.labelText,
|
||||||
this.showLabel = true,
|
this.showLabel = true,
|
||||||
this.hintText = '',
|
this.hintText = '',
|
||||||
@ -46,6 +51,9 @@ class CustomAppTextField extends StatefulWidget {
|
|||||||
this.suffixIcon,
|
this.suffixIcon,
|
||||||
this.labelFontSize = 18.0,
|
this.labelFontSize = 18.0,
|
||||||
this.inputFontSize = 18.0,
|
this.inputFontSize = 18.0,
|
||||||
|
this.autofillHints,
|
||||||
|
this.textInputAction,
|
||||||
|
this.onFieldSubmitted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -99,35 +107,44 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
obscureText: widget.obscureText,
|
obscureText: widget.obscureText,
|
||||||
keyboardType: widget.keyboardType,
|
keyboardType: widget.keyboardType,
|
||||||
|
autofillHints: widget.autofillHints,
|
||||||
|
textInputAction: widget.textInputAction,
|
||||||
|
onFieldSubmitted: widget.onFieldSubmitted,
|
||||||
enabled: widget.enabled,
|
enabled: widget.enabled,
|
||||||
readOnly: widget.readOnly,
|
readOnly: widget.readOnly,
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
fontSize: widget.inputFontSize,
|
fontSize: widget.inputFontSize,
|
||||||
color: widget.enabled ? Colors.black87 : Colors.grey
|
color: widget.enabled ? Colors.black87 : Colors.grey),
|
||||||
),
|
|
||||||
validator: widget.validator ??
|
validator: widget.validator ??
|
||||||
(value) {
|
(value) {
|
||||||
if (!widget.enabled || widget.readOnly) return null;
|
if (!widget.enabled || widget.readOnly) return null;
|
||||||
if (widget.isRequired && (value == null || value.isEmpty)) {
|
if (widget.isRequired &&
|
||||||
return 'Ce champ est obligatoire';
|
(value == null || value.isEmpty)) {
|
||||||
}
|
return 'Ce champ est obligatoire';
|
||||||
return null;
|
}
|
||||||
},
|
return null;
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: widget.hintText,
|
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,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
suffixIcon: widget.suffixIcon != null
|
suffixIcon: widget.suffixIcon != null
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.only(right: 0.0),
|
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,
|
: null,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
|||||||
@ -23,26 +23,36 @@ class ImageButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MouseRegion(
|
return SizedBox(
|
||||||
cursor: SystemMouseCursors.click,
|
width: width,
|
||||||
child: GestureDetector(
|
height: height,
|
||||||
onTap: onPressed,
|
child: Semantics(
|
||||||
child: Container(
|
button: true,
|
||||||
width: width,
|
label: text,
|
||||||
height: height,
|
child: TextButton(
|
||||||
decoration: BoxDecoration(
|
onPressed: onPressed,
|
||||||
image: DecorationImage(
|
style: TextButton.styleFrom(
|
||||||
image: AssetImage(bg),
|
padding: EdgeInsets.zero,
|
||||||
fit: BoxFit.fill,
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
mouseCursor: SystemMouseCursors.click,
|
||||||
|
shape:
|
||||||
|
const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Ink(
|
||||||
child: Text(
|
decoration: BoxDecoration(
|
||||||
text,
|
image: DecorationImage(
|
||||||
style: GoogleFonts.merienda(
|
image: AssetImage(bg),
|
||||||
color: textColor,
|
fit: BoxFit.fill,
|
||||||
fontSize: fontSize, // Utilisation du paramètre
|
),
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: fontSize, // Utilisation du paramètre
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user