mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
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:
co-authored by
Claude Opus 4.7
parent
17712c25f1
commit
6303951b3d
@@ -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(
|
||||||
|
children: [
|
||||||
|
_StatusDot(lastSeen: device.lastSeen),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
formatter.format(device.lastSeen),
|
formatter.format(device.lastSeen),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodyMedium,
|
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(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
'${device.isRegistered ? (device.owner.isEmpty ? '—' : device.owner) : device.ipv4Address} · ${device.macAddress}',
|
'${device.isRegistered ? (device.owner.isEmpty ? '—' : device.owner) : device.ipv4Address} · ${device.macAddress}',
|
||||||
if (!device.isRegistered && device.vendor.isNotEmpty) device.vendor,
|
),
|
||||||
|
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)}',
|
'Last seen: ${formatter.format(device.lastSeen)}',
|
||||||
].join('\n'),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user