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:
@@ -10,9 +10,11 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/info_badge.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
import '../../../../core/config/environment.dart';
|
||||
import '../../../../core/l10n/locale_provider.dart';
|
||||
import '../../../../core/theme/theme_provider.dart';
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
@@ -26,7 +28,7 @@ import '../widgets/kyc_status_widget.dart';
|
||||
import '../bloc/profile_bloc.dart';
|
||||
|
||||
/// Page Mon Profil - UnionFlow Mobile
|
||||
///
|
||||
///
|
||||
/// Page complète de gestion du profil utilisateur avec informations personnelles,
|
||||
/// préférences, sécurité, et paramètres avancés.
|
||||
class ProfilePage extends StatefulWidget {
|
||||
@@ -40,7 +42,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
|
||||
// Contrôleurs pour les champs de texte
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
@@ -50,7 +52,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
final _cityController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _bioController = TextEditingController();
|
||||
|
||||
|
||||
// État du profil
|
||||
bool _isEditing = false;
|
||||
bool _isLoading = false;
|
||||
@@ -66,15 +68,22 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
// Infos app (chargées via package_info_plus)
|
||||
String _appVersion = '...';
|
||||
String _platformInfo = '...';
|
||||
String _osVersion = '...';
|
||||
String _envInfo = '...';
|
||||
// Tailles stockage (calculées au chargement)
|
||||
String _cacheSize = '...';
|
||||
String _imagesSize = '...';
|
||||
String _offlineSize = '...';
|
||||
// Options développeur (persistées via SharedPreferences)
|
||||
bool _devMode = false;
|
||||
bool _detailedLogs = false;
|
||||
final List<String> _themes = ['Système', 'Clair', 'Sombre'];
|
||||
|
||||
static const _keyNotifPush = 'notif_push';
|
||||
static const _keyNotifEmail = 'notif_email';
|
||||
static const _keyNotifSon = 'notif_son';
|
||||
static const _keyDevMode = 'dev_mode';
|
||||
static const _keyDetailedLogs = 'detailed_logs';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -136,37 +145,51 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: UFAppBar(
|
||||
title: 'MON PROFIL',
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
foregroundColor: Theme.of(context).brightness == Brightness.dark
|
||||
? AppColors.textPrimaryDark
|
||||
: AppColors.textPrimaryLight,
|
||||
title: 'Mon Profil',
|
||||
moduleGradient: ModuleColors.profilGradient,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_isEditing ? Icons.save_outlined : Icons.edit_outlined, size: 20),
|
||||
onPressed: () => _isEditing ? _saveProfile() : _startEditing(),
|
||||
),
|
||||
],
|
||||
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('PERSO')),
|
||||
Tab(child: Text('PRÉFÉRENCES')),
|
||||
Tab(child: Text('SÉCURITÉ')),
|
||||
Tab(child: Text('AVANCÉ')),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 8),
|
||||
_buildTabBar(),
|
||||
SizedBox(
|
||||
height: 600, // Ajuster selon contenu ou utiliser NestedScrollView
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPersonalInfoTab(),
|
||||
_buildPreferencesTab(),
|
||||
_buildSecurityTab(),
|
||||
_buildAdvancedTab(),
|
||||
],
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: _buildHeader(),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPersonalInfoTab(),
|
||||
_buildPreferencesTab(),
|
||||
_buildSecurityTab(),
|
||||
_buildAdvancedTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -176,6 +199,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Widget _buildHeader() {
|
||||
return BlocBuilder<ProfileBloc, ProfileState>(
|
||||
builder: (context, state) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final membre = (state is ProfileLoaded)
|
||||
? state.membre
|
||||
: (state is ProfileUpdated ? state.membre
|
||||
@@ -189,39 +213,39 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Color badgeColor;
|
||||
if (isLoading) {
|
||||
badgeText = 'CHARGEMENT...';
|
||||
badgeColor = AppColors.textSecondaryLight;
|
||||
badgeColor = ColorTokens.textSecondary;
|
||||
} else if (hasError) {
|
||||
badgeText = 'ERREUR CHARGEMENT';
|
||||
badgeColor = AppColors.error;
|
||||
badgeColor = ColorTokens.error;
|
||||
} else if (membre != null) {
|
||||
// Priorité au rôle s'il est disponible
|
||||
if (membre.role != null && membre.role!.isNotEmpty) {
|
||||
badgeText = membre.role!.replaceAll('_', ' ');
|
||||
badgeColor = AppColors.primaryGreen;
|
||||
badgeColor = ModuleColors.profil;
|
||||
} else {
|
||||
// Fallback sur le statut
|
||||
switch (membre.statut) {
|
||||
case StatutMembre.actif:
|
||||
badgeText = membre.cotisationAJour ? 'MEMBRE ACTIF' : 'COTISATION EN RETARD';
|
||||
badgeColor = membre.cotisationAJour ? AppColors.success : AppColors.warning;
|
||||
badgeColor = membre.cotisationAJour ? ColorTokens.success : ColorTokens.warning;
|
||||
break;
|
||||
case StatutMembre.inactif:
|
||||
badgeText = 'INACTIF';
|
||||
badgeColor = AppColors.textSecondaryLight;
|
||||
badgeColor = ColorTokens.textSecondary;
|
||||
break;
|
||||
case StatutMembre.suspendu:
|
||||
badgeText = 'SUSPENDU';
|
||||
badgeColor = AppColors.error;
|
||||
badgeColor = ColorTokens.error;
|
||||
break;
|
||||
case StatutMembre.enAttente:
|
||||
badgeText = 'EN ATTENTE';
|
||||
badgeColor = AppColors.warning;
|
||||
badgeColor = ColorTokens.warning;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
badgeText = 'CHARGEMENT...';
|
||||
badgeColor = AppColors.textSecondaryLight;
|
||||
badgeColor = ColorTokens.textSecondary;
|
||||
}
|
||||
|
||||
// Ancienneté réelle
|
||||
@@ -274,7 +298,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
Text(
|
||||
_emailController.text.toLowerCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 11),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 11, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (isLoading)
|
||||
@@ -291,7 +315,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: Text(
|
||||
'Réessayer',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
color: ModuleColors.profil,
|
||||
fontSize: 10,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -344,10 +368,11 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
children: [
|
||||
Text(value, style: AppTypography.headerSmall.copyWith(fontSize: 14)),
|
||||
Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold, color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -388,35 +413,16 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
);
|
||||
}
|
||||
|
||||
/// Barre d'onglets
|
||||
Widget _buildTabBar() {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(text: 'PERSO'),
|
||||
Tab(text: 'PRÉF'),
|
||||
Tab(text: 'SÉCU'),
|
||||
Tab(text: 'AVANCÉ'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// _buildTabBar() supprimé : migré dans UFAppBar.bottom (pattern Adhésions)
|
||||
|
||||
/// Onglet informations personnelles
|
||||
Widget _buildPersonalInfoTab() {
|
||||
return SingleChildScrollView(
|
||||
return RefreshIndicator(
|
||||
color: ModuleColors.profil,
|
||||
onRefresh: () async =>
|
||||
context.read<ProfileBloc>().add(const LoadMe()),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -533,8 +539,9 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
);
|
||||
), // Column
|
||||
), // SingleChildScrollView
|
||||
); // RefreshIndicator
|
||||
}
|
||||
|
||||
/// Section d'informations
|
||||
@@ -551,7 +558,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(icon, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
@@ -579,7 +586,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
int maxLines = 1,
|
||||
String? hintText,
|
||||
}) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
@@ -587,35 +594,34 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
maxLines: maxLines,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: label.toUpperCase(),
|
||||
labelStyle: AppTypography.subtitleSmall.copyWith(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
hintText: hintText,
|
||||
prefixIcon: Icon(icon, color: enabled ? AppColors.primaryGreen : AppColors.textSecondaryLight, size: 16),
|
||||
prefixIcon: Icon(icon, color: enabled ? ModuleColors.profil : ColorTokens.textSecondary, size: 16),
|
||||
filled: true,
|
||||
fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
fillColor: scheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1),
|
||||
borderSide: BorderSide(color: ModuleColors.profil, width: 1),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
@@ -631,15 +637,15 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
/// Boutons d'action
|
||||
Widget _buildActionButtons() {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : Colors.white,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -652,8 +658,8 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isLoading ? null : _cancelEditing,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[100],
|
||||
foregroundColor: Colors.grey[700],
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
foregroundColor: AppColors.textSecondary,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
@@ -666,7 +672,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isLoading ? null : _saveProfile,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: ModuleColors.profil,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -689,7 +695,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _startEditing,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: ModuleColors.profil,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -720,9 +726,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
final isDark = Theme.of(ctx).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(ctx).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
await Navigator.of(context).push(
|
||||
@@ -738,18 +742,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.language, color: textSecondary, size: 22),
|
||||
Icon(Icons.language, color: scheme.onSurfaceVariant, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)),
|
||||
Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)),
|
||||
Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)),
|
||||
Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: textSecondary, size: 20),
|
||||
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -823,9 +827,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
final isDark = Theme.of(ctx).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(ctx).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
@@ -837,18 +839,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.privacy_tip, color: textSecondary, size: 22),
|
||||
Icon(Icons.privacy_tip, color: scheme.onSurfaceVariant, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)),
|
||||
Text('Visibilité, partage de données, suppression de compte', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)),
|
||||
Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)),
|
||||
Text('Visibilité, partage de données, suppression de compte', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: textSecondary, size: 20),
|
||||
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -950,21 +952,21 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
'Télécharger mes données',
|
||||
'Exporter toutes vos données personnelles',
|
||||
Icons.download,
|
||||
AppColors.primaryGreen,
|
||||
ModuleColors.profil,
|
||||
() => _exportUserData(),
|
||||
),
|
||||
_buildActionItem(
|
||||
'Déconnecter tous les appareils',
|
||||
'Fermer toutes les sessions actives',
|
||||
Icons.logout,
|
||||
AppColors.warning,
|
||||
ColorTokens.warning,
|
||||
() => _logoutAllDevices(),
|
||||
),
|
||||
_buildActionItem(
|
||||
'Supprimer mon compte',
|
||||
'Action irréversible - toutes les données seront perdues',
|
||||
Icons.delete_forever,
|
||||
Colors.red,
|
||||
ColorTokens.error,
|
||||
() => _showDeleteAccountDialog(),
|
||||
),
|
||||
],
|
||||
@@ -1019,14 +1021,22 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_buildSwitchPreference(
|
||||
'Mode développeur',
|
||||
'Afficher les options de débogage',
|
||||
false,
|
||||
(value) => _showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}'),
|
||||
_devMode,
|
||||
(value) async {
|
||||
setState(() => _devMode = value);
|
||||
await _savePreference(_keyDevMode, value);
|
||||
_showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}');
|
||||
},
|
||||
),
|
||||
_buildSwitchPreference(
|
||||
'Logs détaillés',
|
||||
'Enregistrer plus d\'informations de débogage',
|
||||
false,
|
||||
(value) => _showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'}'),
|
||||
'Logs réseau et erreurs — actif au prochain démarrage',
|
||||
_detailedLogs,
|
||||
(value) async {
|
||||
setState(() => _detailedLogs = value);
|
||||
await _savePreference(_keyDetailedLogs, value);
|
||||
_showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'} (prochain démarrage)');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1041,6 +1051,9 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
_buildInfoItem('Version de l\'app', _appVersion),
|
||||
_buildInfoItem('Plateforme', _platformInfo),
|
||||
_buildInfoItem('Système', _osVersion),
|
||||
_buildInfoItem('Environnement', _envInfo),
|
||||
if (_devMode) _buildInfoItem('API URL', AppConfig.apiBaseUrl),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1054,9 +1067,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
[
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
final isDark = Theme.of(ctx).brightness == Brightness.dark;
|
||||
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(ctx).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
@@ -1068,18 +1079,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit_note, color: textSecondary, size: 22),
|
||||
Icon(Icons.edit_note, color: scheme.onSurfaceVariant, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)),
|
||||
Text('Suggestions, bugs ou idées d\'amélioration', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)),
|
||||
Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600)),
|
||||
Text('Suggestions, bugs ou idées d\'amélioration', style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: textSecondary, size: 20),
|
||||
Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1113,7 +1124,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(icon, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
@@ -1159,7 +1170,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
List<String> options,
|
||||
Function(String?) onChanged,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1171,21 +1182,31 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
onChanged: onChanged,
|
||||
icon: const Icon(Icons.arrow_drop_down, color: AppColors.primaryGreen, size: 18),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
dropdownColor: scheme.surface,
|
||||
icon: Icon(Icons.arrow_drop_down, color: ModuleColors.profil, size: 18),
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 12,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
items: options.map((option) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: option,
|
||||
child: Text(option),
|
||||
child: Text(
|
||||
option,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 12,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
@@ -1202,7 +1223,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
bool value,
|
||||
Function(bool) onChanged,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -1217,7 +1238,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 10,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1228,7 +1249,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
activeColor: ModuleColors.profil,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1242,21 +1263,20 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(icon, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -1268,12 +1288,12 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, color: textSecondary, size: 12),
|
||||
Icon(Icons.arrow_forward_ios, color: scheme.onSurfaceVariant, size: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1287,19 +1307,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
IconData icon,
|
||||
bool isCurrentDevice,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentDevice
|
||||
? AppColors.primaryGreen.withOpacity(0.05)
|
||||
: (isDark ? AppColors.darkSurface : AppColors.lightSurface),
|
||||
? ModuleColors.profil.withOpacity(0.05)
|
||||
: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isCurrentDevice
|
||||
? AppColors.primaryGreen.withOpacity(0.3)
|
||||
: (isDark ? AppColors.darkBorder : AppColors.lightBorder),
|
||||
? ModuleColors.profil.withOpacity(0.3)
|
||||
: scheme.outlineVariant,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
@@ -1307,7 +1326,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isCurrentDevice ? AppColors.primaryGreen : textSecondary,
|
||||
color: isCurrentDevice ? ModuleColors.profil : scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -1323,13 +1342,13 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
if (isCurrentDevice) ...[
|
||||
const SizedBox(width: 8),
|
||||
const InfoBadge(text: 'ACTUEL', backgroundColor: AppColors.primaryGreen),
|
||||
InfoBadge(text: 'ACTUEL', backgroundColor: ModuleColors.profil),
|
||||
],
|
||||
],
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1347,7 +1366,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Color color,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
@@ -1374,7 +1393,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
subtitle,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontSize: 10,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1389,20 +1408,20 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
/// Élément de stockage
|
||||
Widget _buildStorageItem(String title, String size, VoidCallback onTap) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.folder_outlined, color: AppColors.primaryGreen, size: 16),
|
||||
Icon(Icons.folder_outlined, color: ModuleColors.profil, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -1412,10 +1431,10 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
),
|
||||
Text(
|
||||
size,
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.clear, color: AppColors.error, size: 14),
|
||||
Icon(Icons.clear, color: ColorTokens.error, size: 14),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1424,20 +1443,20 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
|
||||
/// Élément d'information
|
||||
Widget _buildInfoItem(String title, String value) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
color: scheme.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold, color: scheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -1565,7 +1584,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
);
|
||||
}
|
||||
|
||||
/// Charger les préférences notifications depuis SharedPreferences
|
||||
/// Charger les préférences depuis SharedPreferences
|
||||
Future<void> _loadPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
@@ -1573,6 +1592,8 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_notifPush = prefs.getBool(_keyNotifPush) ?? true;
|
||||
_notifEmail = prefs.getBool(_keyNotifEmail) ?? false;
|
||||
_notifSon = prefs.getBool(_keyNotifSon) ?? true;
|
||||
_devMode = prefs.getBool(_keyDevMode) ?? AppConfig.enableDebugMode;
|
||||
_detailedLogs = prefs.getBool(_keyDetailedLogs) ?? AppConfig.enableLogging;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1588,7 +1609,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_appVersion = '${info.version} (Build ${info.buildNumber})';
|
||||
_appVersion = '${info.version} (build ${info.buildNumber})';
|
||||
final os = kIsWeb
|
||||
? 'Web'
|
||||
: Platform.isAndroid
|
||||
@@ -1597,6 +1618,10 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
? 'iOS'
|
||||
: Platform.operatingSystem;
|
||||
_platformInfo = '$os · ${info.appName}';
|
||||
_osVersion = kIsWeb
|
||||
? 'Navigateur web'
|
||||
: Platform.operatingSystemVersion;
|
||||
_envInfo = AppConfig.environment.name.toUpperCase();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1684,7 +1709,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
confirmPassCtrl.dispose();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
backgroundColor: ModuleColors.profil,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Modifier'),
|
||||
@@ -1782,7 +1807,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
_showFinalDeleteConfirmation();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: ColorTokens.error,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Continuer'),
|
||||
@@ -1836,7 +1861,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
confirmCtrl.dispose();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: ColorTokens.error,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('SUPPRIMER DÉFINITIVEMENT'),
|
||||
@@ -1953,7 +1978,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
message.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
backgroundColor: ColorTokens.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
@@ -1967,7 +1992,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
message.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: AppColors.error,
|
||||
backgroundColor: ColorTokens.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user