refactor(settings): migrate wifi_settings God VM to Riverpod

This commit is contained in:
2026-04-22 02:37:21 +02:00
parent 86642b9587
commit 746230a541
18 changed files with 1252 additions and 877 deletions

View File

@@ -0,0 +1,15 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:settings/src/core/providers/wifi_repository_provider.dart';
import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart';
part 'saved_wifi_networks_provider.g.dart';
@riverpod
Future<List<WifiNetworkEntity>> savedWifiNetworks(
Ref ref,
String deviceIdentificator,
) async {
return ref
.read(wifiRepositoryProvider)
.getWifiNetworks(deviceIdentificator: deviceIdentificator);
}

View File

@@ -0,0 +1,87 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'saved_wifi_networks_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(savedWifiNetworks)
const savedWifiNetworksProvider = SavedWifiNetworksFamily._();
final class SavedWifiNetworksProvider
extends
$FunctionalProvider<
AsyncValue<List<WifiNetworkEntity>>,
List<WifiNetworkEntity>,
FutureOr<List<WifiNetworkEntity>>
>
with
$FutureModifier<List<WifiNetworkEntity>>,
$FutureProvider<List<WifiNetworkEntity>> {
const SavedWifiNetworksProvider._({
required SavedWifiNetworksFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'savedWifiNetworksProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$savedWifiNetworksHash();
@override
String toString() {
return r'savedWifiNetworksProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<WifiNetworkEntity>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<WifiNetworkEntity>> create(Ref ref) {
final argument = this.argument as String;
return savedWifiNetworks(ref, argument);
}
@override
bool operator ==(Object other) {
return other is SavedWifiNetworksProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$savedWifiNetworksHash() => r'421ac8fad874ad3bd90efdf6ea4a3c43a478cb88';
final class SavedWifiNetworksFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<WifiNetworkEntity>>, String> {
const SavedWifiNetworksFamily._()
: super(
retry: null,
name: r'savedWifiNetworksProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
SavedWifiNetworksProvider call(String deviceIdentificator) =>
SavedWifiNetworksProvider._(argument: deviceIdentificator, from: this);
@override
String toString() => r'savedWifiNetworksProvider';
}

View File

@@ -0,0 +1,8 @@
import 'package:get_it/get_it.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
part 'web_socket_service_provider.g.dart';
@Riverpod(keepAlive: true)
WebSocketService webSocketService(Ref ref) => GetIt.I<WebSocketService>();

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'web_socket_service_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(webSocketService)
const webSocketServiceProvider = WebSocketServiceProvider._();
final class WebSocketServiceProvider
extends
$FunctionalProvider<
WebSocketService,
WebSocketService,
WebSocketService
>
with $Provider<WebSocketService> {
const WebSocketServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'webSocketServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$webSocketServiceHash();
@$internal
@override
$ProviderElement<WebSocketService> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
WebSocketService create(Ref ref) {
return webSocketService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(WebSocketService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<WebSocketService>(value),
);
}
}
String _$webSocketServiceHash() => r'4a522db698e3aced3c90dc3ea1c2bf9e85f88830';

View File

@@ -0,0 +1,35 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'wifi_connect_form_provider.g.dart';
class WifiConnectFormState {
const WifiConnectFormState({
this.canSave = false,
this.obscurePassword = true,
});
final bool canSave;
final bool obscurePassword;
WifiConnectFormState copyWith({bool? canSave, bool? obscurePassword}) {
return WifiConnectFormState(
canSave: canSave ?? this.canSave,
obscurePassword: obscurePassword ?? this.obscurePassword,
);
}
}
@riverpod
class WifiConnectForm extends _$WifiConnectForm {
@override
WifiConnectFormState build() => const WifiConnectFormState();
void setCanSave(bool value) {
if (value == state.canSave) return;
state = state.copyWith(canSave: value);
}
void togglePasswordVisibility() {
state = state.copyWith(obscurePassword: !state.obscurePassword);
}
}

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wifi_connect_form_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(WifiConnectForm)
const wifiConnectFormProvider = WifiConnectFormProvider._();
final class WifiConnectFormProvider
extends $NotifierProvider<WifiConnectForm, WifiConnectFormState> {
const WifiConnectFormProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'wifiConnectFormProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$wifiConnectFormHash();
@$internal
@override
WifiConnectForm create() => WifiConnectForm();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(WifiConnectFormState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<WifiConnectFormState>(value),
);
}
}
String _$wifiConnectFormHash() => r'44d3409aa045e7a50174ce54e69a24beaf128566';
abstract class _$WifiConnectForm extends $Notifier<WifiConnectFormState> {
WifiConnectFormState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<WifiConnectFormState, WifiConnectFormState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<WifiConnectFormState, WifiConnectFormState>,
WifiConnectFormState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -0,0 +1,105 @@
import 'dart:async';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:settings/src/core/providers/wifi_repository_provider.dart';
import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:uuid/uuid.dart';
part 'wifi_controller.g.dart';
enum WifiControllerAction { connectAndSave, remove, set }
@riverpod
class WifiController extends _$WifiController {
WifiControllerAction? _lastAction;
WifiControllerAction? get lastAction => _lastAction;
@override
FutureOr<void> build() {}
Future<void> connectAndSave({
required String deviceIdentificator,
required String ssid,
required String bssid,
required String password,
}) async {
_lastAction = WifiControllerAction.connectAndSave;
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(commandsRepositoryProvider).send(
request: SendCommandRequestModel(
device: deviceIdentificator,
command: DeviceCommand.setWifi,
data: {'ssid': ssid, 'bssid': bssid, 'password': password},
),
);
await ref.read(wifiRepositoryProvider).createWifiNetwork(
id: const Uuid().v4(),
deviceIdentificator: deviceIdentificator,
ssid: ssid,
bssid: bssid,
password: password,
);
ref.invalidate(savedWifiNetworksProvider(deviceIdentificator));
final networks = await ref
.read(savedWifiNetworksProvider(deviceIdentificator).future);
unawaited(
ref
.read(sfTrackingProvider)
.legacySettingsWifiAdded(totalCount: networks.length),
);
});
}
Future<void> removeNetwork({
required String deviceIdentificator,
required String networkId,
}) async {
_lastAction = WifiControllerAction.remove;
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref
.read(wifiRepositoryProvider)
.deleteWifiNetwork(networkId: networkId);
ref.invalidate(savedWifiNetworksProvider(deviceIdentificator));
final networks = await ref
.read(savedWifiNetworksProvider(deviceIdentificator).future);
unawaited(
ref
.read(sfTrackingProvider)
.legacySettingsWifiRemoved(totalCount: networks.length),
);
});
}
Future<void> setNetwork({
required String deviceIdentificator,
required WifiNetworkEntity network,
}) async {
_lastAction = WifiControllerAction.set;
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(commandsRepositoryProvider).send(
request: SendCommandRequestModel(
device: deviceIdentificator,
command: DeviceCommand.setWifi,
data: {
'ssid': network.ssid,
'bssid': network.bssid,
'password': network.password ?? '',
},
),
);
unawaited(
ref
.read(wifiCurrentNetworkProvider.notifier)
.requestCurrent(deviceIdentificator),
);
});
}
}

View File

@@ -0,0 +1,55 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wifi_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(WifiController)
const wifiControllerProvider = WifiControllerProvider._();
final class WifiControllerProvider
extends $AsyncNotifierProvider<WifiController, void> {
const WifiControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'wifiControllerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$wifiControllerHash();
@$internal
@override
WifiController create() => WifiController();
}
String _$wifiControllerHash() => r'498786d32573636cb57284969a25f64936f91a6e';
abstract class _$WifiController extends $AsyncNotifier<void> {
FutureOr<void> build();
@$mustCallSuper
@override
void runBuild() {
build();
final ref = this.ref as $Ref<AsyncValue<void>, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<void>, void>,
AsyncValue<void>,
Object?,
Object?
>;
element.handleValue(ref, null);
}
}

