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,168 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../data/services/coordination_service.dart';
import '../../../domain/entities/camera_lock.dart';
import 'lock_event.dart';
import 'lock_state.dart';
class LockBloc extends Bloc<LockEvent, LockState> {
final CoordinationService _coordinationService;
StreamSubscription? _locksSub;
StreamSubscription? _notifSub;
StreamSubscription? _connSub;
LockBloc({
required CoordinationService coordinationService,
required String keyboardId,
}) : _coordinationService = coordinationService,
super(const LockState()) {
on<TryLock>(_onTryLock);
on<ReleaseLock>(_onReleaseLock);
on<ReleaseAllLocks>(_onReleaseAllLocks);
on<RequestTakeover>(_onRequestTakeover);
on<ConfirmTakeover>(_onConfirmTakeover);
on<ResetLockExpiration>(_onResetLockExpiration);
on<LocksUpdated>(_onLocksUpdated);
on<LockNotificationReceived>(_onLockNotificationReceived);
on<CoordinatorConnectionChanged>(_onCoordinatorConnectionChanged);
// Subscribe to coordinator streams
_locksSub = _coordinationService.locks.listen((locks) {
add(LocksUpdated(locks));
});
_notifSub = _coordinationService.notifications.listen((notification) {
if (notification != null) {
add(LockNotificationReceived(notification));
}
});
_connSub = _coordinationService.connected.listen((connected) {
add(CoordinatorConnectionChanged(connected));
});
}
Future<void> _onTryLock(TryLock event, Emitter<LockState> emit) async {
final result = await _coordinationService.tryLock(
event.cameraId,
priority: event.priority,
);
if (!result.acquired) {
final lock = result.lock;
final owner = lock?.ownerName ?? 'unknown';
emit(state.copyWith(
lastNotification: 'Camera ${event.cameraId} locked by $owner',
));
}
}
Future<void> _onReleaseLock(
ReleaseLock event, Emitter<LockState> emit) async {
await _coordinationService.releaseLock(event.cameraId);
}
Future<void> _onReleaseAllLocks(
ReleaseAllLocks event, Emitter<LockState> emit) async {
final myLocks = await _coordinationService.getMyLockedCameras();
for (final cameraId in myLocks) {
await _coordinationService.releaseLock(cameraId);
}
}
Future<void> _onRequestTakeover(
RequestTakeover event, Emitter<LockState> emit) async {
final success = await _coordinationService.requestTakeover(
event.cameraId,
priority: event.priority,
);
if (success) {
emit(state.copyWith(
lastNotification: 'Takeover requested for camera ${event.cameraId}',
));
}
}
Future<void> _onConfirmTakeover(
ConfirmTakeover event, Emitter<LockState> emit) async {
await _coordinationService.confirmTakeover(
event.cameraId, event.confirm);
emit(state.copyWith(clearPendingTakeover: true));
}
Future<void> _onResetLockExpiration(
ResetLockExpiration event, Emitter<LockState> emit) async {
await _coordinationService.resetExpiration(event.cameraId);
}
void _onLocksUpdated(LocksUpdated event, Emitter<LockState> emit) {
emit(state.copyWith(locks: event.locks));
}
void _onLockNotificationReceived(
LockNotificationReceived event, Emitter<LockState> emit) {
final notification = event.notification;
switch (notification.type) {
case CameraLockNotificationType.confirmTakeOver:
// Show takeover confirmation dialog
emit(state.copyWith(
pendingTakeover: TakeoverRequest(
cameraId: notification.cameraId,
requestingKeyboard: notification.copilotName,
),
));
break;
case CameraLockNotificationType.takenOver:
emit(state.copyWith(
lastNotification:
'Camera ${notification.cameraId} taken over by ${notification.copilotName}',
));
break;
case CameraLockNotificationType.expireSoon:
emit(state.copyWith(
lastNotification:
'Lock on camera ${notification.cameraId} expiring soon',
));
break;
case CameraLockNotificationType.confirmed:
emit(state.copyWith(
lastNotification:
'Takeover confirmed for camera ${notification.cameraId}',
));
break;
case CameraLockNotificationType.rejected:
emit(state.copyWith(
lastNotification:
'Takeover rejected for camera ${notification.cameraId}',
));
break;
case CameraLockNotificationType.unlocked:
case CameraLockNotificationType.acquired:
// Handled by lock state updates
break;
}
}
void _onCoordinatorConnectionChanged(
CoordinatorConnectionChanged event, Emitter<LockState> emit) {
emit(state.copyWith(coordinatorConnected: event.connected));
}
@override
Future<void> close() {
_locksSub?.cancel();
_notifSub?.cancel();
_connSub?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,60 @@
import '../../../domain/entities/camera_lock.dart';
abstract class LockEvent {}
/// Try to acquire a lock on a camera
class TryLock extends LockEvent {
final int cameraId;
final CameraLockPriority priority;
TryLock(this.cameraId, {this.priority = CameraLockPriority.low});
}
/// Release a camera lock
class ReleaseLock extends LockEvent {
final int cameraId;
ReleaseLock(this.cameraId);
}
/// Release all locks held by this keyboard
class ReleaseAllLocks extends LockEvent {}
/// Request takeover of a camera locked by another keyboard
class RequestTakeover extends LockEvent {
final int cameraId;
final CameraLockPriority priority;
RequestTakeover(this.cameraId, {this.priority = CameraLockPriority.low});
}
/// Confirm or reject a takeover request from another keyboard
class ConfirmTakeover extends LockEvent {
final int cameraId;
final bool confirm;
ConfirmTakeover(this.cameraId, {required this.confirm});
}
/// Reset lock expiration (keep-alive during PTZ)
class ResetLockExpiration extends LockEvent {
final int cameraId;
ResetLockExpiration(this.cameraId);
}
/// Internal: lock state updated from coordinator WebSocket
class LocksUpdated extends LockEvent {
final Map<int, CameraLock> locks;
LocksUpdated(this.locks);
}
/// Internal: lock notification received from coordinator
class LockNotificationReceived extends LockEvent {
final CameraLockNotification notification;
LockNotificationReceived(this.notification);
}
/// Internal: coordinator connection status changed
class CoordinatorConnectionChanged extends LockEvent {
final bool connected;
CoordinatorConnectionChanged(this.connected);
}

View File

@@ -0,0 +1,75 @@
import '../../../domain/entities/camera_lock.dart';
class LockState {
/// All known camera locks
final Map<int, CameraLock> locks;
/// Whether the coordinator is connected
final bool coordinatorConnected;
/// Pending takeover confirmation request (show dialog to user)
final TakeoverRequest? pendingTakeover;
/// Last notification message (for snackbar/toast)
final String? lastNotification;
/// Error message
final String? error;
const LockState({
this.locks = const {},
this.coordinatorConnected = false,
this.pendingTakeover,
this.lastNotification,
this.error,
});
LockState copyWith({
Map<int, CameraLock>? locks,
bool? coordinatorConnected,
TakeoverRequest? pendingTakeover,
bool clearPendingTakeover = false,
String? lastNotification,
bool clearNotification = false,
String? error,
bool clearError = false,
}) {
return LockState(
locks: locks ?? this.locks,
coordinatorConnected: coordinatorConnected ?? this.coordinatorConnected,
pendingTakeover:
clearPendingTakeover ? null : (pendingTakeover ?? this.pendingTakeover),
lastNotification:
clearNotification ? null : (lastNotification ?? this.lastNotification),
error: clearError ? null : (error ?? this.error),
);
}
/// Check if a camera is locked by this keyboard
bool isCameraLockedByMe(int cameraId, String keyboardId) {
final lock = locks[cameraId];
return lock != null &&
lock.ownerName.toLowerCase() == keyboardId.toLowerCase();
}
/// Check if a camera is locked by another keyboard
bool isCameraLockedByOther(int cameraId, String keyboardId) {
final lock = locks[cameraId];
return lock != null &&
lock.ownerName.toLowerCase() != keyboardId.toLowerCase();
}
/// Get the lock for a camera, if any
CameraLock? getLock(int cameraId) => locks[cameraId];
}
/// Pending takeover request shown as a dialog
class TakeoverRequest {
final int cameraId;
final String requestingKeyboard;
const TakeoverRequest({
required this.cameraId,
required this.requestingKeyboard,
});
}