Restructuration majeure du projet: migration de flutt vers app, ajout de l'API et mise à jour du site web
This commit is contained in:
98
app/lib/chat/widgets/chat_input.dart
Normal file
98
app/lib/chat/widgets/chat_input.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Zone de saisie de message
|
||||
///
|
||||
/// Ce widget permet à l'utilisateur de saisir et envoyer des messages
|
||||
|
||||
class ChatInput extends StatefulWidget {
|
||||
final Function(String) onSendText;
|
||||
final Function(dynamic)? onSendFile;
|
||||
final Function(dynamic)? onSendImage;
|
||||
final bool enableAttachments;
|
||||
final bool enabled;
|
||||
final String hintText;
|
||||
final String? disabledMessage;
|
||||
final int? maxLength;
|
||||
|
||||
const ChatInput({
|
||||
super.key,
|
||||
required this.onSendText,
|
||||
this.onSendFile,
|
||||
this.onSendImage,
|
||||
this.enableAttachments = true,
|
||||
this.enabled = true,
|
||||
this.hintText = 'Saisissez votre message...',
|
||||
this.disabledMessage = 'Vous ne pouvez pas répondre à cette annonce',
|
||||
this.maxLength,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatInput> createState() => _ChatInputState();
|
||||
}
|
||||
|
||||
class _ChatInputState extends State<ChatInput> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.enabled) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: Colors.grey.shade200,
|
||||
child: Text(
|
||||
widget.disabledMessage ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.enableAttachments)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.attach_file),
|
||||
onPressed: () {
|
||||
// TODO: Gérer les pièces jointes
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
maxLength: widget.maxLength,
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
if (_textController.text.trim().isNotEmpty) {
|
||||
widget.onSendText(_textController.text.trim());
|
||||
_textController.clear();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
80
app/lib/chat/widgets/chat_screen.dart
Normal file
80
app/lib/chat/widgets/chat_screen.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Écran principal d'une conversation
|
||||
///
|
||||
/// Ce widget affiche une conversation complète avec :
|
||||
/// - Liste des messages
|
||||
/// - Zone de saisie
|
||||
/// - En-tête et pied de page personnalisables
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final String conversationId;
|
||||
final String? title;
|
||||
final Widget? header;
|
||||
final Widget? footer;
|
||||
final bool enableAttachments;
|
||||
final bool showTypingIndicator;
|
||||
final bool enableReadReceipts;
|
||||
final bool isAnnouncement;
|
||||
final bool canReply;
|
||||
|
||||
const ChatScreen({
|
||||
super.key,
|
||||
required this.conversationId,
|
||||
this.title,
|
||||
this.header,
|
||||
this.footer,
|
||||
this.enableAttachments = true,
|
||||
this.showTypingIndicator = true,
|
||||
this.enableReadReceipts = true,
|
||||
this.isAnnouncement = false,
|
||||
this.canReply = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// TODO: Initialiser les données du chat
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.title ?? 'Chat'),
|
||||
// TODO: Ajouter les actions de l'AppBar
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (widget.header != null) widget.header!,
|
||||
Expanded(
|
||||
child: Container(
|
||||
// TODO: Implémenter la liste des messages
|
||||
child: const Center(child: Text('Messages à venir...')),
|
||||
),
|
||||
),
|
||||
if (widget.footer != null) widget.footer!,
|
||||
if (widget.canReply)
|
||||
Container(
|
||||
// TODO: Implémenter la zone de saisie
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text('Zone de saisie à venir...'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: Libérer les ressources
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
78
app/lib/chat/widgets/conversations_list.dart
Normal file
78
app/lib/chat/widgets/conversations_list.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Liste des conversations
|
||||
///
|
||||
/// Ce widget affiche la liste des conversations de l'utilisateur
|
||||
/// avec leurs derniers messages et statuts
|
||||
|
||||
class ConversationsList extends StatefulWidget {
|
||||
final List<dynamic>? conversations;
|
||||
final bool loadFromHive;
|
||||
final Function(dynamic)? onConversationSelected;
|
||||
final bool showLastMessage;
|
||||
final bool showUnreadCount;
|
||||
final bool showAnnouncementBadge;
|
||||
final bool showPinnedFirst;
|
||||
final Widget? emptyStateWidget;
|
||||
|
||||
const ConversationsList({
|
||||
super.key,
|
||||
this.conversations,
|
||||
this.loadFromHive = true,
|
||||
this.onConversationSelected,
|
||||
this.showLastMessage = true,
|
||||
this.showUnreadCount = true,
|
||||
this.showAnnouncementBadge = true,
|
||||
this.showPinnedFirst = true,
|
||||
this.emptyStateWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ConversationsList> createState() => _ConversationsListState();
|
||||
}
|
||||
|
||||
class _ConversationsListState extends State<ConversationsList> {
|
||||
late List<dynamic> _conversations;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadConversations();
|
||||
}
|
||||
|
||||
Future<void> _loadConversations() async {
|
||||
if (widget.loadFromHive) {
|
||||
// TODO: Charger depuis Hive
|
||||
} else {
|
||||
_conversations = widget.conversations ?? [];
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_conversations.isEmpty) {
|
||||
return widget.emptyStateWidget ?? const Center(child: Text('Aucune conversation'));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _conversations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final conversation = _conversations[index];
|
||||
// TODO: Créer le widget de conversation
|
||||
return ListTile(
|
||||
title: Text('Conversation ${index + 1}'),
|
||||
subtitle: const Text('Derniers messages...'),
|
||||
onTap: () => widget.onConversationSelected?.call(conversation),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
69
app/lib/chat/widgets/message_bubble.dart
Normal file
69
app/lib/chat/widgets/message_bubble.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Bulle de message
|
||||
///
|
||||
/// Ce widget affiche un message dans une conversation
|
||||
/// avec les informations associées
|
||||
|
||||
class MessageBubble extends StatelessWidget {
|
||||
final dynamic message; // TODO: Remplacer par MessageModel
|
||||
final bool showSenderInfo;
|
||||
final bool showTimestamp;
|
||||
final bool showStatus;
|
||||
final bool isAnnouncement;
|
||||
final double maxWidth;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.showSenderInfo = true,
|
||||
this.showTimestamp = true,
|
||||
this.showStatus = true,
|
||||
this.isAnnouncement = false,
|
||||
this.maxWidth = 300,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showSenderInfo) CircleAvatar(child: Text('S')),
|
||||
Expanded(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isAnnouncement ? Colors.orange.shade100 : Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showSenderInfo)
|
||||
Text(
|
||||
'Expéditeur',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text('Contenu du message...'),
|
||||
if (showTimestamp || showStatus)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (showTimestamp) Text('12:34', style: TextStyle(fontSize: 12)),
|
||||
if (showStatus) const SizedBox(width: 4),
|
||||
if (showStatus) Icon(Icons.check, size: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
159
app/lib/chat/widgets/notification_settings_widget.dart
Normal file
159
app/lib/chat/widgets/notification_settings_widget.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/notification_settings.dart';
|
||||
|
||||
/// Widget pour les paramètres de notification
|
||||
///
|
||||
/// Permet à l'utilisateur de configurer ses préférences de notification
|
||||
|
||||
class NotificationSettingsWidget extends StatelessWidget {
|
||||
final NotificationSettings settings;
|
||||
final Function(NotificationSettings) onSettingsChanged;
|
||||
|
||||
const NotificationSettingsWidget({
|
||||
super.key,
|
||||
required this.settings,
|
||||
required this.onSettingsChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
// Notifications générales
|
||||
SwitchListTile(
|
||||
title: const Text('Activer les notifications'),
|
||||
subtitle: const Text('Recevoir des notifications pour les nouveaux messages'),
|
||||
value: settings.enableNotifications,
|
||||
onChanged: (value) {
|
||||
onSettingsChanged(settings.copyWith(enableNotifications: value));
|
||||
},
|
||||
),
|
||||
|
||||
if (settings.enableNotifications) ...[
|
||||
// Sons et vibrations
|
||||
SwitchListTile(
|
||||
title: const Text('Sons'),
|
||||
subtitle: const Text('Jouer un son à la réception'),
|
||||
value: settings.soundEnabled,
|
||||
onChanged: (value) {
|
||||
onSettingsChanged(settings.copyWith(soundEnabled: value));
|
||||
},
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Vibration'),
|
||||
subtitle: const Text('Vibrer à la réception'),
|
||||
value: settings.vibrationEnabled,
|
||||
onChanged: (value) {
|
||||
onSettingsChanged(settings.copyWith(vibrationEnabled: value));
|
||||
},
|
||||
),
|
||||
|
||||
// Aperçu des messages
|
||||
SwitchListTile(
|
||||
title: const Text('Aperçu du message'),
|
||||
subtitle: const Text('Afficher le contenu dans la notification'),
|
||||
value: settings.showPreview,
|
||||
onChanged: (value) {
|
||||
onSettingsChanged(settings.copyWith(showPreview: value));
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// Mode Ne pas déranger
|
||||
SwitchListTile(
|
||||
title: const Text('Ne pas déranger'),
|
||||
subtitle: settings.doNotDisturb && settings.doNotDisturbStart != null
|
||||
? Text('Actif de ${_formatTime(settings.doNotDisturbStart!)} à ${_formatTime(settings.doNotDisturbEnd!)}')
|
||||
: null,
|
||||
value: settings.doNotDisturb,
|
||||
onChanged: (value) {
|
||||
if (value) {
|
||||
_showTimeRangePicker(context);
|
||||
} else {
|
||||
onSettingsChanged(settings.copyWith(doNotDisturb: false));
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
if (settings.doNotDisturb)
|
||||
ListTile(
|
||||
title: const Text('Horaires'),
|
||||
subtitle: Text('${_formatTime(settings.doNotDisturbStart!)} - ${_formatTime(settings.doNotDisturbEnd!)}'),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
onTap: () => _showTimeRangePicker(context),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// Conversations en silencieux
|
||||
if (settings.mutedConversations.isNotEmpty) ...[
|
||||
const ListTile(
|
||||
title: Text('Conversations en silencieux'),
|
||||
subtitle: Text('Ces conversations n\'enverront pas de notifications'),
|
||||
),
|
||||
...settings.mutedConversations.map(
|
||||
(conversationId) => ListTile(
|
||||
title: Text('Conversation $conversationId'), // TODO: Récupérer le vrai nom
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.volume_up),
|
||||
onPressed: () {
|
||||
final muted = List<String>.from(settings.mutedConversations);
|
||||
muted.remove(conversationId);
|
||||
onSettingsChanged(settings.copyWith(mutedConversations: muted));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Future<void> _showTimeRangePicker(BuildContext context) async {
|
||||
TimeOfDay? startTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: settings.doNotDisturbStart != null
|
||||
? TimeOfDay.fromDateTime(settings.doNotDisturbStart!)
|
||||
: const TimeOfDay(hour: 22, minute: 0),
|
||||
helpText: 'Heure de début',
|
||||
);
|
||||
|
||||
if (startTime != null) {
|
||||
final now = DateTime.now();
|
||||
final start = DateTime(now.year, now.month, now.day, startTime.hour, startTime.minute);
|
||||
|
||||
TimeOfDay? endTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: settings.doNotDisturbEnd != null
|
||||
? TimeOfDay.fromDateTime(settings.doNotDisturbEnd!)
|
||||
: const TimeOfDay(hour: 8, minute: 0),
|
||||
helpText: 'Heure de fin',
|
||||
);
|
||||
|
||||
if (endTime != null) {
|
||||
DateTime end = DateTime(now.year, now.month, now.day, endTime.hour, endTime.minute);
|
||||
|
||||
// Si l'heure de fin est avant l'heure de début, on considère qu'elle est le lendemain
|
||||
if (end.isBefore(start)) {
|
||||
end = end.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
onSettingsChanged(
|
||||
settings.copyWith(
|
||||
doNotDisturb: true,
|
||||
doNotDisturbStart: start,
|
||||
doNotDisturbEnd: end,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user