View File

@@ -0,0 +1,77 @@
import 'dart:async';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
part 'wifi_current_network_provider.g.dart';
class WifiCurrentNetworkState {
const WifiCurrentNetworkState({this.network, this.isLoading = false});
final WifiNetworkEntity? network;
final bool isLoading;
WifiCurrentNetworkState copyWith({
WifiNetworkEntity? network,
bool? isLoading,
bool clearNetwork = false,
}) {
return WifiCurrentNetworkState(
network: clearNetwork ? null : (network ?? this.network),
isLoading: isLoading ?? this.isLoading,
);
}
}
@riverpod
class WifiCurrentNetwork extends _$WifiCurrentNetwork {
Timer? _timeout;
@override
WifiCurrentNetworkState build() {
final sub = ref
.read(webSocketServiceProvider)
.events
.listen(_onEvent);
ref.onDispose(() {
sub.cancel();
_timeout?.cancel();
});
return const WifiCurrentNetworkState();
}
void _onEvent(WebSocketEvent event) {
if (event is! WifiEvent) return;
_timeout?.cancel();
state = state.copyWith(
network: WifiNetworkEntity(
id: '',
ssid: event.ssid,
bssid: event.bssid,
password: event.password,
),
isLoading: false,
);
}
Future<void> requestCurrent(String deviceIdentificator) async {
state = state.copyWith(isLoading: true);
try {
await ref.read(commandsRepositoryProvider).send(
request: SendCommandRequestModel(
device: deviceIdentificator,
command: DeviceCommand.wifiCurrent,
),
);
} catch (_) {
state = state.copyWith(isLoading: false);
return;
}
_timeout?.cancel();
_timeout = Timer(const Duration(seconds: 15), () {
if (state.isLoading) state = state.copyWith(isLoading: false);
});
}
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wifi_current_network_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(WifiCurrentNetwork)
const wifiCurrentNetworkProvider = WifiCurrentNetworkProvider._();
final class WifiCurrentNetworkProvider
extends $NotifierProvider<WifiCurrentNetwork, WifiCurrentNetworkState> {
const WifiCurrentNetworkProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'wifiCurrentNetworkProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$wifiCurrentNetworkHash();
@$internal
@override
WifiCurrentNetwork create() => WifiCurrentNetwork();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(WifiCurrentNetworkState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<WifiCurrentNetworkState>(value),
);
}
}
String _$wifiCurrentNetworkHash() =>
r'308b925398753ccb641bd602f4efa61f8b5bc58f';
abstract class _$WifiCurrentNetwork extends $Notifier<WifiCurrentNetworkState> {
WifiCurrentNetworkState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<WifiCurrentNetworkState, WifiCurrentNetworkState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<WifiCurrentNetworkState, WifiCurrentNetworkState>,
WifiCurrentNetworkState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -0,0 +1,116 @@
import 'dart:async';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:settings/src/features/wifi_settings/domain/entities/scanned_wifi_network.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
part 'wifi_scan_provider.g.dart';
class WifiScanTimeoutException implements Exception {
const WifiScanTimeoutException();
}
class WifiScanState {
const WifiScanState({
this.networks = const [],
this.isScanning = false,
this.secondsRemaining = 0,
});
final List<ScannedWifiNetwork> networks;
final bool isScanning;
final int secondsRemaining;
WifiScanState copyWith({
List<ScannedWifiNetwork>? networks,
bool? isScanning,
int? secondsRemaining,
}) {
return WifiScanState(
networks: networks ?? this.networks,
isScanning: isScanning ?? this.isScanning,
secondsRemaining: secondsRemaining ?? this.secondsRemaining,
);
}
}
@riverpod
class WifiScan extends _$WifiScan {
static const _scanDurationSeconds = 30;
Timer? _countdown;
Completer<void>? _completer;
@override
WifiScanState build() {
final sub = ref
.read(webSocketServiceProvider)
.events
.listen(_onEvent);
ref.onDispose(() {
sub.cancel();
_countdown?.cancel();
if (_completer != null && !_completer!.isCompleted) {
_completer!.complete();
}
});
return const WifiScanState();
}
void _onEvent(WebSocketEvent event) {
if (event is! WifiSearchEvent) return;
_countdown?.cancel();
final networks = event.wifis
.map(
(wifi) => ScannedWifiNetwork(
ssid: wifi['ssid'] as String? ?? '',
bssid: wifi['bssid'] as String? ?? '',
),
)
.where((network) => network.ssid.isNotEmpty)
.toList();
state = state.copyWith(
networks: networks,
isScanning: false,
secondsRemaining: 0,
);
if (_completer != null && !_completer!.isCompleted) {
_completer!.complete();
}
}
Future<void> scan(String deviceIdentificator) async {
state = state.copyWith(
isScanning: true,
secondsRemaining: _scanDurationSeconds,
);
await ref.read(commandsRepositoryProvider).send(
request: SendCommandRequestModel(
device: deviceIdentificator,
command: DeviceCommand.wifiSearch,
),
);
_completer = Completer<void>();
_countdown?.cancel();
_countdown = Timer.periodic(const Duration(seconds: 1), (_) {
final remaining = state.secondsRemaining - 1;
if (remaining <= 0) {
_countdown?.cancel();
if (state.isScanning) {
state = state.copyWith(isScanning: false, secondsRemaining: 0);
if (!_completer!.isCompleted) {
_completer!.completeError(const WifiScanTimeoutException());
}
}
} else {
state = state.copyWith(secondsRemaining: remaining);
}
});
return _completer!.future;
}
}

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wifi_scan_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(WifiScan)
const wifiScanProvider = WifiScanProvider._();
final class WifiScanProvider
extends $NotifierProvider<WifiScan, WifiScanState> {
const WifiScanProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'wifiScanProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$wifiScanHash();
@$internal
@override
WifiScan create() => WifiScan();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(WifiScanState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<WifiScanState>(value),
);
}
}
String _$wifiScanHash() => r'8a0d4bbb2c7514505c6868691b32720846f0470a';
abstract class _$WifiScan extends $Notifier<WifiScanState> {
WifiScanState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<WifiScanState, WifiScanState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<WifiScanState, WifiScanState>,
WifiScanState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,356 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:uuid/uuid.dart';
import '../../../../core/domain/repositories/wifi_repository.dart';
import '../../../../core/providers/wifi_repository_provider.dart';
import '../../domain/entities/scanned_wifi_network.dart';
import '../../domain/entities/wifi_network_entity.dart';
import 'wifi_settings_view_state.dart';
final wifiSettingsViewModelProvider =
NotifierProvider.autoDispose<WifiSettingsViewModel, WifiSettingsViewState>(
WifiSettingsViewModel.new,
);
class WifiSettingsViewModel extends Notifier<WifiSettingsViewState> {
late final WifiRepository _repository;
late final CommandsRepository _commands;
late final SfTrackingRepository _tracking;
late final WebSocketService _webSocket;
StreamSubscription<WebSocketEvent>? _webSocketSubscription;
Timer? _scanCountdown;
Timer? _currentNetworkTimeout;
static const _scanDurationSeconds = 30;
@override
WifiSettingsViewState build() {
_repository = ref.read(wifiRepositoryProvider);
_commands = ref.read(commandsRepositoryProvider);
_tracking = ref.read(sfTrackingProvider);
_webSocket = GetIt.I<WebSocketService>();
_webSocketSubscription = _webSocket.events.listen(_onWebSocketEvent);
ref.onDispose(() {
_webSocketSubscription?.cancel();
_scanCountdown?.cancel();
_currentNetworkTimeout?.cancel();
});
Future.microtask(_load);
return const WifiSettingsViewState();
}
String? get _identificator =>
ref.read(selectedDeviceProvider).value?.identificator;
void _onWebSocketEvent(WebSocketEvent event) {
switch (event) {
case WifiEvent():
debugPrint('[WiFi] WS received WIFI_CURRENT: ssid=${event.ssid} bssid=${event.bssid}');
_currentNetworkTimeout?.cancel();
final network = WifiNetworkEntity(
id: '',
ssid: event.ssid,
bssid: event.bssid,
password: event.password,
);
state = state.copyWith(
currentNetwork: network,
isConnecting: false,
isLoadingCurrentNetwork: false,
);
case WifiSearchEvent():
debugPrint('[WiFi] WS received WIFI_SEARCH: ${event.wifis.length} networks');
_scanCountdown?.cancel();
final networks = event.wifis
.map(
(wifi) => ScannedWifiNetwork(
ssid: wifi['ssid'] as String? ?? '',
bssid: wifi['bssid'] as String? ?? '',
),
)
.where((network) => network.ssid.isNotEmpty)
.toList();
state = state.copyWith(
availableNetworks: networks,
isScanning: false,
scanSecondsRemaining: 0,
);
default:
break;
}
}
Future<void> _load() async {
final identificator = _identificator;
if (identificator == null) {
state = state.copyWith(isLoading: false);
return;
}
try {
debugPrint('[WiFi] loading saved networks for $identificator');
final networks = await _repository.getWifiNetworks(
deviceIdentificator: identificator,
);
if (!ref.mounted) return;
debugPrint('[WiFi] loaded ${networks.length} saved networks');
state = state.copyWith(
savedNetworks: networks,
isLoading: false,
isLoadingCurrentNetwork: true,
);
debugPrint('[WiFi] sending WIFI_CURRENT command');
unawaited(
_commands
.send(
request: SendCommandRequestModel(
device: identificator,
command: DeviceCommand.wifiCurrent,
),
)
.catchError((_) {
debugPrint('[WiFi] WIFI_CURRENT command failed');
if (ref.mounted) {
state = state.copyWith(isLoadingCurrentNetwork: false);
}
}),
);
_currentNetworkTimeout?.cancel();
_currentNetworkTimeout = Timer(const Duration(seconds: 15), () {
if (!ref.mounted) return;
if (state.isLoadingCurrentNetwork) {
state = state.copyWith(isLoadingCurrentNetwork: false);
}
});
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
error: WifiSettingsError.loadFailed,
);
}
}
Future<void> scanNetworks() async {
final identificator = _identificator;
if (identificator == null) return;
state = state.copyWith(
isScanning: true,
scanSecondsRemaining: _scanDurationSeconds,
error: null,
);
try {
debugPrint('[WiFi] sending WIFI_SEARCH command');
await _commands.send(
request: SendCommandRequestModel(
device: identificator,
command: DeviceCommand.wifiSearch,
),
);
debugPrint('[WiFi] WIFI_SEARCH sent, waiting for WS response (${_scanDurationSeconds}s timeout)');
_scanCountdown?.cancel();
_scanCountdown = Timer.periodic(const Duration(seconds: 1), (_) {
if (!ref.mounted) {
_scanCountdown?.cancel();
return;
}
final remaining = state.scanSecondsRemaining - 1;
if (remaining <= 0) {
_scanCountdown?.cancel();
if (state.isScanning) {
state = state.copyWith(
isScanning: false,
scanSecondsRemaining: 0,
error: WifiSettingsError.scanFailed,
);
}
} else {
state = state.copyWith(scanSecondsRemaining: remaining);
}
});
} catch (_) {
if (!ref.mounted) return;
_scanCountdown?.cancel();
state = state.copyWith(
isScanning: false,
scanSecondsRemaining: 0,
error: WifiSettingsError.scanFailed,
);
}
}
Future<void> connectAndSave({
required String ssid,
required String bssid,
required String password,
}) async {
final identificator = _identificator;
if (identificator == null) return;
state = state.copyWith(isConnecting: true, error: null, success: null);
try {
debugPrint('[WiFi] connecting to $ssid ($bssid)');
await _commands.send(
request: SendCommandRequestModel(
device: identificator,
command: DeviceCommand.setWifi,
data: {'ssid': ssid, 'bssid': bssid, 'password': password},
),
);
debugPrint('[WiFi] SET_WIFI sent, saving network');
final id = const Uuid().v4();
await _repository.createWifiNetwork(
id: id,
deviceIdentificator: identificator,
ssid: ssid,
bssid: bssid,
password: password,
);
if (!ref.mounted) return;
final networks = await _repository.getWifiNetworks(
deviceIdentificator: identificator,
);
if (!ref.mounted) return;
debugPrint('[WiFi] network saved, total: ${networks.length}');
unawaited(_tracking.legacySettingsWifiAdded(totalCount: networks.length));
state = state.copyWith(
savedNetworks: networks,
isConnecting: false,
success: WifiSettingsSuccess.networkSaved,
);
} catch (e) {
debugPrint('[WiFi] connectAndSave failed: $e');
if (!ref.mounted) return;
state = state.copyWith(
isConnecting: false,
error: WifiSettingsError.saveFailed,
);
}
}
Future<void> removeNetwork(String networkId) async {
final identificator = _identificator;
if (identificator == null) return;
state = state.copyWith(isSaving: true, error: null, success: null);
try {
debugPrint('[WiFi] deleting network $networkId');
await _repository.deleteWifiNetwork(networkId: networkId);
if (!ref.mounted) return;
final networks = await _repository.getWifiNetworks(
deviceIdentificator: identificator,
);
if (!ref.mounted) return;
debugPrint('[WiFi] network deleted, remaining: ${networks.length}');
unawaited(
_tracking.legacySettingsWifiRemoved(totalCount: networks.length),
);
state = state.copyWith(
savedNetworks: networks,
isSaving: false,
success: WifiSettingsSuccess.networkDeleted,
);
} catch (e) {
debugPrint('[WiFi] delete failed: $e');
if (!ref.mounted) return;
state = state.copyWith(
isSaving: false,
error: WifiSettingsError.deleteFailed,
);
}
}
Future<void> setNetwork(WifiNetworkEntity network) async {
final identificator = _identificator;
if (identificator == null) return;
state = state.copyWith(isConnecting: true, error: null, success: null);
try {
debugPrint('[WiFi] setting saved network: ${network.ssid} (${network.bssid})');
await _commands.send(
request: SendCommandRequestModel(
device: identificator,
command: DeviceCommand.setWifi,
data: {
'ssid': network.ssid,
'bssid': network.bssid,
'password': network.password ?? '',
},
),
);
if (!ref.mounted) return;
debugPrint('[WiFi] SET_WIFI sent, requesting WIFI_CURRENT');
state = state.copyWith(
isConnecting: false,
isLoadingCurrentNetwork: true,
success: WifiSettingsSuccess.networkSet,
);
unawaited(
_commands
.send(
request: SendCommandRequestModel(
device: identificator,
command: DeviceCommand.wifiCurrent,
),
)
.catchError((_) {
debugPrint('[WiFi] WIFI_CURRENT after set failed');
if (ref.mounted) {
state = state.copyWith(isLoadingCurrentNetwork: false);
}
}),
);
_currentNetworkTimeout?.cancel();
_currentNetworkTimeout = Timer(const Duration(seconds: 15), () {
if (!ref.mounted) return;
if (state.isLoadingCurrentNetwork) {
state = state.copyWith(isLoadingCurrentNetwork: false);
}
});
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isConnecting: false,
error: WifiSettingsError.setFailed,
);
}
}
void clearError() {
if (state.error != null) state = state.copyWith(error: null);
}
void clearSuccess() {
if (state.success != null) state = state.copyWith(success: null);
}
}

