Files
phylum/client/lib/ui/explorer/explorer_controller.dart
2025-07-01 11:46:26 +05:30

221 lines
6.8 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/db/resource_helpers.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/ui/app/router.dart';
import 'package:state_notifier/state_notifier.dart';
import '../app/routes.dart';
import 'selection_mode.dart';
class ExplorerState {
final ExplorerRoute route;
final String? folderId;
final Resource? folder;
final List<Resource> resources;
final Set<String> selectedIds;
final int focusIndex;
final bool showFocus;
final int selectionStartIndex;
final bool dragging;
final Future? refresh;
final bool refreshError;
Resource? get focussed => focusIndex < 0 || focusIndex >= resources.length ? null : resources[focusIndex];
String? get focusId => focussed?.id;
Iterable<Resource> 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.route,
required this.folderId,
this.folder,
this.resources = const [],
this.selectedIds = const {},
this.focusIndex = -1,
this.showFocus = false,
this.selectionStartIndex = -1,
this.dragging = false,
this.refresh,
this.refreshError = false,
});
ExplorerState copyWith({
Resource? folder,
List<Resource>? resources,
Set<String>? selectedIds,
int? focusIndex,
bool? showFocus,
int? selectionStartIndex,
bool? dragging,
Future? refresh,
bool updateRefresh = false,
bool? refreshError,
}) =>
ExplorerState(
route: route,
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,
refresh: updateRefresh ? refresh : this.refresh,
refreshError: refreshError ?? this.refreshError,
);
bool isSelected(String id) {
return selectedIds.contains(id);
}
}
class ExplorerController extends StateNotifier<ExplorerState> {
final PhylumAccount _account;
final PhylumRouterDelegate _routerDelegate;
StreamSubscription<Resource?>? _folderSubscription;
StreamSubscription<List<Resource>>? _childrenSubscription;
ExplorerController({
required PhylumAccount account,
required PhylumRouterDelegate routerDelegate,
}) : _account = account,
_routerDelegate = routerDelegate,
super(ExplorerState(
route: routerDelegate.currentConfiguration as ExplorerRoute,
folderId: (routerDelegate.currentConfiguration as ExplorerRoute).folderId(account),
)) {
_routerDelegate.addListener(updatePage);
updatePage();
}
void updatePage() {
final route = _routerDelegate.currentConfiguration as ExplorerRoute;
_folderSubscription?.cancel();
final folderId = route.folderId(_account);
if (folderId != null) {
_folderSubscription = _account.db.watchResource(folderId).listen((e) => state = state.copyWith(folder: e));
if (folderId != _account.user.home) {
_account.db.markResourceAccess(folderId);
}
} else {
_folderSubscription = null;
}
_childrenSubscription?.cancel();
_childrenSubscription = route.items(_account).listen((e) => _updateResourceList(e));
state = ExplorerState(route: route, folderId: folderId);
refresh();
}
@override
void dispose() {
_routerDelegate.removeListener(updatePage);
_folderSubscription?.cancel();
_childrenSubscription?.cancel();
super.dispose();
}
void _updateResourceList(List<Resource> 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);
}
bool clearSelection() {
if (state.selectedIds.isEmpty) return false;
state = state.copyWith(
selectedIds: const {},
selectionStartIndex: state.selectionStartIndex,
focusIndex: state.focusIndex,
showFocus: false,
);
return true;
}
int updateSelection(int Function(int)? indexFn, SelectionMode mode, bool highlight) {
Set<String>? 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,
);
return index;
}
Future refresh() {
if (state.refresh != null) return state.refresh!;
final c = Completer();
state = state.copyWith(refresh: c.future, updateRefresh: true);
state.route.refresh(_account).then((success) {
c.complete();
if (mounted) {
state = state.copyWith(
refresh: null,
updateRefresh: true,
refreshError: !success,
);
}
}).onError((err, stack) {
c.completeError(err!, stack);
});
return c.future;
}
}