membre add
This commit is contained in:
@@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
|
||||
class AppKeys {
|
||||
// Noms des boîtes Hive
|
||||
static const String userBoxName = 'user';
|
||||
static const String usersBoxNameOld = 'users';
|
||||
static const String amicaleBoxName = 'amicale';
|
||||
static const String clientsBoxName = 'clients';
|
||||
static const String operationsBoxName = 'operations';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
|
||||
part 'membre_model.g.dart';
|
||||
|
||||
@@ -169,4 +170,46 @@ class MembreModel extends HiveObject {
|
||||
isActive: isActive ?? this.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
// Convertir un MembreModel vers UserModel pour l'édition
|
||||
UserModel toUserModel() {
|
||||
return UserModel(
|
||||
id: id,
|
||||
email: email,
|
||||
name: name,
|
||||
username: username,
|
||||
firstName: firstName,
|
||||
role: role,
|
||||
createdAt: createdAt,
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isActive: isActive,
|
||||
isSynced: false,
|
||||
fkEntite: fkEntite,
|
||||
fkTitre: fkTitre,
|
||||
phone: phone,
|
||||
mobile: mobile,
|
||||
dateNaissance: dateNaissance,
|
||||
dateEmbauche: dateEmbauche,
|
||||
sectName: sectName,
|
||||
);
|
||||
}
|
||||
|
||||
// Créer un MembreModel depuis un UserModel mis à jour
|
||||
static MembreModel fromUserModel(UserModel user, MembreModel originalMembre) {
|
||||
return originalMembre.copyWith(
|
||||
name: user.name,
|
||||
firstName: user.firstName,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
fkEntite: user.fkEntite,
|
||||
role: user.role,
|
||||
sectName: user.sectName,
|
||||
fkTitre: user.fkTitre,
|
||||
phone: user.phone,
|
||||
mobile: user.mobile,
|
||||
dateNaissance: user.dateNaissance,
|
||||
dateEmbauche: user.dateEmbauche,
|
||||
isActive: user.isActive,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,13 +99,13 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Sauvegarder un membre
|
||||
Future<void> saveMembre(MembreModel membre) async {
|
||||
Future<void> saveMembreBox(MembreModel membre) async {
|
||||
await _membreBox.put(membre.id, membre);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Supprimer un membre
|
||||
Future<void> deleteMembre(int id) async {
|
||||
Future<void> deleteMembreBox(int id) async {
|
||||
await _membreBox.delete(id);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -113,48 +113,33 @@ class MembreRepository extends ChangeNotifier {
|
||||
// === MÉTHODES API ===
|
||||
|
||||
// Créer un membre via l'API
|
||||
Future<bool> createMembre(MembreModel membre) async {
|
||||
Future<MembreModel?> createMembre(MembreModel membre) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Préparer les données pour l'API - exclure l'id pour la création
|
||||
final data = membre.toJson();
|
||||
// Convertir en UserModel pour l'API
|
||||
final userModel = membre.toUserModel();
|
||||
final data = userModel.toJson();
|
||||
data.remove('id'); // L'API génère l'ID
|
||||
data.remove('created_at'); // L'API génère created_at
|
||||
// Appeler l'API pour créer le membre
|
||||
final response = await ApiService.instance.post('/membres', data: data);
|
||||
|
||||
// Appeler l'API users
|
||||
final response = await ApiService.instance.post('/users', data: data);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
// Récupérer l'ID du nouveau membre
|
||||
final membreId = response.data['id'] is String ? int.parse(response.data['id']) : response.data['id'] as int;
|
||||
// Créer le membre avec les données retournées par l'API
|
||||
final createdMember = MembreModel.fromJson(response.data);
|
||||
|
||||
// Créer le membre localement avec l'ID retourné par l'API
|
||||
final newMembre = MembreModel(
|
||||
id: membreId,
|
||||
fkEntite: membre.fkEntite,
|
||||
role: membre.role,
|
||||
fkTitre: membre.fkTitre,
|
||||
name: membre.name,
|
||||
firstName: membre.firstName,
|
||||
username: membre.username,
|
||||
sectName: membre.sectName,
|
||||
email: membre.email,
|
||||
phone: membre.phone,
|
||||
mobile: membre.mobile,
|
||||
dateNaissance: membre.dateNaissance,
|
||||
dateEmbauche: membre.dateEmbauche,
|
||||
createdAt: DateTime.now(),
|
||||
isActive: membre.isActive,
|
||||
);
|
||||
// Sauvegarder localement
|
||||
await saveMembreBox(createdMember);
|
||||
|
||||
await saveMembre(newMembre);
|
||||
return true;
|
||||
return createdMember; // Retourner le membre créé
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la création du membre: $e');
|
||||
return false;
|
||||
rethrow; // Propager l'exception pour la gestion d'erreurs
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
@@ -167,15 +152,15 @@ class MembreRepository extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Préparer les données pour l'API
|
||||
final data = membre.toJson();
|
||||
// Convertir en UserModel pour l'API
|
||||
final userModel = membre.toUserModel();
|
||||
|
||||
// Appeler l'API pour mettre à jour le membre
|
||||
final response = await ApiService.instance.put('/membres/${membre.id}', data: data);
|
||||
// Appeler l'API users au lieu de membres
|
||||
final response = await ApiService.instance.put('/users/${membre.id}', data: userModel.toJson());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Sauvegarder le membre mis à jour localement
|
||||
await saveMembre(membre);
|
||||
await saveMembreBox(membre);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -190,17 +175,17 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Supprimer un membre via l'API
|
||||
Future<bool> deleteMembreViaApi(int id) async {
|
||||
Future<bool> deleteMembre(int id) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Appeler l'API pour supprimer le membre
|
||||
final response = await ApiService.instance.delete('/membres/$id');
|
||||
// Appeler l'API users au lieu de membres (correction ici)
|
||||
final response = await ApiService.instance.delete('/users/$id');
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
// Supprimer le membre localement
|
||||
await deleteMembre(id);
|
||||
await deleteMembreBox(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -259,12 +244,12 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Récupérer les membres depuis l'API
|
||||
Future<List<MembreModel>> fetchMembresFromApi() async {
|
||||
Future<List<MembreModel>> fetchMembres() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await ApiService.instance.get('/membres');
|
||||
final response = await ApiService.instance.get('/users');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final membresData = response.data;
|
||||
|
||||
@@ -423,6 +423,49 @@ class UserRepository extends ChangeNotifier {
|
||||
await _userBox.delete(id);
|
||||
}
|
||||
|
||||
/// Mettre à jour un utilisateur (pour le profil personnel et la gestion des membres)
|
||||
Future<UserModel> updateUser(UserModel updatedUser) async {
|
||||
try {
|
||||
debugPrint('🔄 Mise à jour utilisateur: ${updatedUser.email}');
|
||||
|
||||
// D'ABORD essayer de synchroniser avec l'API
|
||||
try {
|
||||
final hasConnection = await ApiService.instance.hasInternetConnection();
|
||||
if (hasConnection) {
|
||||
// Tentative de mise à jour sur l'API
|
||||
await ApiService.instance.updateUser(updatedUser);
|
||||
debugPrint('✅ Utilisateur mis à jour sur l\'API');
|
||||
|
||||
// Si succès API, sauvegarder localement avec sync = true
|
||||
final syncedUser = updatedUser.copyWith(
|
||||
isSynced: true,
|
||||
lastSyncedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _userBox.put(syncedUser.id, syncedUser);
|
||||
|
||||
// Si c'est l'utilisateur connecté, mettre à jour le service
|
||||
if (currentUser?.id == syncedUser.id) {
|
||||
await CurrentUserService.instance.setUser(syncedUser);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return syncedUser;
|
||||
} else {
|
||||
debugPrint('⚠️ Pas de connexion internet');
|
||||
throw Exception('Pas de connexion internet');
|
||||
}
|
||||
} catch (apiError) {
|
||||
debugPrint('❌ Erreur API lors de la mise à jour: $apiError');
|
||||
// Relancer l'erreur pour qu'elle soit gérée par l'appelant
|
||||
rethrow;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur mise à jour utilisateur: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES POUR LES DONNÉES ===
|
||||
|
||||
/// Récupérer la dernière opération active
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:retry/retry.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
|
||||
class ApiService {
|
||||
static ApiService? _instance;
|
||||
@@ -179,31 +180,32 @@ class ApiService {
|
||||
final response = await _dio.post(AppKeys.loginEndpoint, data: {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'type': type, // Ajouter le type de connexion (user ou admin)
|
||||
'type': type,
|
||||
});
|
||||
|
||||
// Vérifier la structure de la réponse
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final status = data['status'] as String?;
|
||||
|
||||
// Afficher le message en cas d'erreur
|
||||
// Si le statut n'est pas 'success', créer une exception avec le message de l'API
|
||||
if (status != 'success') {
|
||||
final message = data['message'] as String?;
|
||||
debugPrint('Erreur d\'authentification: $message');
|
||||
final message = data['message'] as String? ?? 'Erreur de connexion';
|
||||
throw ApiException(message);
|
||||
}
|
||||
|
||||
// Si le statut est 'success', récupérer le session_id
|
||||
if (status == 'success' && data.containsKey('session_id')) {
|
||||
// Si succès, configurer la session
|
||||
if (data.containsKey('session_id')) {
|
||||
final sessionId = data['session_id'];
|
||||
// Définir la session pour les futures requêtes
|
||||
if (sessionId != null) {
|
||||
setSessionId(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de la connexion', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,21 +247,39 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserModel> updateUser(UserModel user) async {
|
||||
try {
|
||||
final response = await _dio.put('/users/${user.id}', data: user.toJson());
|
||||
|
||||
// Vérifier la structure de la réponse
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
// Si l'API retourne {status: "success", message: "..."}
|
||||
if (data.containsKey('status') && data['status'] == 'success') {
|
||||
// L'API confirme le succès mais ne retourne pas l'objet user
|
||||
// On retourne l'utilisateur original qui a été envoyé
|
||||
debugPrint('✅ API updateUser success: ${data['message']}');
|
||||
return user;
|
||||
}
|
||||
|
||||
// Si l'API retourne directement un UserModel (fallback)
|
||||
return UserModel.fromJson(data);
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
throw ApiException('Erreur inattendue lors de la mise à jour', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
// Appliquer la même logique aux autres méthodes
|
||||
Future<UserModel> createUser(UserModel user) async {
|
||||
try {
|
||||
final response = await _dio.post('/users', data: user.toJson());
|
||||
return UserModel.fromJson(response.data);
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserModel> updateUser(UserModel user) async {
|
||||
try {
|
||||
final response = await _dio.put('/users/${user.id}', data: user.toJson());
|
||||
return UserModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
throw ApiException('Erreur inattendue lors de la création', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
273
app/lib/core/utils/api_exception.dart
Normal file
273
app/lib/core/utils/api_exception.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
/// Exception personnalisée pour les erreurs API avec méthodes d'affichage intégrées
|
||||
class ApiException implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
final String? errorCode;
|
||||
final Map<String, dynamic>? details;
|
||||
final Object? originalError;
|
||||
|
||||
const ApiException(
|
||||
this.message, {
|
||||
this.statusCode,
|
||||
this.errorCode,
|
||||
this.details,
|
||||
this.originalError,
|
||||
});
|
||||
|
||||
/// Créer une ApiException depuis une DioException
|
||||
factory ApiException.fromDioException(DioException dioException) {
|
||||
final response = dioException.response;
|
||||
final statusCode = response?.statusCode;
|
||||
|
||||
// Essayer d'extraire le message de la réponse API
|
||||
String message = 'Erreur de communication avec le serveur';
|
||||
String? errorCode;
|
||||
Map<String, dynamic>? details;
|
||||
|
||||
if (response?.data != null) {
|
||||
try {
|
||||
final data = response!.data as Map<String, dynamic>;
|
||||
|
||||
// Message spécifique de l'API
|
||||
if (data.containsKey('message')) {
|
||||
message = data['message'] as String;
|
||||
}
|
||||
|
||||
// Code d'erreur spécifique
|
||||
if (data.containsKey('error_code')) {
|
||||
errorCode = data['error_code'] as String;
|
||||
}
|
||||
|
||||
// Détails supplémentaires
|
||||
if (data.containsKey('errors')) {
|
||||
details = data['errors'] as Map<String, dynamic>?;
|
||||
}
|
||||
} catch (e) {
|
||||
// Si on ne peut pas parser la réponse, utiliser le message par défaut
|
||||
}
|
||||
}
|
||||
|
||||
// Messages par défaut selon le code de statut
|
||||
if (response?.data == null || message == 'Erreur de communication avec le serveur') {
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
message = 'Données invalides';
|
||||
break;
|
||||
case 401:
|
||||
message = 'Non autorisé : veuillez vous reconnecter';
|
||||
break;
|
||||
case 403:
|
||||
message = 'Accès interdit';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Ressource non trouvée';
|
||||
break;
|
||||
case 409:
|
||||
message = 'Conflit : données déjà existantes';
|
||||
break;
|
||||
case 422:
|
||||
message = 'Données de validation incorrectes';
|
||||
break;
|
||||
case 500:
|
||||
message = 'Erreur serveur interne';
|
||||
break;
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
message = 'Service temporairement indisponible';
|
||||
break;
|
||||
default:
|
||||
switch (dioException.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
message = 'Délai d\'attente dépassé';
|
||||
break;
|
||||
case DioExceptionType.connectionError:
|
||||
message = 'Problème de connexion réseau';
|
||||
break;
|
||||
case DioExceptionType.cancel:
|
||||
message = 'Requête annulée';
|
||||
break;
|
||||
default:
|
||||
message = 'Erreur de communication avec le serveur';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ApiException(
|
||||
message,
|
||||
statusCode: statusCode,
|
||||
errorCode: errorCode,
|
||||
details: details,
|
||||
originalError: dioException,
|
||||
);
|
||||
}
|
||||
|
||||
/// Créer une ApiException depuis n'importe quelle erreur
|
||||
factory ApiException.fromError(Object error) {
|
||||
if (error is ApiException) {
|
||||
return error;
|
||||
}
|
||||
|
||||
final errorString = error.toString();
|
||||
|
||||
if (errorString.contains('SocketException') || errorString.contains('NetworkException')) {
|
||||
return const ApiException('Problème de connexion réseau');
|
||||
}
|
||||
if (errorString.contains('TimeoutException')) {
|
||||
return const ApiException('Délai d\'attente dépassé');
|
||||
}
|
||||
|
||||
return ApiException('Erreur inattendue', originalError: error);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
|
||||
/// Obtenir un message d'erreur formaté pour l'affichage
|
||||
String get displayMessage => message;
|
||||
|
||||
/// Vérifier si c'est une erreur de validation
|
||||
bool get isValidationError => statusCode == 422 || statusCode == 400;
|
||||
|
||||
/// Vérifier si c'est une erreur d'authentification
|
||||
bool get isAuthError => statusCode == 401 || statusCode == 403;
|
||||
|
||||
/// Vérifier si c'est une erreur de conflit (données déjà existantes)
|
||||
bool get isConflictError => statusCode == 409;
|
||||
|
||||
/// Vérifier si c'est une erreur réseau
|
||||
bool get isNetworkError =>
|
||||
statusCode == null ||
|
||||
statusCode == 502 ||
|
||||
statusCode == 503 ||
|
||||
statusCode == 504 ||
|
||||
originalError is DioException && (originalError as DioException).type == DioExceptionType.connectionError;
|
||||
|
||||
// === MÉTHODES D'AFFICHAGE INTÉGRÉES ===
|
||||
|
||||
/// Afficher cette erreur (méthode d'instance)
|
||||
void show(BuildContext context, {Duration? duration}) {
|
||||
if (context.mounted) {
|
||||
final isInDialog = _isInDialog(context);
|
||||
|
||||
if (isInDialog) {
|
||||
_showOverlaySnackBar(context, displayMessage, Colors.red, duration);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(displayMessage),
|
||||
backgroundColor: Colors.red,
|
||||
duration: duration ?? const Duration(seconds: 5),
|
||||
action: SnackBarAction(
|
||||
label: 'Fermer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode statique pour afficher une erreur depuis n'importe quel objet
|
||||
static void showError(BuildContext context, Object error, {Duration? duration}) {
|
||||
final apiException = ApiException.fromError(error);
|
||||
apiException.show(context, duration: duration);
|
||||
}
|
||||
|
||||
/// Méthode statique pour afficher un message de succès (compatible avec les Dialogs)
|
||||
static void showSuccess(BuildContext context, String message, {Duration? duration}) {
|
||||
if (context.mounted) {
|
||||
final isInDialog = _isInDialog(context);
|
||||
|
||||
if (isInDialog) {
|
||||
_showOverlaySnackBar(context, message, Colors.green, duration);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
duration: duration ?? const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifier si le contexte est dans un Dialog
|
||||
static bool _isInDialog(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<Dialog>() != null;
|
||||
}
|
||||
|
||||
/// Afficher un SnackBar en overlay au-dessus de tout
|
||||
static void _showOverlaySnackBar(BuildContext context, String message, Color backgroundColor, Duration? duration) {
|
||||
final overlay = Overlay.of(context);
|
||||
late OverlayEntry overlayEntry;
|
||||
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 20, // En haut de l'écran
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
backgroundColor == Colors.red ? Icons.error_outline : Icons.check_circle_outline,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white, size: 18),
|
||||
onPressed: () => overlayEntry.remove(),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 30, minHeight: 30),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(overlayEntry);
|
||||
|
||||
// Auto-suppression après la durée spécifiée
|
||||
Timer(duration ?? const Duration(seconds: 5), () {
|
||||
if (overlayEntry.mounted) {
|
||||
overlayEntry.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user