Files
tasq/lib/screens/network_map/widgets/device_node.dart
T

192 lines
6.4 KiB
Dart

import 'package:flutter/material.dart';
import '../../../models/network/network_device.dart';
import 'network_device_icon.dart';
/// A styled node representing one network device on the topology canvas.
///
/// Accepts an explicit [nodeSize] so role-based sizing can be applied by the
/// canvas without this widget needing to know the layout strategy.
class DeviceNode extends StatelessWidget {
const DeviceNode({
super.key,
required this.device,
this.portCount,
this.isSelected = false,
this.isHighlighted = false,
this.nodeSize = const Size(160, 110),
this.onTap,
});
final NetworkDevice device;
final int? portCount;
final bool isSelected;
final bool isHighlighted;
final Size nodeSize;
final VoidCallback? onTap;
Color _accentForRole(ColorScheme cs, NetworkDeviceRole? role) {
switch (role) {
case NetworkDeviceRole.core:
return cs.error;
case NetworkDeviceRole.distribution:
return cs.tertiary;
case NetworkDeviceRole.access:
return cs.primary;
case NetworkDeviceRole.edge:
return cs.secondary;
case NetworkDeviceRole.endpoint:
case null:
return cs.outline;
}
}
Color? _statusColor(ColorScheme cs, NetworkDeviceStatus status) {
switch (status) {
case NetworkDeviceStatus.online:
return const Color(0xFF34A853); // Google green — universally understood
case NetworkDeviceStatus.offline:
return cs.error;
case NetworkDeviceStatus.warning:
return const Color(0xFFFBBC04); // Amber
case NetworkDeviceStatus.unknown:
return null; // No dot shown
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
final accent = _accentForRole(cs, device.role);
final statusColor = _statusColor(cs, device.status);
final bgColor = isSelected
? cs.primaryContainer
: isHighlighted
? cs.secondaryContainer.withValues(alpha: 0.6)
: cs.surfaceContainerHigh;
final borderColor = isSelected
? cs.primary
: isHighlighted
? cs.primary.withValues(alpha: 0.8)
: accent.withValues(alpha: 0.6);
final borderWidth = isSelected ? 2.0 : (isHighlighted ? 1.5 : 1.0);
return AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOut,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: borderColor, width: borderWidth),
boxShadow: (isHighlighted || isSelected)
? [
BoxShadow(
color: cs.primary.withValues(alpha: 0.18),
blurRadius: 12,
offset: const Offset(0, 2),
),
]
: null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
Container(
width: nodeSize.width,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
NetworkDeviceIcon(
kind: device.kind,
color: accent,
size: 20,
),
const SizedBox(width: 6),
Expanded(
child: Text(
device.kind.label,
style: tt.labelSmall?.copyWith(color: accent),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 4),
Text(
device.name,
style: tt.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (device.vendor != null || device.model != null) ...[
const SizedBox(height: 2),
Text(
[
if (device.vendor != null) device.vendor,
if (device.model != null) device.model,
].whereType<String>().join(''),
style: tt.labelSmall?.copyWith(
color: cs.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
if (portCount != null) ...[
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.cable_outlined,
size: 12,
color: cs.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'$portCount ports',
style: tt.labelSmall?.copyWith(
color: cs.onSurfaceVariant,
),
),
],
),
],
],
),
),
// Status dot — top-right, only shown when status is not unknown
if (statusColor != null)
Positioned(
top: 8,
right: 8,
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
border: Border.all(
color: bgColor,
width: 1.5,
),
),
),
),
],
),
),
),
);
}
}