Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/shared/widgets/common/unified_page_layout.dart
2025-09-17 17:54:06 +00:00

240 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
/// Layout de page unifié pour toutes les features de l'application
///
/// Fournit une structure cohérente avec :
/// - AppBar standardisée avec actions personnalisables
/// - Body avec padding et scroll automatique
/// - FloatingActionButton optionnel
/// - Gestion des états de chargement et d'erreur
class UnifiedPageLayout extends StatelessWidget {
/// Titre de la page affiché dans l'AppBar
final String title;
/// Sous-titre optionnel affiché sous le titre
final String? subtitle;
/// Icône principale de la page
final IconData? icon;
/// Couleur de l'icône (par défaut : primaryColor)
final Color? iconColor;
/// Actions personnalisées dans l'AppBar
final List<Widget>? actions;
/// Contenu principal de la page
final Widget body;
/// FloatingActionButton optionnel
final Widget? floatingActionButton;
/// Position du FloatingActionButton
final FloatingActionButtonLocation? floatingActionButtonLocation;
/// Indique si la page est en cours de chargement
final bool isLoading;
/// Message d'erreur à afficher
final String? errorMessage;
/// Callback pour rafraîchir la page
final VoidCallback? onRefresh;
/// Padding personnalisé pour le body (par défaut : 16.0)
final EdgeInsetsGeometry? padding;
/// Indique si le body doit être scrollable (par défaut : true)
final bool scrollable;
/// Couleur de fond personnalisée
final Color? backgroundColor;
/// Indique si l'AppBar doit être affichée (par défaut : true)
final bool showAppBar;
const UnifiedPageLayout({
super.key,
required this.title,
required this.body,
this.subtitle,
this.icon,
this.iconColor,
this.actions,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.isLoading = false,
this.errorMessage,
this.onRefresh,
this.padding,
this.scrollable = true,
this.backgroundColor,
this.showAppBar = true,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor ?? AppTheme.backgroundLight,
appBar: showAppBar ? _buildAppBar(context) : null,
body: _buildBody(context),
floatingActionButton: floatingActionButton,
floatingActionButtonLocation: floatingActionButtonLocation,
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 1,
surfaceTintColor: Colors.white,
title: Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: iconColor ?? AppTheme.primaryColor,
size: 24,
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
if (subtitle != null)
Text(
subtitle!,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
actions: actions,
);
}
Widget _buildBody(BuildContext context) {
Widget content = body;
// Gestion des états d'erreur
if (errorMessage != null) {
content = _buildErrorState(context);
}
// Gestion de l'état de chargement
else if (isLoading) {
content = _buildLoadingState();
}
// Application du padding
if (padding != null || (padding == null && scrollable)) {
content = Padding(
padding: padding ?? const EdgeInsets.all(16.0),
child: content,
);
}
// Gestion du scroll
if (scrollable && errorMessage == null && !isLoading) {
if (onRefresh != null) {
content = RefreshIndicator(
onRefresh: () async => onRefresh!(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: content,
),
);
} else {
content = SingleChildScrollView(child: content);
}
}
return SafeArea(child: content);
}
Widget _buildLoadingState() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
),
SizedBox(height: 16),
Text(
'Chargement...',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
],
),
);
}
Widget _buildErrorState(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: AppTheme.errorColor,
),
const SizedBox(height: 16),
Text(
'Une erreur est survenue',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 24),
if (onRefresh != null)
ElevatedButton.icon(
onPressed: onRefresh,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
}