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:flutter_bloc/flutter_bloc.dart';
import '../../../data/services/bridge_service.dart';
import '../../../data/services/coordination_service.dart';
import 'ptz_event.dart';
import 'ptz_state.dart';
class PtzBloc extends Bloc<PtzEvent, PtzState> {
final BridgeService _bridgeService;
final CoordinationService _coordinationService;
Timer? _lockResetTimer;
PtzBloc({
required BridgeService bridgeService,
required CoordinationService coordinationService,
}) : _bridgeService = bridgeService,
_coordinationService = coordinationService,
super(const PtzState()) {
on<PtzPanStart>(_onPanStart);
on<PtzTiltStart>(_onTiltStart);
on<PtzZoomStart>(_onZoomStart);
on<PtzStop>(_onStop);
on<PtzGoToPreset>(_onGoToPreset);
on<PtzSetCamera>(_onSetCamera);
}
/// Ensure we have a lock on the camera before PTZ movement.
/// Returns true if lock was acquired or already held.
Future<bool> _ensureLock(int cameraId, Emitter<PtzState> emit) async {
// Already locked by us
if (_coordinationService.isCameraLockedByMe(cameraId)) {
emit(state.copyWith(lockStatus: PtzLockStatus.locked));
return true;
}
emit(state.copyWith(lockStatus: PtzLockStatus.acquiring));
final result = await _coordinationService.tryLock(cameraId);
if (result.acquired) {
emit(state.copyWith(lockStatus: PtzLockStatus.locked));
_startLockResetTimer(cameraId);
return true;
}
// Lock denied — someone else has it
emit(state.copyWith(
lockStatus: PtzLockStatus.denied,
lockedBy: result.lock?.ownerName,
error: 'Camera locked by ${result.lock?.ownerName ?? "another keyboard"}',
));
return false;
}
void _startLockResetTimer(int cameraId) {
_lockResetTimer?.cancel();
_lockResetTimer = Timer.periodic(const Duration(minutes: 2), (_) {
if (_coordinationService.isCameraLockedByMe(cameraId)) {
_coordinationService.resetExpiration(cameraId);
} else {
_lockResetTimer?.cancel();
}
});
}
Future<void> _onPanStart(
PtzPanStart event,
Emitter<PtzState> emit,
) async {
emit(state.copyWith(activeCameraId: event.cameraId));
if (!await _ensureLock(event.cameraId, emit)) return;
final direction =
event.direction == 'left' ? PtzDirection.left : PtzDirection.right;
emit(state.copyWith(
currentDirection: direction,
isMoving: true,
error: null,
));
try {
await _bridgeService.ptzPan(event.cameraId, event.direction, event.speed);
} catch (e) {
emit(state.copyWith(error: e.toString(), isMoving: false));
}
}
Future<void> _onTiltStart(
PtzTiltStart event,
Emitter<PtzState> emit,
) async {
emit(state.copyWith(activeCameraId: event.cameraId));
if (!await _ensureLock(event.cameraId, emit)) return;
final direction =
event.direction == 'up' ? PtzDirection.up : PtzDirection.down;
emit(state.copyWith(
currentDirection: direction,
isMoving: true,
error: null,
));
try {
await _bridgeService.ptzTilt(
event.cameraId, event.direction, event.speed);
} catch (e) {
emit(state.copyWith(error: e.toString(), isMoving: false));
}
}
Future<void> _onZoomStart(
PtzZoomStart event,
Emitter<PtzState> emit,
) async {
emit(state.copyWith(activeCameraId: event.cameraId));
if (!await _ensureLock(event.cameraId, emit)) return;
final direction =
event.direction == 'in' ? PtzDirection.zoomIn : PtzDirection.zoomOut;
emit(state.copyWith(
currentDirection: direction,
isMoving: true,
error: null,
));
try {
await _bridgeService.ptzZoom(
event.cameraId, event.direction, event.speed);
} catch (e) {
emit(state.copyWith(error: e.toString(), isMoving: false));
}
}
Future<void> _onStop(
PtzStop event,
Emitter<PtzState> emit,
) async {
emit(state.copyWith(
currentDirection: PtzDirection.none,
isMoving: false,
error: null,
));
try {
await _bridgeService.ptzStop(event.cameraId);
} catch (e) {
emit(state.copyWith(error: e.toString()));
}
}
Future<void> _onGoToPreset(
PtzGoToPreset event,
Emitter<PtzState> emit,
) async {
if (!await _ensureLock(event.cameraId, emit)) return;
emit(state.copyWith(error: null));
try {
await _bridgeService.ptzPreset(event.cameraId, event.preset);
} catch (e) {
emit(state.copyWith(error: e.toString()));
}
}
void _onSetCamera(
PtzSetCamera event,
Emitter<PtzState> emit,
) {
if (event.cameraId == null) {
_lockResetTimer?.cancel();
emit(state.copyWith(
clearCamera: true, lockStatus: PtzLockStatus.none));
} else {
emit(state.copyWith(activeCameraId: event.cameraId));
}
}
@override
Future<void> close() {
_lockResetTimer?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,90 @@
import 'package:equatable/equatable.dart';
abstract class PtzEvent extends Equatable {
const PtzEvent();
@override
List<Object?> get props => [];
}
/// Start panning
class PtzPanStart extends PtzEvent {
final int cameraId;
final String direction; // 'left' or 'right'
final int speed;
const PtzPanStart({
required this.cameraId,
required this.direction,
this.speed = 50,
});
@override
List<Object?> get props => [cameraId, direction, speed];
}
/// Start tilting
class PtzTiltStart extends PtzEvent {
final int cameraId;
final String direction; // 'up' or 'down'
final int speed;
const PtzTiltStart({
required this.cameraId,
required this.direction,
this.speed = 50,
});
@override
List<Object?> get props => [cameraId, direction, speed];
}
/// Start zooming
class PtzZoomStart extends PtzEvent {
final int cameraId;
final String direction; // 'in' or 'out'
final int speed;
const PtzZoomStart({
required this.cameraId,
required this.direction,
this.speed = 50,
});
@override
List<Object?> get props => [cameraId, direction, speed];
}
/// Stop all PTZ movement
class PtzStop extends PtzEvent {
final int cameraId;
const PtzStop(this.cameraId);
@override
List<Object?> get props => [cameraId];
}
/// Go to preset
class PtzGoToPreset extends PtzEvent {
final int cameraId;
final int preset;
const PtzGoToPreset({
required this.cameraId,
required this.preset,
});
@override
List<Object?> get props => [cameraId, preset];
}
/// Set camera for PTZ control
class PtzSetCamera extends PtzEvent {
final int? cameraId;
const PtzSetCamera(this.cameraId);
@override
List<Object?> get props => [cameraId];
}

View File

@@ -0,0 +1,50 @@
import 'package:equatable/equatable.dart';
enum PtzDirection { none, left, right, up, down, zoomIn, zoomOut }
enum PtzLockStatus { none, acquiring, locked, denied }
class PtzState extends Equatable {
final int? activeCameraId;
final PtzDirection currentDirection;
final bool isMoving;
final PtzLockStatus lockStatus;
final String? lockedBy;
final String? error;
const PtzState({
this.activeCameraId,
this.currentDirection = PtzDirection.none,
this.isMoving = false,
this.lockStatus = PtzLockStatus.none,
this.lockedBy,
this.error,
});
bool get hasActiveCamera => activeCameraId != null;
bool get hasLock => lockStatus == PtzLockStatus.locked;
PtzState copyWith({
int? activeCameraId,
PtzDirection? currentDirection,
bool? isMoving,
PtzLockStatus? lockStatus,
String? lockedBy,
String? error,
bool clearCamera = false,
}) {
return PtzState(
activeCameraId:
clearCamera ? null : (activeCameraId ?? this.activeCameraId),
currentDirection: currentDirection ?? this.currentDirection,
isMoving: isMoving ?? this.isMoving,
lockStatus: lockStatus ?? this.lockStatus,
lockedBy: lockedBy ?? this.lockedBy,
error: error,
);
}
@override
List<Object?> get props =>
[activeCameraId, currentDirection, isMoving, lockStatus, lockedBy, error];
}