Initial commit: COPILOT D6 Flutter keyboard controller
Flutter web app replacing legacy WPF CCTV surveillance keyboard controller. Includes wall overview, section view with monitor grid, camera input, PTZ control, alarm/lock/sequence BLoCs, and legacy-matching UI styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
189
copilot_keyboard/lib/data/services/state_service.dart
Normal file
189
copilot_keyboard/lib/data/services/state_service.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../../domain/entities/monitor_state.dart';
|
||||
import '../../domain/entities/alarm_state.dart';
|
||||
import '../models/bridge_event.dart';
|
||||
import 'bridge_service.dart';
|
||||
import 'alarm_service.dart';
|
||||
|
||||
/// Service for tracking overall system state (monitors + alarms)
|
||||
class StateService {
|
||||
final BridgeService _bridgeService;
|
||||
final AlarmService _alarmService;
|
||||
final Logger _logger = Logger();
|
||||
|
||||
StreamSubscription? _eventSubscription;
|
||||
|
||||
// Monitor state
|
||||
final _monitorStatesController =
|
||||
BehaviorSubject<Map<int, MonitorState>>.seeded({});
|
||||
|
||||
// Combined state stream (monitors with alarm flags)
|
||||
final _combinedMonitorStatesController =
|
||||
BehaviorSubject<Map<int, MonitorState>>.seeded({});
|
||||
|
||||
StateService({
|
||||
required BridgeService bridgeService,
|
||||
required AlarmService alarmService,
|
||||
}) : _bridgeService = bridgeService,
|
||||
_alarmService = alarmService;
|
||||
|
||||
/// Stream of monitor states (without alarm info)
|
||||
Stream<Map<int, MonitorState>> get monitorStates =>
|
||||
_monitorStatesController.stream;
|
||||
|
||||
/// Stream of monitor states with alarm flags
|
||||
Stream<Map<int, MonitorState>> get combinedMonitorStates =>
|
||||
_combinedMonitorStatesController.stream;
|
||||
|
||||
/// Stream of active alarms (delegated to AlarmService)
|
||||
Stream<List<AlarmState>> get activeAlarms => _alarmService.alarms;
|
||||
|
||||
/// Current monitor states
|
||||
Map<int, MonitorState> get currentMonitorStates =>
|
||||
_monitorStatesController.value;
|
||||
|
||||
/// Get state for a specific monitor
|
||||
MonitorState? getMonitorState(int viewerId) {
|
||||
return _combinedMonitorStatesController.value[viewerId];
|
||||
}
|
||||
|
||||
/// Initialize state tracking
|
||||
Future<void> initialize() async {
|
||||
// Subscribe to bridge events
|
||||
_eventSubscription = _bridgeService.eventStream.listen(_handleBridgeEvent);
|
||||
|
||||
// Subscribe to alarm changes to update monitor flags
|
||||
_alarmService.alarms.listen((_) => _updateCombinedStates());
|
||||
|
||||
_logger.i('StateService initialized');
|
||||
}
|
||||
|
||||
/// Sync initial state from all bridges
|
||||
Future<void> syncFromBridges() async {
|
||||
_logger.i('Syncing state from bridges...');
|
||||
|
||||
final connectionStatus = _bridgeService.currentConnectionStatus;
|
||||
final monitors = <int, MonitorState>{};
|
||||
|
||||
for (final entry in connectionStatus.entries) {
|
||||
if (!entry.value) continue; // Skip disconnected servers
|
||||
|
||||
try {
|
||||
final serverMonitors =
|
||||
await _bridgeService.getMonitorStates(entry.key);
|
||||
|
||||
for (final monitorJson in serverMonitors) {
|
||||
final state = MonitorState.fromJson(monitorJson);
|
||||
monitors[state.viewerId] = state;
|
||||
}
|
||||
|
||||
_logger.d(
|
||||
'Synced ${serverMonitors.length} monitors from ${entry.key}');
|
||||
} catch (e) {
|
||||
_logger.e('Failed to sync monitors from ${entry.key}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_monitorStatesController.add(monitors);
|
||||
_updateCombinedStates();
|
||||
|
||||
// Also sync alarms
|
||||
await _alarmService.queryAllAlarms();
|
||||
|
||||
_logger.i('State sync complete: ${monitors.length} monitors');
|
||||
}
|
||||
|
||||
/// Handle incoming bridge event
|
||||
void _handleBridgeEvent(BridgeEvent event) {
|
||||
if (event.isViewerConnected || event.isViewerSelectionChanged) {
|
||||
_handleViewerConnected(event);
|
||||
} else if (event.isViewerCleared) {
|
||||
_handleViewerCleared(event);
|
||||
} else if (event.isEventStarted ||
|
||||
event.isEventStopped ||
|
||||
event.isAlarmQueueNotification) {
|
||||
// Delegate alarm events to AlarmService
|
||||
_alarmService.handleAlarmEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle viewer connected event
|
||||
void _handleViewerConnected(BridgeEvent event) {
|
||||
final viewer = event.viewer;
|
||||
final channel = event.channel;
|
||||
final playMode = event.playMode;
|
||||
|
||||
if (viewer == null) return;
|
||||
|
||||
final monitors = Map<int, MonitorState>.from(_monitorStatesController.value);
|
||||
final existing = monitors[viewer];
|
||||
|
||||
monitors[viewer] = MonitorState(
|
||||
viewerId: viewer,
|
||||
currentChannel: channel ?? existing?.currentChannel ?? 0,
|
||||
playMode: PlayMode.fromValue(playMode ?? existing?.playMode.value ?? 0),
|
||||
serverId: event.serverId,
|
||||
lastUpdated: event.timestamp,
|
||||
);
|
||||
|
||||
_monitorStatesController.add(monitors);
|
||||
_updateCombinedStates();
|
||||
|
||||
_logger.d('Monitor $viewer connected to channel $channel');
|
||||
}
|
||||
|
||||
/// Handle viewer cleared event
|
||||
void _handleViewerCleared(BridgeEvent event) {
|
||||
final viewer = event.viewer;
|
||||
if (viewer == null) return;
|
||||
|
||||
final monitors = Map<int, MonitorState>.from(_monitorStatesController.value);
|
||||
final existing = monitors[viewer];
|
||||
|
||||
if (existing != null) {
|
||||
monitors[viewer] = existing.cleared();
|
||||
} else {
|
||||
monitors[viewer] = MonitorState(
|
||||
viewerId: viewer,
|
||||
currentChannel: 0,
|
||||
playMode: PlayMode.unknown,
|
||||
serverId: event.serverId,
|
||||
lastUpdated: event.timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
_monitorStatesController.add(monitors);
|
||||
_updateCombinedStates();
|
||||
|
||||
_logger.d('Monitor $viewer cleared');
|
||||
}
|
||||
|
||||
/// Update combined states with alarm flags
|
||||
void _updateCombinedStates() {
|
||||
final monitors = _monitorStatesController.value;
|
||||
final combined = <int, MonitorState>{};
|
||||
|
||||
for (final entry in monitors.entries) {
|
||||
final hasAlarm = _alarmService.isMonitorBlocked(entry.key);
|
||||
combined[entry.key] = entry.value.withAlarm(hasAlarm);
|
||||
}
|
||||
|
||||
_combinedMonitorStatesController.add(combined);
|
||||
}
|
||||
|
||||
/// Check if a monitor is blocked by an alarm
|
||||
bool isMonitorBlocked(int viewerId) {
|
||||
return _alarmService.isMonitorBlocked(viewerId);
|
||||
}
|
||||
|
||||
/// Dispose resources
|
||||
void dispose() {
|
||||
_eventSubscription?.cancel();
|
||||
_monitorStatesController.close();
|
||||
_combinedMonitorStatesController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user