import 'dart:async'; import 'dart:math'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:state_notifier/state_notifier.dart'; import 'page.dart'; import 'explorer_navigator.dart'; import 'selection_mode.dart'; class ExplorerState { final String? folderId; final Resource? folder; final List resources; final Set selectedIds; final int focusIndex; final bool showFocus; final int selectionStartIndex; final bool dragging; final bool refreshing; final bool refreshError; Resource? get focussed => focusIndex < 0 || focusIndex >= resources.length ? null : resources[focusIndex]; String? get focusId => focussed?.id; Iterable get selected => resources.where((r) => selectedIds.contains(r.id)); Resource? get selectedSingle => selectedIds.length != 1 ? null : resources.where((r) => selectedIds.contains(r.id)).first; Resource? get focussedIfSelected => selectedIds.isEmpty ? null : focussed; const ExplorerState({ required this.folderId, this.folder, this.resources = const [], this.selectedIds = const {}, this.focusIndex = -1, this.showFocus = false, this.selectionStartIndex = -1, this.dragging = false, this.refreshing = false, this.refreshError = false, }); ExplorerState copyWith({ Resource? folder, List? resources, Set? selectedIds, int? focusIndex, bool? showFocus, int? selectionStartIndex, bool? dragging, bool? refreshing, bool? refreshError, }) => ExplorerState( folderId: folderId, folder: folder ?? this.folder, resources: resources ?? this.resources, selectedIds: selectedIds ?? this.selectedIds, focusIndex: focusIndex ?? this.focusIndex, showFocus: showFocus ?? this.showFocus, selectionStartIndex: selectionStartIndex ?? this.selectionStartIndex, dragging: dragging ?? this.dragging, refreshing: refreshing ?? this.refreshing, refreshError: refreshError ?? this.refreshError, ); bool isSelected(String id) { return selectedIds.contains(id); } } class ExplorerController extends StateNotifier { final PhylumAccount _account; final ExplorerNavigator _navHistoryManager; late void Function() _removeHistoryManagerListener; StreamSubscription? _folderSubscription; StreamSubscription>? _childrenSubscription; ExplorerController({ required PhylumAccount account, required ExplorerNavigator navHistoryManager, }) : _account = account, _navHistoryManager = navHistoryManager, super(ExplorerState(folderId: null)) { _removeHistoryManagerListener = navHistoryManager.addListener((state) { updatePage(state.current); }, fireImmediately: true); } @override void dispose() { _removeHistoryManagerListener.call(); _folderSubscription?.cancel(); _childrenSubscription?.cancel(); super.dispose(); } void updatePage(ExplorerPage page) { final folderId = page is ExplorerPageFolder ? (page).folderId : null; state = ExplorerState(folderId: folderId); _folderSubscription?.cancel(); _childrenSubscription?.cancel(); _folderSubscription = page.watchResource(_account).listen((e) => state = state.copyWith(folder: e)); _childrenSubscription = page.watchChildren(_account).listen((e) => _updateResourceList(e)); refresh(); } void _updateResourceList(List resources) { final extras = Set.of(state.selectedIds); extras.removeAll(state.resources.map((r) => r.id)); final selectedIds = Set.of(state.selectedIds); selectedIds.removeAll(extras); // selection. final focusIndex = resources.indexWhere((r) => r.id == state.focusId); state = state.copyWith( resources: resources, focusIndex: focusIndex, selectedIds: selectedIds, ); } void setDragging(bool dragging) { state = state.copyWith(dragging: dragging); } void updateSelection(int Function(int)? indexFn, SelectionMode mode, bool highlight) { Set? selectedIds; int index = indexFn?.call(state.focusIndex) ?? state.focusIndex; index = min(state.resources.length - 1, max(0, index)); int selectionStartIndex = state.selectionStartIndex; switch (mode) { case SelectionMode.range: if (selectionStartIndex == -1) { selectionStartIndex = index; } selectedIds = Set.of(state.resources.getRange(min(selectionStartIndex, index), max(selectionStartIndex, index) + 1).map((r) => r.id)); break; case SelectionMode.single: selectedIds = {state.resources[index].id}; selectionStartIndex = index; break; case SelectionMode.toggle: selectedIds = Set.of(state.selectedIds); if (!selectedIds.add(state.resources[index].id)) { selectedIds.remove(state.resources[index].id); } selectionStartIndex = index; break; case SelectionMode.add: selectedIds = Set.of(state.selectedIds); selectedIds.add(state.resources[index].id); selectionStartIndex = index; break; case SelectionMode.none: selectedIds = const {}; selectionStartIndex = index; break; case SelectionMode.all: selectedIds = Set.of(state.resources.map((r) => r.id)); case SelectionMode.noChange: break; } state = state.copyWith( selectedIds: selectedIds, selectionStartIndex: selectionStartIndex, focusIndex: index, showFocus: highlight, ); } Future refresh() { if (state.refreshing) return Future.value(state); state = state.copyWith(refreshing: true); return _navHistoryManager.current.refresh(_account).then((success) { if (mounted) { state = state.copyWith( refreshing: false, refreshError: !success, ); } }); } }