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:
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../blocs/wall/wall_bloc.dart';
|
||||
import '../../blocs/wall/wall_state.dart';
|
||||
import 'camera_input_widget.dart';
|
||||
import 'function_buttons_widget.dart';
|
||||
|
||||
/// Bottom toolbar with camera input and action buttons
|
||||
class BottomToolbar extends StatelessWidget {
|
||||
final VoidCallback? onSearchPressed;
|
||||
final VoidCallback? onPrepositionPressed;
|
||||
final VoidCallback? onPlaybackPressed;
|
||||
final VoidCallback? onAlarmListPressed;
|
||||
final VoidCallback? onLockPressed;
|
||||
final VoidCallback? onSequencePressed;
|
||||
final Function(String)? onFunctionButtonPressed;
|
||||
|
||||
const BottomToolbar({
|
||||
super.key,
|
||||
this.onSearchPressed,
|
||||
this.onPrepositionPressed,
|
||||
this.onPlaybackPressed,
|
||||
this.onAlarmListPressed,
|
||||
this.onLockPressed,
|
||||
this.onSequencePressed,
|
||||
this.onFunctionButtonPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF1A202C),
|
||||
border: Border(
|
||||
top: BorderSide(color: Color(0xFF4A5568), width: 1),
|
||||
),
|
||||
),
|
||||
child: BlocBuilder<WallBloc, WallState>(
|
||||
builder: (context, state) {
|
||||
final hasSelection = state.selectedViewerId != null;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Top row - Camera input and function buttons
|
||||
Row(
|
||||
children: [
|
||||
// Camera input
|
||||
const Expanded(
|
||||
child: CameraInputWidget(),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
// Function buttons
|
||||
FunctionButtonsWidget(
|
||||
onButtonPressed: onFunctionButtonPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Bottom row - Context actions
|
||||
Row(
|
||||
children: [
|
||||
if (hasSelection) ...[
|
||||
// Search button
|
||||
_ActionButton(
|
||||
icon: Icons.search,
|
||||
label: 'Hledat',
|
||||
onPressed: onSearchPressed,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Preposition button
|
||||
_ActionButton(
|
||||
icon: Icons.place,
|
||||
label: 'Prepozice',
|
||||
onPressed: onPrepositionPressed,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Playback button
|
||||
_ActionButton(
|
||||
icon: Icons.history,
|
||||
label: 'PvZ',
|
||||
onPressed: onPlaybackPressed,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Alarm list button
|
||||
_ActionButton(
|
||||
icon: Icons.notifications,
|
||||
label: 'Alarmy',
|
||||
onPressed: onAlarmListPressed,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Sequence button
|
||||
_ActionButton(
|
||||
icon: Icons.loop,
|
||||
label: 'Sekvence',
|
||||
onPressed: onSequencePressed,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Lock button
|
||||
_LockButton(
|
||||
viewerState: state.getViewerState(state.selectedViewerId!),
|
||||
onPressed: onLockPressed,
|
||||
),
|
||||
] else ...[
|
||||
// Show selection hint when nothing selected
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Vyberte monitor pro zobrazení akcí',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
// Selected monitor info
|
||||
if (hasSelection)
|
||||
_SelectionInfo(
|
||||
viewerId: state.selectedViewerId!,
|
||||
viewerState: state.getViewerState(state.selectedViewerId!),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const _ActionButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: const Color(0xFF2D3748),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: onPressed != null ? Colors.white : const Color(0xFF4A5568),
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: onPressed != null ? Colors.white : const Color(0xFF4A5568),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LockButton extends StatelessWidget {
|
||||
final ViewerState viewerState;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const _LockButton({
|
||||
required this.viewerState,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLocked = viewerState.isLocked;
|
||||
final isLockedByOther = viewerState.isLockedByOther;
|
||||
|
||||
return Material(
|
||||
color: isLocked
|
||||
? (isLockedByOther ? const Color(0xFFDC2626) : const Color(0xFF38A169))
|
||||
: const Color(0xFF2D3748),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: InkWell(
|
||||
onTap: isLockedByOther ? null : onPressed,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isLocked ? Icons.lock : Icons.lock_open,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
isLocked ? 'Zamčeno' : 'Zamknout',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectionInfo extends StatelessWidget {
|
||||
final int viewerId;
|
||||
final ViewerState viewerState;
|
||||
|
||||
const _SelectionInfo({
|
||||
required this.viewerId,
|
||||
required this.viewerState,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D3748),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: const Color(0xFF00D4FF), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.tv,
|
||||
color: Color(0xFF00D4FF),
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Monitor $viewerId',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (viewerState.hasCamera) ...[
|
||||
const SizedBox(width: 12),
|
||||
const Icon(
|
||||
Icons.videocam,
|
||||
color: Colors.white70,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${viewerState.currentCameraId}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (viewerState.hasAlarm) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDC2626),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'ALARM',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user