View File

@@ -1,27 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/scanned_wifi_network.dart';
import '../../domain/entities/wifi_network_entity.dart';
part 'wifi_settings_view_state.freezed.dart';
enum WifiSettingsError { loadFailed, scanFailed, connectFailed, saveFailed, deleteFailed, setFailed }
enum WifiSettingsSuccess { networkSaved, networkDeleted, connected, networkSet }
@freezed
abstract class WifiSettingsViewState with _$WifiSettingsViewState {
const factory WifiSettingsViewState({
@Default([]) List<WifiNetworkEntity> savedNetworks,
@Default([]) List<ScannedWifiNetwork> availableNetworks,
WifiNetworkEntity? currentNetwork,
@Default(true) bool isLoading,
@Default(false) bool isLoadingCurrentNetwork,
@Default(false) bool isScanning,
@Default(0) int scanSecondsRemaining,
@Default(false) bool isSaving,
@Default(false) bool isConnecting,
WifiSettingsError? error,
WifiSettingsSuccess? success,
}) = _WifiSettingsViewState;
}

View File

@@ -1,337 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'wifi_settings_view_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$WifiSettingsViewState {
List<WifiNetworkEntity> get savedNetworks; List<ScannedWifiNetwork> get availableNetworks; WifiNetworkEntity? get currentNetwork; bool get isLoading; bool get isLoadingCurrentNetwork; bool get isScanning; int get scanSecondsRemaining; bool get isSaving; bool get isConnecting; WifiSettingsError? get error; WifiSettingsSuccess? get success;
/// Create a copy of WifiSettingsViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$WifiSettingsViewStateCopyWith<WifiSettingsViewState> get copyWith => _$WifiSettingsViewStateCopyWithImpl<WifiSettingsViewState>(this as WifiSettingsViewState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is WifiSettingsViewState&&const DeepCollectionEquality().equals(other.savedNetworks, savedNetworks)&&const DeepCollectionEquality().equals(other.availableNetworks, availableNetworks)&&(identical(other.currentNetwork, currentNetwork) || other.currentNetwork == currentNetwork)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingCurrentNetwork, isLoadingCurrentNetwork) || other.isLoadingCurrentNetwork == isLoadingCurrentNetwork)&&(identical(other.isScanning, isScanning) || other.isScanning == isScanning)&&(identical(other.scanSecondsRemaining, scanSecondsRemaining) || other.scanSecondsRemaining == scanSecondsRemaining)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.error, error) || other.error == error)&&(identical(other.success, success) || other.success == success));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(savedNetworks),const DeepCollectionEquality().hash(availableNetworks),currentNetwork,isLoading,isLoadingCurrentNetwork,isScanning,scanSecondsRemaining,isSaving,isConnecting,error,success);
@override
String toString() {
return 'WifiSettingsViewState(savedNetworks: $savedNetworks, availableNetworks: $availableNetworks, currentNetwork: $currentNetwork, isLoading: $isLoading, isLoadingCurrentNetwork: $isLoadingCurrentNetwork, isScanning: $isScanning, scanSecondsRemaining: $scanSecondsRemaining, isSaving: $isSaving, isConnecting: $isConnecting, error: $error, success: $success)';
}
}
/// @nodoc
abstract mixin class $WifiSettingsViewStateCopyWith<$Res> {
factory $WifiSettingsViewStateCopyWith(WifiSettingsViewState value, $Res Function(WifiSettingsViewState) _then) = _$WifiSettingsViewStateCopyWithImpl;
@useResult
$Res call({
List<WifiNetworkEntity> savedNetworks, List<ScannedWifiNetwork> availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success
});
$WifiNetworkEntityCopyWith<$Res>? get currentNetwork;
}
/// @nodoc
class _$WifiSettingsViewStateCopyWithImpl<$Res>
implements $WifiSettingsViewStateCopyWith<$Res> {
_$WifiSettingsViewStateCopyWithImpl(this._self, this._then);
final WifiSettingsViewState _self;
final $Res Function(WifiSettingsViewState) _then;
/// Create a copy of WifiSettingsViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? savedNetworks = null,Object? availableNetworks = null,Object? currentNetwork = freezed,Object? isLoading = null,Object? isLoadingCurrentNetwork = null,Object? isScanning = null,Object? scanSecondsRemaining = null,Object? isSaving = null,Object? isConnecting = null,Object? error = freezed,Object? success = freezed,}) {
return _then(_self.copyWith(
savedNetworks: null == savedNetworks ? _self.savedNetworks : savedNetworks // ignore: cast_nullable_to_non_nullable
as List<WifiNetworkEntity>,availableNetworks: null == availableNetworks ? _self.availableNetworks : availableNetworks // ignore: cast_nullable_to_non_nullable
as List<ScannedWifiNetwork>,currentNetwork: freezed == currentNetwork ? _self.currentNetwork : currentNetwork // ignore: cast_nullable_to_non_nullable
as WifiNetworkEntity?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingCurrentNetwork: null == isLoadingCurrentNetwork ? _self.isLoadingCurrentNetwork : isLoadingCurrentNetwork // ignore: cast_nullable_to_non_nullable
as bool,isScanning: null == isScanning ? _self.isScanning : isScanning // ignore: cast_nullable_to_non_nullable
as bool,scanSecondsRemaining: null == scanSecondsRemaining ? _self.scanSecondsRemaining : scanSecondsRemaining // ignore: cast_nullable_to_non_nullable
as int,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as WifiSettingsError?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as WifiSettingsSuccess?,
));
}
/// Create a copy of WifiSettingsViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WifiNetworkEntityCopyWith<$Res>? get currentNetwork {
if (_self.currentNetwork == null) {
return null;
}
return $WifiNetworkEntityCopyWith<$Res>(_self.currentNetwork!, (value) {
return _then(_self.copyWith(currentNetwork: value));
});
}
}
/// Adds pattern-matching-related methods to [WifiSettingsViewState].
extension WifiSettingsViewStatePatterns on WifiSettingsViewState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _WifiSettingsViewState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _WifiSettingsViewState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _WifiSettingsViewState value) $default,){
final _that = this;
switch (_that) {
case _WifiSettingsViewState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _WifiSettingsViewState value)? $default,){
final _that = this;
switch (_that) {
case _WifiSettingsViewState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<WifiNetworkEntity> savedNetworks, List<ScannedWifiNetwork> availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _WifiSettingsViewState() when $default != null:
return $default(_that.savedNetworks,_that.availableNetworks,_that.currentNetwork,_that.isLoading,_that.isLoadingCurrentNetwork,_that.isScanning,_that.scanSecondsRemaining,_that.isSaving,_that.isConnecting,_that.error,_that.success);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<WifiNetworkEntity> savedNetworks, List<ScannedWifiNetwork> availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success) $default,) {final _that = this;
switch (_that) {
case _WifiSettingsViewState():
return $default(_that.savedNetworks,_that.availableNetworks,_that.currentNetwork,_that.isLoading,_that.isLoadingCurrentNetwork,_that.isScanning,_that.scanSecondsRemaining,_that.isSaving,_that.isConnecting,_that.error,_that.success);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<WifiNetworkEntity> savedNetworks, List<ScannedWifiNetwork> availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success)? $default,) {final _that = this;
switch (_that) {
case _WifiSettingsViewState() when $default != null:
return $default(_that.savedNetworks,_that.availableNetworks,_that.currentNetwork,_that.isLoading,_that.isLoadingCurrentNetwork,_that.isScanning,_that.scanSecondsRemaining,_that.isSaving,_that.isConnecting,_that.error,_that.success);case _:
return null;
}
}
}
/// @nodoc
class _WifiSettingsViewState implements WifiSettingsViewState {
const _WifiSettingsViewState({final List<WifiNetworkEntity> savedNetworks = const [], final List<ScannedWifiNetwork> availableNetworks = const [], this.currentNetwork, this.isLoading = true, this.isLoadingCurrentNetwork = false, this.isScanning = false, this.scanSecondsRemaining = 0, this.isSaving = false, this.isConnecting = false, this.error, this.success}): _savedNetworks = savedNetworks,_availableNetworks = availableNetworks;
final List<WifiNetworkEntity> _savedNetworks;
@override@JsonKey() List<WifiNetworkEntity> get savedNetworks {
if (_savedNetworks is EqualUnmodifiableListView) return _savedNetworks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_savedNetworks);
}
final List<ScannedWifiNetwork> _availableNetworks;
@override@JsonKey() List<ScannedWifiNetwork> get availableNetworks {
if (_availableNetworks is EqualUnmodifiableListView) return _availableNetworks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_availableNetworks);
}
@override final WifiNetworkEntity? currentNetwork;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isLoadingCurrentNetwork;
@override@JsonKey() final bool isScanning;
@override@JsonKey() final int scanSecondsRemaining;
@override@JsonKey() final bool isSaving;
@override@JsonKey() final bool isConnecting;
@override final WifiSettingsError? error;
@override final WifiSettingsSuccess? success;
/// Create a copy of WifiSettingsViewState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$WifiSettingsViewStateCopyWith<_WifiSettingsViewState> get copyWith => __$WifiSettingsViewStateCopyWithImpl<_WifiSettingsViewState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WifiSettingsViewState&&const DeepCollectionEquality().equals(other._savedNetworks, _savedNetworks)&&const DeepCollectionEquality().equals(other._availableNetworks, _availableNetworks)&&(identical(other.currentNetwork, currentNetwork) || other.currentNetwork == currentNetwork)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingCurrentNetwork, isLoadingCurrentNetwork) || other.isLoadingCurrentNetwork == isLoadingCurrentNetwork)&&(identical(other.isScanning, isScanning) || other.isScanning == isScanning)&&(identical(other.scanSecondsRemaining, scanSecondsRemaining) || other.scanSecondsRemaining == scanSecondsRemaining)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.error, error) || other.error == error)&&(identical(other.success, success) || other.success == success));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_savedNetworks),const DeepCollectionEquality().hash(_availableNetworks),currentNetwork,isLoading,isLoadingCurrentNetwork,isScanning,scanSecondsRemaining,isSaving,isConnecting,error,success);
@override
String toString() {
return 'WifiSettingsViewState(savedNetworks: $savedNetworks, availableNetworks: $availableNetworks, currentNetwork: $currentNetwork, isLoading: $isLoading, isLoadingCurrentNetwork: $isLoadingCurrentNetwork, isScanning: $isScanning, scanSecondsRemaining: $scanSecondsRemaining, isSaving: $isSaving, isConnecting: $isConnecting, error: $error, success: $success)';
}
}
/// @nodoc
abstract mixin class _$WifiSettingsViewStateCopyWith<$Res> implements $WifiSettingsViewStateCopyWith<$Res> {
factory _$WifiSettingsViewStateCopyWith(_WifiSettingsViewState value, $Res Function(_WifiSettingsViewState) _then) = __$WifiSettingsViewStateCopyWithImpl;
@override @useResult
$Res call({
List<WifiNetworkEntity> savedNetworks, List<ScannedWifiNetwork> availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success
});
@override $WifiNetworkEntityCopyWith<$Res>? get currentNetwork;
}
/// @nodoc
class __$WifiSettingsViewStateCopyWithImpl<$Res>
implements _$WifiSettingsViewStateCopyWith<$Res> {
__$WifiSettingsViewStateCopyWithImpl(this._self, this._then);
final _WifiSettingsViewState _self;
final $Res Function(_WifiSettingsViewState) _then;
/// Create a copy of WifiSettingsViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? savedNetworks = null,Object? availableNetworks = null,Object? currentNetwork = freezed,Object? isLoading = null,Object? isLoadingCurrentNetwork = null,Object? isScanning = null,Object? scanSecondsRemaining = null,Object? isSaving = null,Object? isConnecting = null,Object? error = freezed,Object? success = freezed,}) {
return _then(_WifiSettingsViewState(
savedNetworks: null == savedNetworks ? _self._savedNetworks : savedNetworks // ignore: cast_nullable_to_non_nullable
as List<WifiNetworkEntity>,availableNetworks: null == availableNetworks ? _self._availableNetworks : availableNetworks // ignore: cast_nullable_to_non_nullable
as List<ScannedWifiNetwork>,currentNetwork: freezed == currentNetwork ? _self.currentNetwork : currentNetwork // ignore: cast_nullable_to_non_nullable
as WifiNetworkEntity?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingCurrentNetwork: null == isLoadingCurrentNetwork ? _self.isLoadingCurrentNetwork : isLoadingCurrentNetwork // ignore: cast_nullable_to_non_nullable
as bool,isScanning: null == isScanning ? _self.isScanning : isScanning // ignore: cast_nullable_to_non_nullable
as bool,scanSecondsRemaining: null == scanSecondsRemaining ? _self.scanSecondsRemaining : scanSecondsRemaining // ignore: cast_nullable_to_non_nullable
as int,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as WifiSettingsError?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as WifiSettingsSuccess?,
));
}
/// Create a copy of WifiSettingsViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WifiNetworkEntityCopyWith<$Res>? get currentNetwork {
if (_self.currentNetwork == null) {
return null;
}
return $WifiNetworkEntityCopyWith<$Res>(_self.currentNetwork!, (value) {
return _then(_self.copyWith(currentNetwork: value));
});
}
}
// dart format on

