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:
349
copilot_keyboard/lib/presentation/screens/keyboard_screen.dart
Normal file
349
copilot_keyboard/lib/presentation/screens/keyboard_screen.dart
Normal file
@@ -0,0 +1,349 @@
|
||||
import 'package:flutter/material.dart' hide LockState;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../config/app_config.dart';
|
||||
import '../../data/services/function_button_service.dart';
|
||||
import '../../injection_container.dart';
|
||||
import '../blocs/connection/connection_bloc.dart';
|
||||
import '../blocs/camera/camera_bloc.dart';
|
||||
import '../blocs/monitor/monitor_bloc.dart';
|
||||
import '../blocs/ptz/ptz_bloc.dart';
|
||||
import '../blocs/alarm/alarm_bloc.dart';
|
||||
import '../blocs/lock/lock_bloc.dart';
|
||||
import '../blocs/lock/lock_event.dart';
|
||||
import '../blocs/lock/lock_state.dart';
|
||||
import '../blocs/sequence/sequence_bloc.dart';
|
||||
import '../blocs/sequence/sequence_event.dart';
|
||||
import '../blocs/wall/wall_bloc.dart';
|
||||
import '../blocs/wall/wall_event.dart';
|
||||
import '../widgets/wall_grid/wall_grid.dart';
|
||||
import '../widgets/toolbar/bottom_toolbar.dart';
|
||||
import '../widgets/connection_status_bar.dart';
|
||||
import '../widgets/ptz_control.dart';
|
||||
import '../widgets/sequence_panel.dart';
|
||||
import '../widgets/takeover_dialog.dart';
|
||||
|
||||
class KeyboardScreen extends StatelessWidget {
|
||||
const KeyboardScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ConnectionBloc>(create: (_) => sl<ConnectionBloc>()),
|
||||
BlocProvider<CameraBloc>(create: (_) => sl<CameraBloc>()),
|
||||
BlocProvider<MonitorBloc>(create: (_) => sl<MonitorBloc>()),
|
||||
BlocProvider<PtzBloc>(create: (_) => sl<PtzBloc>()),
|
||||
BlocProvider<AlarmBloc>(create: (_) => sl<AlarmBloc>()),
|
||||
BlocProvider<LockBloc>(create: (_) => sl<LockBloc>()),
|
||||
BlocProvider<SequenceBloc>(
|
||||
create: (_) => sl<SequenceBloc>()..add(LoadSequences()),
|
||||
),
|
||||
BlocProvider<WallBloc>(
|
||||
create: (_) => sl<WallBloc>()..add(const LoadWallConfig()),
|
||||
),
|
||||
],
|
||||
child: BlocListener<LockBloc, LockState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
prev.pendingTakeover != curr.pendingTakeover &&
|
||||
curr.pendingTakeover != null,
|
||||
listener: (context, state) {
|
||||
if (state.pendingTakeover != null) {
|
||||
showTakeoverDialog(context, state.pendingTakeover!);
|
||||
}
|
||||
},
|
||||
child: const _KeyboardScreenContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyboardScreenContent extends StatelessWidget {
|
||||
const _KeyboardScreenContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0D1117),
|
||||
body: Column(
|
||||
children: [
|
||||
// Top bar with connection status
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF161B22),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFF30363D), width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'COPILOT Keyboard',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'v0.3.0-build5',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF7F7F7F),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const ConnectionStatusBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Main content area
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Wide layout with PTZ on the side
|
||||
if (constraints.maxWidth > 1200) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Wall grid
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const WallGrid(),
|
||||
),
|
||||
),
|
||||
// PTZ controls sidebar
|
||||
Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF161B22),
|
||||
border: Border(
|
||||
left: BorderSide(color: Color(0xFF30363D), width: 1),
|
||||
),
|
||||
),
|
||||
child: const SingleChildScrollView(
|
||||
child: PtzControl(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
// Narrow layout - PTZ in bottom sheet or collapsed
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const WallGrid(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Bottom toolbar
|
||||
BottomToolbar(
|
||||
onSearchPressed: () => _showSearchDialog(context),
|
||||
onPrepositionPressed: () => _showPrepositionDialog(context),
|
||||
onPlaybackPressed: () => _showPlaybackOverlay(context),
|
||||
onAlarmListPressed: () => _showAlarmListDialog(context),
|
||||
onSequencePressed: () => _showSequenceDialog(context),
|
||||
onLockPressed: () => _toggleLock(context),
|
||||
onFunctionButtonPressed: (buttonId) =>
|
||||
_executeFunctionButton(context, buttonId),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSearchDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF1A202C),
|
||||
title: const Text(
|
||||
'Hledat kameru',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Číslo nebo název kamery',
|
||||
hintStyle: TextStyle(color: Colors.white54),
|
||||
prefixIcon: Icon(Icons.search, color: Colors.white54),
|
||||
border: OutlineInputBorder(),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white24),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFF00D4FF)),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Funkce bude implementována v další fázi.',
|
||||
style: TextStyle(color: Colors.white54, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Zavřít'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPrepositionDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF1A202C),
|
||||
title: const Text(
|
||||
'Prepozice',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
content: const SizedBox(
|
||||
width: 400,
|
||||
height: 300,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Seznam prepozic bude implementován v další fázi.',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Zavřít'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPlaybackOverlay(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Režim přehrávání (PvZ) bude implementován v další fázi.'),
|
||||
backgroundColor: Color(0xFF2D3748),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSequenceDialog(BuildContext context) {
|
||||
final wallState = context.read<WallBloc>().state;
|
||||
final viewerId = wallState.selectedViewerId;
|
||||
if (viewerId == null) return;
|
||||
|
||||
// Refresh sequence list
|
||||
context.read<SequenceBloc>().add(LoadSequences());
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: context.read<SequenceBloc>(),
|
||||
child: AlertDialog(
|
||||
backgroundColor: const Color(0xFF1A202C),
|
||||
title: Text(
|
||||
'Sekvence — Monitor $viewerId',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 500,
|
||||
height: 400,
|
||||
child: SingleChildScrollView(
|
||||
child: SequencePanel(viewerId: viewerId),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Zavřít'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAlarmListDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF1A202C),
|
||||
title: const Text(
|
||||
'Historie alarmů',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
content: const SizedBox(
|
||||
width: 500,
|
||||
height: 400,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Seznam alarmů bude implementován v další fázi.',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Zavřít'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleLock(BuildContext context) {
|
||||
final wallState = context.read<WallBloc>().state;
|
||||
final viewerId = wallState.selectedViewerId;
|
||||
if (viewerId == null) return;
|
||||
|
||||
final viewerState = wallState.getViewerState(viewerId);
|
||||
final cameraId = viewerState.currentCameraId;
|
||||
if (cameraId <= 0) return;
|
||||
|
||||
final lockBloc = context.read<LockBloc>();
|
||||
final lockState = lockBloc.state;
|
||||
|
||||
if (lockState.isCameraLockedByMe(
|
||||
cameraId, sl<AppConfig>().keyboardId)) {
|
||||
lockBloc.add(ReleaseLock(cameraId));
|
||||
} else {
|
||||
lockBloc.add(TryLock(cameraId));
|
||||
}
|
||||
}
|
||||
|
||||
void _executeFunctionButton(BuildContext context, String buttonId) {
|
||||
final wallBloc = context.read<WallBloc>();
|
||||
final wallId = wallBloc.state.config?.id ?? '1';
|
||||
final service = sl<FunctionButtonService>();
|
||||
|
||||
if (!service.hasActions(wallId, buttonId)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$buttonId — žádná akce pro tuto stěnu'),
|
||||
backgroundColor: const Color(0xFF2D3748),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
service.execute(wallId, buttonId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user