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>
169 lines
5.0 KiB
Dart
169 lines
5.0 KiB
Dart
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();
|
|
}
|
|
}
|