merge: squash develop into master (login autofill + clavier #98)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-25 12:00:51 +01:00
parent 6749f2025a
commit 619e39219f
3 changed files with 308 additions and 227 deletions

View File

@ -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';
@ -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
@ -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) {
@ -160,7 +169,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
final imageDimensions = snapshot.data!;
final imageHeight = h;
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
final imageWidth = imageHeight *
(imageDimensions.width / imageDimensions.height);
final remainingWidth = w - imageWidth;
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
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,
@ -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(
@ -309,6 +333,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
),
),
),
),
// Pied de page (Wrap pour éviter overflow sur petite largeur)
Positioned(
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).
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,9 +455,11 @@ 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: AutofillGroup(
child: Form(
key: _formKey,
child: Column(
@ -442,6 +471,12 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
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,
@ -454,6 +489,11 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
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,
@ -466,16 +506,21 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.red.shade300),
border: Border.all(
color: Colors.red.shade300),
),
child: Row(
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),
Expanded(
child: Text(
_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),
TextButton(
onPressed: () { /* TODO */ },
onPressed: () {/* TODO */},
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.merienda(
@ -506,7 +551,8 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
),
),
TextButton(
onPressed: () => context.go('/register-choice'),
onPressed: () =>
context.go('/register-choice'),
child: Text(
'Créer un compte',
style: GoogleFonts.merienda(
@ -522,6 +568,7 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 12, top: 8),
child: Wrap(
@ -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())),
);
}
},

View File

@ -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
@ -99,35 +107,44 @@ 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
),
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)) {
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
? 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,

View File

@ -23,13 +23,23 @@ class ImageButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onPressed,
child: Container(
return SizedBox(
width: width,
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(
image: DecorationImage(
image: AssetImage(bg),
@ -48,6 +58,8 @@ class ImageButton extends StatelessWidget {
),
),
),
),
),
);
}
}