mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add mDNS scanner status to Status and Home screens
Add an mDNS Scanner status card to the Status screen mirroring the ARP card, and replace the ARP card on the Home screen with a combined Status card that summarizes both scanners in one line each. Extract the shared duration formatting helper into utils/duration_formatter.dart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c6fdf9e181
commit
75f597bfe4
@@ -17,8 +17,10 @@
|
|||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
- [ ] Add pagination to the devices list screen
|
- [ ] Add a "go to first" and "go to last" buttons to paginations (notification list for now)
|
||||||
- [ ] Add the mDNS/Bonjour scanner status to the Status and Home screens
|
- [x] Replace the ARP scanner widget in the home screen with a Scanning status widget that provides a one liner for each scanner
|
||||||
|
- [ ] Add pagination to the devices list screen - now you can't see all devices
|
||||||
|
- [x] Add the mDNS/Bonjour scanner status to the Status and Home screens
|
||||||
- [x] Change the notifications list so it has explicit paging (not infinite paging)
|
- [x] Change the notifications list so it has explicit paging (not infinite paging)
|
||||||
- [x] List recorded devices
|
- [x] List recorded devices
|
||||||
- [x] Register a device
|
- [x] Register a device
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../widgets/arp_scanner_card.dart';
|
|
||||||
import '../widgets/device_summary_card.dart';
|
import '../widgets/device_summary_card.dart';
|
||||||
|
import '../widgets/scanners_status_card.dart';
|
||||||
import 'notifications_list.dart';
|
import 'notifications_list.dart';
|
||||||
|
|
||||||
const _twoColumnBreakpoint = 700.0;
|
const _twoColumnBreakpoint = 700.0;
|
||||||
@@ -33,7 +33,7 @@ class HomeScreen extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
DeviceSummaryCard(),
|
DeviceSummaryCard(),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
ArpScannerCard(),
|
ScannersStatusCard(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -54,7 +54,7 @@ class HomeScreen extends StatelessWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(top: 16, bottom: 20),
|
padding: EdgeInsets.only(top: 16, bottom: 20),
|
||||||
child: ArpScannerCard(),
|
child: ScannersStatusCard(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
class MdnsScannerStatus {
|
||||||
|
final bool isListening;
|
||||||
|
final double? listeningForSeconds;
|
||||||
|
final int devicesSeen;
|
||||||
|
final double? lastDeviceSeenSecondsAgo;
|
||||||
|
|
||||||
|
const MdnsScannerStatus({
|
||||||
|
required this.isListening,
|
||||||
|
this.listeningForSeconds,
|
||||||
|
required this.devicesSeen,
|
||||||
|
this.lastDeviceSeenSecondsAgo,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MdnsScannerStatus.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MdnsScannerStatus(
|
||||||
|
isListening: json['is_listening'] as bool,
|
||||||
|
listeningForSeconds: (json['listening_for_seconds'] as num?)?.toDouble(),
|
||||||
|
devicesSeen: (json['devices_seen'] as num).toInt(),
|
||||||
|
lastDeviceSeenSecondsAgo: (json['last_device_seen_seconds_ago'] as num?)
|
||||||
|
?.toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../widgets/arp_scanner_card.dart';
|
import '../widgets/arp_scanner_card.dart';
|
||||||
|
import '../widgets/mdns_scanner_card.dart';
|
||||||
|
|
||||||
class StatusScreen extends StatelessWidget {
|
class StatusScreen extends StatelessWidget {
|
||||||
const StatusScreen({super.key});
|
const StatusScreen({super.key});
|
||||||
@@ -13,6 +14,8 @@ class StatusScreen extends StatelessWidget {
|
|||||||
Text('Status', style: Theme.of(context).textTheme.headlineMedium),
|
Text('Status', style: Theme.of(context).textTheme.headlineMedium),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const ArpScannerCard(),
|
const ArpScannerCard(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const MdnsScannerCard(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
String formatSeconds(double seconds) {
|
||||||
|
final total = seconds.round().clamp(0, double.maxFinite.toInt());
|
||||||
|
if (total < 60) return '${total}s';
|
||||||
|
final m = total ~/ 60;
|
||||||
|
final s = total % 60;
|
||||||
|
return '${m}m ${s}s';
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:encrypter/encrypter/xor.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:frontend/utils/pref_utils.dart';
|
import 'package:frontend/utils/pref_utils.dart';
|
||||||
import '../model/arp_scanner_status.dart';
|
import '../model/arp_scanner_status.dart';
|
||||||
|
import '../model/mdns_scanner_status.dart';
|
||||||
import '../model/device.dart';
|
import '../model/device.dart';
|
||||||
import '../model/device_event.dart';
|
import '../model/device_event.dart';
|
||||||
import '../model/device_summary.dart';
|
import '../model/device_summary.dart';
|
||||||
@@ -105,8 +106,9 @@ class BackendAPI {
|
|||||||
if (isRegistered != null) params['is_registered'] = isRegistered;
|
if (isRegistered != null) params['is_registered'] = isRegistered;
|
||||||
if (owner != null && owner.isNotEmpty) params['owner'] = owner;
|
if (owner != null && owner.isNotEmpty) params['owner'] = owner;
|
||||||
if (deviceType != null) {
|
if (deviceType != null) {
|
||||||
params['device_type'] =
|
params['device_type'] = deviceType == DeviceType.unknown
|
||||||
deviceType == DeviceType.unknown ? '' : deviceType.apiName;
|
? ''
|
||||||
|
: deviceType.apiName;
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await _dio.get('/devices', queryParameters: params);
|
final response = await _dio.get('/devices', queryParameters: params);
|
||||||
@@ -172,6 +174,13 @@ class BackendAPI {
|
|||||||
return ArpScannerStatus.fromJson(response.data as Map<String, dynamic>);
|
return ArpScannerStatus.fromJson(response.data as Map<String, dynamic>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<MdnsScannerStatus> getMdnsScannerStatus() async {
|
||||||
|
debugPrint('About to call GET /mdns_scanner/status');
|
||||||
|
final response = await _dio.get('/mdns_scanner/status');
|
||||||
|
debugPrint('Received: ${response.data}');
|
||||||
|
return MdnsScannerStatus.fromJson(response.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Notification>> listNotifications(
|
Future<List<Notification>> listNotifications(
|
||||||
bool? isNew,
|
bool? isNew,
|
||||||
int offset, {
|
int offset, {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../model/arp_scanner_status.dart';
|
import '../model/arp_scanner_status.dart';
|
||||||
|
import '../utils/duration_formatter.dart';
|
||||||
import '../utils/oott_api.dart';
|
import '../utils/oott_api.dart';
|
||||||
|
|
||||||
class ArpScannerCard extends StatefulWidget {
|
class ArpScannerCard extends StatefulWidget {
|
||||||
@@ -25,16 +26,10 @@ class _ArpScannerCardState extends State<ArpScannerCard> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_load();
|
_load();
|
||||||
_refreshTimer = Timer.periodic(
|
_refreshTimer = Timer.periodic(const Duration(seconds: 5), (_) => _load());
|
||||||
const Duration(seconds: 5),
|
_tickTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
(_) => _load(),
|
if (mounted) setState(() {});
|
||||||
);
|
});
|
||||||
_tickTimer = Timer.periodic(
|
|
||||||
const Duration(seconds: 1),
|
|
||||||
(_) {
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -130,7 +125,7 @@ class _ArpScannerCardState extends State<ArpScannerCard> {
|
|||||||
|
|
||||||
if (_status!.isRunning) {
|
if (_status!.isRunning) {
|
||||||
final sub = _status!.runningForSeconds != null
|
final sub = _status!.runningForSeconds != null
|
||||||
? 'Running for ${_formatSeconds(_status!.runningForSeconds! + elapsed)}'
|
? 'Running for ${formatSeconds(_status!.runningForSeconds! + elapsed)}'
|
||||||
: null;
|
: null;
|
||||||
return (Colors.green, 'Running', sub);
|
return (Colors.green, 'Running', sub);
|
||||||
}
|
}
|
||||||
@@ -142,17 +137,9 @@ class _ArpScannerCardState extends State<ArpScannerCard> {
|
|||||||
return (
|
return (
|
||||||
Colors.amber,
|
Colors.amber,
|
||||||
'Waiting for next run',
|
'Waiting for next run',
|
||||||
'Next run in ${_formatSeconds(remaining)}',
|
'Next run in ${formatSeconds(remaining)}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (Colors.grey, 'Not yet started', null);
|
return (Colors.grey, 'Not yet started', null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatSeconds(double seconds) {
|
|
||||||
final total = seconds.round().clamp(0, double.maxFinite.toInt());
|
|
||||||
if (total < 60) return '${total}s';
|
|
||||||
final m = total ~/ 60;
|
|
||||||
final s = total % 60;
|
|
||||||
return '${m}m ${s}s';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../model/mdns_scanner_status.dart';
|
||||||
|
import '../utils/duration_formatter.dart';
|
||||||
|
import '../utils/oott_api.dart';
|
||||||
|
|
||||||
|
class MdnsScannerCard extends StatefulWidget {
|
||||||
|
const MdnsScannerCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MdnsScannerCard> createState() => _MdnsScannerCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MdnsScannerCardState extends State<MdnsScannerCard> {
|
||||||
|
MdnsScannerStatus? _status;
|
||||||
|
DateTime? _statusReceivedAt;
|
||||||
|
bool _isLoading = true;
|
||||||
|
String? _error;
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
Timer? _tickTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
_refreshTimer = Timer.periodic(const Duration(seconds: 5), (_) => _load());
|
||||||
|
_tickTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
_tickTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
try {
|
||||||
|
final status = await BackendAPI.instance.getMdnsScannerStatus();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_status = status;
|
||||||
|
_statusReceivedAt = DateTime.now();
|
||||||
|
_error = null;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_error = e.toString();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final (color, label, sublabels) = _resolveState(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => context.go('/status'),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, color: color, size: 14),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'mDNS Scanner',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(label, style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
for (final sublabel in sublabels) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
sublabel,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Color, String, List<String>) _resolveState(BuildContext context) {
|
||||||
|
if (_error != null || _status == null) {
|
||||||
|
return (
|
||||||
|
Colors.red,
|
||||||
|
'Error',
|
||||||
|
[
|
||||||
|
'Unable to reach the server or a server-side error occurred. Check the logs for details.',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final elapsed = _statusReceivedAt != null
|
||||||
|
? DateTime.now().difference(_statusReceivedAt!).inSeconds.toDouble()
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
if (_status!.isListening) {
|
||||||
|
final sublabels = <String>[];
|
||||||
|
if (_status!.listeningForSeconds != null) {
|
||||||
|
sublabels.add(
|
||||||
|
'Listening for ${formatSeconds(_status!.listeningForSeconds! + elapsed)} · ${_status!.devicesSeen} devices seen',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sublabels.add('${_status!.devicesSeen} devices seen');
|
||||||
|
}
|
||||||
|
if (_status!.lastDeviceSeenSecondsAgo != null) {
|
||||||
|
sublabels.add(
|
||||||
|
'Last device ${formatSeconds(_status!.lastDeviceSeenSecondsAgo! + elapsed)} ago',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (Colors.green, 'Listening', sublabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Colors.grey, 'Not yet started', <String>[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../model/arp_scanner_status.dart';
|
||||||
|
import '../model/mdns_scanner_status.dart';
|
||||||
|
import '../utils/duration_formatter.dart';
|
||||||
|
import '../utils/oott_api.dart';
|
||||||
|
|
||||||
|
class ScannersStatusCard extends StatefulWidget {
|
||||||
|
const ScannersStatusCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ScannersStatusCard> createState() => _ScannersStatusCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScannersStatusCardState extends State<ScannersStatusCard> {
|
||||||
|
ArpScannerStatus? _arpStatus;
|
||||||
|
String? _arpError;
|
||||||
|
MdnsScannerStatus? _mdnsStatus;
|
||||||
|
String? _mdnsError;
|
||||||
|
DateTime? _statusReceivedAt;
|
||||||
|
bool _isLoading = true;
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
Timer? _tickTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
_refreshTimer = Timer.periodic(const Duration(seconds: 5), (_) => _load());
|
||||||
|
_tickTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
_tickTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
ArpScannerStatus? arp;
|
||||||
|
String? arpError;
|
||||||
|
try {
|
||||||
|
arp = await BackendAPI.instance.getArpScannerStatus();
|
||||||
|
} catch (e) {
|
||||||
|
arpError = e.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
MdnsScannerStatus? mdns;
|
||||||
|
String? mdnsError;
|
||||||
|
try {
|
||||||
|
mdns = await BackendAPI.instance.getMdnsScannerStatus();
|
||||||
|
} catch (e) {
|
||||||
|
mdnsError = e.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_arpStatus = arp;
|
||||||
|
_arpError = arpError;
|
||||||
|
_mdnsStatus = mdns;
|
||||||
|
_mdnsError = mdnsError;
|
||||||
|
_statusReceivedAt = DateTime.now();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
double get _elapsed => _statusReceivedAt != null
|
||||||
|
? DateTime.now().difference(_statusReceivedAt!).inSeconds.toDouble()
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final (arpColor, arpText) = _resolveArp();
|
||||||
|
final (mdnsColor, mdnsText) = _resolveMdns();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => context.go('/status'),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Status', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_scannerRow(context, arpColor, 'ARP', arpText),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_scannerRow(context, mdnsColor, 'mDNS', mdnsText),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _scannerRow(
|
||||||
|
BuildContext context,
|
||||||
|
Color color,
|
||||||
|
String name,
|
||||||
|
String statusText,
|
||||||
|
) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, color: color, size: 12),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(name, style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
statusText,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Color, String) _resolveArp() {
|
||||||
|
if (_arpError != null || _arpStatus == null) {
|
||||||
|
return (Colors.red, 'Error');
|
||||||
|
}
|
||||||
|
if (_arpStatus!.isRunning) {
|
||||||
|
final secs = (_arpStatus!.runningForSeconds ?? 0) + _elapsed;
|
||||||
|
return (Colors.green, 'Running for ${formatSeconds(secs)}');
|
||||||
|
}
|
||||||
|
if (_arpStatus!.nextRunInSeconds != null) {
|
||||||
|
final remaining = (_arpStatus!.nextRunInSeconds! - _elapsed).clamp(
|
||||||
|
0.0,
|
||||||
|
double.infinity,
|
||||||
|
);
|
||||||
|
return (Colors.amber, 'Next run in ${formatSeconds(remaining)}');
|
||||||
|
}
|
||||||
|
return (Colors.grey, 'Not yet started');
|
||||||
|
}
|
||||||
|
|
||||||
|
(Color, String) _resolveMdns() {
|
||||||
|
if (_mdnsError != null || _mdnsStatus == null) {
|
||||||
|
return (Colors.red, 'Error');
|
||||||
|
}
|
||||||
|
if (_mdnsStatus!.isListening) {
|
||||||
|
if (_mdnsStatus!.lastDeviceSeenSecondsAgo != null) {
|
||||||
|
final secs = _mdnsStatus!.lastDeviceSeenSecondsAgo! + _elapsed;
|
||||||
|
return (Colors.green, 'Last device seen ${formatSeconds(secs)} ago');
|
||||||
|
}
|
||||||
|
return (Colors.green, 'No devices seen yet');
|
||||||
|
}
|
||||||
|
return (Colors.grey, 'Not yet started');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user