uto_manager/lib/application/blocs/unit_tree_cubit.dart

226 lines
6.6 KiB
Dart

import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/repositories/unit_repository.dart';
import 'package:uto_manager/domain/services/unit_tree_service.dart';
import 'package:uto_manager/infrastructure/repositories/drift_position_slot_repository.dart';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
/// State for the unit tree cubit.
sealed class UnitTreeState extends Equatable {
const UnitTreeState();
}
/// Initial state before any data is loaded.
class UnitTreeInitial extends UnitTreeState {
const UnitTreeInitial();
@override
List<Object?> get props => [];
}
/// State while the unit tree is loading.
class UnitTreeLoading extends UnitTreeState {
const UnitTreeLoading();
@override
List<Object?> get props => [];
}
/// State when the unit tree has been loaded successfully.
class UnitTreeLoaded extends UnitTreeState {
const UnitTreeLoaded({
required this.units,
required this.slots,
required this.roots,
this.selectedUnitId,
});
/// Flat list of all unit nodes.
final List<UnitNode> units;
/// Flat list of all position slots.
final List<PositionSlot> slots;
/// Root-level [UnitTreeNode]s (built from units + slots).
final List<UnitTreeNode> roots;
/// The currently selected unit ID, or null if none selected.
final String? selectedUnitId;
/// Returns the selected [UnitNode], or null.
UnitNode? get selectedUnit =>
selectedUnitId == null ? null : units.firstWhere((u) => u.unitId == selectedUnitId);
/// Returns slots for the selected unit.
List<PositionSlot> get selectedUnitSlots =>
selectedUnitId == null ? [] : slots.where((s) => s.unitId == selectedUnitId).toList();
UnitTreeLoaded copyWith({
List<UnitNode>? units,
List<PositionSlot>? slots,
List<UnitTreeNode>? roots,
String? selectedUnitId,
bool clearSelection = false,
}) {
return UnitTreeLoaded(
units: units ?? this.units,
slots: slots ?? this.slots,
roots: roots ?? this.roots,
selectedUnitId: clearSelection ? null : (selectedUnitId ?? this.selectedUnitId),
);
}
@override
List<Object?> get props => [units, slots, roots, selectedUnitId];
}
/// State when an error occurred loading the unit tree.
class UnitTreeError extends UnitTreeState {
const UnitTreeError(this.message);
final String message;
@override
List<Object?> get props => [message];
}
// ---------------------------------------------------------------------------
// Cubit
// ---------------------------------------------------------------------------
/// Manages the unit tree state, including loading, selection, and re-parenting.
class UnitTreeCubit extends Cubit<UnitTreeState> {
UnitTreeCubit({
required this.unitRepository,
required this.slotRepository,
required this.treeService,
}) : super(const UnitTreeInitial());
final UnitRepository unitRepository;
final DriftPositionSlotRepository slotRepository;
final UnitTreeService treeService;
StreamSubscription<List<UnitNode>>? _unitSubscription;
StreamSubscription<List<PositionSlot>>? _slotSubscription;
List<UnitNode> _units = [];
List<PositionSlot> _slots = [];
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
/// Starts watching the database for unit and slot changes.
void startWatching() {
emit(const UnitTreeLoading());
_unitSubscription = unitRepository.watchAll().listen(
(units) {
_units = units;
_rebuild();
},
onError: (Object e) => emit(UnitTreeError(e.toString())),
);
_slotSubscription = slotRepository.watchAll().listen(
(slots) {
_slots = slots;
_rebuild();
},
onError: (Object e) => emit(UnitTreeError(e.toString())),
);
}
void _rebuild() {
final roots = treeService.buildTree(_units, _slots);
final currentSelection =
state is UnitTreeLoaded ? (state as UnitTreeLoaded).selectedUnitId : null;
emit(UnitTreeLoaded(
units: _units, slots: _slots, roots: roots, selectedUnitId: currentSelection,),);
}
// ---------------------------------------------------------------------------
// Selection
// ---------------------------------------------------------------------------
/// Selects the unit with [unitId].
void selectUnit(String unitId) {
if (state is UnitTreeLoaded) {
emit((state as UnitTreeLoaded).copyWith(selectedUnitId: unitId));
}
}
/// Clears the current unit selection.
void clearSelection() {
if (state is UnitTreeLoaded) {
emit((state as UnitTreeLoaded).copyWith(clearSelection: true));
}
}
// ---------------------------------------------------------------------------
// Mutations
// ---------------------------------------------------------------------------
/// Adds a new [unit] to the tree.
Future<void> addUnit(UnitNode unit) async {
try {
await unitRepository.insert(unit);
} catch (e) {
emit(UnitTreeError(e.toString()));
}
}
/// Re-parents [unitId] under [newParentId] after doctrinal validation.
Future<bool> reparentUnit(String unitId, String newParentId) async {
if (state is! UnitTreeLoaded) return false;
final loaded = state as UnitTreeLoaded;
final child = loaded.units.firstWhere((u) => u.unitId == unitId);
final newParent = loaded.units.firstWhere((u) => u.unitId == newParentId);
final validation = treeService.validateReparent(child, newParent, loaded.units);
if (!validation.isValid) return false;
try {
await unitRepository.reparent(unitId, newParentId);
return true;
} catch (e) {
emit(UnitTreeError(e.toString()));
return false;
}
}
/// Deletes [unitId] and all its descendants.
Future<void> deleteSubtree(String unitId) async {
try {
await unitRepository.deleteSubtree(unitId);
} catch (e) {
emit(UnitTreeError(e.toString()));
}
}
/// Clears all units and slots (template reset).
Future<void> clearAll() async {
try {
await slotRepository.deleteAll();
await unitRepository.deleteAll();
} catch (e) {
emit(UnitTreeError(e.toString()));
}
}
@override
Future<void> close() {
_unitSubscription?.cancel();
_slotSubscription?.cancel();
return super.close();
}
}