import 'dart:async'; /// Shared helpers for case-insensitive, trimmed, multi-field table search. class TableSearch { TableSearch._(); /// Trims leading/trailing whitespace. Does not alter casing. static String normalize(String? query) => query?.trim() ?? ''; /// Trimmed + lowercased query for matching. static String normalizedQuery(String? query) => normalize(query).toLowerCase(); /// Returns true when [query] is empty or any [values] contain the query. static bool matches(String? query, Iterable values) { final q = normalizedQuery(query); if (q.isEmpty) return true; for (final value in values) { if (value == null) continue; final text = value.toString().trim().toLowerCase(); if (text.isEmpty) continue; if (text.contains(q)) return true; } return false; } /// Filters [items] to those where any extracted field matches [query]. static List filter( Iterable items, String? query, Iterable Function(T item) valuesOf, ) { final q = normalizedQuery(query); if (q.isEmpty) { return items is List ? items : items.toList(); } return items.where((item) => matches(q, valuesOf(item))).toList(); } } /// Debounces search input so API-backed lists are not hit on every keystroke. class SearchDebouncer { SearchDebouncer({this.duration = const Duration(milliseconds: 300)}); final Duration duration; Timer? _timer; /// Runs [action] after [duration]. Empty queries run immediately. void run(String query, void Function(String normalized) action) { final normalized = TableSearch.normalize(query); _timer?.cancel(); if (normalized.isEmpty) { action(normalized); return; } _timer = Timer(duration, () => action(normalized)); } void dispose() { _timer?.cancel(); _timer = null; } }