240 lines
6.5 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|