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
4.5 KiB
Dart
169 lines
4.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../blocs/wall/wall_bloc.dart';
|
|
import '../blocs/wall/wall_event.dart';
|
|
import '../blocs/wall/wall_state.dart';
|
|
import '../widgets/overview/wall_overview.dart';
|
|
import '../widgets/section/section_view.dart';
|
|
|
|
class MainScreen extends StatefulWidget {
|
|
const MainScreen({super.key});
|
|
|
|
@override
|
|
State<MainScreen> createState() => _MainScreenState();
|
|
}
|
|
|
|
class _MainScreenState extends State<MainScreen> {
|
|
String? _selectedSectionId;
|
|
final FocusNode _focusNode = FocusNode();
|
|
WallBloc? _wallBloc;
|
|
|
|
@override
|
|
void dispose() {
|
|
_focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocBuilder<WallBloc, WallState>(
|
|
builder: (blocContext, state) {
|
|
// Store bloc reference for keyboard handler
|
|
_wallBloc = BlocProvider.of<WallBloc>(blocContext, listen: false);
|
|
|
|
return KeyboardListener(
|
|
focusNode: _focusNode,
|
|
autofocus: true,
|
|
onKeyEvent: _handleKeyEvent,
|
|
child: Scaffold(
|
|
backgroundColor: const Color(0xFF0A0E14),
|
|
body: _buildBody(blocContext, state),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildBody(BuildContext context, WallState state) {
|
|
if (state.isLoading || state.config == null) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(
|
|
color: Color(0xFF00D4FF),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Show section view or wall overview
|
|
if (_selectedSectionId != null) {
|
|
final section = state.config!.sections.firstWhere(
|
|
(s) => s.id == _selectedSectionId,
|
|
orElse: () => state.config!.sections.first,
|
|
);
|
|
return SectionView(
|
|
section: section,
|
|
wallState: state,
|
|
onBack: () => setState(() => _selectedSectionId = null),
|
|
onViewerTap: (viewerId) {
|
|
context.read<WallBloc>().add(SelectViewer(viewerId));
|
|
// Re-request focus for keyboard input after tile tap
|
|
_focusNode.requestFocus();
|
|
},
|
|
);
|
|
}
|
|
|
|
return WallOverview(
|
|
config: state.config!,
|
|
wallState: state,
|
|
onSectionTap: (sectionId) {
|
|
setState(() => _selectedSectionId = sectionId);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _handleKeyEvent(KeyEvent event) {
|
|
if (event is! KeyDownEvent) return;
|
|
|
|
final bloc = _wallBloc;
|
|
if (bloc == null) return;
|
|
|
|
final state = bloc.state;
|
|
final key = event.logicalKey;
|
|
|
|
// Escape - go back or deselect
|
|
if (key == LogicalKeyboardKey.escape) {
|
|
if (state.selectedViewerId != null) {
|
|
bloc.add(const DeselectViewer());
|
|
} else if (_selectedSectionId != null) {
|
|
setState(() => _selectedSectionId = null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Only handle camera input when a viewer is selected
|
|
if (state.selectedViewerId == null) return;
|
|
|
|
// Digit keys 0-9
|
|
if (key.keyId >= LogicalKeyboardKey.digit0.keyId &&
|
|
key.keyId <= LogicalKeyboardKey.digit9.keyId) {
|
|
final digit = key.keyId - LogicalKeyboardKey.digit0.keyId;
|
|
bloc.add(AddCameraDigit(digit));
|
|
return;
|
|
}
|
|
|
|
// Numpad digits
|
|
if (key.keyId >= LogicalKeyboardKey.numpad0.keyId &&
|
|
key.keyId <= LogicalKeyboardKey.numpad9.keyId) {
|
|
final digit = key.keyId - LogicalKeyboardKey.numpad0.keyId;
|
|
bloc.add(AddCameraDigit(digit));
|
|
return;
|
|
}
|
|
|
|
// Enter - execute CrossSwitch
|
|
if (key == LogicalKeyboardKey.enter ||
|
|
key == LogicalKeyboardKey.numpadEnter) {
|
|
if (state.canExecuteCrossSwitch) {
|
|
bloc.add(const ExecuteCrossSwitch());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Backspace - remove last digit
|
|
if (key == LogicalKeyboardKey.backspace) {
|
|
bloc.add(const BackspaceCameraDigit());
|
|
return;
|
|
}
|
|
|
|
// Delete - cancel edit
|
|
if (key == LogicalKeyboardKey.delete) {
|
|
bloc.add(const CancelCameraEdit());
|
|
return;
|
|
}
|
|
|
|
// Escape - cancel edit or deselect
|
|
if (key == LogicalKeyboardKey.escape) {
|
|
if (state.isEditing) {
|
|
bloc.add(const CancelCameraEdit());
|
|
} else {
|
|
bloc.add(const DeselectViewer());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// F1-F3 for prefix selection
|
|
if (key == LogicalKeyboardKey.f1) {
|
|
bloc.add(const SetCameraPrefix(500));
|
|
return;
|
|
}
|
|
if (key == LogicalKeyboardKey.f2) {
|
|
bloc.add(const SetCameraPrefix(501));
|
|
return;
|
|
}
|
|
if (key == LogicalKeyboardKey.f3) {
|
|
bloc.add(const SetCameraPrefix(502));
|
|
return;
|
|
}
|
|
}
|
|
}
|