diff --git a/em2rp/lib/utils/debouncer.dart b/em2rp/lib/utils/debouncer.dart new file mode 100644 index 0000000..96211ba --- /dev/null +++ b/em2rp/lib/utils/debouncer.dart @@ -0,0 +1,21 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; + +/// Utilitaire pour différer l'exécution d'une action après un délai. +/// Utilisé principalement pour les champs de recherche afin d'éviter +/// des requêtes à chaque frappe clavier. +class Debouncer { + final Duration delay; + Timer? _timer; + + Debouncer({this.delay = const Duration(milliseconds: 400)}); + + void call(VoidCallback action) { + _timer?.cancel(); + _timer = Timer(delay, action); + } + + void dispose() { + _timer?.cancel(); + } +} diff --git a/em2rp/lib/views/container_form_page.dart b/em2rp/lib/views/container_form_page.dart index bd8096d..31c3261 100644 --- a/em2rp/lib/views/container_form_page.dart +++ b/em2rp/lib/views/container_form_page.dart @@ -9,6 +9,7 @@ import 'package:em2rp/utils/debug_log.dart'; import 'package:em2rp/utils/id_generator.dart'; import 'package:em2rp/services/data_service.dart'; import 'package:em2rp/services/api_service.dart'; +import 'package:em2rp/utils/debouncer.dart'; class ContainerFormPage extends StatefulWidget { final ContainerModel? container; @@ -658,6 +659,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { EquipmentCategory? _filterCategory; String _searchQuery = ''; late Set _tempSelectedIds; + final _searchDebouncer = Debouncer(); final List _paginatedEquipments = []; bool _isLoadingMore = false; @@ -677,6 +679,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { void dispose() { _searchController.dispose(); _scrollController.dispose(); + _searchDebouncer.dispose(); super.dispose(); } @@ -790,7 +793,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { setState(() { _searchQuery = value; }); - _reloadData(); + _searchDebouncer(_reloadData); }, ), const SizedBox(height: 16), diff --git a/em2rp/lib/views/equipment_management_page.dart b/em2rp/lib/views/equipment_management_page.dart index a1cb416..3890792 100644 --- a/em2rp/lib/views/equipment_management_page.dart +++ b/em2rp/lib/views/equipment_management_page.dart @@ -20,6 +20,7 @@ import 'package:em2rp/utils/equipment_delete_utils.dart'; import 'package:em2rp/mixins/selection_mode_mixin.dart'; import 'package:em2rp/views/widgets/common/search_actions_bar.dart'; import 'package:em2rp/views/widgets/notification_badge.dart'; +import 'package:em2rp/utils/debouncer.dart'; class EquipmentManagementPage extends StatefulWidget { const EquipmentManagementPage({super.key}); @@ -33,6 +34,7 @@ class _EquipmentManagementPageState extends State with SelectionModeMixin { final TextEditingController _searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); + final _searchDebouncer = Debouncer(); EquipmentCategory? _selectedCategory; List? _cachedEquipment; bool _isLoadingMore = false; // Flag pour éviter les appels multiples @@ -87,6 +89,7 @@ class _EquipmentManagementPageState extends State _scrollController.removeListener(_onScroll); _scrollController.dispose(); _searchController.dispose(); + _searchDebouncer.dispose(); // Désactiver le mode pagination en quittant context.read().disablePagination(); super.dispose(); @@ -169,9 +172,7 @@ class _EquipmentManagementPageState extends State SearchActionsBar( controller: _searchController, hintText: 'Rechercher par nom, modèle ou ID...', - onChanged: (value) { - context.read().setSearchQuery(value); - }, + onChanged: (value) => _searchDebouncer(() => context.read().setSearchQuery(value)), onClear: () { _searchController.clear(); context.read().setSearchQuery(''); @@ -342,9 +343,7 @@ class _EquipmentManagementPageState extends State SearchActionsBar( controller: _searchController, hintText: 'Rechercher par nom, modèle ou ID...', - onChanged: (value) { - context.read().setSearchQuery(value); - }, + onChanged: (value) => _searchDebouncer(() => context.read().setSearchQuery(value)), onClear: () { _searchController.clear(); context.read().setSearchQuery(''); @@ -501,9 +500,11 @@ class _EquipmentManagementPageState extends State return ListView.builder( controller: _scrollController, itemCount: itemCount, - // ✅ Ajouter une estimation de la hauteur pour améliorer le scroll - // Note : À ajuster selon la hauteur réelle de vos cartes - // itemExtent: 140, // Décommentez si toutes les cartes ont la même hauteur + // ✅ prototypeItem utilisé car les cartes ont des hauteurs variables : + // - Les équipements standards (ListTile + margin) font ~88px + // - Les consommables/câbles affichent _buildQuantityDisplay en plus (~30px) + // - prototypeItem permet à Flutter d'optimiser le scroll sans couper les items + prototypeItem: const SizedBox(height: 88), // ✅ Augmenter le cache pour un scroll plus fluide cacheExtent: 500, // Précharger 500px en plus itemBuilder: (context, index) { diff --git a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart index 9180687..91f917f 100644 --- a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart +++ b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart @@ -1,4 +1,4 @@ -import 'package:em2rp/utils/debug_log.dart'; +import 'package:em2rp/utils/debug_log.dart'; import 'package:flutter/material.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; @@ -7,6 +7,7 @@ import 'package:em2rp/services/event_availability_service.dart'; import 'package:em2rp/services/data_service.dart'; import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/utils/colors.dart'; +import 'package:em2rp/utils/debouncer.dart'; /// Type de s├®lection dans le dialog enum SelectionType { equipment, container } @@ -108,6 +109,7 @@ class _EquipmentSelectionDialogState extends State { bool _isLoadingConflicts = false; String _searchQuery = ''; + final _searchDebouncer = Debouncer(); // Nouvelles options d'affichage bool _showConflictingItems = false; // Afficher les ├®quipements/bo├«tes en conflit @@ -435,6 +437,7 @@ class _EquipmentSelectionDialogState extends State { _searchController.dispose(); _scrollController.dispose(); // Nettoyer le ScrollController _selectionChangeNotifier.dispose(); // Nettoyer le ValueNotifier + _searchDebouncer.dispose(); super.dispose(); } @@ -1209,7 +1212,7 @@ class _EquipmentSelectionDialogState extends State { onChanged: (value) { setState(() => _searchQuery = value.toLowerCase()); // Recharger depuis le d├®but avec le nouveau filtre - _reloadData(); + _searchDebouncer(_reloadData); }, ), @@ -1379,12 +1382,12 @@ class _EquipmentSelectionDialogState extends State { // Containers final filteredContainers = _paginatedContainers.where((container) { if (!_showConflictingItems) { - // V├®rifier si le container lui-m├¬me est en conflit + // Vérifier si le container lui-même est en conflit if (_conflictingContainerIds.contains(container.id)) { return false; } - // V├®rifier si le container a des ├®quipements enfants en conflit + // Vérifier si le container a des équipements enfants en conflit final hasConflictingChildren = container.equipmentIds.any( (eqId) => _conflictingEquipmentIds.contains(eqId), ); @@ -1412,60 +1415,73 @@ class _EquipmentSelectionDialogState extends State { } return false; }, - child: ListView( - controller: _scrollController, - padding: const EdgeInsets.all(16), - children: [ - // Header - _buildSectionHeader( - _displayType == SelectionType.equipment ? '├ëquipements' : 'Containers', - _displayType == SelectionType.equipment ? Icons.inventory_2 : Icons.inventory, - itemWidgets.length, - ), - const SizedBox(height: 12), - - // Items - ...itemWidgets, - - // Indicateur de chargement en bas - if (_isLoadingMore) - const Padding( - padding: EdgeInsets.all(16), - child: Center( - child: CircularProgressIndicator(color: AppColors.rouge), + child: Builder( + builder: (context) { + // ✅ Construction de la liste complète avant le ListView.builder + // pour permettre l'utilisation de ListView.builder (lazy rendering) + // à la place de ListView(children:[]) qui construit tous les widgets + // en mémoire d'un coup. + // Pas d'itemExtent : les cards ont des hauteurs très variables + // (80px sans conflit, jusqu'à 300px+ avec détails de conflits). + final List allChildren = [ + // Header + _buildSectionHeader( + _displayType == SelectionType.equipment ? 'Équipements' : 'Containers', + _displayType == SelectionType.equipment ? Icons.inventory_2 : Icons.inventory, + itemWidgets.length, ), - ), + const SizedBox(height: 12), - // Message si fin de liste - if (!_isLoadingMore && !(_displayType == SelectionType.equipment ? _hasMoreEquipments : _hasMoreContainers)) - Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Text( - 'Fin de la liste', - style: TextStyle(color: Colors.grey.shade600, fontSize: 14), + // Items + ...itemWidgets, + + // Indicateur de chargement en bas + if (_isLoadingMore) + const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: CircularProgressIndicator(color: AppColors.rouge), + ), ), - ), - ), - // Message si rien trouv├® - if (itemWidgets.isEmpty && !_isLoadingMore) - Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey.shade400), - const SizedBox(height: 16), - Text( - 'Aucun r├®sultat trouv├®', - style: TextStyle(fontSize: 16, color: Colors.grey.shade600), + // Message si fin de liste + if (!_isLoadingMore && !(_displayType == SelectionType.equipment ? _hasMoreEquipments : _hasMoreContainers)) + Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text( + 'Fin de la liste', + style: TextStyle(color: Colors.grey.shade600, fontSize: 14), ), - ], + ), ), - ), - ), - ], + + // Message si rien trouvé + if (itemWidgets.isEmpty && !_isLoadingMore) + Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Icon(Icons.search_off, size: 64, color: Colors.grey.shade400), + const SizedBox(height: 16), + Text( + 'Aucun résultat trouvé', + style: TextStyle(fontSize: 16, color: Colors.grey.shade600), + ), + ], + ), + ), + ), + ]; + + return ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: allChildren.length, + itemBuilder: (context, index) => allChildren[index], + ); + }, ), ); },