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:
168
copilot_keyboard/lib/presentation/blocs/lock/lock_bloc.dart
Normal file
168
copilot_keyboard/lib/presentation/blocs/lock/lock_bloc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
60
copilot_keyboard/lib/presentation/blocs/lock/lock_event.dart
Normal file
60
copilot_keyboard/lib/presentation/blocs/lock/lock_event.dart
Normal 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);
|
||||
}
|
||||
75
copilot_keyboard/lib/presentation/blocs/lock/lock_state.dart
Normal file
75
copilot_keyboard/lib/presentation/blocs/lock/lock_state.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user