feat(mobile): Implement Keycloak WebView authentication with HTTP callback
- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/models/evenement_model.dart';
|
||||
import '../../domain/repositories/evenement_repository.dart';
|
||||
import 'evenement_event.dart';
|
||||
import 'evenement_state.dart';
|
||||
|
||||
/// BLoC pour la gestion des événements
|
||||
@injectable
|
||||
class EvenementBloc extends Bloc<EvenementEvent, EvenementState> {
|
||||
final EvenementRepository _repository;
|
||||
|
||||
EvenementBloc(this._repository) : super(const EvenementInitial()) {
|
||||
on<LoadEvenementsAVenir>(_onLoadEvenementsAVenir);
|
||||
on<LoadEvenementsPublics>(_onLoadEvenementsPublics);
|
||||
on<LoadEvenements>(_onLoadEvenements);
|
||||
on<LoadEvenementById>(_onLoadEvenementById);
|
||||
on<SearchEvenements>(_onSearchEvenements);
|
||||
on<FilterEvenementsByType>(_onFilterEvenementsByType);
|
||||
on<CreateEvenement>(_onCreateEvenement);
|
||||
on<UpdateEvenement>(_onUpdateEvenement);
|
||||
on<DeleteEvenement>(_onDeleteEvenement);
|
||||
on<ChangeStatutEvenement>(_onChangeStatutEvenement);
|
||||
on<LoadStatistiquesEvenements>(_onLoadStatistiquesEvenements);
|
||||
on<ResetEvenementState>(_onResetEvenementState);
|
||||
}
|
||||
|
||||
/// Charge les événements à venir
|
||||
Future<void> _onLoadEvenementsAVenir(
|
||||
LoadEvenementsAVenir event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
if (event.refresh || state is EvenementInitial) {
|
||||
emit(const EvenementLoading());
|
||||
} else if (state is EvenementLoaded) {
|
||||
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
||||
}
|
||||
|
||||
final evenements = await _repository.getEvenementsAVenir(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
);
|
||||
|
||||
if (event.refresh || event.page == 0) {
|
||||
emit(EvenementLoaded(
|
||||
evenements: evenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
));
|
||||
} else {
|
||||
final currentState = state as EvenementLoaded;
|
||||
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
||||
..addAll(evenements);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
evenements: allEvenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
final currentEvenements = state is EvenementLoaded
|
||||
? (state as EvenementLoaded).evenements
|
||||
: null;
|
||||
emit(EvenementError(
|
||||
message: e.toString(),
|
||||
evenements: currentEvenements,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge les événements publics
|
||||
Future<void> _onLoadEvenementsPublics(
|
||||
LoadEvenementsPublics event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
if (event.refresh || state is EvenementInitial) {
|
||||
emit(const EvenementLoading());
|
||||
} else if (state is EvenementLoaded) {
|
||||
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
||||
}
|
||||
|
||||
final evenements = await _repository.getEvenementsPublics(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
);
|
||||
|
||||
if (event.refresh || event.page == 0) {
|
||||
emit(EvenementLoaded(
|
||||
evenements: evenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
));
|
||||
} else {
|
||||
final currentState = state as EvenementLoaded;
|
||||
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
||||
..addAll(evenements);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
evenements: allEvenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
final currentEvenements = state is EvenementLoaded
|
||||
? (state as EvenementLoaded).evenements
|
||||
: null;
|
||||
emit(EvenementError(
|
||||
message: e.toString(),
|
||||
evenements: currentEvenements,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge tous les événements
|
||||
Future<void> _onLoadEvenements(
|
||||
LoadEvenements event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
if (event.refresh || state is EvenementInitial) {
|
||||
emit(const EvenementLoading());
|
||||
} else if (state is EvenementLoaded) {
|
||||
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
||||
}
|
||||
|
||||
final evenements = await _repository.getEvenements(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
sortField: event.sortField,
|
||||
sortDirection: event.sortDirection,
|
||||
);
|
||||
|
||||
if (event.refresh || event.page == 0) {
|
||||
emit(EvenementLoaded(
|
||||
evenements: evenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
));
|
||||
} else {
|
||||
final currentState = state as EvenementLoaded;
|
||||
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
||||
..addAll(evenements);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
evenements: allEvenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
final currentEvenements = state is EvenementLoaded
|
||||
? (state as EvenementLoaded).evenements
|
||||
: null;
|
||||
emit(EvenementError(
|
||||
message: e.toString(),
|
||||
evenements: currentEvenements,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge un événement par ID
|
||||
Future<void> _onLoadEvenementById(
|
||||
LoadEvenementById event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const EvenementLoading());
|
||||
|
||||
final evenement = await _repository.getEvenementById(event.id);
|
||||
|
||||
emit(EvenementDetailLoaded(evenement));
|
||||
} catch (e) {
|
||||
emit(EvenementError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche d'événements
|
||||
Future<void> _onSearchEvenements(
|
||||
SearchEvenements event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
if (event.refresh || event.page == 0) {
|
||||
emit(const EvenementLoading());
|
||||
} else if (state is EvenementLoaded) {
|
||||
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
||||
}
|
||||
|
||||
final evenements = await _repository.rechercherEvenements(
|
||||
event.terme,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
);
|
||||
|
||||
if (evenements.isEmpty && event.page == 0) {
|
||||
emit(EvenementSearchEmpty(event.terme));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.refresh || event.page == 0) {
|
||||
emit(EvenementLoaded(
|
||||
evenements: evenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
searchTerm: event.terme,
|
||||
));
|
||||
} else {
|
||||
final currentState = state as EvenementLoaded;
|
||||
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
||||
..addAll(evenements);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
evenements: allEvenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
searchTerm: event.terme,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
final currentEvenements = state is EvenementLoaded
|
||||
? (state as EvenementLoaded).evenements
|
||||
: null;
|
||||
emit(EvenementError(
|
||||
message: e.toString(),
|
||||
evenements: currentEvenements,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtre par type d'événement
|
||||
Future<void> _onFilterEvenementsByType(
|
||||
FilterEvenementsByType event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
if (event.refresh || event.page == 0) {
|
||||
emit(const EvenementLoading());
|
||||
} else if (state is EvenementLoaded) {
|
||||
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
||||
}
|
||||
|
||||
final evenements = await _repository.getEvenementsByType(
|
||||
event.type,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
);
|
||||
|
||||
if (evenements.isEmpty && event.page == 0) {
|
||||
emit(const EvenementEmpty(message: 'Aucun événement de ce type trouvé'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.refresh || event.page == 0) {
|
||||
emit(EvenementLoaded(
|
||||
evenements: evenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
filterType: event.type,
|
||||
));
|
||||
} else {
|
||||
final currentState = state as EvenementLoaded;
|
||||
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
||||
..addAll(evenements);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
evenements: allEvenements,
|
||||
hasReachedMax: evenements.length < event.size,
|
||||
currentPage: event.page,
|
||||
filterType: event.type,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
final currentEvenements = state is EvenementLoaded
|
||||
? (state as EvenementLoaded).evenements
|
||||
: null;
|
||||
emit(EvenementError(
|
||||
message: e.toString(),
|
||||
evenements: currentEvenements,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un nouvel événement
|
||||
Future<void> _onCreateEvenement(
|
||||
CreateEvenement event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const EvenementLoading());
|
||||
|
||||
final evenement = await _repository.createEvenement(event.evenement);
|
||||
|
||||
emit(EvenementOperationSuccess(
|
||||
message: 'Événement créé avec succès',
|
||||
evenement: evenement,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(EvenementError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Met à jour un événement
|
||||
Future<void> _onUpdateEvenement(
|
||||
UpdateEvenement event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const EvenementLoading());
|
||||
|
||||
final evenement = await _repository.updateEvenement(event.id, event.evenement);
|
||||
|
||||
emit(EvenementOperationSuccess(
|
||||
message: 'Événement mis à jour avec succès',
|
||||
evenement: evenement,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(EvenementError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime un événement
|
||||
Future<void> _onDeleteEvenement(
|
||||
DeleteEvenement event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const EvenementLoading());
|
||||
|
||||
await _repository.deleteEvenement(event.id);
|
||||
|
||||
emit(const EvenementOperationSuccess(
|
||||
message: 'Événement supprimé avec succès',
|
||||
));
|
||||
} catch (e) {
|
||||
emit(EvenementError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Change le statut d'un événement
|
||||
Future<void> _onChangeStatutEvenement(
|
||||
ChangeStatutEvenement event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const EvenementLoading());
|
||||
|
||||
final evenement = await _repository.changerStatutEvenement(
|
||||
event.id,
|
||||
event.nouveauStatut,
|
||||
);
|
||||
|
||||
emit(EvenementOperationSuccess(
|
||||
message: 'Statut de l\'événement modifié avec succès',
|
||||
evenement: evenement,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(EvenementError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge les statistiques
|
||||
Future<void> _onLoadStatistiquesEvenements(
|
||||
LoadStatistiquesEvenements event,
|
||||
Emitter<EvenementState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const EvenementLoading());
|
||||
|
||||
final statistiques = await _repository.getStatistiquesEvenements();
|
||||
|
||||
emit(EvenementStatistiquesLoaded(statistiques));
|
||||
} catch (e) {
|
||||
emit(EvenementError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Réinitialise l'état
|
||||
void _onResetEvenementState(
|
||||
ResetEvenementState event,
|
||||
Emitter<EvenementState> emit,
|
||||
) {
|
||||
emit(const EvenementInitial());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../../core/models/evenement_model.dart';
|
||||
|
||||
/// Événements du BLoC Evenement
|
||||
abstract class EvenementEvent extends Equatable {
|
||||
const EvenementEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Charge les événements à venir
|
||||
class LoadEvenementsAVenir extends EvenementEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
final bool refresh;
|
||||
|
||||
const LoadEvenementsAVenir({
|
||||
this.page = 0,
|
||||
this.size = 10,
|
||||
this.refresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, refresh];
|
||||
}
|
||||
|
||||
/// Charge les événements publics
|
||||
class LoadEvenementsPublics extends EvenementEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
final bool refresh;
|
||||
|
||||
const LoadEvenementsPublics({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.refresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, refresh];
|
||||
}
|
||||
|
||||
/// Charge tous les événements
|
||||
class LoadEvenements extends EvenementEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
final String sortField;
|
||||
final String sortDirection;
|
||||
final bool refresh;
|
||||
|
||||
const LoadEvenements({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.sortField = 'dateDebut',
|
||||
this.sortDirection = 'asc',
|
||||
this.refresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, sortField, sortDirection, refresh];
|
||||
}
|
||||
|
||||
/// Charge un événement par ID
|
||||
class LoadEvenementById extends EvenementEvent {
|
||||
final String id;
|
||||
|
||||
const LoadEvenementById(this.id);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// Recherche d'événements
|
||||
class SearchEvenements extends EvenementEvent {
|
||||
final String terme;
|
||||
final int page;
|
||||
final int size;
|
||||
final bool refresh;
|
||||
|
||||
const SearchEvenements({
|
||||
required this.terme,
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.refresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [terme, page, size, refresh];
|
||||
}
|
||||
|
||||
/// Filtre par type d'événement
|
||||
class FilterEvenementsByType extends EvenementEvent {
|
||||
final TypeEvenement type;
|
||||
final int page;
|
||||
final int size;
|
||||
final bool refresh;
|
||||
|
||||
const FilterEvenementsByType({
|
||||
required this.type,
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.refresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, page, size, refresh];
|
||||
}
|
||||
|
||||
/// Crée un nouvel événement
|
||||
class CreateEvenement extends EvenementEvent {
|
||||
final EvenementModel evenement;
|
||||
|
||||
const CreateEvenement(this.evenement);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [evenement];
|
||||
}
|
||||
|
||||
/// Met à jour un événement
|
||||
class UpdateEvenement extends EvenementEvent {
|
||||
final String id;
|
||||
final EvenementModel evenement;
|
||||
|
||||
const UpdateEvenement(this.id, this.evenement);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, evenement];
|
||||
}
|
||||
|
||||
/// Supprime un événement
|
||||
class DeleteEvenement extends EvenementEvent {
|
||||
final String id;
|
||||
|
||||
const DeleteEvenement(this.id);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// Change le statut d'un événement
|
||||
class ChangeStatutEvenement extends EvenementEvent {
|
||||
final String id;
|
||||
final StatutEvenement nouveauStatut;
|
||||
|
||||
const ChangeStatutEvenement(this.id, this.nouveauStatut);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, nouveauStatut];
|
||||
}
|
||||
|
||||
/// Charge les statistiques
|
||||
class LoadStatistiquesEvenements extends EvenementEvent {
|
||||
const LoadStatistiquesEvenements();
|
||||
}
|
||||
|
||||
/// Réinitialise l'état
|
||||
class ResetEvenementState extends EvenementEvent {
|
||||
const ResetEvenementState();
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../../core/models/evenement_model.dart';
|
||||
|
||||
/// États du BLoC Evenement
|
||||
abstract class EvenementState extends Equatable {
|
||||
const EvenementState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// État initial
|
||||
class EvenementInitial extends EvenementState {
|
||||
const EvenementInitial();
|
||||
}
|
||||
|
||||
/// État de chargement
|
||||
class EvenementLoading extends EvenementState {
|
||||
const EvenementLoading();
|
||||
}
|
||||
|
||||
/// État de chargement avec données existantes (pour pagination)
|
||||
class EvenementLoadingMore extends EvenementState {
|
||||
final List<EvenementModel> evenements;
|
||||
|
||||
const EvenementLoadingMore(this.evenements);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [evenements];
|
||||
}
|
||||
|
||||
/// État de succès avec liste d'événements
|
||||
class EvenementLoaded extends EvenementState {
|
||||
final List<EvenementModel> evenements;
|
||||
final bool hasReachedMax;
|
||||
final int currentPage;
|
||||
final String? searchTerm;
|
||||
final TypeEvenement? filterType;
|
||||
|
||||
const EvenementLoaded({
|
||||
required this.evenements,
|
||||
this.hasReachedMax = false,
|
||||
this.currentPage = 0,
|
||||
this.searchTerm,
|
||||
this.filterType,
|
||||
});
|
||||
|
||||
EvenementLoaded copyWith({
|
||||
List<EvenementModel>? evenements,
|
||||
bool? hasReachedMax,
|
||||
int? currentPage,
|
||||
String? searchTerm,
|
||||
TypeEvenement? filterType,
|
||||
}) {
|
||||
return EvenementLoaded(
|
||||
evenements: evenements ?? this.evenements,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
searchTerm: searchTerm ?? this.searchTerm,
|
||||
filterType: filterType ?? this.filterType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
evenements,
|
||||
hasReachedMax,
|
||||
currentPage,
|
||||
searchTerm,
|
||||
filterType,
|
||||
];
|
||||
}
|
||||
|
||||
/// État de succès avec un événement spécifique
|
||||
class EvenementDetailLoaded extends EvenementState {
|
||||
final EvenementModel evenement;
|
||||
|
||||
const EvenementDetailLoaded(this.evenement);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [evenement];
|
||||
}
|
||||
|
||||
/// État de succès avec statistiques
|
||||
class EvenementStatistiquesLoaded extends EvenementState {
|
||||
final Map<String, dynamic> statistiques;
|
||||
|
||||
const EvenementStatistiquesLoaded(this.statistiques);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [statistiques];
|
||||
}
|
||||
|
||||
/// État de succès après création/modification
|
||||
class EvenementOperationSuccess extends EvenementState {
|
||||
final String message;
|
||||
final EvenementModel? evenement;
|
||||
|
||||
const EvenementOperationSuccess({
|
||||
required this.message,
|
||||
this.evenement,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, evenement];
|
||||
}
|
||||
|
||||
/// État d'erreur
|
||||
class EvenementError extends EvenementState {
|
||||
final String message;
|
||||
final List<EvenementModel>? evenements; // Pour conserver les données en cas d'erreur de pagination
|
||||
|
||||
const EvenementError({
|
||||
required this.message,
|
||||
this.evenements,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, evenements];
|
||||
}
|
||||
|
||||
/// État de recherche vide
|
||||
class EvenementSearchEmpty extends EvenementState {
|
||||
final String searchTerm;
|
||||
|
||||
const EvenementSearchEmpty(this.searchTerm);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [searchTerm];
|
||||
}
|
||||
|
||||
/// État de liste vide
|
||||
class EvenementEmpty extends EvenementState {
|
||||
final String message;
|
||||
|
||||
const EvenementEmpty({
|
||||
this.message = 'Aucun événement trouvé',
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
Reference in New Issue
Block a user