Bon checkpoint + Refactoring
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../state_management/event_bloc.dart';
|
||||
import '../dialogs/add_event_dialog.dart';
|
||||
|
||||
/// Écran principal des événements, affichant une liste d'événements.
|
||||
class EventScreen extends StatefulWidget {
|
||||
final String userId;
|
||||
final String userFirstName;
|
||||
@@ -63,9 +64,11 @@ class _EventScreenState extends State<EventScreen> {
|
||||
body: BlocBuilder<EventBloc, EventState>(
|
||||
builder: (context, state) {
|
||||
if (state is EventLoading) {
|
||||
print('[LOG] Chargement en cours des événements...');
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is EventLoaded) {
|
||||
final events = state.events;
|
||||
print('[LOG] Nombre d\'événements à afficher: ${events.length}');
|
||||
if (events.isEmpty) {
|
||||
return const Center(child: Text('Aucun événement disponible.'));
|
||||
}
|
||||
@@ -74,6 +77,7 @@ class _EventScreenState extends State<EventScreen> {
|
||||
itemCount: events.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = events[index];
|
||||
print('[LOG] Affichage de l\'événement $index : ${event.title}');
|
||||
return EventCard(
|
||||
key: ValueKey(event.id),
|
||||
event: event,
|
||||
@@ -99,6 +103,7 @@ class _EventScreenState extends State<EventScreen> {
|
||||
},
|
||||
);
|
||||
} else if (state is EventError) {
|
||||
print('[ERROR] Message d\'erreur: ${state.message}');
|
||||
return Center(child: Text('Erreur: ${state.message}'));
|
||||
}
|
||||
return const Center(child: Text('Aucun événement disponible.'));
|
||||
|
||||
@@ -45,11 +45,13 @@ class _FriendsScreenState extends State<FriendsScreen> {
|
||||
/// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis.
|
||||
void _onScroll() {
|
||||
final provider = Provider.of<FriendsProvider>(context, listen: false);
|
||||
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent &&
|
||||
|
||||
// Ajout d'une marge de 200 pixels pour détecter le bas de la liste plus tôt
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200 &&
|
||||
!provider.isLoading && provider.hasMore) {
|
||||
debugPrint("[LOG] Scroll : fin de liste atteinte, chargement de la page suivante");
|
||||
// Charger plus d'amis si on atteint la fin de la liste
|
||||
provider.fetchFriends(widget.userId);
|
||||
debugPrint("[LOG] Scroll : Fin de liste atteinte, chargement de la page suivante.");
|
||||
provider.fetchFriends(widget.userId, loadMore: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +67,12 @@ class _FriendsScreenState extends State<FriendsScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
// Log de l'action de rafraîchissement
|
||||
debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis");
|
||||
// Rafraîchir la liste des amis
|
||||
friendsProvider.fetchFriends(widget.userId);
|
||||
if (!friendsProvider.isLoading) {
|
||||
debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis");
|
||||
friendsProvider.fetchFriends(widget.userId);
|
||||
} else {
|
||||
debugPrint("[LOG] Rafraîchissement en cours, action ignorée.");
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -110,27 +114,29 @@ class _FriendsScreenState extends State<FriendsScreen> {
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
),
|
||||
itemCount: friendsProvider.friendsList.length,
|
||||
itemCount: friendsProvider.friendsList.length + (friendsProvider.isLoading && friendsProvider.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= friendsProvider.friendsList.length) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final friend = friendsProvider.friendsList[index];
|
||||
debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}");
|
||||
|
||||
return FriendsCircle(
|
||||
friend: friend,
|
||||
onTap: () {
|
||||
// Log pour l'action de visualisation des détails d'un ami
|
||||
debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}");
|
||||
// Naviguer vers l'écran des détails de l'ami
|
||||
FriendDetailScreen.open(
|
||||
context,
|
||||
friend.friendId,
|
||||
friend.firstName ?? 'Ami inconnu',
|
||||
friend.friendFirstName,
|
||||
friend.imageUrl ?? '',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../assets/animations/friend_expanding_card.dart';
|
||||
import '../../../data/providers/friends_provider.dart';
|
||||
@@ -7,10 +8,9 @@ import '../../widgets/friend_detail_screen.dart';
|
||||
import '../../widgets/friends_appbar.dart';
|
||||
import '../../widgets/search_friends.dart';
|
||||
|
||||
/// [FriendsScreenWithProvider] est un écran qui affiche la liste des amis.
|
||||
/// Il utilise le provider [FriendsProvider] pour gérer les états et les données.
|
||||
/// Chaque action est loguée pour permettre une traçabilité complète.
|
||||
class FriendsScreenWithProvider extends StatelessWidget {
|
||||
final Logger _logger = Logger(); // Logger pour une meilleure traçabilité
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -29,6 +29,7 @@ class FriendsScreenWithProvider extends StatelessWidget {
|
||||
final friends = friendsProvider.friendsList;
|
||||
|
||||
if (friends.isEmpty) {
|
||||
_logger.i("[LOG] Aucun ami trouvé");
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucun ami trouvé',
|
||||
@@ -51,19 +52,22 @@ class FriendsScreenWithProvider extends StatelessWidget {
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
onDismissed: (direction) {
|
||||
debugPrint("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}");
|
||||
_logger.i("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}");
|
||||
friendsProvider.removeFriend(friend.friendId);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Ami supprimé : ${friend.friendFirstName}")),
|
||||
);
|
||||
},
|
||||
child: FriendExpandingCard(
|
||||
name: friend.firstName ?? 'Ami inconnu',
|
||||
name: friend.friendFirstName ?? 'Ami inconnu',
|
||||
imageUrl: friend.imageUrl ?? '',
|
||||
description: "Amis depuis ${friend.friendId}",
|
||||
onTap: () => _navigateToFriendDetail(context, friend),
|
||||
onMessageTap: () {
|
||||
debugPrint("[LOG] Envoi d'un message à l'ami : ${friend.firstName ?? 'Ami inconnu'}");
|
||||
_logger.i("[LOG] Envoi d'un message à l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}");
|
||||
},
|
||||
onRemoveTap: () {
|
||||
debugPrint("[LOG] Tentative de suppression de l'ami : ${friend.firstName ?? 'Ami inconnu'}");
|
||||
_logger.i("[LOG] Tentative de suppression de l'ami : ${friend.friendFirstName ?? 'Ami inconnu'}");
|
||||
friendsProvider.removeFriend(friend.friendId);
|
||||
},
|
||||
),
|
||||
@@ -79,14 +83,13 @@ class FriendsScreenWithProvider extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Navigue vers l'écran des détails de l'utilisateur (ami) récupéré via son `friendId`.
|
||||
void _navigateToFriendDetail(BuildContext context, Friend friend) {
|
||||
debugPrint("[LOG] Navigation vers les détails de l'ami : ${friend.firstName ?? 'Ami inconnu'}");
|
||||
_logger.i("[LOG] Navigation vers les détails de l'ami : ${friend.friendFirstName}");
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => FriendDetailScreen(
|
||||
name: friend.firstName ?? 'Ami inconnu',
|
||||
name: friend.friendFirstName,
|
||||
imageUrl: friend.imageUrl ?? '',
|
||||
friendId: friend.friendId, // Passer l'ID pour récupérer les détails complets
|
||||
friendId: friend.friendId,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import '../../widgets/statistics_section_card.dart';
|
||||
import '../../widgets/support_section_card.dart';
|
||||
import '../../widgets/user_info_card.dart';
|
||||
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
|
||||
@@ -65,10 +65,14 @@ class EventBloc extends Bloc<EventEvent, EventState> {
|
||||
// Gestion du chargement des événements
|
||||
Future<void> _onLoadEvents(LoadEvents event, Emitter<EventState> emit) async {
|
||||
emit(EventLoading());
|
||||
print('[LOG] Début du chargement des événements pour l\'utilisateur ${event.userId}');
|
||||
|
||||
try {
|
||||
final events = await remoteDataSource.getAllEvents();
|
||||
final events = await remoteDataSource.getEventsCreatedByUserAndFriends(event.userId);
|
||||
print('[LOG] Événements chargés: ${events.length} éléments récupérés.');
|
||||
emit(EventLoaded(events));
|
||||
} catch (e) {
|
||||
print('[ERROR] Erreur lors du chargement des événements: $e');
|
||||
emit(EventError('Erreur lors du chargement des événements.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
|
||||
/// [AccountDeletionCard] est un widget permettant à l'utilisateur de supprimer son compte.
|
||||
/// Il affiche une confirmation avant d'effectuer l'action de suppression.
|
||||
class AccountDeletionCard extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
|
||||
@@ -23,6 +25,7 @@ class AccountDeletionCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche un dialogue de confirmation pour la suppression du compte.
|
||||
void _showDeleteConfirmationDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -39,14 +42,17 @@ class AccountDeletionCard extends StatelessWidget {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () {
|
||||
debugPrint("[LOG] Suppression du compte annulée.");
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text('Annuler', style: TextStyle(color: AppColors.accentColor)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
print("[LOG] Suppression du compte confirmée.");
|
||||
debugPrint("[LOG] Suppression du compte confirmée.");
|
||||
Navigator.of(context).pop();
|
||||
// Logique de suppression du compte
|
||||
// Logique de suppression du compte ici.
|
||||
},
|
||||
child: const Text(
|
||||
'Supprimer',
|
||||
|
||||
@@ -1,35 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'custom_list_tile.dart';
|
||||
|
||||
import '../../../../core/constants/colors.dart';
|
||||
|
||||
/// [EditOptionsCard] permet à l'utilisateur d'accéder aux options d'édition du profil,
|
||||
/// incluant la modification du profil, la photo et le mot de passe.
|
||||
/// Les interactions sont entièrement loguées pour une traçabilité complète.
|
||||
class EditOptionsCard extends StatelessWidget {
|
||||
const EditOptionsCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de EditOptionsCard");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
color: AppColors.cardColor.withOpacity(0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 4,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.3),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomListTile(
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.edit,
|
||||
label: 'Éditer le profil',
|
||||
onTap: () => print("[LOG] Édition du profil."),
|
||||
logMessage: "Édition du profil",
|
||||
onTap: () => debugPrint("[LOG] Édition du profil activée."),
|
||||
),
|
||||
CustomListTile(
|
||||
_buildDivider(),
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.camera_alt,
|
||||
label: 'Changer la photo de profil',
|
||||
onTap: () => print("[LOG] Changement de la photo de profil."),
|
||||
logMessage: "Changement de la photo de profil",
|
||||
onTap: () =>
|
||||
debugPrint("[LOG] Changement de la photo de profil activé."),
|
||||
),
|
||||
CustomListTile(
|
||||
_buildDivider(),
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.lock,
|
||||
label: 'Changer le mot de passe',
|
||||
onTap: () => print("[LOG] Changement du mot de passe."),
|
||||
logMessage: "Changement du mot de passe",
|
||||
onTap: () => debugPrint("[LOG] Changement du mot de passe activé."),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit chaque option de la carte avec une animation de feedback visuel.
|
||||
Widget _buildOption(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String logMessage,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
debugPrint("[LOG] $logMessage");
|
||||
onTap();
|
||||
},
|
||||
splashColor: AppColors.accentColor.withOpacity(0.3),
|
||||
highlightColor: AppColors.accentColor.withOpacity(0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.accentColor),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un séparateur entre les options pour une meilleure structure visuelle.
|
||||
Widget _buildDivider() {
|
||||
return Divider(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
height: 1,
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
|
||||
class ExpandableSectionCard extends StatelessWidget {
|
||||
/// [ExpandableSectionCard] est une carte qui peut s'étendre pour révéler des éléments enfants.
|
||||
/// Ce composant inclut des animations d'extension, des logs pour chaque action et une expérience utilisateur optimisée.
|
||||
class ExpandableSectionCard extends StatefulWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final List<Widget> children;
|
||||
@@ -13,25 +15,73 @@ class ExpandableSectionCard extends StatelessWidget {
|
||||
required this.children,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ExpandableSectionCardState createState() => _ExpandableSectionCardState();
|
||||
}
|
||||
|
||||
class _ExpandableSectionCardState extends State<ExpandableSectionCard> with SingleTickerProviderStateMixin {
|
||||
bool _isExpanded = false;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _iconRotation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
_iconRotation = Tween<double>(begin: 0, end: 0.5).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleExpansion() {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
_isExpanded ? _controller.forward() : _controller.reverse();
|
||||
debugPrint("[LOG] ${_isExpanded ? 'Ouverture' : 'Fermeture'} de l'ExpandableSectionCard : ${widget.title}");
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
child: ExpansionTile(
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
elevation: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(widget.icon, color: AppColors.accentColor),
|
||||
title: Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
trailing: RotationTransition(
|
||||
turns: _iconRotation,
|
||||
child: Icon(Icons.expand_more, color: AppColors.accentColor),
|
||||
),
|
||||
onTap: _toggleExpansion,
|
||||
),
|
||||
),
|
||||
leading: Icon(icon, color: AppColors.accentColor),
|
||||
iconColor: AppColors.accentColor,
|
||||
collapsedIconColor: AppColors.accentColor,
|
||||
children: children,
|
||||
// Contenu de l'expansion
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
firstChild: Container(),
|
||||
secondChild: Column(children: widget.children),
|
||||
crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ class FriendsCircle extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Combine firstName et lastName ou utilise "Ami inconnu" par défaut.
|
||||
String displayName = [friend.firstName, friend.lastName]
|
||||
.where((namePart) => namePart.isNotEmpty)
|
||||
String displayName = [friend.friendFirstName, friend.friendLastName]
|
||||
.where((namePart) => namePart != null && namePart.isNotEmpty)
|
||||
.join(" ")
|
||||
.trim();
|
||||
|
||||
@@ -44,8 +44,10 @@ class FriendsCircle extends StatelessWidget {
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty
|
||||
? NetworkImage(friend.imageUrl!) // Utilise NetworkImage si l'URL est valide
|
||||
: AssetImage('lib/assets/images/default_avatar.png') as ImageProvider, // Utilise AssetImage pour l'avatar par défaut
|
||||
? (friend.imageUrl!.startsWith('http') // Vérifie si l'image est une URL réseau
|
||||
? NetworkImage(friend.imageUrl!)
|
||||
: AssetImage(friend.imageUrl!) as ImageProvider) // Utilise AssetImage si c'est une ressource locale
|
||||
: const AssetImage('lib/assets/images/default_avatar.png'), // Utilise AssetImage pour l'avatar par défaut
|
||||
onBackgroundImageError: (error, stackTrace) {
|
||||
_logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error');
|
||||
},
|
||||
@@ -71,3 +73,4 @@ class FriendsCircle extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import '../../../../core/constants/colors.dart';
|
||||
import '../../../../data/providers/user_provider.dart';
|
||||
import '../../../../domain/entities/user.dart';
|
||||
|
||||
/// [ProfileHeader] est un widget qui affiche l'en-tête du profil utilisateur.
|
||||
/// Comprend le nom de l'utilisateur, une image de fond, et un bouton de déconnexion avec confirmation.
|
||||
/// [ProfileHeader] : Un widget d'en-tête de profil utilisateur visuellement amélioré
|
||||
/// avec un gradient élégant, des animations, et un bouton de déconnexion stylisé.
|
||||
/// Entièrement logué pour une traçabilité complète.
|
||||
class ProfileHeader extends StatelessWidget {
|
||||
final User user;
|
||||
|
||||
@@ -13,97 +14,157 @@ class ProfileHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de ProfileHeader pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
|
||||
|
||||
return SliverAppBar(
|
||||
expandedHeight: 200.0,
|
||||
expandedHeight: 250.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: AppColors.darkPrimary,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: Text(
|
||||
flexibleSpace: _buildFlexibleSpaceBar(user),
|
||||
actions: [_buildLogoutButton(context)],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un FlexibleSpaceBar avec un gradient et des animations.
|
||||
/// Affiche le nom de l'utilisateur et l'image de profil avec un effet visuel enrichi.
|
||||
Widget _buildFlexibleSpaceBar(User user) {
|
||||
debugPrint("[LOG] Construction de FlexibleSpaceBar avec nom et image de profil.");
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Text(
|
||||
'Profil de ${user.userFirstName}',
|
||||
style: TextStyle(
|
||||
color: AppColors.accentColor,
|
||||
fontSize: 20.0,
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
background: Image.network(
|
||||
user.profileImageUrl,
|
||||
),
|
||||
background: _buildProfileImageWithGradient(user.profileImageUrl),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'image de profil avec un overlay en gradient.
|
||||
/// En cas d'erreur de chargement, affiche une image par défaut.
|
||||
Widget _buildProfileImageWithGradient(String profileImageUrl) {
|
||||
debugPrint("[LOG] Chargement de l'image de profil avec overlay de gradient.");
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.network(
|
||||
profileImageUrl,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(child: CircularProgressIndicator(color: AppColors.accentColor));
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
// Log en cas d'erreur de chargement de l'image
|
||||
print("[ERROR] Erreur lors du chargement de l'image de profil : $error");
|
||||
return Image.asset('lib/assets/images/default_avatar.png', fit: BoxFit.cover);
|
||||
debugPrint("[ERROR] Erreur lors du chargement de l'image de profil : $error");
|
||||
return Image.asset(
|
||||
'lib/assets/images/default_avatar.png',
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
onPressed: () {
|
||||
print("[LOG] Bouton de déconnexion cliqué."); // Log du clic du bouton de déconnexion
|
||||
_showLogoutConfirmationDialog(context); // Affiche le dialogue de confirmation
|
||||
},
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.transparent, AppColors.darkPrimary.withOpacity(0.8)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un bouton de déconnexion stylisé avec animation.
|
||||
/// Log chaque interaction pour assurer une traçabilité complète.
|
||||
Widget _buildLogoutButton(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
splashRadius: 20,
|
||||
onPressed: () {
|
||||
debugPrint("[LOG] Clic sur le bouton de déconnexion.");
|
||||
_showLogoutConfirmationDialog(context);
|
||||
},
|
||||
tooltip: 'Déconnexion',
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche une boîte de dialogue de confirmation pour la déconnexion.
|
||||
/// Log chaque action et résultat dans le terminal.
|
||||
/// Log chaque action et résultat pour une visibilité dans le terminal.
|
||||
void _showLogoutConfirmationDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// Log affichage du dialogue
|
||||
print("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion.");
|
||||
debugPrint("[LOG] Affichage de la boîte de dialogue de confirmation de déconnexion.");
|
||||
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
backgroundColor: AppColors.backgroundColor,
|
||||
title: const Text(
|
||||
'Confirmer la déconnexion',
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
content: const Text(
|
||||
'Voulez-vous vraiment vous déconnecter ?',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
// Bouton d'annulation de la déconnexion
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
print("[LOG] Déconnexion annulée par l'utilisateur.");
|
||||
Navigator.of(context).pop(); // Ferme le dialogue sans déconnecter
|
||||
},
|
||||
child: Text(
|
||||
'Annuler',
|
||||
style: TextStyle(color: AppColors.accentColor),
|
||||
),
|
||||
),
|
||||
// Bouton de confirmation de la déconnexion
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
print("[LOG] Déconnexion confirmée."); // Log de la confirmation
|
||||
Provider.of<UserProvider>(context, listen: false).resetUser(); // Réinitialise les infos utilisateur
|
||||
print("[LOG] Informations utilisateur réinitialisées dans UserProvider.");
|
||||
|
||||
Navigator.of(context).pop(); // Ferme la boîte de dialogue
|
||||
print("[LOG] Boîte de dialogue de confirmation fermée.");
|
||||
|
||||
Navigator.of(context).pushReplacementNamed('/'); // Redirige vers l'écran de connexion
|
||||
print("[LOG] Redirection vers l'écran de connexion.");
|
||||
},
|
||||
child: const Text(
|
||||
'Déconnexion',
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
_buildCancelButton(context),
|
||||
_buildConfirmButton(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((_) {
|
||||
// Log lorsque le dialogue est fermé pour toute raison (confirmation ou annulation)
|
||||
print("[LOG] La boîte de dialogue de confirmation de déconnexion est fermée.");
|
||||
debugPrint("[LOG] Fermeture de la boîte de dialogue de déconnexion.");
|
||||
});
|
||||
}
|
||||
|
||||
/// Construit le bouton pour annuler la déconnexion avec log.
|
||||
Widget _buildCancelButton(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
debugPrint("[LOG] L'utilisateur a annulé la déconnexion.");
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Annuler',
|
||||
style: TextStyle(color: AppColors.accentColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le bouton pour confirmer la déconnexion, avec log et réinitialisation des données utilisateur.
|
||||
Widget _buildConfirmButton(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
debugPrint("[LOG] L'utilisateur a confirmé la déconnexion.");
|
||||
|
||||
// Réinitialisation des informations de l'utilisateur
|
||||
Provider.of<UserProvider>(context, listen: false).resetUser();
|
||||
debugPrint("[LOG] Informations utilisateur réinitialisées dans UserProvider.");
|
||||
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushReplacementNamed('/'); // Redirection vers l'écran de connexion
|
||||
debugPrint("[LOG] Redirection vers l'écran de connexion.");
|
||||
},
|
||||
child: const Text(
|
||||
'Déconnexion',
|
||||
style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
|
||||
/// [StatTile] affiche une statistique utilisateur avec une icône, un label et une valeur.
|
||||
/// Ce composant inclut des animations et une traçabilité des interactions.
|
||||
class StatTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
@@ -15,17 +17,40 @@ class StatTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: AppColors.accentColor),
|
||||
title: Text(label, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
debugPrint("[LOG] Initialisation de StatTile pour la statistique : $label");
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
tween: Tween<double>(begin: 0.9, end: 1.0),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (context, scale, child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: AppColors.accentColor,
|
||||
size: 28,
|
||||
),
|
||||
title: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
trailing: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import '../../../../core/constants/colors.dart';
|
||||
import '../../../../domain/entities/user.dart';
|
||||
import 'stat_tile.dart';
|
||||
|
||||
/// [StatisticsSectionCard] affiche les statistiques principales de l'utilisateur avec des animations.
|
||||
/// Ce composant est optimisé pour une expérience interactive et une traçabilité complète des actions via les logs.
|
||||
class StatisticsSectionCard extends StatelessWidget {
|
||||
final User user;
|
||||
|
||||
@@ -10,10 +12,13 @@ class StatisticsSectionCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de StatisticsSectionCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
color: AppColors.cardColor.withOpacity(0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -28,13 +33,73 @@ class StatisticsSectionCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
StatTile(icon: Icons.event, label: 'Événements Participés', value: '${user.eventsCount}'),
|
||||
StatTile(icon: Icons.place, label: 'Établissements Visités', value: '${user.visitedPlacesCount}'),
|
||||
StatTile(icon: Icons.post_add, label: 'Publications', value: '${user.postsCount}'),
|
||||
StatTile(icon: Icons.group, label: 'Amis/Followers', value: '${user.friendsCount}'),
|
||||
// Liste des statistiques avec animations
|
||||
_buildAnimatedStatTile(
|
||||
icon: Icons.event,
|
||||
label: 'Événements Participés',
|
||||
value: '${user.eventsCount}',
|
||||
logMessage: "Affichage des événements participés : ${user.eventsCount}",
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildAnimatedStatTile(
|
||||
icon: Icons.place,
|
||||
label: 'Établissements Visités',
|
||||
value: '${user.visitedPlacesCount}',
|
||||
logMessage: "Affichage des établissements visités : ${user.visitedPlacesCount}",
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildAnimatedStatTile(
|
||||
icon: Icons.post_add,
|
||||
label: 'Publications',
|
||||
value: '${user.postsCount}',
|
||||
logMessage: "Affichage des publications : ${user.postsCount}",
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildAnimatedStatTile(
|
||||
icon: Icons.group,
|
||||
label: 'Amis/Followers',
|
||||
value: '${user.friendsCount}',
|
||||
logMessage: "Affichage des amis/followers : ${user.friendsCount}",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit chaque `StatTile` avec une animation de transition en fondu et logue chaque statistique.
|
||||
Widget _buildAnimatedStatTile({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required String logMessage,
|
||||
}) {
|
||||
debugPrint("[LOG] $logMessage");
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
curve: Curves.easeOut,
|
||||
builder: (context, opacity, child) {
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: StatTile(
|
||||
icon: icon,
|
||||
label: label,
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un séparateur visuel entre chaque statistique.
|
||||
Widget _buildDivider() {
|
||||
return Divider(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
height: 1,
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
import 'custom_list_tile.dart';
|
||||
|
||||
/// [SupportSectionCard] affiche les options de support et assistance.
|
||||
/// Inclut des animations, du retour haptique, et des logs détaillés pour chaque action.
|
||||
class SupportSectionCard extends StatelessWidget {
|
||||
const SupportSectionCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de SupportSectionCard.");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
color: AppColors.cardColor.withOpacity(0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 6,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Support et Assistance',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomListTile(
|
||||
icon: Icons.help,
|
||||
label: 'Support et Assistance',
|
||||
onTap: () => print("[LOG] Accès au Support et Assistance."),
|
||||
),
|
||||
CustomListTile(
|
||||
icon: Icons.article,
|
||||
label: 'Conditions d\'utilisation',
|
||||
onTap: () => print("[LOG] Accès aux conditions d'utilisation."),
|
||||
),
|
||||
CustomListTile(
|
||||
icon: Icons.privacy_tip,
|
||||
label: 'Politique de confidentialité',
|
||||
onTap: () => print("[LOG] Accès à la politique de confidentialité."),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.help_outline,
|
||||
label: 'Support et Assistance',
|
||||
logMessage: "Accès au Support et Assistance.",
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.article_outlined,
|
||||
label: 'Conditions d\'utilisation',
|
||||
logMessage: "Accès aux conditions d'utilisation.",
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildOption(
|
||||
context,
|
||||
icon: Icons.privacy_tip_outlined,
|
||||
label: 'Politique de confidentialité',
|
||||
logMessage: "Accès à la politique de confidentialité.",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit chaque option de support avec une animation de feedback visuel.
|
||||
Widget _buildOption(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String logMessage,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact(); // Retour haptique léger
|
||||
debugPrint("[LOG] $logMessage");
|
||||
// Ajout de la navigation ou de l'action ici.
|
||||
},
|
||||
splashColor: AppColors.accentColor.withOpacity(0.3),
|
||||
highlightColor: AppColors.cardColor.withOpacity(0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.accentColor, size: 28),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: Colors.white70),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un séparateur entre les options pour une meilleure structure visuelle.
|
||||
Widget _buildDivider() {
|
||||
return Divider(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
height: 1,
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/constants/colors.dart';
|
||||
import '../../../../domain/entities/user.dart';
|
||||
import '../../data/providers/user_provider.dart';
|
||||
|
||||
/// [UserInfoCard] affiche les informations essentielles de l'utilisateur de manière concise.
|
||||
/// Conçu pour minimiser les répétitions tout en garantissant une expérience utilisateur fluide.
|
||||
class UserInfoCard extends StatelessWidget {
|
||||
final User user;
|
||||
|
||||
@@ -9,38 +13,64 @@ class UserInfoCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("[LOG] Initialisation de UserInfoCard pour l'utilisateur : ${user.userFirstName} ${user.userLastName}");
|
||||
|
||||
return Card(
|
||||
color: AppColors.cardColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 2,
|
||||
color: AppColors.cardColor.withOpacity(0.9),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 5,
|
||||
shadowColor: AppColors.darkPrimary.withOpacity(0.4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundImage: NetworkImage(user.profileImageUrl),
|
||||
backgroundColor: Colors.transparent,
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
curve: Curves.elasticOut,
|
||||
builder: (context, scale, child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundImage: NetworkImage(user.profileImageUrl),
|
||||
backgroundColor: Colors.transparent,
|
||||
onBackgroundImageError: (error, stackTrace) {
|
||||
debugPrint("[ERROR] Erreur de chargement de l'image de profil : $error");
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(Icons.person, size: 50, color: Colors.grey.shade300),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'${user.userFirstName} ${user.userLastName}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: AppColors.accentColor,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
user.email,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
decoration: TextDecoration.underline,
|
||||
if (!context.select((UserProvider provider) => provider.isEmailDisplayedElsewhere)) // Afficher seulement si non affiché ailleurs
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint("[LOG] Clic sur l'email de l'utilisateur : ${user.email}");
|
||||
},
|
||||
child: Text(
|
||||
user.email,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade300,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColors.accentColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user