uto_manager/lib/application/blocs/template_cubit.dart

232 lines
7.0 KiB
Dart

import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uuid/uuid.dart';
import 'package:uto_manager/domain/entities/position_slot.dart' as domain;
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/repositories/unit_repository.dart';
import 'package:uto_manager/domain/services/doctrinal_templates.dart';
import 'package:uto_manager/domain/services/template_instantiator.dart';
import 'package:uto_manager/infrastructure/codecs/unit_tree_codec.dart';
import 'package:uto_manager/infrastructure/database/app_database.dart';
import 'package:uto_manager/infrastructure/database/daos/template_dao.dart';
import 'package:uto_manager/infrastructure/repositories/drift_position_slot_repository.dart';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
/// State for the template cubit.
sealed class TemplateState extends Equatable {
const TemplateState();
}
class TemplateInitial extends TemplateState {
const TemplateInitial();
@override
List<Object?> get props => [];
}
class TemplateLoading extends TemplateState {
const TemplateLoading();
@override
List<Object?> get props => [];
}
class TemplateLoaded extends TemplateState {
const TemplateLoaded(this.templates);
final List<Template> templates;
@override
List<Object?> get props => [templates];
}
class TemplateError extends TemplateState {
const TemplateError(this.message);
final String message;
@override
List<Object?> get props => [message];
}
// ---------------------------------------------------------------------------
// Cubit
// ---------------------------------------------------------------------------
/// Manages saved unit tree templates and doctrinal template instantiation.
class TemplateCubit extends Cubit<TemplateState> {
TemplateCubit({
required this.dao,
required this.unitRepository,
required this.slotRepository,
}) : super(const TemplateInitial());
final TemplateDao dao;
final UnitRepository unitRepository;
final DriftPositionSlotRepository slotRepository;
final _codec = const UnitTreeCodec();
final _instantiator = TemplateInstantiator(idGenerator: const Uuid().v4);
// ---------------------------------------------------------------------------
// Loading
// ---------------------------------------------------------------------------
/// Loads all saved templates from the database.
Future<void> loadTemplates() async {
emit(const TemplateLoading());
try {
final templates = await dao.getAllTemplates();
emit(TemplateLoaded(templates));
} catch (e) {
emit(TemplateError(e.toString()));
}
}
// ---------------------------------------------------------------------------
// Doctrinal template instantiation
// ---------------------------------------------------------------------------
/// Instantiates a doctrinal template by [key] (e.g., 'mbct', 'sbct') and
/// persists all generated units and slots to the database in a single batch.
///
/// Returns true on success, false on failure.
Future<bool> instantiateDoctrinalTemplate(String key) async {
final definition = DoctrinalTemplates.all[key];
if (definition == null) {
emit(TemplateError('Unknown template key: "$key"'));
return false;
}
try {
final result = _instantiator.instantiate(definition);
await unitRepository.insertAll(result.units);
await slotRepository.insertAll(result.slots);
return true;
} catch (e) {
emit(TemplateError('Failed to instantiate template "$key": $e'));
return false;
}
}
// ---------------------------------------------------------------------------
// Save / Delete
// ---------------------------------------------------------------------------
/// Saves a template with [name], [bctType], and [jsonData].
Future<void> saveTemplate({
required String templateId,
required String name,
required String bctType,
required Map<String, dynamic> jsonData,
}) async {
try {
final now = DateTime.now();
await dao.upsertTemplate(
TemplatesCompanion.insert(
templateId: templateId,
name: name,
bctType: bctType,
jsonData: jsonEncode(jsonData),
createdAt: now,
updatedAt: now,
),
);
await loadTemplates();
} catch (e) {
emit(TemplateError(e.toString()));
}
}
/// Saves the current live unit tree (from [units] and [slots]) as a named
/// template snapshot in the database.
Future<void> saveSnapshot({
required String name,
required String bctType,
required List<UnitNode> units,
required List<domain.PositionSlot> slots,
}) async {
try {
final jsonMap = _codec.encodeToMap(
units: units,
slots: slots,
personnel: const [],
);
await saveTemplate(
templateId: const Uuid().v4(),
name: name,
bctType: bctType,
jsonData: jsonMap,
);
} catch (e) {
emit(TemplateError(e.toString()));
}
}
/// Deletes the template with [templateId].
Future<void> deleteTemplate(String templateId) async {
try {
await dao.deleteTemplate(templateId);
await loadTemplates();
} catch (e) {
emit(TemplateError(e.toString()));
}
}
/// Returns the JSON data for [templateId], or null if not found.
Future<Map<String, dynamic>?> getTemplateJson(String templateId) async {
try {
final template = await dao.getTemplateById(templateId);
if (template == null) return null;
return jsonDecode(template.jsonData) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
// ---------------------------------------------------------------------------
// Export / Import
// ---------------------------------------------------------------------------
/// Encodes [units] and [slots] to a JSON string for export.
String exportToJson({
required List<UnitNode> units,
required List<domain.PositionSlot> slots,
}) {
return _codec.encode(units: units, slots: slots, personnel: const []);
}
/// Decodes a JSON export string and persists the units and slots to the
/// database. Clears existing data first if [replaceExisting] is true.
///
/// Returns true on success, false on failure.
Future<bool> importFromJson(
String json, {
bool replaceExisting = false,
}) async {
try {
final snapshot = _codec.decode(json);
if (replaceExisting) {
await slotRepository.deleteAll();
await unitRepository.deleteAll();
}
await unitRepository.insertAll(snapshot.units);
await slotRepository.insertAll(snapshot.slots);
return true;
} on FormatException catch (e) {
emit(TemplateError('Import failed — invalid format: ${e.message}'));
return false;
} catch (e) {
emit(TemplateError('Import failed: $e'));
return false;
}
}
}