import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'data_grid_config.dart'; import 'data_grid_source.dart'; import 'data_grid_state.dart'; /// Controller for a DataGrid instance. /// /// Manages loading, searching, sorting, and selection state. class DataGridController extends StateNotifier> { DataGridController({ required this.source, required this.config, required this.idSelector, }) : super(DataGridState( sortColumnIndex: config.defaultSortColumn, sortDescending: config.defaultSortDescending, )) { // Initial load _load(); } /// Data source for fetching items. final DataGridSource source; /// Grid configuration. final DataGridConfig config; /// Function to extract unique ID from an item. final Object Function(T item) idSelector; /// Debounce timer for search. Timer? _searchDebounce; @override void dispose() { _searchDebounce?.cancel(); super.dispose(); } /// Loads or reloads data from the source. Future _load({bool refresh = false}) async { if (refresh) { state = state.copyWith(isLoading: true, error: null); } else { state = state.copyWith(isLoading: true, isInitialLoad: true, error: null); } try { final sortField = state.sortColumnIndex != null ? config.columns[state.sortColumnIndex!].effectiveSortField : null; final (offset, limit) = _getPaginationParams(); final result = await source.fetch( searchQuery: state.searchQuery.isEmpty ? null : state.searchQuery, sortField: sortField, sortDescending: state.sortDescending, offset: offset, limit: limit, ); state = state.copyWith( items: result.items, totalCount: result.totalCount, hasMore: result.hasMore, isLoading: false, isInitialLoad: false, error: null, ); } catch (e, stack) { debugPrint('DataGrid load error: $e\n$stack'); state = state.copyWith( isLoading: false, isInitialLoad: false, error: e, ); } } /// Gets pagination parameters based on data mode. (int?, int?) _getPaginationParams() { return switch (config.dataMode) { AllDataMode() => (null, null), PaginatedDataMode(:final pageSize) => ( state.currentPage * pageSize, pageSize, ), InfiniteDataMode(:final initialLoad) => (0, initialLoad), }; } /// Refreshes the grid data. Future refresh() => _load(refresh: true); /// Sets the search query with debouncing. void search(String query) { _searchDebounce?.cancel(); _searchDebounce = Timer(const Duration(milliseconds: 300), () { if (state.searchQuery != query) { state = state.copyWith(searchQuery: query, currentPage: 0); _load(refresh: true); } }); } /// Clears the search query. void clearSearch() { _searchDebounce?.cancel(); if (state.searchQuery.isNotEmpty) { state = state.copyWith(searchQuery: '', currentPage: 0); _load(refresh: true); } } /// Sorts by the given column index. void sortBy(int columnIndex) { final column = config.columns[columnIndex]; if (!column.sortable) return; final newDescending = state.sortColumnIndex == columnIndex ? !state.sortDescending : false; state = state.copyWith( sortColumnIndex: columnIndex, sortDescending: newDescending, currentPage: 0, ); _load(refresh: true); } /// Clears sorting. void clearSort() { state = state.copyWith( sortColumnIndex: null, sortDescending: false, currentPage: 0, ); _load(refresh: true); } /// Toggles selection of an item. void toggleSelection(T item) { if (!config.rowsSelectable) return; final id = idSelector(item); final newSelection = Set.from(state.selectedIds); if (newSelection.contains(id)) { newSelection.remove(id); } else { newSelection.add(id); } state = state.copyWith(selectedIds: newSelection); } /// Selects all visible items. void selectAll() { if (!config.rowsSelectable) return; final allIds = state.items.map(idSelector).toSet(); state = state.copyWith(selectedIds: allIds); } /// Clears all selections. void clearSelection() { state = state.copyWith(selectedIds: {}); } /// Toggles select all / clear all. void toggleSelectAll() { if (state.allSelected) { clearSelection(); } else { selectAll(); } } /// Checks if an item is selected. bool isSelected(T item) => state.selectedIds.contains(idSelector(item)); /// Gets the selected items. List getSelectedItems() { return state.items.where((item) => isSelected(item)).toList(); } /// Goes to a specific page (for paginated mode). void goToPage(int page) { if (config.dataMode is! PaginatedDataMode) return; final mode = config.dataMode as PaginatedDataMode; final maxPage = (state.totalCount / mode.pageSize).ceil() - 1; if (page < 0 || page > maxPage) return; state = state.copyWith(currentPage: page); _load(refresh: true); } /// Goes to the next page. void nextPage() => goToPage(state.currentPage + 1); /// Goes to the previous page. void previousPage() => goToPage(state.currentPage - 1); /// Loads more items (for infinite scroll mode). Future loadMore() async { if (config.dataMode is! InfiniteDataMode) return; if (state.isLoading || !state.hasMore) return; state = state.copyWith(isLoading: true); try { final sortField = state.sortColumnIndex != null ? config.columns[state.sortColumnIndex!].effectiveSortField : null; final result = await source.fetch( searchQuery: state.searchQuery.isEmpty ? null : state.searchQuery, sortField: sortField, sortDescending: state.sortDescending, offset: state.items.length, limit: (config.dataMode as InfiniteDataMode).initialLoad, ); state = state.copyWith( items: [...state.items, ...result.items], totalCount: result.totalCount, hasMore: result.hasMore, isLoading: false, ); } catch (e) { state = state.copyWith(isLoading: false, error: e); } } } /// Creates a DataGridController provider for a specific grid. /// /// Usage: /// ```dart /// final containersGridProvider = dataGridProvider( /// source: containersDataSource, /// config: containersGridConfig, /// idSelector: (c) => c.id, /// ); /// ``` StateNotifierProvider, DataGridState> dataGridProvider({ required DataGridSource source, required DataGridConfig config, required Object Function(T) idSelector, }) { return StateNotifierProvider, DataGridState>( (ref) => DataGridController( source: source, config: config, idSelector: idSelector, ), ); }