merge: squash develop into master (login autofill + clavier #98)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6749f2025a
commit
619e39219f
@ -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) {
|
||||||
@ -160,7 +169,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
final imageDimensions = snapshot.data!;
|
final imageDimensions = snapshot.data!;
|
||||||
final imageHeight = h;
|
final imageHeight = h;
|
||||||
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
|
final imageWidth = imageHeight *
|
||||||
|
(imageDimensions.width / imageDimensions.height);
|
||||||
final remainingWidth = w - imageWidth;
|
final remainingWidth = w - imageWidth;
|
||||||
final leftMargin = remainingWidth / 4;
|
final leftMargin = remainingWidth / 4;
|
||||||
|
|
||||||
@ -193,6 +203,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
height: h * 0.5, // 50% de la hauteur de l'écran
|
height: h * 0.5, // 50% de la hauteur de l'écran
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(w * 0.02), // 2% de 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,6 +333,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Pied de page (Wrap pour éviter overflow sur petite largeur)
|
// Pied de page (Wrap pour éviter overflow sur petite largeur)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -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,9 +455,11 @@ 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: AutofillGroup(
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -442,6 +471,12 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
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: 48,
|
fieldHeight: 48,
|
||||||
@ -454,6 +489,11 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
showLabel: false,
|
showLabel: false,
|
||||||
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: 48,
|
fieldHeight: 48,
|
||||||
@ -466,16 +506,21 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red.shade50,
|
color: Colors.red.shade50,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.red.shade300),
|
border: Border.all(
|
||||||
|
color: Colors.red.shade300),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline, color: Colors.red.shade700, size: 20),
|
Icon(Icons.error_outline,
|
||||||
|
color: Colors.red.shade700,
|
||||||
|
size: 20),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_errorMessage!,
|
_errorMessage!,
|
||||||
style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700),
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.red.shade700),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -495,7 +540,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () { /* TODO */ },
|
onPressed: () {/* TODO */},
|
||||||
child: Text(
|
child: Text(
|
||||||
'Mot de passe oublié ?',
|
'Mot de passe oublié ?',
|
||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
@ -506,7 +551,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.go('/register-choice'),
|
onPressed: () =>
|
||||||
|
context.go('/register-choice'),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Créer un compte',
|
'Créer un compte',
|
||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
@ -522,6 +568,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12, top: 8),
|
padding: const EdgeInsets.only(bottom: 12, top: 8),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
@ -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 &&
|
||||||
|
(value == null || value.isEmpty)) {
|
||||||
return 'Ce champ est obligatoire';
|
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,13 +23,23 @@ class ImageButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MouseRegion(
|
return SizedBox(
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: onPressed,
|
|
||||||
child: Container(
|
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
child: Semantics(
|
||||||
|
button: true,
|
||||||
|
label: text,
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape:
|
||||||
|
const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
),
|
||||||
|
child: Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage(bg),
|
image: AssetImage(bg),
|
||||||
@ -48,6 +58,8 @@ class ImageButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user