View File

@@ -1,13 +1,14 @@
import 'package:legacy_theme/legacy_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_controller.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import '../state/wifi_settings_view_model.dart';
void showConnectWifiSheet(
BuildContext context, {
required String deviceIdentificator,
String? ssid,
String? bssid,
}) {
@@ -15,15 +16,24 @@ void showConnectWifiSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _ConnectWifiSheet(ssid: ssid, bssid: bssid),
builder: (_) => _ConnectWifiSheet(
deviceIdentificator: deviceIdentificator,
ssid: ssid,
bssid: bssid,
),
);
}
class _ConnectWifiSheet extends ConsumerStatefulWidget {
final String deviceIdentificator;
final String? ssid;
final String? bssid;
const _ConnectWifiSheet({this.ssid, this.bssid});
const _ConnectWifiSheet({
required this.deviceIdentificator,
this.ssid,
this.bssid,
});
@override
ConsumerState<_ConnectWifiSheet> createState() => _ConnectWifiSheetState();
@@ -33,46 +43,55 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
late final TextEditingController _ssidController;
late final TextEditingController _bssidController;
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void initState() {
super.initState();
_ssidController = TextEditingController(text: widget.ssid ?? '');
_bssidController = TextEditingController(text: widget.bssid ?? '');
_ssidController.addListener(_refreshCanSave);
_bssidController.addListener(_refreshCanSave);
_passwordController.addListener(_refreshCanSave);
WidgetsBinding.instance.addPostFrameCallback((_) => _refreshCanSave());
}
@override
void dispose() {
_ssidController.removeListener(_refreshCanSave);
_bssidController.removeListener(_refreshCanSave);
_passwordController.removeListener(_refreshCanSave);
_ssidController.dispose();
_bssidController.dispose();
_passwordController.dispose();
super.dispose();
}
bool get _canSave =>
_ssidController.text.trim().isNotEmpty &&
_bssidController.text.trim().isNotEmpty &&
_passwordController.text.trim().isNotEmpty;
void _refreshCanSave() {
final canSave = _ssidController.text.trim().isNotEmpty &&
_bssidController.text.trim().isNotEmpty &&
_passwordController.text.trim().isNotEmpty;
ref.read(wifiConnectFormProvider.notifier).setCanSave(canSave);
}
void _submit() {
if (!_canSave) return;
final formState = ref.read(wifiConnectFormProvider);
if (!formState.canSave) return;
final vm = ref.read(wifiSettingsViewModelProvider.notifier);
vm.connectAndSave(
ssid: _ssidController.text.trim(),
bssid: _bssidController.text.trim(),
password: _passwordController.text.trim(),
);
ref.read(wifiControllerProvider.notifier).connectAndSave(
deviceIdentificator: widget.deviceIdentificator,
ssid: _ssidController.text.trim(),
bssid: _bssidController.text.trim(),
password: _passwordController.text.trim(),
);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
final isConnecting = ref.watch(
wifiSettingsViewModelProvider.select((s) => s.isConnecting),
);
final formState = ref.watch(wifiConnectFormProvider);
final isSubmitting = ref
.watch(wifiControllerProvider.select((s) => s.isLoading));
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
final hasPrefilled = widget.ssid != null;
@@ -81,7 +100,7 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
top: false,
@@ -119,12 +138,13 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
),
),
TextButton(
onPressed: _canSave && !isConnecting ? _submit : null,
child: isConnecting
onPressed: formState.canSave && !isSubmitting
? _submit
: null,
child: isSubmitting
? SizedBox(
width: SizeUtils.getByScreen(small: 20, big: 22),
height:
SizeUtils.getByScreen(small: 20, big: 22),
height: SizeUtils.getByScreen(small: 20, big: 22),
child: CircularProgressIndicator(
strokeWidth: 2,
color: primaryColor,
@@ -133,7 +153,9 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
: Text(
context.translate(I18n.save),
style: TextStyle(
color: _canSave ? primaryColor : Colors.grey,
color: formState.canSave
? primaryColor
: Colors.grey,
fontWeight: FontWeight.w600,
fontSize: SizeUtils.getByScreen(
small: 16,
@@ -152,11 +174,10 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
const SizedBox(height: 8),
TextField(
controller: _ssidController,
readOnly: hasPrefilled,
onChanged: (_) => setState(() {}),
decoration: _inputDecoration(
hintText: context.translate(I18n.wifiSsidHint),
primaryColor: primaryColor,
@@ -170,11 +191,10 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
const SizedBox(height: 8),
TextField(
controller: _bssidController,
readOnly: hasPrefilled,
onChanged: (_) => setState(() {}),
decoration: _inputDecoration(
hintText: context.translate(I18n.wifiBssidHint),
primaryColor: primaryColor,
@@ -188,20 +208,20 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
const SizedBox(height: 8),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
onChanged: (_) => setState(() {}),
obscureText: formState.obscurePassword,
decoration: _inputDecoration(
hintText: context.translate(I18n.wifiPasswordHint),
primaryColor: primaryColor,
).copyWith(
suffixIcon: IconButton(
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
onPressed: ref
.read(wifiConnectFormProvider.notifier)
.togglePasswordVisibility,
icon: Icon(
_obscurePassword
formState.obscurePassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey,
@@ -226,18 +246,21 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> {
return InputDecoration(
hintText: hintText,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: primaryColor, width: 2),
),
contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 14),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 14,
),
);
}
}

View File

@@ -1,93 +1,145 @@
import 'package:design_system/design_system.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:navigation/navigation.dart';
import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_controller.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_scan_provider.dart';
import 'package:settings/src/features/wifi_settings/presentation/widgets/add_wifi_network_sheet.dart';
import 'package:settings/src/features/wifi_settings/presentation/widgets/available_wifi_network_card.dart';
import 'package:settings/src/features/wifi_settings/presentation/widgets/wifi_network_card.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:utils/utils.dart';
import 'state/wifi_settings_view_model.dart';
import 'state/wifi_settings_view_state.dart';
import 'widgets/available_wifi_network_card.dart';
import 'widgets/add_wifi_network_sheet.dart';
import 'widgets/wifi_network_card.dart';
class WifiSettingsScreen extends ConsumerWidget {
class WifiSettingsScreen extends ConsumerStatefulWidget {
final NavigationContract navigationContract;
const WifiSettingsScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(wifiSettingsViewModelProvider);
final vm = ref.read(wifiSettingsViewModelProvider.notifier);
ConsumerState<WifiSettingsScreen> createState() => _WifiSettingsScreenState();
}
class _WifiSettingsScreenState extends ConsumerState<WifiSettingsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final identificator =
ref.read(selectedDeviceProvider).value?.identificator;
if (identificator == null) return;
ref
.read(wifiCurrentNetworkProvider.notifier)
.requestCurrent(identificator);
});
}
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
final device = ref.watch(selectedDeviceProvider).value;
final identificator = device?.identificator;
ref.listen(wifiSettingsViewModelProvider.select((s) => s.error), (
_,
error,
) {
if (error == null) return;
final message = switch (error) {
WifiSettingsError.loadFailed => context.translate(I18n.wifiLoadError),
WifiSettingsError.scanFailed => context.translate(I18n.wifiScanFailed),
WifiSettingsError.connectFailed =>
context.translate(I18n.wifiConnectFailed),
WifiSettingsError.saveFailed => context.translate(I18n.wifiSaveFailed),
WifiSettingsError.deleteFailed =>
context.translate(I18n.wifiDeleteFailed),
WifiSettingsError.setFailed =>
context.translate(I18n.wifiSetFailed),
ref.watch(wifiCurrentNetworkProvider);
ref.watch(wifiScanProvider);
ref.listen(wifiControllerProvider, (prev, next) async {
if (prev == null || !prev.isLoading || next.isLoading) return;
if (next.hasError) {
await next.showErrorOn(context);
return;
}
final action =
ref.read(wifiControllerProvider.notifier).lastAction;
final message = switch (action) {
WifiControllerAction.connectAndSave => I18n.wifiNetworkSaved,
WifiControllerAction.remove => I18n.wifiNetworkDeleted,
WifiControllerAction.set => I18n.wifiNetworkSet,
null => I18n.deviceUpdatedSuccess,
};
showTopSnackbar(context, message: message, type: MessageType.error);
vm.clearError();
await showSuccessDialog(context, message);
});
ref.listen(wifiSettingsViewModelProvider.select((s) => s.success), (
_,
success,
) {
if (success == null) return;
final message = switch (success) {
WifiSettingsSuccess.networkSaved =>
context.translate(I18n.wifiNetworkSaved),
WifiSettingsSuccess.networkDeleted =>
context.translate(I18n.wifiNetworkDeleted),
WifiSettingsSuccess.connected => context.translate(I18n.wifiConnected),
WifiSettingsSuccess.networkSet =>
context.translate(I18n.wifiNetworkSet),
};
showTopSnackbar(context, message: message, type: MessageType.success);
vm.clearSuccess();
});
if (identificator == null) {
return Scaffold(
appBar: _appBar(context, primaryColor, onScan: null),
body: const SizedBox.shrink(),
);
}
final savedAsync = ref.watch(savedWifiNetworksProvider(identificator));
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: () => navigationContract.goBack(),
icon: Icon(
Icons.adaptive.arrow_back,
color: primaryColor,
size: SizeUtils.getByScreen(small: 32, big: 28),
appBar: _appBar(
context,
primaryColor,
onScan: () async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
try {
await ref
.read(wifiScanProvider.notifier)
.scan(identificator);
} on WifiScanTimeoutException {
if (!context.mounted) return;
await showErrorDialog(context, I18n.wifiScanFailed);
} catch (_) {
if (!context.mounted) return;
await showErrorDialog(context, I18n.wifiScanFailed);
}
},
),
body: SafeArea(
top: false,
child: savedAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => Center(
child: Text(context.translate(I18n.wifiLoadError)),
),
data: (savedNetworks) => _Body(
deviceIdentificator: identificator,
savedNetworks: savedNetworks,
),
),
title: Text(
'WiFi',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
fontWeight: FontWeight.w500,
letterSpacing: 0,
color: primaryColor,
),
),
);
}
PreferredSizeWidget _appBar(
BuildContext context,
Color primaryColor, {
required VoidCallback? onScan,
}) {
return AppBar(
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: () => widget.navigationContract.goBack(),
icon: Icon(
Icons.adaptive.arrow_back,
color: primaryColor,
size: SizeUtils.getByScreen(small: 32, big: 28),
),
actions: [
),
title: Text(
'WiFi',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
fontWeight: FontWeight.w500,
letterSpacing: 0,
color: primaryColor,
),
),
actions: [
if (onScan != null)
Padding(
padding: EdgeInsets.only(
right: SizeUtils.getByScreen(small: 16, big: 14),
@@ -98,10 +150,7 @@ class WifiSettingsScreen extends ConsumerWidget {
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.scanNetworks();
},
onPressed: onScan,
icon: Icon(
Icons.wifi_find,
color: Colors.white,
@@ -110,31 +159,31 @@ class WifiSettingsScreen extends ConsumerWidget {
),
),
),
],
),
body: SafeArea(
top: false,
child: state.isLoading
? const Center(child: CircularProgressIndicator())
: _Body(),
),
],
);
}
}
class _Body extends ConsumerWidget {
const _Body();
final String deviceIdentificator;
final List<WifiNetworkEntity> savedNetworks;
const _Body({
required this.deviceIdentificator,
required this.savedNetworks,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(wifiSettingsViewModelProvider);
final primaryColor = context.sfColors.legacyPrimary;
final current = ref.watch(wifiCurrentNetworkProvider);
final scan = ref.watch(wifiScanProvider);
return SingleChildScrollView(
child: Padding(
padding: SizeUtils.getByScreen(
small: EdgeInsets.symmetric(horizontal: 22, vertical: 10),
big: EdgeInsets.symmetric(horizontal: 21, vertical: 8),
small: const EdgeInsets.symmetric(horizontal: 22, vertical: 10),
big: const EdgeInsets.symmetric(horizontal: 21, vertical: 8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -147,7 +196,7 @@ class _Body extends ConsumerWidget {
context.translate(I18n.wifiCurrentNetwork),
primaryColor,
),
if (state.isLoadingCurrentNetwork)
if (current.isLoading)
SizedBox(
width: SizeUtils.getByScreen(small: 16, big: 14),
height: SizeUtils.getByScreen(small: 16, big: 14),
@@ -159,12 +208,12 @@ class _Body extends ConsumerWidget {
],
),
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)),
if (state.currentNetwork != null)
if (current.network != null)
_CurrentNetworkCard(
ssid: state.currentNetwork!.ssid,
bssid: state.currentNetwork!.bssid,
ssid: current.network!.ssid,
bssid: current.network!.bssid,
)
else if (!state.isLoadingCurrentNetwork)
else if (!current.isLoading)
Padding(
padding: EdgeInsets.only(
bottom: SizeUtils.getByScreen(small: 12, big: 10),
@@ -173,12 +222,14 @@ class _Body extends ConsumerWidget {
context.translate(I18n.wifiNoCurrentNetwork),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 14, big: 15),
color: Theme.of(context).colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(178),
),
),
),
if (state.availableNetworks.isNotEmpty || state.isScanning) ...[
if (scan.networks.isNotEmpty || scan.isScanning) ...[
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -188,19 +239,21 @@ class _Body extends ConsumerWidget {
context.translate(I18n.wifiAvailableNetworks),
primaryColor,
),
if (state.isScanning)
if (scan.isScanning)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${state.scanSecondsRemaining}s',
'${scan.secondsRemaining}s',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
fontSize:
SizeUtils.getByScreen(small: 13, big: 12),
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
SizedBox(width: SizeUtils.getByScreen(small: 6, big: 5)),
SizedBox(
width: SizeUtils.getByScreen(small: 6, big: 5)),
SizedBox(
width: SizeUtils.getByScreen(small: 16, big: 14),
height: SizeUtils.getByScreen(small: 16, big: 14),
@@ -214,11 +267,12 @@ class _Body extends ConsumerWidget {
],
),
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)),
...state.availableNetworks.map(
...scan.networks.map(
(network) => AvailableWifiNetworkCard(
network: network,
onTap: () => showConnectWifiSheet(
context,
deviceIdentificator: deviceIdentificator,
ssid: network.ssid,
bssid: network.bssid,
),
@@ -230,12 +284,12 @@ class _Body extends ConsumerWidget {
context,
context.translate(
I18n.wifiSavedNetworks,
args: {'count': state.savedNetworks.length.toString()},
args: {'count': savedNetworks.length.toString()},
),
primaryColor,
),
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)),
if (state.savedNetworks.isEmpty)
if (savedNetworks.isEmpty)
Padding(
padding: EdgeInsets.only(
bottom: SizeUtils.getByScreen(small: 12, big: 10),
@@ -244,20 +298,23 @@ class _Body extends ConsumerWidget {
context.translate(I18n.noWifiNetworks),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 14, big: 15),
color: Theme.of(context).colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(178),
),
),
)
else
...state.savedNetworks.map(
...savedNetworks.map(
(network) => WifiNetworkCard(
network: network,
onTap: () async {
if (!await guardDeviceCommand(context, ref)) return;
ref
.read(wifiSettingsViewModelProvider.notifier)
.setNetwork(network);
ref.read(wifiControllerProvider.notifier).setNetwork(
deviceIdentificator: deviceIdentificator,
network: network,
);
},
onDelete: () => _confirmDelete(context, ref, network),
),
@@ -283,15 +340,15 @@ class _Body extends ConsumerWidget {
Future<void> _confirmDelete(
BuildContext context,
WidgetRef ref,
dynamic network,
WifiNetworkEntity network,
) async {
if (!await guardDeviceCommand(context, ref)) return;
if (!context.mounted) return;
final primaryColor = context.sfColors.legacyPrimary;
showDialog(
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
builder: (dialogContext) => AlertDialog(
title: Text(context.translate(I18n.removeWifiNetwork)),
content: Text(
context.translate(
@@ -301,7 +358,7 @@ class _Body extends ConsumerWidget {
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => Navigator.pop(dialogContext),
child: Text(
context.translate(I18n.cancel),
style: TextStyle(color: primaryColor),
@@ -309,10 +366,11 @@ class _Body extends ConsumerWidget {
),
TextButton(
onPressed: () {
ref
.read(wifiSettingsViewModelProvider.notifier)
.removeNetwork(network.id);
Navigator.pop(context);
ref.read(wifiControllerProvider.notifier).removeNetwork(
deviceIdentificator: deviceIdentificator,
networkId: network.id,
);
Navigator.pop(dialogContext);
},
child: Text(
context.translate(I18n.delete),
@@ -329,10 +387,7 @@ class _CurrentNetworkCard extends StatelessWidget {
final String ssid;
final String bssid;
const _CurrentNetworkCard({
required this.ssid,
required this.bssid,
});
const _CurrentNetworkCard({required this.ssid, required this.bssid});
@override
Widget build(BuildContext context) {
@@ -347,7 +402,7 @@ class _CurrentNetworkCard extends StatelessWidget {
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.all(Radius.circular(16)),
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
child: Row(
children: [
@@ -369,11 +424,13 @@ class _CurrentNetworkCard extends StatelessWidget {
fontSize: SizeUtils.getByScreen(small: 15, big: 16),
),
),
SizedBox(height: 2),
const SizedBox(height: 2),
Text(
bssid,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(178),
fontSize: SizeUtils.getByScreen(small: 13, big: 14),
),

View File

@@ -0,0 +1,270 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:mocktail/mocktail.dart';
import 'package:settings/src/core/domain/repositories/wifi_repository.dart';
import 'package:settings/src/core/providers/wifi_repository_provider.dart';
import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart';
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_controller.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/testing.dart';
import 'package:sf_tracking/sf_tracking.dart';
class MockWifiRepository extends Mock implements WifiRepository {}
class MockCommandsRepository extends Mock implements CommandsRepository {}
class FakeWebSocketService implements WebSocketService {
final _controller = StreamController<WebSocketEvent>.broadcast();
@override
Stream<WebSocketEvent> get events => _controller.stream;
void close() => _controller.close();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
const _network = WifiNetworkEntity(
id: 'net-1',
ssid: 'home',
bssid: '00:11:22:33:44:55',
password: 'secret',
);
void main() {
setUpAll(() {
registerFallbackValue(
SendCommandRequestModel(
device: 'x',
command: DeviceCommand.wifiCurrent,
),
);
});
ProviderContainer buildContainer({
required WifiRepository repo,
required CommandsRepository commands,
}) {
final ws = FakeWebSocketService();
return makeContainer(
overrides: [
wifiRepositoryProvider.overrideWithValue(repo),
commandsRepositoryProvider.overrideWithValue(commands),
webSocketServiceProvider.overrideWithValue(ws),
sfTrackingProvider.overrideWithValue(
SfTrackingRepository(clients: const []),
),
],
);
}
group('WifiController.connectAndSave', () {
test('sends SET_WIFI, persists, and refetches on success', () async {
final repo = MockWifiRepository();
final commands = MockCommandsRepository();
when(() => commands.send(request: any(named: 'request')))
.thenAnswer((_) async {});
when(
() => repo.createWifiNetwork(
id: any(named: 'id'),
deviceIdentificator: any(named: 'deviceIdentificator'),
ssid: any(named: 'ssid'),
bssid: any(named: 'bssid'),
password: any(named: 'password'),
),
).thenAnswer((_) async {});
when(
() => repo.getWifiNetworks(
deviceIdentificator: any(named: 'deviceIdentificator'),
),
).thenAnswer((_) async => const [_network]);
final container = buildContainer(repo: repo, commands: commands);
addTearDown(container.dispose);
await container.read(wifiControllerProvider.notifier).connectAndSave(
deviceIdentificator: 'imei-1',
ssid: 'home',
bssid: '00:11:22:33:44:55',
password: 'secret',
);
expect(container.read(wifiControllerProvider), isA<AsyncData<void>>());
verify(() => commands.send(request: any(named: 'request'))).called(1);
verify(
() => repo.createWifiNetwork(
id: any(named: 'id'),
deviceIdentificator: 'imei-1',
ssid: 'home',
bssid: '00:11:22:33:44:55',
password: 'secret',
),
).called(1);
});
test('exposes AsyncError when command fails', () async {
final repo = MockWifiRepository();
final commands = MockCommandsRepository();
when(() => commands.send(request: any(named: 'request')))
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
final container = buildContainer(repo: repo, commands: commands);
addTearDown(container.dispose);
await container.read(wifiControllerProvider.notifier).connectAndSave(
deviceIdentificator: 'imei-1',
ssid: 'home',
bssid: '00:11:22:33:44:55',
password: 'secret',
);
expect(container.read(wifiControllerProvider), isA<AsyncError<void>>());
verifyNever(
() => repo.createWifiNetwork(
id: any(named: 'id'),
deviceIdentificator: any(named: 'deviceIdentificator'),
ssid: any(named: 'ssid'),
bssid: any(named: 'bssid'),
password: any(named: 'password'),
),
);
});
});
group('WifiController.removeNetwork', () {
test('deletes and transitions to AsyncData', () async {
final repo = MockWifiRepository();
final commands = MockCommandsRepository();
when(() => repo.deleteWifiNetwork(networkId: any(named: 'networkId')))
.thenAnswer((_) async {});
when(
() => repo.getWifiNetworks(
deviceIdentificator: any(named: 'deviceIdentificator'),
),
).thenAnswer((_) async => const []);
final container = buildContainer(repo: repo, commands: commands);
addTearDown(container.dispose);
await container.read(wifiControllerProvider.notifier).removeNetwork(
deviceIdentificator: 'imei-1',
networkId: 'net-1',
);
expect(container.read(wifiControllerProvider), isA<AsyncData<void>>());
verify(() => repo.deleteWifiNetwork(networkId: 'net-1')).called(1);
});
test('exposes AsyncError when repository fails', () async {
final repo = MockWifiRepository();
final commands = MockCommandsRepository();
when(() => repo.deleteWifiNetwork(networkId: any(named: 'networkId')))
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
final container = buildContainer(repo: repo, commands: commands);
addTearDown(container.dispose);
await container.read(wifiControllerProvider.notifier).removeNetwork(
deviceIdentificator: 'imei-1',
networkId: 'net-1',
);
expect(container.read(wifiControllerProvider), isA<AsyncError<void>>());
});
});
group('WifiController.setNetwork', () {
test('sends SET_WIFI on success', () async {
final repo = MockWifiRepository();
final commands = MockCommandsRepository();
when(() => commands.send(request: any(named: 'request')))
.thenAnswer((_) async {});
final container = buildContainer(repo: repo, commands: commands);
addTearDown(container.dispose);
await container.read(wifiControllerProvider.notifier).setNetwork(
deviceIdentificator: 'imei-1',
network: _network,
);
expect(container.read(wifiControllerProvider), isA<AsyncData<void>>());
final sent = verify(
() => commands.send(request: captureAny(named: 'request')),
).captured;
final setWifi = sent.firstWhere(
(r) => (r as SendCommandRequestModel).command == DeviceCommand.setWifi,
) as SendCommandRequestModel;
expect(setWifi.data, {
'ssid': 'home',
'bssid': '00:11:22:33:44:55',
'password': 'secret',
});
});
test('exposes AsyncError when command fails', () async {
final repo = MockWifiRepository();
final commands = MockCommandsRepository();
when(() => commands.send(request: any(named: 'request')))
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
final container = buildContainer(repo: repo, commands: commands);
addTearDown(container.dispose);
await container.read(wifiControllerProvider.notifier).setNetwork(
deviceIdentificator: 'imei-1',
network: _network,
);
expect(container.read(wifiControllerProvider), isA<AsyncError<void>>());
});
});
group('lastAction tracking', () {
test('records connectAndSave', () async {
final repo = MockWifiRepository();
final commands = MockCommandsRepository();
when(() => commands.send(request: any(named: 'request')))
.thenAnswer((_) async {});
when(
() => repo.createWifiNetwork(
id: any(named: 'id'),
deviceIdentificator: any(named: 'deviceIdentificator'),
ssid: any(named: 'ssid'),
bssid: any(named: 'bssid'),
password: any(named: 'password'),
),
).thenAnswer((_) async {});
when(
() => repo.getWifiNetworks(
deviceIdentificator: any(named: 'deviceIdentificator'),
),
).thenAnswer((_) async => const []);
final container = buildContainer(repo: repo, commands: commands);
addTearDown(container.dispose);
await container.read(wifiControllerProvider.notifier).connectAndSave(
deviceIdentificator: 'imei-1',
ssid: 'home',
bssid: '00:11:22:33:44:55',
password: 'secret',
);
expect(
container.read(wifiControllerProvider.notifier).lastAction,
WifiControllerAction.connectAndSave,
);
});
});
}