Refactor frontend: extract widgets and eliminate duplicated scanner state

- Convert ArpScannerCard to a self-managing StatefulWidget (owns its own 5s refresh + 1s tick timers), removing the duplicated state management that existed in both HomeScreen and StatusScreen
- Extract NotificationCard to home/notification_card.dart
- Extract DeviceSummaryCard (with its 1-min refresh timer) to widgets/device_summary_card.dart
- HomeScreen drops from 515 to 253 lines; StatusScreen from 82 to 19 lines

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-28 19:23:27 -04:00
co-authored by Claude Sonnet 4.6
parent 251d8a479e
commit f113da9cd0
5 changed files with 322 additions and 362 deletions
+69 -22
View File
@@ -1,23 +1,70 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:frontend/model/arp_scanner_status.dart';
class ArpScannerCard extends StatelessWidget {
final ArpScannerStatus? status;
final DateTime? statusReceivedAt;
final String? error;
final bool isLoading;
import '../model/arp_scanner_status.dart';
import '../utils/oott_api.dart';
const ArpScannerCard({
super.key,
this.status,
this.statusReceivedAt,
this.error,
this.isLoading = false,
});
class ArpScannerCard extends StatefulWidget {
const ArpScannerCard({super.key});
@override
State<ArpScannerCard> createState() => _ArpScannerCardState();
}
class _ArpScannerCardState extends State<ArpScannerCard> {
ArpScannerStatus? _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.getArpScannerStatus();
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) {
if (_isLoading) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16),
@@ -64,7 +111,7 @@ class ArpScannerCard extends StatelessWidget {
}
(Color, String, String?) _resolveState(BuildContext context) {
if (error != null || status == null) {
if (_error != null || _status == null) {
return (
Colors.red,
'Error',
@@ -72,18 +119,18 @@ class ArpScannerCard extends StatelessWidget {
);
}
final elapsed = statusReceivedAt != null
? DateTime.now().difference(statusReceivedAt!).inSeconds.toDouble()
final elapsed = _statusReceivedAt != null
? DateTime.now().difference(_statusReceivedAt!).inSeconds.toDouble()
: 0.0;
if (status!.isRunning) {
final sub = status!.runningForSeconds != null
? 'Running for ${_formatSeconds(status!.runningForSeconds! + elapsed)}'
if (_status!.isRunning) {
final sub = _status!.runningForSeconds != null
? 'Running for ${_formatSeconds(_status!.runningForSeconds! + elapsed)}'
: null;
return (Colors.green, 'Running', sub);
}
if (status!.nextRunInSeconds != null) {
final remaining = (status!.nextRunInSeconds! - elapsed).clamp(
if (_status!.nextRunInSeconds != null) {
final remaining = (_status!.nextRunInSeconds! - elapsed).clamp(
0.0,
double.infinity,
);