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:
klas
2026-02-12 14:57:38 +01:00
commit 40143734fc
125 changed files with 65073 additions and 0 deletions

View 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();
}
}