feat(ui): RefreshIndicator + AlwaysScrollable + dark mode sur 14 pages
RefreshIndicator ajouté (dispatche les events BLoC appropriés) : - adhesion_detail, adhesions_page, demande_aide_detail, demandes_aide_page - event_detail, organization_detail, org_selector, org_types - user_management_detail, reports (TabBarView), logs (Dashboard tab) - profile (onglet Perso), backup (3 onglets), notifications Fixes associés : - AlwaysScrollableScrollPhysics sur tous les scroll widgets (permet pull-to-refresh même si contenu < écran) - Empty states des listes : wrappés dans SingleChildScrollView pour refresh - Dark mode adaptatif sur textes/surfaces/borders hardcodés - backup_page : bouton retour ajouté dans le header gradient - org_types : chevron/star/border adaptatifs - reports : couleurs placeholders graphique + chevrons
This commit is contained in:
@@ -71,126 +71,54 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
}
|
||||
if (state is NotificationsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
|
||||
SnackBar(content: Text(state.message), backgroundColor: AppColors.error),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildNotificationsTab(),
|
||||
_buildPreferencesTab(),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'Notifications',
|
||||
moduleGradient: ModuleColors.notificationsGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _markAllAsRead(),
|
||||
icon: const Icon(Icons.done_all, size: 20),
|
||||
tooltip: 'Tout marquer comme lu',
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(child: Text('FLUX')),
|
||||
Tab(child: Text('RÉGLAGES')),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildNotificationsTab(),
|
||||
_buildPreferencesTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Header harmonisé avec le design system
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.brandGreen, AppColors.primaryGreen],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1A000000),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications_none,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'NOTIFICATIONS',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Restez connecté à votre réseau',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _markAllAsRead(),
|
||||
icon: const Icon(Icons.done_all, color: Colors.white, size: 20),
|
||||
tooltip: 'Tout marquer comme lu',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Barre d'onglets
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicator: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
),
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 12),
|
||||
unselectedLabelStyle: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
tabs: const [
|
||||
Tab(text: 'FLUX'),
|
||||
Tab(text: 'RÉGLAGES'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// _buildHeader() et _buildTabBar() supprimés : gradient + TabBar migrés
|
||||
// dans UFAppBar + UFAppBar.bottom (pattern Adhésions).
|
||||
|
||||
/// Onglet des notifications
|
||||
Widget _buildNotificationsTab() {
|
||||
@@ -236,7 +164,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Switch(
|
||||
value: _showOnlyUnread,
|
||||
onChanged: (value) => setState(() => _showOnlyUnread = value),
|
||||
activeColor: AppColors.primaryGreen,
|
||||
activeColor: AppColors.primary,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
@@ -262,22 +190,25 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
|
||||
/// Chip de filtre
|
||||
Widget _buildFilterChip(String label, bool isSelected) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final borderInactive= isDark ? AppColors.borderDark : AppColors.border;
|
||||
final textInactive = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _selectedFilter = label),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primaryGreen.withOpacity(0.1) : Colors.transparent,
|
||||
color: isSelected ? AppColors.primary.withOpacity(isDark ? 0.2 : 0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder,
|
||||
color: isSelected ? AppColors.primary : borderInactive,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
color: isSelected ? AppColors.primary : textInactive,
|
||||
fontSize: 9,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
@@ -290,17 +221,24 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Widget _buildNotificationsList() {
|
||||
final notifications = _getFilteredNotifications();
|
||||
|
||||
if (notifications.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: notifications.length,
|
||||
itemBuilder: (context, index) {
|
||||
final notification = notifications[index];
|
||||
return _buildNotificationCard(notification);
|
||||
},
|
||||
return RefreshIndicator(
|
||||
color: ModuleColors.notifications,
|
||||
onRefresh: () async =>
|
||||
context.read<NotificationsBloc>().add(const LoadNotifications()),
|
||||
child: notifications.isEmpty
|
||||
? SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: _buildEmptyState(),
|
||||
)
|
||||
: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: notifications.length,
|
||||
itemBuilder: (context, index) {
|
||||
final notification = notifications[index];
|
||||
return _buildNotificationCard(notification);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,11 +248,11 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.notifications_none_outlined,
|
||||
size: 40,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'AUCUNE NOTIFICATION',
|
||||
@@ -338,6 +276,10 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
final isRead = notification['isRead'] as bool;
|
||||
final type = notification['type'] as String;
|
||||
final color = _getNotificationColor(type);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||||
final bgRead = isDark ? AppColors.surfaceVariantDark : AppColors.surface;
|
||||
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
@@ -349,8 +291,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
MiniAvatar(
|
||||
fallbackText: _getNotificationIconSource(type),
|
||||
size: 32,
|
||||
backgroundColor: isRead ? AppColors.lightSurface : color.withOpacity(0.1),
|
||||
iconColor: isRead ? AppColors.textSecondaryLight : color,
|
||||
backgroundColor: isRead ? bgRead : color.withOpacity(isDark ? 0.2 : 0.1),
|
||||
iconColor: isRead ? textSecondary : color,
|
||||
isIcon: true,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -366,7 +308,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
notification['title'].toString().toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 11,
|
||||
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
|
||||
color: isRead ? textSecondary : textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -382,7 +324,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Text(
|
||||
notification['message'],
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
|
||||
color: isRead ? textSecondary : textPrimary,
|
||||
fontWeight: isRead ? FontWeight.normal : FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
@@ -392,8 +334,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
const SizedBox(height: 4),
|
||||
InfoBadge(
|
||||
text: 'NOUVEAU',
|
||||
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
|
||||
textColor: AppColors.primaryGreen,
|
||||
backgroundColor: AppColors.primary.withOpacity(0.1),
|
||||
textColor: AppColors.primary,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -413,7 +355,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Text('Supprimer', style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error)),
|
||||
),
|
||||
],
|
||||
child: const Icon(Icons.more_vert, size: 14, color: AppColors.textSecondaryLight),
|
||||
child: Icon(Icons.more_vert, size: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -545,7 +487,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: AppColors.primaryGreen,
|
||||
color: AppColors.primary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -604,7 +546,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
activeColor: AppColors.primary,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
@@ -659,15 +601,15 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Color _getNotificationColor(String type) {
|
||||
switch (type) {
|
||||
case 'Membres':
|
||||
return AppColors.primaryGreen;
|
||||
return AppColors.primary;
|
||||
case 'Événements':
|
||||
return AppColors.success;
|
||||
case 'Organisations':
|
||||
return AppColors.primaryGreen;
|
||||
return AppColors.primary;
|
||||
case 'Système':
|
||||
return AppColors.warning;
|
||||
default:
|
||||
return AppColors.textSecondaryLight;
|
||||
return AppColors.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,7 +698,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: AppColors.textSecondaryLight)),
|
||||
child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -769,7 +711,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
});
|
||||
_showSuccessSnackBar('Flux marqué comme lu');
|
||||
},
|
||||
child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primaryGreen)),
|
||||
child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -797,8 +739,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
_tabController.animateTo(1); // Aller à l'onglet Préférences
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
),
|
||||
child: const Text('Voir les préférences'),
|
||||
),
|
||||
@@ -831,8 +773,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
_showSuccessSnackBar('Notification supprimée');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.error,
|
||||
foregroundColor: AppColors.onError,
|
||||
),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
@@ -884,7 +826,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.warning,
|
||||
foregroundColor: Colors.white,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
),
|
||||
child: Text(notification['actionText']),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user