Show online/offline status dot beside device last-seen

Highlights devices seen within the last 10 minutes with a green dot so
recent activity is scannable at a glance; older devices get a muted
outline-colored dot with an "Appears offline" tooltip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-01 07:15:30 -04:00
co-authored by Claude Opus 4.7
parent 17712c25f1
commit 6303951b3d
+62 -10
View File
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import '../model/device.dart'; import '../model/device.dart';
import '../model/device_type.dart'; import '../model/device_type.dart';
import '../theme/app_colors.dart';
import '../utils/friendly_date_formatter.dart'; import '../utils/friendly_date_formatter.dart';
import '../widgets/status_badge.dart'; import '../widgets/status_badge.dart';
import 'device_actions.dart'; import 'device_actions.dart';
@@ -11,6 +12,7 @@ import 'device_list_sort.dart';
// Column layout shared between the header and data rows so cells line up. // Column layout shared between the header and data rows so cells line up.
const double _iconWidth = 40; const double _iconWidth = 40;
const double _trailingWidth = 48; const double _trailingWidth = 48;
const Duration _onlineThreshold = Duration(minutes: 10);
class _ColumnSpec { class _ColumnSpec {
final DeviceSortColumn column; final DeviceSortColumn column;
@@ -249,10 +251,18 @@ Widget _cellContent(
style: theme.textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'), style: theme.textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
); );
case DeviceSortColumn.lastSeen: case DeviceSortColumn.lastSeen:
return Text( return Row(
formatter.format(device.lastSeen), children: [
overflow: TextOverflow.ellipsis, _StatusDot(lastSeen: device.lastSeen),
style: theme.textTheme.bodyMedium, const SizedBox(width: 6),
Flexible(
child: Text(
formatter.format(device.lastSeen),
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium,
),
),
],
); );
case DeviceSortColumn.vendor: case DeviceSortColumn.vendor:
return Text( return Text(
@@ -306,12 +316,31 @@ class DeviceRowCompact extends StatelessWidget {
), ),
], ],
), ),
subtitle: Text( subtitle: DefaultTextStyle.merge(
[ style: theme.textTheme.bodyMedium?.copyWith(
'${device.isRegistered ? (device.owner.isEmpty ? '' : device.owner) : device.ipv4Address} · ${device.macAddress}', color: theme.colorScheme.onSurfaceVariant,
if (!device.isRegistered && device.vendor.isNotEmpty) device.vendor, ),
'Last seen: ${formatter.format(device.lastSeen)}', child: Column(
].join('\n'), crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${device.isRegistered ? (device.owner.isEmpty ? '' : device.owner) : device.ipv4Address} · ${device.macAddress}',
),
if (!device.isRegistered && device.vendor.isNotEmpty)
Text(device.vendor),
Row(
children: [
_StatusDot(lastSeen: device.lastSeen),
const SizedBox(width: 6),
Flexible(
child: Text(
'Last seen: ${formatter.format(device.lastSeen)}',
),
),
],
),
],
),
), ),
isThreeLine: true, isThreeLine: true,
trailing: _DeviceActionsMenu(device: device, onRefresh: onRefresh), trailing: _DeviceActionsMenu(device: device, onRefresh: onRefresh),
@@ -320,6 +349,29 @@ class DeviceRowCompact extends StatelessWidget {
} }
} }
class _StatusDot extends StatelessWidget {
final DateTime lastSeen;
const _StatusDot({required this.lastSeen});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isOnline = DateTime.now().difference(lastSeen) < _onlineThreshold;
final color = isOnline
? theme.extension<AppColorExtension>()!.success
: theme.colorScheme.outline;
return Tooltip(
message: isOnline ? 'Online' : 'Appears offline',
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
);
}
}
class _DeviceActionsMenu extends StatelessWidget { class _DeviceActionsMenu extends StatelessWidget {
final Device device; final Device device;
final VoidCallback onRefresh; final VoidCallback onRefresh;