226 lines
6.6 KiB
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();
|
|
}
|
|
}
|