import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../data/services/bridge_service.dart'; import '../../../domain/entities/wall_config.dart'; import 'wall_event.dart'; import 'wall_state.dart'; class WallBloc extends Bloc { final BridgeService _bridgeService; Timer? _editTimeoutTimer; /// Legacy cancel timeout: 5 seconds of inactivity cancels edit mode. static const _editTimeout = Duration(seconds: 5); WallBloc({required BridgeService bridgeService}) : _bridgeService = bridgeService, super(const WallState()) { on(_onLoadWallConfig); on(_onSelectViewer); on(_onDeselectViewer); on(_onSetCameraPrefix); on(_onAddCameraDigit); on(_onBackspaceCameraDigit); on(_onCancelCameraEdit); on(_onCycleCameraPrefix); on(_onExecuteCrossSwitch); on(_onUpdateViewerCamera); on(_onSetViewerAlarm); on(_onSetViewerLock); on(_onToggleSectionExpanded); } @override Future close() { _editTimeoutTimer?.cancel(); return super.close(); } void _restartEditTimeout() { _editTimeoutTimer?.cancel(); _editTimeoutTimer = Timer(_editTimeout, () { add(const CancelCameraEdit()); }); } void _cancelEditTimeout() { _editTimeoutTimer?.cancel(); } void _onLoadWallConfig(LoadWallConfig event, Emitter emit) { emit(state.copyWith(isLoading: true, clearError: true)); try { // Use provided config or load sample final config = event.config ?? WallConfig.sample(); // Initialize viewer states for all viewers final viewerStates = {}; for (final viewerId in config.allViewerIds) { viewerStates[viewerId] = ViewerState(viewerId: viewerId); } // Expand all sections by default final expandedSections = config.sections.map((s) => s.id).toSet(); emit(state.copyWith( config: config, isLoading: false, viewerStates: viewerStates, expandedSections: expandedSections, )); } catch (e) { emit(state.copyWith( isLoading: false, error: 'Failed to load wall config: $e', )); } } void _onSelectViewer(SelectViewer event, Emitter emit) { if (state.config == null) return; // Find the physical monitor containing this viewer final monitor = state.config!.findMonitorByViewerId(event.viewerId); _cancelEditTimeout(); emit(state.copyWith( selectedViewerId: event.viewerId, selectedPhysicalMonitorId: monitor?.id, cameraNumberInput: '', isEditing: false, )); } void _onDeselectViewer(DeselectViewer event, Emitter emit) { _cancelEditTimeout(); emit(state.copyWith( clearSelection: true, cameraNumberInput: '', isEditing: false, )); } void _onSetCameraPrefix(SetCameraPrefix event, Emitter emit) { if (event.prefix != 500 && event.prefix != 501 && event.prefix != 502) { return; } emit(state.copyWith(cameraPrefix: event.prefix)); } void _onAddCameraDigit(AddCameraDigit event, Emitter emit) { if (event.digit < 0 || event.digit > 9) return; if (state.selectedViewerId == null) return; if (state.cameraNumberInput.length >= 6) return; // Max 6 digits (legacy) _restartEditTimeout(); emit(state.copyWith( cameraNumberInput: state.cameraNumberInput + event.digit.toString(), isEditing: true, )); } void _onBackspaceCameraDigit( BackspaceCameraDigit event, Emitter emit) { if (state.cameraNumberInput.isEmpty) return; final newInput = state.cameraNumberInput .substring(0, state.cameraNumberInput.length - 1); if (newInput.isEmpty) { _cancelEditTimeout(); emit(state.copyWith(cameraNumberInput: '', isEditing: false)); } else { _restartEditTimeout(); emit(state.copyWith(cameraNumberInput: newInput)); } } void _onCancelCameraEdit(CancelCameraEdit event, Emitter emit) { _cancelEditTimeout(); emit(state.copyWith(cameraNumberInput: '', isEditing: false)); } void _onCycleCameraPrefix( CycleCameraPrefix event, Emitter emit) { const prefixes = [500, 501, 502]; final idx = prefixes.indexOf(state.cameraPrefix); final next = prefixes[(idx + 1) % prefixes.length]; emit(state.copyWith(cameraPrefix: next)); } Future _onExecuteCrossSwitch( ExecuteCrossSwitch event, Emitter emit) async { print('CrossSwitch: canExecute=${state.canExecuteCrossSwitch}, selectedViewer=${state.selectedViewerId}, cameraInput=${state.cameraNumberInput}, fullCamera=${state.fullCameraNumber}'); if (!state.canExecuteCrossSwitch) { print('CrossSwitch: Cannot execute - returning early'); return; } final viewerId = state.selectedViewerId!; final cameraId = state.fullCameraNumber!; try { print('CrossSwitch: Calling viewerConnectLive(viewer=$viewerId, camera=$cameraId)'); // Execute CrossSwitch via bridge service final result = await _bridgeService.viewerConnectLive(viewerId, cameraId); print('CrossSwitch: Result = $result'); // Update local state final viewerStates = Map.from(state.viewerStates); viewerStates[viewerId] = state.getViewerState(viewerId).copyWith( currentCameraId: cameraId, isLive: true, ); _cancelEditTimeout(); emit(state.copyWith( viewerStates: viewerStates, cameraNumberInput: '', isEditing: false, )); } catch (e) { emit(state.copyWith(error: 'CrossSwitch failed: $e')); } } void _onUpdateViewerCamera( UpdateViewerCamera event, Emitter emit) { final viewerStates = Map.from(state.viewerStates); viewerStates[event.viewerId] = (viewerStates[event.viewerId] ?? ViewerState(viewerId: event.viewerId)) .copyWith( currentCameraId: event.cameraId, isLive: event.isLive, ); emit(state.copyWith(viewerStates: viewerStates)); } void _onSetViewerAlarm(SetViewerAlarm event, Emitter emit) { final viewerStates = Map.from(state.viewerStates); viewerStates[event.viewerId] = (viewerStates[event.viewerId] ?? ViewerState(viewerId: event.viewerId)) .copyWith(hasAlarm: event.hasAlarm); emit(state.copyWith(viewerStates: viewerStates)); } void _onSetViewerLock(SetViewerLock event, Emitter emit) { final viewerStates = Map.from(state.viewerStates); viewerStates[event.viewerId] = (viewerStates[event.viewerId] ?? ViewerState(viewerId: event.viewerId)) .copyWith( isLocked: event.isLocked, lockedBy: event.lockedBy, ); emit(state.copyWith(viewerStates: viewerStates)); } void _onToggleSectionExpanded( ToggleSectionExpanded event, Emitter emit) { final expandedSections = Set.from(state.expandedSections); if (expandedSections.contains(event.sectionId)) { expandedSections.remove(event.sectionId); } else { expandedSections.add(event.sectionId); } emit(state.copyWith(expandedSections: expandedSections)); } }