Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
44
lib/features/feed/data/repositories/feed_repository.dart
Normal file
44
lib/features/feed/data/repositories/feed_repository.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import '../../domain/entities/feed_item.dart';
|
||||
|
||||
@lazySingleton
|
||||
class FeedRepository {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
FeedRepository(this._apiClient);
|
||||
|
||||
/// Récupère le flux d'actualité depuis le backend Quarkus.
|
||||
/// Vérifier la route backend (ex. /api/feed ou /api/posts) et adapter _feedPath si besoin.
|
||||
static const String _feedPath = '/api/feed';
|
||||
|
||||
Future<List<FeedItem>> getFeed({int page = 0, int size = 10}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
_feedPath,
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
|
||||
final List<dynamic> data = response.data['content'] ?? response.data; // Gère la pagination Spring/Quarkus
|
||||
|
||||
return data.map((json) {
|
||||
// Mapping manuel basique depuis le JSON API vers l'entité locale
|
||||
// À ajuster selon la structure JSON exacte renvoyée par l'API
|
||||
return FeedItem(
|
||||
id: json['id']?.toString() ?? '',
|
||||
type: FeedItemType.post, // Par défaut, ou selon json['type']
|
||||
authorName: json['authorName'] ?? json['author']?['name'] ?? 'Auteur inconnu',
|
||||
authorAvatarUrl: json['authorAvatarUrl'] ?? json['author']?['avatarUrl'],
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : DateTime.now(),
|
||||
content: json['content'] ?? '',
|
||||
likesCount: json['likesCount'] ?? 0,
|
||||
commentsCount: json['commentsCount'] ?? 0,
|
||||
isLikedByMe: json['isLikedByMe'] ?? false,
|
||||
);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
// Propagation de l'erreur pour la gestion globale
|
||||
throw Exception('Erreur lors de la récupération du flux externe: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
51
lib/features/feed/domain/entities/feed_item.dart
Normal file
51
lib/features/feed/domain/entities/feed_item.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
enum FeedItemType { post, event, contribution, notification }
|
||||
|
||||
/// Entité principale représentant un élément de n'importe quel flux (DRY)
|
||||
class FeedItem extends Equatable {
|
||||
final String id;
|
||||
final FeedItemType type;
|
||||
final String authorName; // Nom de l'utilisateur ou de l'entité
|
||||
final String? authorAvatarUrl;
|
||||
final DateTime createdAt;
|
||||
final String content;
|
||||
|
||||
// Interactions sociales
|
||||
final int likesCount;
|
||||
final int commentsCount;
|
||||
final bool isLikedByMe;
|
||||
|
||||
// Actions spécifiques (ex: Payer, S'inscrire)
|
||||
final String? customActionLabel;
|
||||
final String? actionUrlTarget; // Deep link ou route
|
||||
|
||||
const FeedItem({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.authorName,
|
||||
this.authorAvatarUrl,
|
||||
required this.createdAt,
|
||||
required this.content,
|
||||
this.likesCount = 0,
|
||||
this.commentsCount = 0,
|
||||
this.isLikedByMe = false,
|
||||
this.customActionLabel,
|
||||
this.actionUrlTarget,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
type,
|
||||
authorName,
|
||||
authorAvatarUrl,
|
||||
createdAt,
|
||||
content,
|
||||
likesCount,
|
||||
commentsCount,
|
||||
isLikedByMe,
|
||||
customActionLabel,
|
||||
actionUrlTarget,
|
||||
];
|
||||
}
|
||||
96
lib/features/feed/presentation/bloc/unified_feed_bloc.dart
Normal file
96
lib/features/feed/presentation/bloc/unified_feed_bloc.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import 'unified_feed_event.dart';
|
||||
import 'unified_feed_state.dart';
|
||||
import '../../domain/entities/feed_item.dart';
|
||||
import '../../data/repositories/feed_repository.dart';
|
||||
|
||||
/// BLoC Centralisé pour le mur d'actualité (DRY).
|
||||
/// Aucune logique graphique, juste la gestion d'états.
|
||||
@injectable
|
||||
class UnifiedFeedBloc extends Bloc<UnifiedFeedEvent, UnifiedFeedState> {
|
||||
final FeedRepository _repository;
|
||||
|
||||
UnifiedFeedBloc(this._repository) : super(UnifiedFeedInitial()) {
|
||||
on<LoadFeedRequested>(_onLoadFeedRequested);
|
||||
on<FeedLoadMoreRequested>(_onLoadMoreRequested);
|
||||
on<ClearLoadMoreError>(_onClearLoadMoreError);
|
||||
on<FeedItemLiked>(_onFeedItemLiked);
|
||||
}
|
||||
|
||||
void _onClearLoadMoreError(ClearLoadMoreError event, Emitter<UnifiedFeedState> emit) {
|
||||
if (state is UnifiedFeedLoaded) {
|
||||
emit((state as UnifiedFeedLoaded).copyWith(loadMoreErrorMessage: null));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadFeedRequested(LoadFeedRequested event, Emitter<UnifiedFeedState> emit) async {
|
||||
if (!event.isRefresh) {
|
||||
emit(UnifiedFeedLoading());
|
||||
}
|
||||
|
||||
try {
|
||||
final items = await _repository.getFeed(page: 0, size: 10);
|
||||
|
||||
// On suppose qu'on n'a pas atteint la fin si on a reçu la taille max demandée (10)
|
||||
final hasReachedMax = items.length < 10;
|
||||
|
||||
emit(UnifiedFeedLoaded(items: items, hasReachedMax: hasReachedMax));
|
||||
} catch (e) {
|
||||
emit(UnifiedFeedError('Erreur de chargement du flux: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreRequested(FeedLoadMoreRequested event, Emitter<UnifiedFeedState> emit) async {
|
||||
if (state is UnifiedFeedLoaded) {
|
||||
final currentState = state as UnifiedFeedLoaded;
|
||||
if (currentState.hasReachedMax || currentState.isFetchingMore) return;
|
||||
|
||||
emit(currentState.copyWith(isFetchingMore: true));
|
||||
|
||||
try {
|
||||
final nextPage = (currentState.items.length / 10).floor();
|
||||
final moreItems = await _repository.getFeed(page: nextPage, size: 10);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
items: List.of(currentState.items)..addAll(moreItems),
|
||||
hasReachedMax: moreItems.isEmpty,
|
||||
isFetchingMore: false,
|
||||
));
|
||||
} catch (e, st) {
|
||||
AppLogger.error('UnifiedFeedBloc: chargement supplémentaire échoué', error: e, stackTrace: st);
|
||||
emit(currentState.copyWith(
|
||||
isFetchingMore: false,
|
||||
loadMoreErrorMessage: 'Impossible de charger plus',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onFeedItemLiked(FeedItemLiked event, Emitter<UnifiedFeedState> emit) {
|
||||
if (state is UnifiedFeedLoaded) {
|
||||
final currentState = state as UnifiedFeedLoaded;
|
||||
final updatedItems = currentState.items.map((item) {
|
||||
if (item.id == event.itemId) {
|
||||
return FeedItem(
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
authorName: item.authorName,
|
||||
authorAvatarUrl: item.authorAvatarUrl,
|
||||
createdAt: item.createdAt,
|
||||
content: item.content,
|
||||
likesCount: item.isLikedByMe ? item.likesCount - 1 : item.likesCount + 1,
|
||||
commentsCount: item.commentsCount,
|
||||
isLikedByMe: !item.isLikedByMe,
|
||||
customActionLabel: item.customActionLabel,
|
||||
actionUrlTarget: item.actionUrlTarget,
|
||||
);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
|
||||
emit(currentState.copyWith(items: updatedItems));
|
||||
}
|
||||
}
|
||||
}
|
||||
30
lib/features/feed/presentation/bloc/unified_feed_event.dart
Normal file
30
lib/features/feed/presentation/bloc/unified_feed_event.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class UnifiedFeedEvent extends Equatable {
|
||||
const UnifiedFeedEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadFeedRequested extends UnifiedFeedEvent {
|
||||
final bool isRefresh;
|
||||
const LoadFeedRequested({this.isRefresh = false});
|
||||
|
||||
@override
|
||||
List<Object> get props => [isRefresh];
|
||||
}
|
||||
|
||||
class FeedLoadMoreRequested extends UnifiedFeedEvent {}
|
||||
|
||||
/// Efface le message d'erreur « load more » après affichage du SnackBar.
|
||||
class ClearLoadMoreError extends UnifiedFeedEvent {}
|
||||
|
||||
// Exemples d'événements interactifs sans tout polluer
|
||||
class FeedItemLiked extends UnifiedFeedEvent {
|
||||
final String itemId;
|
||||
const FeedItemLiked(this.itemId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [itemId];
|
||||
}
|
||||
54
lib/features/feed/presentation/bloc/unified_feed_state.dart
Normal file
54
lib/features/feed/presentation/bloc/unified_feed_state.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/entities/feed_item.dart';
|
||||
|
||||
abstract class UnifiedFeedState extends Equatable {
|
||||
const UnifiedFeedState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class UnifiedFeedInitial extends UnifiedFeedState {}
|
||||
|
||||
class UnifiedFeedLoading extends UnifiedFeedState {}
|
||||
|
||||
class UnifiedFeedLoaded extends UnifiedFeedState {
|
||||
final List<FeedItem> items;
|
||||
final bool hasReachedMax;
|
||||
final bool isFetchingMore;
|
||||
/// Message d'erreur affiché une fois (ex. « Impossible de charger plus »), à consommer puis effacer par l'UI.
|
||||
final String? loadMoreErrorMessage;
|
||||
|
||||
const UnifiedFeedLoaded({
|
||||
required this.items,
|
||||
this.hasReachedMax = false,
|
||||
this.isFetchingMore = false,
|
||||
this.loadMoreErrorMessage,
|
||||
});
|
||||
|
||||
UnifiedFeedLoaded copyWith({
|
||||
List<FeedItem>? items,
|
||||
bool? hasReachedMax,
|
||||
bool? isFetchingMore,
|
||||
String? loadMoreErrorMessage,
|
||||
}) {
|
||||
return UnifiedFeedLoaded(
|
||||
items: items ?? this.items,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
isFetchingMore: isFetchingMore ?? false,
|
||||
loadMoreErrorMessage: loadMoreErrorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [items, hasReachedMax, isFetchingMore, loadMoreErrorMessage];
|
||||
}
|
||||
|
||||
class UnifiedFeedError extends UnifiedFeedState {
|
||||
final String message;
|
||||
|
||||
const UnifiedFeedError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
Reference in New Issue
Block a user