diff --git a/frontend/lib/navigation.dart b/frontend/lib/navigation.dart index 5dcc448..a2a8b27 100644 --- a/frontend/lib/navigation.dart +++ b/frontend/lib/navigation.dart @@ -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,9 +110,19 @@ class MainShell extends StatelessWidget { if (width < _mediumBreakpoint) { return Scaffold( appBar: _buildAppBar(context), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: child, + body: Column( + children: [ + const OfflineBanner(), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: child, + ), + ), + ], ), bottomNavigationBar: NavigationBar( selectedIndex: selectedIndex, @@ -156,9 +167,18 @@ class MainShell extends StatelessWidget { ), Expanded( child: Container( - padding: const EdgeInsets.all(20), color: Theme.of(context).colorScheme.surface, - child: child, + child: Column( + children: [ + const OfflineBanner(), + Expanded( + child: Padding( + padding: const EdgeInsets.all(20), + child: child, + ), + ), + ], + ), ), ), ], diff --git a/frontend/lib/utils/backend_reachability.dart b/frontend/lib/utils/backend_reachability.dart new file mode 100644 index 0000000..3139157 --- /dev/null +++ b/frontend/lib/utils/backend_reachability.dart @@ -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>? _subscription; + Future 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 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 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 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(); + } +} diff --git a/frontend/lib/utils/oott_api.dart b/frontend/lib/utils/oott_api.dart index e307166..a890096 100644 --- a/frontend/lib/utils/oott_api.dart +++ b/frontend/lib/utils/oott_api.dart @@ -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 diff --git a/frontend/lib/utils/polled_value.dart b/frontend/lib/utils/polled_value.dart index f32f28f..f621747 100644 --- a/frontend/lib/utils/polled_value.dart +++ b/frontend/lib/utils/polled_value.dart @@ -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 extends ChangeNotifier { PolledValue({ required Future Function({CancelToken? cancelToken}) fetch, @@ -15,13 +32,23 @@ class PolledValue extends ChangeNotifier { }) : _fetch = fetch, _pollInterval = pollInterval, _staleErrorAfter = staleErrorAfter { - _load(); - _pollTimer = Timer.periodic(pollInterval, (_) => _load()); + _reachability = BackendReachability.instance; + _reachability.addListener(_onReachabilityChanged); + if (!_reachability.isOnline) { + _pauseReasons.add(PolledPauseReason.unreachable); + } + if (_pauseReasons.isEmpty) { + _load(); + _startPolling(); + } } final Future Function({CancelToken? cancelToken}) _fetch; final Duration _pollInterval; final Duration _staleErrorAfter; + final Set _pauseReasons = {}; + late final BackendReachability _reachability; + Timer? _pollTimer; CancelToken? _cancelToken; bool _disposed = false; @@ -68,23 +95,49 @@ class PolledValue 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; - _load(); - _pollTimer = Timer.periodic(_pollInterval, (_) => _load()); + final removed = _pauseReasons.remove(reason); + if (removed && _pauseReasons.isEmpty) { + _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(); } } diff --git a/frontend/lib/widgets/arp_scanner_card.dart b/frontend/lib/widgets/arp_scanner_card.dart index 7779c25..716a24d 100644 --- a/frontend/lib/widgets/arp_scanner_card.dart +++ b/frontend/lib/widgets/arp_scanner_card.dart @@ -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 { @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 { ); } - 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 { ); } - (Color, String, String?) _resolveState() { - if (_polled.freshness == PolledFreshness.error) { + (Color, String, String?) _resolveState(PolledFreshness freshness) { + if (freshness == PolledFreshness.error) { return ( Colors.red, 'Error', diff --git a/frontend/lib/widgets/device_summary_card.dart b/frontend/lib/widgets/device_summary_card.dart index 553224e..0a9d1cc 100644 --- a/frontend/lib/widgets/device_summary_card.dart +++ b/frontend/lib/widgets/device_summary_card.dart @@ -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 { 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 { '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 { ); } - List _buildBody(BuildContext context) { - switch (_polled.freshness) { + List _buildBody(BuildContext context, PolledFreshness freshness) { + switch (freshness) { case PolledFreshness.initialLoading: return const [Center(child: CircularProgressIndicator())]; case PolledFreshness.error: diff --git a/frontend/lib/widgets/mdns_scanner_card.dart b/frontend/lib/widgets/mdns_scanner_card.dart index 9d63c92..2ed5383 100644 --- a/frontend/lib/widgets/mdns_scanner_card.dart +++ b/frontend/lib/widgets/mdns_scanner_card.dart @@ -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 { @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 { ); } - 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 { ); } - (Color, String, List) _resolveState() { - if (_polled.freshness == PolledFreshness.error) { + (Color, String, List) _resolveState(PolledFreshness freshness) { + if (freshness == PolledFreshness.error) { return ( Colors.red, 'Error', diff --git a/frontend/lib/widgets/offline_banner.dart b/frontend/lib/widgets/offline_banner.dart new file mode 100644 index 0000000..e3b4426 --- /dev/null +++ b/frontend/lib/widgets/offline_banner.dart @@ -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'), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/frontend/lib/widgets/polled_stale_indicator.dart b/frontend/lib/widgets/polled_stale_indicator.dart index 5baafb3..d72119a 100644 --- a/frontend/lib/widgets/polled_stale_indicator.dart +++ b/frontend/lib/widgets/polled_stale_indicator.dart @@ -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 { 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( diff --git a/frontend/lib/widgets/scanners_status_card.dart b/frontend/lib/widgets/scanners_status_card.dart index bf2ff10..7821c6d 100644 --- a/frontend/lib/widgets/scanners_status_card.dart +++ b/frontend/lib/widgets/scanners_status_card.dart @@ -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 @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 ); } - 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 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 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 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 ); } - (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 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!; diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index aaacbec..04d838b 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index fcce8f7..0b25a1a 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -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" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 8e910f0..f5c75e8 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -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 diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index a139215..4a2a3c1 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); EncrypterPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("EncrypterPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake index e2dbea0..575d2b9 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus encrypter url_launcher_windows )