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>
304 lines
9.1 KiB
Dart
304 lines
9.1 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|