mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Pause polling and show a banner when the backend is unreachable
Adds a centralized BackendReachability singleton fed by a Dio interceptor (connection-level failures only) and connectivity_plus. PolledValue subscribes to it and pauses on offline / reloads immediately on reconnect, replacing the previous per-card error cascade. MainShell renders a global OfflineBanner with Retry and Settings actions; cards downgrade error→stale when offline so the banner is the only red element. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8be65f3ffa
commit
b5da90be59
@@ -8,6 +8,7 @@ import 'devices/device_list.dart';
|
||||
import 'home/home_screen.dart';
|
||||
import 'status/status_screen.dart';
|
||||
import 'utils/pref_utils.dart';
|
||||
import 'widgets/offline_banner.dart';
|
||||
|
||||
// M3 window size class breakpoints
|
||||
const _mediumBreakpoint = 600.0;
|
||||
@@ -109,10 +110,20 @@ class MainShell extends StatelessWidget {
|
||||
if (width < _mediumBreakpoint) {
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(context),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
body: Column(
|
||||
children: [
|
||||
const OfflineBanner(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: (index) =>
|
||||
@@ -156,13 +167,22 @@ class MainShell extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
const OfflineBanner(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'oott_api.dart';
|
||||
|
||||
class BackendReachability extends ChangeNotifier {
|
||||
BackendReachability._internal() {
|
||||
_subscription = Connectivity().onConnectivityChanged.listen(
|
||||
_handleConnectivityChange,
|
||||
);
|
||||
Connectivity().checkConnectivity().then(_handleConnectivityChange);
|
||||
}
|
||||
|
||||
static final BackendReachability instance = BackendReachability._internal();
|
||||
|
||||
StreamSubscription<List<ConnectivityResult>>? _subscription;
|
||||
Future<void> Function()? _prober;
|
||||
|
||||
bool _deviceHasNetwork = true;
|
||||
bool _isBackendReachable = true;
|
||||
DateTime? _lastSuccessAt;
|
||||
DateTime? _lastFailureAt;
|
||||
String? _lastErrorMessage;
|
||||
bool _probing = false;
|
||||
|
||||
bool get deviceHasNetwork => _deviceHasNetwork;
|
||||
bool get isBackendReachable => _isBackendReachable;
|
||||
bool get isOnline => _deviceHasNetwork && _isBackendReachable;
|
||||
bool get isProbing => _probing;
|
||||
DateTime? get lastSuccessAt => _lastSuccessAt;
|
||||
DateTime? get lastFailureAt => _lastFailureAt;
|
||||
String? get lastErrorMessage => _lastErrorMessage;
|
||||
|
||||
void setProber(Future<void> Function() prober) {
|
||||
_prober = prober;
|
||||
}
|
||||
|
||||
void recordSuccess() {
|
||||
_lastSuccessAt = DateTime.now();
|
||||
_lastErrorMessage = null;
|
||||
final wasReachable = _isBackendReachable;
|
||||
_isBackendReachable = true;
|
||||
if (!wasReachable) notifyListeners();
|
||||
}
|
||||
|
||||
void recordFailure(DioException error) {
|
||||
if (!_isConnectionLevel(error)) return;
|
||||
_lastFailureAt = DateTime.now();
|
||||
_lastErrorMessage = dioErrorToUserMessage(error);
|
||||
final wasReachable = _isBackendReachable;
|
||||
_isBackendReachable = false;
|
||||
if (wasReachable) notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> probe() async {
|
||||
final prober = _prober;
|
||||
if (prober == null || _probing) return;
|
||||
_probing = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
await prober();
|
||||
} catch (_) {
|
||||
// Interceptor records the outcome; nothing else to do here.
|
||||
} finally {
|
||||
_probing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleConnectivityChange(List<ConnectivityResult> results) {
|
||||
final hasNetwork = results.any((r) => r != ConnectivityResult.none);
|
||||
if (hasNetwork == _deviceHasNetwork) return;
|
||||
_deviceHasNetwork = hasNetwork;
|
||||
notifyListeners();
|
||||
if (hasNetwork) {
|
||||
unawaited(probe());
|
||||
}
|
||||
}
|
||||
|
||||
bool _isConnectionLevel(DioException error) {
|
||||
if (error.type == DioExceptionType.cancel) return false;
|
||||
// Anything without a response from the server is a connection-level
|
||||
// failure (timeouts, connection refused, DNS, TLS, web fetch errors
|
||||
// surfaced as DioExceptionType.unknown, etc.).
|
||||
return error.response == null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
||||
import 'package:encrypter/encrypter/xor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:frontend/utils/backend_reachability.dart';
|
||||
import 'package:frontend/utils/pref_utils.dart';
|
||||
import '../model/arp_scanner_status.dart';
|
||||
import '../model/mdns_scanner_status.dart';
|
||||
@@ -36,6 +37,17 @@ RetryInterceptor _buildRetryInterceptor(Dio dio) => RetryInterceptor(
|
||||
},
|
||||
);
|
||||
|
||||
InterceptorsWrapper _buildReachabilityInterceptor() => InterceptorsWrapper(
|
||||
onResponse: (response, handler) {
|
||||
BackendReachability.instance.recordSuccess();
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
BackendReachability.instance.recordFailure(error);
|
||||
handler.next(error);
|
||||
},
|
||||
);
|
||||
|
||||
LogInterceptor _buildLogInterceptor() => LogInterceptor(
|
||||
request: true,
|
||||
requestHeader: false,
|
||||
@@ -106,9 +118,11 @@ class BackendAPI {
|
||||
),
|
||||
);
|
||||
_dio.interceptors.add(_buildRetryInterceptor(_dio));
|
||||
_dio.interceptors.add(_buildReachabilityInterceptor());
|
||||
if (kDebugMode) {
|
||||
_dio.interceptors.add(_buildLogInterceptor());
|
||||
}
|
||||
BackendReachability.instance.setProber(() => _dio.get('/test'));
|
||||
}
|
||||
|
||||
// Returns null if the test was successful, and a String with a message about the issue if not
|
||||
|
||||
@@ -3,10 +3,27 @@ import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'backend_reachability.dart';
|
||||
import 'oott_api.dart';
|
||||
|
||||
enum PolledFreshness { initialLoading, fresh, stale, error }
|
||||
|
||||
enum PolledPauseReason { manual, unreachable }
|
||||
|
||||
/// Returns the freshness of [polled] downgraded to [PolledFreshness.stale]
|
||||
/// when the backend is known to be unreachable and a previous value exists.
|
||||
/// This avoids showing per-card error chrome while the global offline banner
|
||||
/// is already communicating the connectivity issue.
|
||||
PolledFreshness effectiveFreshness(PolledValue polled) {
|
||||
final base = polled.freshness;
|
||||
if (base == PolledFreshness.error &&
|
||||
polled.value != null &&
|
||||
!BackendReachability.instance.isOnline) {
|
||||
return PolledFreshness.stale;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
class PolledValue<T> extends ChangeNotifier {
|
||||
PolledValue({
|
||||
required Future<T> Function({CancelToken? cancelToken}) fetch,
|
||||
@@ -15,13 +32,23 @@ class PolledValue<T> extends ChangeNotifier {
|
||||
}) : _fetch = fetch,
|
||||
_pollInterval = pollInterval,
|
||||
_staleErrorAfter = staleErrorAfter {
|
||||
_reachability = BackendReachability.instance;
|
||||
_reachability.addListener(_onReachabilityChanged);
|
||||
if (!_reachability.isOnline) {
|
||||
_pauseReasons.add(PolledPauseReason.unreachable);
|
||||
}
|
||||
if (_pauseReasons.isEmpty) {
|
||||
_load();
|
||||
_pollTimer = Timer.periodic(pollInterval, (_) => _load());
|
||||
_startPolling();
|
||||
}
|
||||
}
|
||||
|
||||
final Future<T> Function({CancelToken? cancelToken}) _fetch;
|
||||
final Duration _pollInterval;
|
||||
final Duration _staleErrorAfter;
|
||||
final Set<PolledPauseReason> _pauseReasons = {};
|
||||
late final BackendReachability _reachability;
|
||||
|
||||
Timer? _pollTimer;
|
||||
CancelToken? _cancelToken;
|
||||
bool _disposed = false;
|
||||
@@ -68,23 +95,49 @@ class PolledValue<T> extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void pause() {
|
||||
void _startPolling() {
|
||||
_pollTimer ??= Timer.periodic(_pollInterval, (_) => _load());
|
||||
}
|
||||
|
||||
void _stopPolling() {
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
_cancelToken?.cancel();
|
||||
}
|
||||
|
||||
void resume() {
|
||||
void pauseFor(PolledPauseReason reason) {
|
||||
final added = _pauseReasons.add(reason);
|
||||
if (added && _pauseReasons.length == 1) {
|
||||
_stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
void resumeFor(PolledPauseReason reason) {
|
||||
if (_disposed) return;
|
||||
final removed = _pauseReasons.remove(reason);
|
||||
if (removed && _pauseReasons.isEmpty) {
|
||||
_load();
|
||||
_pollTimer = Timer.periodic(_pollInterval, (_) => _load());
|
||||
_startPolling();
|
||||
}
|
||||
}
|
||||
|
||||
void pause() => pauseFor(PolledPauseReason.manual);
|
||||
|
||||
void resume() => resumeFor(PolledPauseReason.manual);
|
||||
|
||||
void _onReachabilityChanged() {
|
||||
if (_reachability.isOnline) {
|
||||
resumeFor(PolledPauseReason.unreachable);
|
||||
} else {
|
||||
pauseFor(PolledPauseReason.unreachable);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
_pollTimer?.cancel();
|
||||
_cancelToken?.cancel();
|
||||
_reachability.removeListener(_onReachabilityChanged);
|
||||
_stopPolling();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../model/arp_scanner_status.dart';
|
||||
import '../utils/backend_reachability.dart';
|
||||
import '../utils/duration_formatter.dart';
|
||||
import '../utils/oott_api.dart';
|
||||
import '../utils/polled_value.dart';
|
||||
@@ -44,9 +45,10 @@ class _ArpScannerCardState extends State<ArpScannerCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _polled,
|
||||
listenable: Listenable.merge([_polled, BackendReachability.instance]),
|
||||
builder: (context, _) {
|
||||
if (_polled.freshness == PolledFreshness.initialLoading) {
|
||||
final freshness = effectiveFreshness(_polled);
|
||||
if (freshness == PolledFreshness.initialLoading) {
|
||||
return const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
@@ -55,8 +57,8 @@ class _ArpScannerCardState extends State<ArpScannerCard> {
|
||||
);
|
||||
}
|
||||
|
||||
final (color, label, sublabel) = _resolveState();
|
||||
final isStale = _polled.freshness == PolledFreshness.stale;
|
||||
final (color, label, sublabel) = _resolveState(freshness);
|
||||
final isStale = freshness == PolledFreshness.stale;
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -111,8 +113,8 @@ class _ArpScannerCardState extends State<ArpScannerCard> {
|
||||
);
|
||||
}
|
||||
|
||||
(Color, String, String?) _resolveState() {
|
||||
if (_polled.freshness == PolledFreshness.error) {
|
||||
(Color, String, String?) _resolveState(PolledFreshness freshness) {
|
||||
if (freshness == PolledFreshness.error) {
|
||||
return (
|
||||
Colors.red,
|
||||
'Error',
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../model/device_summary.dart';
|
||||
import '../utils/backend_reachability.dart';
|
||||
import '../utils/oott_api.dart';
|
||||
import '../utils/polled_value.dart';
|
||||
import 'polled_stale_indicator.dart';
|
||||
@@ -42,8 +43,12 @@ class _DeviceSummaryCardState extends State<DeviceSummaryCard> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListenableBuilder(
|
||||
listenable: _polled,
|
||||
listenable: Listenable.merge([
|
||||
_polled,
|
||||
BackendReachability.instance,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
final freshness = effectiveFreshness(_polled);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -53,14 +58,14 @@ class _DeviceSummaryCardState extends State<DeviceSummaryCard> {
|
||||
'Devices',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (_polled.freshness == PolledFreshness.stale) ...[
|
||||
if (freshness == PolledFreshness.stale) ...[
|
||||
const SizedBox(width: 6),
|
||||
PolledStaleIndicator(polled: _polled),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
..._buildBody(context),
|
||||
..._buildBody(context, freshness),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -70,8 +75,8 @@ class _DeviceSummaryCardState extends State<DeviceSummaryCard> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildBody(BuildContext context) {
|
||||
switch (_polled.freshness) {
|
||||
List<Widget> _buildBody(BuildContext context, PolledFreshness freshness) {
|
||||
switch (freshness) {
|
||||
case PolledFreshness.initialLoading:
|
||||
return const [Center(child: CircularProgressIndicator())];
|
||||
case PolledFreshness.error:
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../model/mdns_scanner_status.dart';
|
||||
import '../utils/backend_reachability.dart';
|
||||
import '../utils/duration_formatter.dart';
|
||||
import '../utils/oott_api.dart';
|
||||
import '../utils/polled_value.dart';
|
||||
@@ -44,9 +45,10 @@ class _MdnsScannerCardState extends State<MdnsScannerCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _polled,
|
||||
listenable: Listenable.merge([_polled, BackendReachability.instance]),
|
||||
builder: (context, _) {
|
||||
if (_polled.freshness == PolledFreshness.initialLoading) {
|
||||
final freshness = effectiveFreshness(_polled);
|
||||
if (freshness == PolledFreshness.initialLoading) {
|
||||
return const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
@@ -55,8 +57,8 @@ class _MdnsScannerCardState extends State<MdnsScannerCard> {
|
||||
);
|
||||
}
|
||||
|
||||
final (color, label, sublabels) = _resolveState();
|
||||
final isStale = _polled.freshness == PolledFreshness.stale;
|
||||
final (color, label, sublabels) = _resolveState(freshness);
|
||||
final isStale = freshness == PolledFreshness.stale;
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -111,8 +113,8 @@ class _MdnsScannerCardState extends State<MdnsScannerCard> {
|
||||
);
|
||||
}
|
||||
|
||||
(Color, String, List<String>) _resolveState() {
|
||||
if (_polled.freshness == PolledFreshness.error) {
|
||||
(Color, String, List<String>) _resolveState(PolledFreshness freshness) {
|
||||
if (freshness == PolledFreshness.error) {
|
||||
return (
|
||||
Colors.red,
|
||||
'Error',
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../utils/backend_reachability.dart';
|
||||
|
||||
class OfflineBanner extends StatelessWidget {
|
||||
const OfflineBanner({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reachability = BackendReachability.instance;
|
||||
return ListenableBuilder(
|
||||
listenable: reachability,
|
||||
builder: (context, _) {
|
||||
if (reachability.isOnline) return const SizedBox.shrink();
|
||||
final theme = Theme.of(context);
|
||||
final message = !reachability.deviceHasNetwork
|
||||
? 'No network connection.'
|
||||
: reachability.isProbing
|
||||
? 'Reconnecting to backend…'
|
||||
: reachability.lastErrorMessage ?? 'Cannot reach backend.';
|
||||
return Material(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_off,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: reachability.isProbing
|
||||
? null
|
||||
: () => reachability.probe(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/settings'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
child: const Text('Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utils/backend_reachability.dart';
|
||||
import '../utils/duration_formatter.dart';
|
||||
import '../utils/polled_value.dart';
|
||||
|
||||
@@ -39,9 +40,15 @@ class _PolledStaleIndicatorState extends State<PolledStaleIndicator> {
|
||||
DateTime.now().difference(lastSuccessAt).inSeconds.toDouble(),
|
||||
)
|
||||
: 'a while';
|
||||
final message = widget.polled.lastErrorMessage != null
|
||||
? 'Last updated $ago ago — ${widget.polled.lastErrorMessage}'
|
||||
: 'Last updated $ago ago';
|
||||
final isOffline = !BackendReachability.instance.isOnline;
|
||||
final String message;
|
||||
if (isOffline) {
|
||||
message = 'Offline — last updated $ago ago';
|
||||
} else if (widget.polled.lastErrorMessage != null) {
|
||||
message = 'Last updated $ago ago — ${widget.polled.lastErrorMessage}';
|
||||
} else {
|
||||
message = 'Last updated $ago ago';
|
||||
}
|
||||
return Tooltip(
|
||||
message: message,
|
||||
child: Icon(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../model/arp_scanner_status.dart';
|
||||
import '../model/mdns_scanner_status.dart';
|
||||
import '../navigation.dart';
|
||||
import '../utils/backend_reachability.dart';
|
||||
import '../utils/duration_formatter.dart';
|
||||
import '../utils/oott_api.dart';
|
||||
import '../utils/polled_value.dart';
|
||||
@@ -101,10 +102,16 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: Listenable.merge([_arp, _mdns]),
|
||||
listenable: Listenable.merge([
|
||||
_arp,
|
||||
_mdns,
|
||||
BackendReachability.instance,
|
||||
]),
|
||||
builder: (context, _) {
|
||||
if (_arp.freshness == PolledFreshness.initialLoading ||
|
||||
_mdns.freshness == PolledFreshness.initialLoading) {
|
||||
final arpFreshness = effectiveFreshness(_arp);
|
||||
final mdnsFreshness = effectiveFreshness(_mdns);
|
||||
if (arpFreshness == PolledFreshness.initialLoading ||
|
||||
mdnsFreshness == PolledFreshness.initialLoading) {
|
||||
return const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
@@ -113,8 +120,8 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
);
|
||||
}
|
||||
|
||||
final (arpColor, arpText) = _resolveArp();
|
||||
final (mdnsColor, mdnsText) = _resolveMdns();
|
||||
final (arpColor, arpText) = _resolveArp(arpFreshness);
|
||||
final (mdnsColor, mdnsText) = _resolveMdns(mdnsFreshness);
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -130,9 +137,23 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_scannerRow(context, arpColor, 'ARP', arpText, _arp),
|
||||
_scannerRow(
|
||||
context,
|
||||
arpColor,
|
||||
'ARP',
|
||||
arpText,
|
||||
_arp,
|
||||
arpFreshness,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_scannerRow(context, mdnsColor, 'mDNS', mdnsText, _mdns),
|
||||
_scannerRow(
|
||||
context,
|
||||
mdnsColor,
|
||||
'mDNS',
|
||||
mdnsText,
|
||||
_mdns,
|
||||
mdnsFreshness,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -148,9 +169,10 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
String name,
|
||||
String statusText,
|
||||
PolledValue polled,
|
||||
PolledFreshness freshness,
|
||||
) {
|
||||
Widget dot = Icon(Icons.circle, color: color, size: 12);
|
||||
if (polled.freshness == PolledFreshness.error) {
|
||||
if (freshness == PolledFreshness.error) {
|
||||
dot = Tooltip(message: polled.lastErrorMessage ?? 'Error', child: dot);
|
||||
}
|
||||
return Row(
|
||||
@@ -161,7 +183,7 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
child: Row(
|
||||
children: [
|
||||
Text(name, style: Theme.of(context).textTheme.bodyMedium),
|
||||
if (polled.freshness == PolledFreshness.stale) ...[
|
||||
if (freshness == PolledFreshness.stale) ...[
|
||||
const SizedBox(width: 6),
|
||||
PolledStaleIndicator(polled: polled),
|
||||
],
|
||||
@@ -179,8 +201,8 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
);
|
||||
}
|
||||
|
||||
(Color, String) _resolveArp() {
|
||||
if (_arp.freshness == PolledFreshness.error) {
|
||||
(Color, String) _resolveArp(PolledFreshness freshness) {
|
||||
if (freshness == PolledFreshness.error) {
|
||||
return (Colors.red, 'Error');
|
||||
}
|
||||
final status = _arp.value!;
|
||||
@@ -202,8 +224,8 @@ class _ScannersStatusCardState extends State<ScannersStatusCard>
|
||||
return (Colors.grey, 'Not yet started');
|
||||
}
|
||||
|
||||
(Color, String) _resolveMdns() {
|
||||
if (_mdns.freshness == PolledFreshness.error) {
|
||||
(Color, String) _resolveMdns(PolledFreshness freshness) {
|
||||
if (freshness == PolledFreshness.error) {
|
||||
return (Colors.red, 'Error');
|
||||
}
|
||||
final status = _mdns.value!;
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import connectivity_plus
|
||||
import encrypter
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
EncrypterPlugin.register(with: registry.registrar(forPlugin: "EncrypterPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
||||
+49
-1
@@ -65,6 +65,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
connectivity_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "62ffa266d9a23b79fb3fcbc206afc00bb979417ba57b1324c546b5aab95ba057"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.1.1"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -81,6 +97,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.13"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -336,6 +360,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nm
|
||||
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -408,6 +440,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -669,6 +709,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -678,5 +726,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.8 <4.0.0"
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies:
|
||||
provider: ^6.1.5
|
||||
dio: ^5.9.1
|
||||
dio_smart_retry: ^7.0.1
|
||||
connectivity_plus: ^7.0.0
|
||||
encrypter: ^2.0.0
|
||||
shared_preferences: ^2.5.4
|
||||
fl_chart: ^1.2.0
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <encrypter/encrypter_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
EncrypterPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("EncrypterPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
connectivity_plus
|
||||
encrypter
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user