Files
afterwork/lib/presentation/widgets/custom_button.dart
dahoud 92612abbd7 fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
2026-01-10 10:43:17 +00:00

264 lines
7.0 KiB
Dart

import 'package:flutter/material.dart';
/// Bouton personnalisé moderne et élégant.
///
/// Design épuré avec des proportions optimisées pour une application professionnelle.
/// Supporte Material 3 avec des variantes tonal, outlined et text.
///
/// **Usage:**
/// ```dart
/// CustomButton(
/// text: 'Confirmer',
/// onPressed: () {},
/// variant: ButtonVariant.primary,
/// size: ButtonSize.medium,
/// )
/// ```
class CustomButton extends StatelessWidget {
const CustomButton({
required this.text,
required this.onPressed,
super.key,
this.icon,
this.isLoading = false,
this.isEnabled = true,
this.variant = ButtonVariant.primary,
this.size = ButtonSize.medium,
this.fullWidth = false,
});
final String text;
final VoidCallback onPressed;
final IconData? icon;
final bool isLoading;
final bool isEnabled;
final ButtonVariant variant;
final ButtonSize size;
final bool fullWidth;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDisabled = !isEnabled || isLoading;
final buttonStyle = _getButtonStyle(theme, variant, size);
final textStyle = _getTextStyle(theme, variant, size);
Widget child = isLoading
? SizedBox(
height: size == ButtonSize.small ? 14 : (size == ButtonSize.medium ? 16 : 18),
width: size == ButtonSize.small ? 14 : (size == ButtonSize.medium ? 16 : 18),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
_getLoadingColor(theme, variant),
),
),
)
: Row(
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: _getIconSize(size)),
SizedBox(width: size == ButtonSize.small ? 6 : 8),
],
Text(text, style: textStyle),
],
);
Widget button;
switch (variant) {
case ButtonVariant.primary:
button = ElevatedButton(
onPressed: isDisabled ? null : onPressed,
style: buttonStyle,
child: child,
);
break;
case ButtonVariant.tonal:
button = FilledButton.tonal(
onPressed: isDisabled ? null : onPressed,
style: buttonStyle,
child: child,
);
break;
case ButtonVariant.outlined:
button = OutlinedButton(
onPressed: isDisabled ? null : onPressed,
style: buttonStyle,
child: child,
);
break;
case ButtonVariant.text:
button = TextButton(
onPressed: isDisabled ? null : onPressed,
style: buttonStyle,
child: child,
);
break;
}
return fullWidth ? SizedBox(width: double.infinity, child: button) : button;
}
ButtonStyle _getButtonStyle(
ThemeData theme,
ButtonVariant variant,
ButtonSize size,
) {
final padding = _getPadding(size);
final borderRadius = BorderRadius.circular(_getBorderRadius(size));
final elevation = variant == ButtonVariant.primary ? 2.0 : 0.0;
switch (variant) {
case ButtonVariant.primary:
return ElevatedButton.styleFrom(
padding: padding,
elevation: elevation,
shape: RoundedRectangleBorder(borderRadius: borderRadius),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
case ButtonVariant.tonal:
return FilledButton.styleFrom(
padding: padding,
shape: RoundedRectangleBorder(borderRadius: borderRadius),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
case ButtonVariant.outlined:
return OutlinedButton.styleFrom(
padding: padding,
side: BorderSide(
color: theme.colorScheme.outline,
width: 1.5,
),
shape: RoundedRectangleBorder(borderRadius: borderRadius),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
case ButtonVariant.text:
return TextButton.styleFrom(
padding: padding,
shape: RoundedRectangleBorder(borderRadius: borderRadius),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
}
TextStyle _getTextStyle(
ThemeData theme,
ButtonVariant variant,
ButtonSize size,
) {
final fontSize = _getFontSize(size);
final fontWeight = size == ButtonSize.small ? FontWeight.w500 : FontWeight.w600;
Color textColor;
switch (variant) {
case ButtonVariant.primary:
textColor = theme.colorScheme.onPrimary;
break;
case ButtonVariant.tonal:
textColor = theme.colorScheme.onSecondaryContainer;
break;
case ButtonVariant.outlined:
case ButtonVariant.text:
textColor = theme.colorScheme.primary;
break;
}
return TextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
color: textColor,
letterSpacing: 0.1,
);
}
Color _getLoadingColor(ThemeData theme, ButtonVariant variant) {
switch (variant) {
case ButtonVariant.primary:
return theme.colorScheme.onPrimary;
case ButtonVariant.tonal:
return theme.colorScheme.onSecondaryContainer;
case ButtonVariant.outlined:
case ButtonVariant.text:
return theme.colorScheme.primary;
}
}
EdgeInsets _getPadding(ButtonSize size) {
switch (size) {
case ButtonSize.small:
return const EdgeInsets.symmetric(horizontal: 12, vertical: 6);
case ButtonSize.medium:
return const EdgeInsets.symmetric(horizontal: 16, vertical: 10);
case ButtonSize.large:
return const EdgeInsets.symmetric(horizontal: 20, vertical: 12);
}
}
double _getFontSize(ButtonSize size) {
switch (size) {
case ButtonSize.small:
return 13;
case ButtonSize.medium:
return 14;
case ButtonSize.large:
return 15;
}
}
double _getIconSize(ButtonSize size) {
switch (size) {
case ButtonSize.small:
return 16;
case ButtonSize.medium:
return 18;
case ButtonSize.large:
return 20;
}
}
double _getBorderRadius(ButtonSize size) {
switch (size) {
case ButtonSize.small:
return 8;
case ButtonSize.medium:
return 10;
case ButtonSize.large:
return 12;
}
}
}
/// Variantes de boutons disponibles (Material 3).
enum ButtonVariant {
/// Bouton principal élevé (filled, elevated)
primary,
/// Bouton tonal (filled tonal - Material 3)
tonal,
/// Bouton avec bordure (outlined)
outlined,
/// Bouton texte sans fond (text)
text,
}
/// Tailles de boutons disponibles.
enum ButtonSize {
/// Petite taille - compact
small,
/// Taille moyenne - standard
medium,
/// Grande taille - proéminente
large,
}