Restructuration majeure du projet: migration de flutt vers app, ajout de l'API et mise à jour du site web

This commit is contained in:
d6soft
2025-05-16 09:19:03 +02:00
parent b5aafc424b
commit 5c2620de30
391 changed files with 19780 additions and 7233 deletions

View 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();
}
}

View 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();
}
}

View 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),
);
},
);
}
}

View 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),
],
),
],
),
),
),
],
),
);
}
}

View 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,
),
);
}
}
}
}