refactor(device_management): migrate remote_connection to Riverpod
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
part 'remote_camera_controller.g.dart';
|
||||
|
||||
const int _photoWaitSeconds = 5;
|
||||
|
||||
class RemoteCameraState {
|
||||
const RemoteCameraState({
|
||||
this.isTakingPicture = false,
|
||||
this.isWaitingForPhoto = false,
|
||||
this.photoCountdown = 0,
|
||||
});
|
||||
|
||||
final bool isTakingPicture;
|
||||
final bool isWaitingForPhoto;
|
||||
final int photoCountdown;
|
||||
|
||||
RemoteCameraState copyWith({
|
||||
bool? isTakingPicture,
|
||||
bool? isWaitingForPhoto,
|
||||
int? photoCountdown,
|
||||
}) {
|
||||
return RemoteCameraState(
|
||||
isTakingPicture: isTakingPicture ?? this.isTakingPicture,
|
||||
isWaitingForPhoto: isWaitingForPhoto ?? this.isWaitingForPhoto,
|
||||
photoCountdown: photoCountdown ?? this.photoCountdown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class RemoteCameraController extends _$RemoteCameraController {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
RemoteCameraState build() {
|
||||
ref.onDispose(() => _timer?.cancel());
|
||||
return const RemoteCameraState();
|
||||
}
|
||||
|
||||
Future<void> takePicture(String deviceIdentificator) async {
|
||||
state = state.copyWith(
|
||||
isTakingPicture: true,
|
||||
isWaitingForPhoto: false,
|
||||
photoCountdown: 0,
|
||||
);
|
||||
|
||||
try {
|
||||
await ref.read(commandsRepositoryProvider).send(
|
||||
request: SendCommandRequestModel(
|
||||
device: deviceIdentificator,
|
||||
command: DeviceCommand.requestPhoto,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
state = const RemoteCameraState();
|
||||
rethrow;
|
||||
}
|
||||
|
||||
unawaited(
|
||||
ref.read(sfTrackingProvider).legacyDeviceRemoteConnectionPhotoTaken(),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
isTakingPicture: false,
|
||||
isWaitingForPhoto: true,
|
||||
photoCountdown: _photoWaitSeconds,
|
||||
);
|
||||
|
||||
_timer?.cancel();
|
||||
final completer = Completer<void>();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
final remaining = state.photoCountdown - 1;
|
||||
if (remaining <= 0) {
|
||||
timer.cancel();
|
||||
_timer = null;
|
||||
state = state.copyWith(
|
||||
isWaitingForPhoto: false,
|
||||
photoCountdown: 0,
|
||||
);
|
||||
if (!completer.isCompleted) completer.complete();
|
||||
} else {
|
||||
state = state.copyWith(photoCountdown: remaining);
|
||||
}
|
||||
});
|
||||
|
||||
await completer.future;
|
||||
ref.invalidate(remotePicturesProvider(deviceIdentificator));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'remote_camera_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(RemoteCameraController)
|
||||
const remoteCameraControllerProvider = RemoteCameraControllerProvider._();
|
||||
|
||||
final class RemoteCameraControllerProvider
|
||||
extends $NotifierProvider<RemoteCameraController, RemoteCameraState> {
|
||||
const RemoteCameraControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'remoteCameraControllerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$remoteCameraControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
RemoteCameraController create() => RemoteCameraController();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(RemoteCameraState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<RemoteCameraState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$remoteCameraControllerHash() =>
|
||||
r'2f947b277991072a2d95072528931fcb13569af8';
|
||||
|
||||
abstract class _$RemoteCameraController extends $Notifier<RemoteCameraState> {
|
||||
RemoteCameraState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<RemoteCameraState, RemoteCameraState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<RemoteCameraState, RemoteCameraState>,
|
||||
RemoteCameraState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
part 'remote_picture_index_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class RemotePictureIndex extends _$RemotePictureIndex {
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void prev({required int total}) {
|
||||
if (total <= 0) return;
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyDeviceRemoteConnectionPictureViewed('prev'),
|
||||
);
|
||||
final next = state - 1;
|
||||
state = next < 0 ? total - 1 : next;
|
||||
}
|
||||
|
||||
void next({required int total}) {
|
||||
if (total <= 0) return;
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyDeviceRemoteConnectionPictureViewed('next'),
|
||||
);
|
||||
state = (state + 1) % total;
|
||||
}
|
||||
|
||||
void set(int value) {
|
||||
if (value == state) return;
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyDeviceRemoteConnectionPictureViewed('direct'),
|
||||
);
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'remote_picture_index_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(RemotePictureIndex)
|
||||
const remotePictureIndexProvider = RemotePictureIndexProvider._();
|
||||
|
||||
final class RemotePictureIndexProvider
|
||||
extends $NotifierProvider<RemotePictureIndex, int> {
|
||||
const RemotePictureIndexProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'remotePictureIndexProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$remotePictureIndexHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
RemotePictureIndex create() => RemotePictureIndex();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$remotePictureIndexHash() =>
|
||||
r'239044ffbde1be75d554b1a41acc887214b965df';
|
||||
|
||||
abstract class _$RemotePictureIndex extends $Notifier<int> {
|
||||
int build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<int, int>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<int, int>,
|
||||
int,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:device_management/src/core/providers/pictures_repository_provider.dart';
|
||||
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'remote_pictures_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<PictureEntity>> remotePictures(
|
||||
Ref ref,
|
||||
String deviceIdentificator,
|
||||
) async {
|
||||
return ref
|
||||
.read(picturesRepositoryProvider)
|
||||
.getPictures(deviceId: deviceIdentificator);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'remote_pictures_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(remotePictures)
|
||||
const remotePicturesProvider = RemotePicturesFamily._();
|
||||
|
||||
final class RemotePicturesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<PictureEntity>>,
|
||||
List<PictureEntity>,
|
||||
FutureOr<List<PictureEntity>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<PictureEntity>>,
|
||||
$FutureProvider<List<PictureEntity>> {
|
||||
const RemotePicturesProvider._({
|
||||
required RemotePicturesFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'remotePicturesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$remotePicturesHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'remotePicturesProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<PictureEntity>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<PictureEntity>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return remotePictures(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is RemotePicturesProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$remotePicturesHash() => r'b6a70ae302081c6be2f829fe3a13dc2107cfaee8';
|
||||
|
||||
final class RemotePicturesFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<PictureEntity>>, String> {
|
||||
const RemotePicturesFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'remotePicturesProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
RemotePicturesProvider call(String deviceIdentificator) =>
|
||||
RemotePicturesProvider._(argument: deviceIdentificator, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'remotePicturesProvider';
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
part 'spy_call_controller.g.dart';
|
||||
|
||||
class SpyCallInvalidPhoneException implements Exception {
|
||||
const SpyCallInvalidPhoneException();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class SpyCallController extends _$SpyCallController {
|
||||
@override
|
||||
FutureOr<void> build() {}
|
||||
|
||||
Future<void> call({
|
||||
required String deviceIdentificator,
|
||||
required String phoneE164,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(commandsRepositoryProvider).send(
|
||||
request: SendCommandRequestModel(
|
||||
device: deviceIdentificator,
|
||||
command: DeviceCommand.callCenter,
|
||||
data: {'phone_number': phoneE164},
|
||||
),
|
||||
);
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyDeviceRemoteConnectionCallInitiated(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'spy_call_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(SpyCallController)
|
||||
const spyCallControllerProvider = SpyCallControllerProvider._();
|
||||
|
||||
final class SpyCallControllerProvider
|
||||
extends $AsyncNotifierProvider<SpyCallController, void> {
|
||||
const SpyCallControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'spyCallControllerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$spyCallControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SpyCallController create() => SpyCallController();
|
||||
}
|
||||
|
||||
String _$spyCallControllerHash() => r'f5e88f8b050d5461d60836f0c288f987f9ed3604';
|
||||
|
||||
abstract class _$SpyCallController 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'spy_call_form_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class SpyCallForm extends _$SpyCallForm {
|
||||
@override
|
||||
String build() => 'ES';
|
||||
|
||||
void setIsoCode(String code) {
|
||||
if (code == state) return;
|
||||
state = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'spy_call_form_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(SpyCallForm)
|
||||
const spyCallFormProvider = SpyCallFormProvider._();
|
||||
|
||||
final class SpyCallFormProvider extends $NotifierProvider<SpyCallForm, String> {
|
||||
const SpyCallFormProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'spyCallFormProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$spyCallFormHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SpyCallForm create() => SpyCallForm();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$spyCallFormHash() => r'5e883cd4c326b9ed1aa17f26a24930b3dc64ba65';
|
||||
|
||||
abstract class _$SpyCallForm extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String, String>,
|
||||
String,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/remote_camera_controller.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/widgets/show_picture_dialog.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:legacy_ui/legacy_ui.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_model.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/widgets/show_picture_dialog.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
|
||||
class RemoteCameraScreen extends ConsumerWidget {
|
||||
final NavigationContract navigationContract;
|
||||
@@ -19,88 +22,66 @@ class RemoteCameraScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ref.listen(remoteConnectionViewModelProvider.select((s) => s.errorEvent), (
|
||||
_,
|
||||
next,
|
||||
) {
|
||||
if (next != null) {
|
||||
final message = switch (next) {
|
||||
RemoteConnectionErrorEvent.takePicture => context.translate(
|
||||
I18n.errorTakePicture,
|
||||
),
|
||||
RemoteConnectionErrorEvent.fetchPhotos => context.translate(
|
||||
I18n.errorFetchPhotos,
|
||||
),
|
||||
RemoteConnectionErrorEvent.call => context.translate(I18n.errorCall),
|
||||
RemoteConnectionErrorEvent.invalidPhone => context.translate(
|
||||
I18n.errorMessagePhoneIsInvalid,
|
||||
),
|
||||
};
|
||||
showTopSnackbar(context, message: message, type: MessageType.error);
|
||||
}
|
||||
});
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
|
||||
ref.listen(
|
||||
remoteConnectionViewModelProvider.select((s) => s.successEvent),
|
||||
(_, next) {
|
||||
if (next != null) {
|
||||
final message = switch (next) {
|
||||
RemoteConnectionSuccessEvent.photoTaken => context.translate(
|
||||
I18n.photoTaken,
|
||||
),
|
||||
};
|
||||
showTopSnackbar(context, message: message, type: MessageType.success);
|
||||
ref.read(remoteConnectionViewModelProvider.notifier).clearSuccess();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (device == null) {
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.remoteCamera),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final isLoading = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.isLoadingPictures),
|
||||
);
|
||||
final isTaking = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.isTakingPicture),
|
||||
);
|
||||
final isWaiting = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.isWaitingForPhoto),
|
||||
);
|
||||
final countdown = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.photoCountdown),
|
||||
);
|
||||
final identificator = device.identificator;
|
||||
final cameraState = ref.watch(remoteCameraControllerProvider);
|
||||
final picturesAsync = ref.watch(remotePicturesProvider(identificator));
|
||||
|
||||
Widget body;
|
||||
if (isLoading || isTaking) {
|
||||
if (cameraState.isTakingPicture) {
|
||||
body = const Center(child: CircularProgressIndicator());
|
||||
} else if (isWaiting) {
|
||||
body = _WaitingForPhotoOverlay(remainingSeconds: countdown);
|
||||
} else if (cameraState.isWaitingForPhoto) {
|
||||
body = _WaitingForPhotoOverlay(
|
||||
remainingSeconds: cameraState.photoCountdown,
|
||||
);
|
||||
} else {
|
||||
body = const _GallerySection();
|
||||
body = picturesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) =>
|
||||
Center(child: Text(context.translate(I18n.errorFetchPhotos))),
|
||||
data: (pictures) => _GallerySection(pictures: pictures),
|
||||
);
|
||||
}
|
||||
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.remoteCamera),
|
||||
body: body,
|
||||
footer: isWaiting ? const SizedBox.shrink() : const _TakePictureSection(),
|
||||
footer: cameraState.isWaitingForPhoto
|
||||
? const SizedBox.shrink()
|
||||
: _TakePictureSection(deviceIdentificator: identificator),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GallerySection extends ConsumerWidget {
|
||||
const _GallerySection();
|
||||
final List<PictureEntity> pictures;
|
||||
|
||||
const _GallerySection({required this.pictures});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(remoteConnectionViewModelProvider.notifier);
|
||||
|
||||
final pictures = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.pictures),
|
||||
);
|
||||
if (pictures.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
context.translate(I18n.noPhotosAvailable),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.count(
|
||||
primary: false,
|
||||
padding: SizeUtils.getByScreen(
|
||||
small: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
big: EdgeInsets.symmetric(horizontal: 23, vertical: 11),
|
||||
small: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
big: const EdgeInsets.symmetric(horizontal: 23, vertical: 11),
|
||||
),
|
||||
crossAxisSpacing: 11,
|
||||
mainAxisSpacing: 11,
|
||||
@@ -110,10 +91,10 @@ class _GallerySection extends ConsumerWidget {
|
||||
pictures.length,
|
||||
(int index) => TextButton(
|
||||
onPressed: () {
|
||||
vm.setPictureIndex(index);
|
||||
showDialog(
|
||||
ref.read(remotePictureIndexProvider.notifier).set(index);
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => Dialog(child: ShowPictureDialog()),
|
||||
builder: (_) => const Dialog(child: ShowPictureDialog()),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@@ -135,21 +116,31 @@ class _GallerySection extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class _TakePictureSection extends ConsumerWidget {
|
||||
const _TakePictureSection();
|
||||
final String deviceIdentificator;
|
||||
|
||||
const _TakePictureSection({required this.deviceIdentificator});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(remoteConnectionViewModelProvider.notifier);
|
||||
|
||||
return Padding(
|
||||
padding: SizeUtils.getByScreen(
|
||||
small: EdgeInsets.symmetric(vertical: 12, horizontal: 26),
|
||||
big: EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||
small: const EdgeInsets.symmetric(vertical: 12, horizontal: 26),
|
||||
big: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||
),
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.takePicture();
|
||||
if (!context.mounted) return;
|
||||
try {
|
||||
await ref
|
||||
.read(remoteCameraControllerProvider.notifier)
|
||||
.takePicture(deviceIdentificator);
|
||||
if (!context.mounted) return;
|
||||
await showSuccessDialog(context, I18n.photoTaken);
|
||||
} catch (_) {
|
||||
if (!context.mounted) return;
|
||||
await showErrorDialog(context, I18n.errorTakePicture);
|
||||
}
|
||||
},
|
||||
text: context.translate(I18n.takePicture),
|
||||
color: context.sfColors.legacyPrimary,
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:device_management/src/core/providers/pictures_repository_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
import '../../../../core/domain/repositories/pictures_repository.dart';
|
||||
|
||||
final remoteConnectionViewModelProvider =
|
||||
NotifierProvider.autoDispose<
|
||||
RemoteConnectionViewModel,
|
||||
RemoteConnectionViewState
|
||||
>(RemoteConnectionViewModel.new);
|
||||
|
||||
class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
|
||||
late final TextEditingController phoneController;
|
||||
late final CommandsRepository _commandsRepository;
|
||||
late final PicturesRepository _picturesRepository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
Timer? _photoTimer;
|
||||
|
||||
static const int _photoWaitSeconds = 5;
|
||||
|
||||
@override
|
||||
RemoteConnectionViewState build() {
|
||||
_commandsRepository = ref.read(commandsRepositoryProvider);
|
||||
_picturesRepository = ref.read(picturesRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionStarted());
|
||||
|
||||
phoneController = TextEditingController();
|
||||
phoneController.addListener(_onPhoneChanged);
|
||||
|
||||
ref.onDispose(disposeControllers);
|
||||
|
||||
Future.microtask(load);
|
||||
|
||||
return const RemoteConnectionViewState();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
if (device == null) return;
|
||||
|
||||
state = state.copyWith(deviceId: device.identificator);
|
||||
|
||||
try {
|
||||
final pictures = await _picturesRepository.getPictures(
|
||||
deviceId: device.identificator,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(pictures: pictures, isLoadingPictures: false);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoadingPictures: false);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPhoneChanged() {
|
||||
final text = phoneController.text;
|
||||
if (text == state.phone) return;
|
||||
|
||||
state = state.copyWith(phone: text, errorEvent: null);
|
||||
}
|
||||
|
||||
void updateCountry(String isoCode) {
|
||||
if (isoCode == state.isoCode) return;
|
||||
state = state.copyWith(isoCode: isoCode, errorEvent: null);
|
||||
}
|
||||
|
||||
void prevPicture() {
|
||||
int pictureIndex = state.pictureIndex - 1;
|
||||
|
||||
if (pictureIndex < 0) {
|
||||
pictureIndex = state.pictures.length - 1;
|
||||
}
|
||||
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('prev'));
|
||||
state = state.copyWith(pictureIndex: pictureIndex);
|
||||
}
|
||||
|
||||
void nextPicture() {
|
||||
int pictureIndex = (state.pictureIndex + 1) % state.pictures.length;
|
||||
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('next'));
|
||||
state = state.copyWith(pictureIndex: pictureIndex);
|
||||
}
|
||||
|
||||
void setPictureIndex(int value) {
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('direct'));
|
||||
state = state.copyWith(pictureIndex: value);
|
||||
}
|
||||
|
||||
void clearSuccess() {
|
||||
state = state.copyWith(successEvent: null);
|
||||
}
|
||||
|
||||
Future<void> takePicture() async {
|
||||
try {
|
||||
state = state.copyWith(
|
||||
isTakingPicture: true,
|
||||
successEvent: null,
|
||||
errorEvent: null,
|
||||
);
|
||||
|
||||
final request = SendCommandRequestModel(
|
||||
device: state.deviceId,
|
||||
command: DeviceCommand.requestPhoto,
|
||||
);
|
||||
|
||||
await _commandsRepository.send(request: request);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionPhotoTaken());
|
||||
|
||||
state = state.copyWith(
|
||||
isTakingPicture: false,
|
||||
isWaitingForPhoto: true,
|
||||
photoCountdown: _photoWaitSeconds,
|
||||
);
|
||||
|
||||
_startPhotoCountdown();
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isTakingPicture: false,
|
||||
errorEvent: RemoteConnectionErrorEvent.takePicture,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _startPhotoCountdown() {
|
||||
_photoTimer?.cancel();
|
||||
_photoTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!ref.mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
final remaining = state.photoCountdown - 1;
|
||||
if (remaining <= 0) {
|
||||
timer.cancel();
|
||||
_photoTimer = null;
|
||||
_fetchPhotosAfterCapture();
|
||||
} else {
|
||||
state = state.copyWith(photoCountdown: remaining);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _fetchPhotosAfterCapture() async {
|
||||
try {
|
||||
final pictures = await _picturesRepository.getPictures(
|
||||
deviceId: state.deviceId,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(
|
||||
isWaitingForPhoto: false,
|
||||
photoCountdown: 0,
|
||||
pictures: pictures,
|
||||
successEvent: RemoteConnectionSuccessEvent.photoTaken,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isWaitingForPhoto: false,
|
||||
photoCountdown: 0,
|
||||
errorEvent: RemoteConnectionErrorEvent.fetchPhotos,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> call() async {
|
||||
final parsed = SfPhoneNumber.tryParse(
|
||||
phoneController.text,
|
||||
defaultIsoCode: state.isoCode,
|
||||
);
|
||||
if (parsed == null) {
|
||||
state = state.copyWith(
|
||||
errorEvent: RemoteConnectionErrorEvent.invalidPhone,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state = state.copyWith(isCalling: true);
|
||||
|
||||
final request = SendCommandRequestModel(
|
||||
device: state.deviceId,
|
||||
command: DeviceCommand.callCenter,
|
||||
data: {'phone_number': parsed.e164},
|
||||
);
|
||||
await _commandsRepository.send(request: request);
|
||||
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionCallInitiated());
|
||||
|
||||
state = state.copyWith(isCalling: false);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isCalling: false,
|
||||
errorEvent: RemoteConnectionErrorEvent.call,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void disposeControllers() {
|
||||
phoneController.removeListener(_onPhoneChanged);
|
||||
|
||||
phoneController.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
|
||||
|
||||
part 'remote_connection_view_state.freezed.dart';
|
||||
|
||||
enum RemoteConnectionErrorEvent { takePicture, fetchPhotos, call, invalidPhone }
|
||||
|
||||
enum RemoteConnectionSuccessEvent { photoTaken }
|
||||
|
||||
@freezed
|
||||
abstract class RemoteConnectionViewState with _$RemoteConnectionViewState {
|
||||
const factory RemoteConnectionViewState({
|
||||
@Default('') String deviceId,
|
||||
@Default('ES') String isoCode,
|
||||
@Default('') String phone,
|
||||
@Default([]) List<PictureEntity> pictures,
|
||||
@Default(0) int pictureIndex,
|
||||
@Default(true) bool isLoadingPictures,
|
||||
@Default(false) bool isTakingPicture,
|
||||
@Default(false) bool isWaitingForPhoto,
|
||||
@Default(0) int photoCountdown,
|
||||
@Default(false) bool isCalling,
|
||||
RemoteConnectionErrorEvent? errorEvent,
|
||||
RemoteConnectionSuccessEvent? successEvent,
|
||||
}) = _RemoteConnectionViewState;
|
||||
}
|
||||
@@ -1,310 +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 'remote_connection_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$RemoteConnectionViewState {
|
||||
|
||||
String get deviceId; String get isoCode; String get phone; List<PictureEntity> get pictures; int get pictureIndex; bool get isLoadingPictures; bool get isTakingPicture; bool get isWaitingForPhoto; int get photoCountdown; bool get isCalling; RemoteConnectionErrorEvent? get errorEvent; RemoteConnectionSuccessEvent? get successEvent;
|
||||
/// Create a copy of RemoteConnectionViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$RemoteConnectionViewStateCopyWith<RemoteConnectionViewState> get copyWith => _$RemoteConnectionViewStateCopyWithImpl<RemoteConnectionViewState>(this as RemoteConnectionViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other.pictures, pictures)&&(identical(other.pictureIndex, pictureIndex) || other.pictureIndex == pictureIndex)&&(identical(other.isLoadingPictures, isLoadingPictures) || other.isLoadingPictures == isLoadingPictures)&&(identical(other.isTakingPicture, isTakingPicture) || other.isTakingPicture == isTakingPicture)&&(identical(other.isWaitingForPhoto, isWaitingForPhoto) || other.isWaitingForPhoto == isWaitingForPhoto)&&(identical(other.photoCountdown, photoCountdown) || other.photoCountdown == photoCountdown)&&(identical(other.isCalling, isCalling) || other.isCalling == isCalling)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,deviceId,isoCode,phone,const DeepCollectionEquality().hash(pictures),pictureIndex,isLoadingPictures,isTakingPicture,isWaitingForPhoto,photoCountdown,isCalling,errorEvent,successEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RemoteConnectionViewState(deviceId: $deviceId, isoCode: $isoCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isWaitingForPhoto: $isWaitingForPhoto, photoCountdown: $photoCountdown, isCalling: $isCalling, errorEvent: $errorEvent, successEvent: $successEvent)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $RemoteConnectionViewStateCopyWith<$Res> {
|
||||
factory $RemoteConnectionViewStateCopyWith(RemoteConnectionViewState value, $Res Function(RemoteConnectionViewState) _then) = _$RemoteConnectionViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String deviceId, String isoCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$RemoteConnectionViewStateCopyWithImpl<$Res>
|
||||
implements $RemoteConnectionViewStateCopyWith<$Res> {
|
||||
_$RemoteConnectionViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final RemoteConnectionViewState _self;
|
||||
final $Res Function(RemoteConnectionViewState) _then;
|
||||
|
||||
/// Create a copy of RemoteConnectionViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? isoCode = null,Object? phone = null,Object? pictures = null,Object? pictureIndex = null,Object? isLoadingPictures = null,Object? isTakingPicture = null,Object? isWaitingForPhoto = null,Object? photoCountdown = null,Object? isCalling = null,Object? errorEvent = freezed,Object? successEvent = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
|
||||
as String,pictures: null == pictures ? _self.pictures : pictures // ignore: cast_nullable_to_non_nullable
|
||||
as List<PictureEntity>,pictureIndex: null == pictureIndex ? _self.pictureIndex : pictureIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,isLoadingPictures: null == isLoadingPictures ? _self.isLoadingPictures : isLoadingPictures // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isTakingPicture: null == isTakingPicture ? _self.isTakingPicture : isTakingPicture // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isWaitingForPhoto: null == isWaitingForPhoto ? _self.isWaitingForPhoto : isWaitingForPhoto // ignore: cast_nullable_to_non_nullable
|
||||
as bool,photoCountdown: null == photoCountdown ? _self.photoCountdown : photoCountdown // ignore: cast_nullable_to_non_nullable
|
||||
as int,isCalling: null == isCalling ? _self.isCalling : isCalling // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as RemoteConnectionErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
|
||||
as RemoteConnectionSuccessEvent?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [RemoteConnectionViewState].
|
||||
extension RemoteConnectionViewStatePatterns on RemoteConnectionViewState {
|
||||
/// 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( _RemoteConnectionViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _RemoteConnectionViewState() 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( _RemoteConnectionViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _RemoteConnectionViewState():
|
||||
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( _RemoteConnectionViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _RemoteConnectionViewState() 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( String deviceId, String isoCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _RemoteConnectionViewState() when $default != null:
|
||||
return $default(_that.deviceId,_that.isoCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);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( String deviceId, String isoCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _RemoteConnectionViewState():
|
||||
return $default(_that.deviceId,_that.isoCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);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( String deviceId, String isoCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _RemoteConnectionViewState() when $default != null:
|
||||
return $default(_that.deviceId,_that.isoCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _RemoteConnectionViewState implements RemoteConnectionViewState {
|
||||
const _RemoteConnectionViewState({this.deviceId = '', this.isoCode = 'ES', this.phone = '', final List<PictureEntity> pictures = const [], this.pictureIndex = 0, this.isLoadingPictures = true, this.isTakingPicture = false, this.isWaitingForPhoto = false, this.photoCountdown = 0, this.isCalling = false, this.errorEvent, this.successEvent}): _pictures = pictures;
|
||||
|
||||
|
||||
@override@JsonKey() final String deviceId;
|
||||
@override@JsonKey() final String isoCode;
|
||||
@override@JsonKey() final String phone;
|
||||
final List<PictureEntity> _pictures;
|
||||
@override@JsonKey() List<PictureEntity> get pictures {
|
||||
if (_pictures is EqualUnmodifiableListView) return _pictures;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_pictures);
|
||||
}
|
||||
|
||||
@override@JsonKey() final int pictureIndex;
|
||||
@override@JsonKey() final bool isLoadingPictures;
|
||||
@override@JsonKey() final bool isTakingPicture;
|
||||
@override@JsonKey() final bool isWaitingForPhoto;
|
||||
@override@JsonKey() final int photoCountdown;
|
||||
@override@JsonKey() final bool isCalling;
|
||||
@override final RemoteConnectionErrorEvent? errorEvent;
|
||||
@override final RemoteConnectionSuccessEvent? successEvent;
|
||||
|
||||
/// Create a copy of RemoteConnectionViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$RemoteConnectionViewStateCopyWith<_RemoteConnectionViewState> get copyWith => __$RemoteConnectionViewStateCopyWithImpl<_RemoteConnectionViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other._pictures, _pictures)&&(identical(other.pictureIndex, pictureIndex) || other.pictureIndex == pictureIndex)&&(identical(other.isLoadingPictures, isLoadingPictures) || other.isLoadingPictures == isLoadingPictures)&&(identical(other.isTakingPicture, isTakingPicture) || other.isTakingPicture == isTakingPicture)&&(identical(other.isWaitingForPhoto, isWaitingForPhoto) || other.isWaitingForPhoto == isWaitingForPhoto)&&(identical(other.photoCountdown, photoCountdown) || other.photoCountdown == photoCountdown)&&(identical(other.isCalling, isCalling) || other.isCalling == isCalling)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,deviceId,isoCode,phone,const DeepCollectionEquality().hash(_pictures),pictureIndex,isLoadingPictures,isTakingPicture,isWaitingForPhoto,photoCountdown,isCalling,errorEvent,successEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RemoteConnectionViewState(deviceId: $deviceId, isoCode: $isoCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isWaitingForPhoto: $isWaitingForPhoto, photoCountdown: $photoCountdown, isCalling: $isCalling, errorEvent: $errorEvent, successEvent: $successEvent)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$RemoteConnectionViewStateCopyWith<$Res> implements $RemoteConnectionViewStateCopyWith<$Res> {
|
||||
factory _$RemoteConnectionViewStateCopyWith(_RemoteConnectionViewState value, $Res Function(_RemoteConnectionViewState) _then) = __$RemoteConnectionViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String deviceId, String isoCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$RemoteConnectionViewStateCopyWithImpl<$Res>
|
||||
implements _$RemoteConnectionViewStateCopyWith<$Res> {
|
||||
__$RemoteConnectionViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _RemoteConnectionViewState _self;
|
||||
final $Res Function(_RemoteConnectionViewState) _then;
|
||||
|
||||
/// Create a copy of RemoteConnectionViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? isoCode = null,Object? phone = null,Object? pictures = null,Object? pictureIndex = null,Object? isLoadingPictures = null,Object? isTakingPicture = null,Object? isWaitingForPhoto = null,Object? photoCountdown = null,Object? isCalling = null,Object? errorEvent = freezed,Object? successEvent = freezed,}) {
|
||||
return _then(_RemoteConnectionViewState(
|
||||
deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable
|
||||
as String,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
|
||||
as String,pictures: null == pictures ? _self._pictures : pictures // ignore: cast_nullable_to_non_nullable
|
||||
as List<PictureEntity>,pictureIndex: null == pictureIndex ? _self.pictureIndex : pictureIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,isLoadingPictures: null == isLoadingPictures ? _self.isLoadingPictures : isLoadingPictures // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isTakingPicture: null == isTakingPicture ? _self.isTakingPicture : isTakingPicture // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isWaitingForPhoto: null == isWaitingForPhoto ? _self.isWaitingForPhoto : isWaitingForPhoto // ignore: cast_nullable_to_non_nullable
|
||||
as bool,photoCountdown: null == photoCountdown ? _self.photoCountdown : photoCountdown // ignore: cast_nullable_to_non_nullable
|
||||
as int,isCalling: null == isCalling ? _self.isCalling : isCalling // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as RemoteConnectionErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
|
||||
as RemoteConnectionSuccessEvent?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_model.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class ShowPictureDialog extends ConsumerWidget {
|
||||
@@ -10,15 +12,15 @@ class ShowPictureDialog extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final viewModel = ref.read(remoteConnectionViewModelProvider.notifier);
|
||||
final pictures = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.pictures),
|
||||
);
|
||||
final pictureIndex = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.pictureIndex),
|
||||
);
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
if (device == null) return const SizedBox.shrink();
|
||||
|
||||
if (pictures.isEmpty) {
|
||||
final picturesAsync =
|
||||
ref.watch(remotePicturesProvider(device.identificator));
|
||||
final pictureIndex = ref.watch(remotePictureIndexProvider);
|
||||
final pictures = picturesAsync.value ?? const [];
|
||||
|
||||
if (pictures.isEmpty || pictureIndex >= pictures.length) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
@@ -47,8 +49,12 @@ class ShowPictureDialog extends ConsumerWidget {
|
||||
Expanded(child: _PictureSection(picture: picture)),
|
||||
_MetadataSection(picture: picture),
|
||||
_ControlsSection(
|
||||
prev: viewModel.prevPicture,
|
||||
next: viewModel.nextPicture,
|
||||
prev: () => ref
|
||||
.read(remotePictureIndexProvider.notifier)
|
||||
.prev(total: pictures.length),
|
||||
next: () => ref
|
||||
.read(remotePictureIndexProvider.notifier)
|
||||
.next(total: pictures.length),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -90,7 +96,10 @@ class _MetadataSection extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
dateStr,
|
||||
style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,67 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/spy_call_controller.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/spy_call_form_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_model.dart';
|
||||
import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class SpyCallDialog extends ConsumerWidget {
|
||||
class SpyCallDialog extends ConsumerStatefulWidget {
|
||||
const SpyCallDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(remoteConnectionViewModelProvider.notifier);
|
||||
ConsumerState<SpyCallDialog> createState() => _SpyCallDialogState();
|
||||
}
|
||||
|
||||
ref.listen(remoteConnectionViewModelProvider.select((s) => s.errorEvent), (
|
||||
_,
|
||||
next,
|
||||
) {
|
||||
if (next != null) {
|
||||
final message = switch (next) {
|
||||
RemoteConnectionErrorEvent.invalidPhone => context.translate(
|
||||
I18n.errorMessagePhoneIsInvalid,
|
||||
),
|
||||
RemoteConnectionErrorEvent.call => context.translate(I18n.errorCall),
|
||||
RemoteConnectionErrorEvent.takePicture => context.translate(
|
||||
I18n.errorTakePicture,
|
||||
),
|
||||
RemoteConnectionErrorEvent.fetchPhotos => context.translate(
|
||||
I18n.errorFetchPhotos,
|
||||
),
|
||||
};
|
||||
showTopSnackbar(context, message: message, type: MessageType.error);
|
||||
}
|
||||
});
|
||||
class _SpyCallDialogState extends ConsumerState<SpyCallDialog> {
|
||||
final _phoneController = TextEditingController();
|
||||
|
||||
ref.listen(remoteConnectionViewModelProvider.select((s) => s.isCalling), (
|
||||
prev,
|
||||
isCalling,
|
||||
) {
|
||||
if (prev == true && !isCalling) {
|
||||
final error = ref.read(remoteConnectionViewModelProvider).errorEvent;
|
||||
if (error == null && context.mounted) {
|
||||
Navigator.pop(context);
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.remoteListening),
|
||||
type: MessageType.success,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final isoCode = ref.read(spyCallFormProvider);
|
||||
final parsed = SfPhoneNumber.tryParse(
|
||||
_phoneController.text,
|
||||
defaultIsoCode: isoCode,
|
||||
);
|
||||
if (parsed == null) {
|
||||
await showErrorDialog(context, I18n.errorMessagePhoneIsInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
if (device == null) return;
|
||||
|
||||
await ref.read(spyCallControllerProvider.notifier).call(
|
||||
deviceIdentificator: device.identificator,
|
||||
phoneE164: parsed.e164,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final state = ref.read(spyCallControllerProvider);
|
||||
if (state.hasError) {
|
||||
await showErrorDialog(context, I18n.errorCall);
|
||||
return;
|
||||
}
|
||||
Navigator.pop(context);
|
||||
await showSuccessDialog(context, I18n.remoteListening);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final isoCode = ref.watch(spyCallFormProvider);
|
||||
final isCalling =
|
||||
ref.watch(spyCallControllerProvider.select((s) => s.isLoading));
|
||||
|
||||
return Container(
|
||||
padding: SizeUtils.getByScreen(
|
||||
@@ -66,7 +76,26 @@ class SpyCallDialog extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const _Header(),
|
||||
Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
context.translate(I18n.remoteListening),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 19, big: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: Icon(Icons.close, color: primaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 7)),
|
||||
Text(
|
||||
context.translate(I18n.spyCallSubtitle),
|
||||
@@ -77,106 +106,43 @@ class SpyCallDialog extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)),
|
||||
_PhoneSection(onSubmit: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.call();
|
||||
}),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CountryPrefixPicker(
|
||||
headerText: context.translate(I18n.selectYourCountry),
|
||||
initialSelection: isoCode,
|
||||
onChanged: (country) {
|
||||
final code = country.code;
|
||||
if (code != null) {
|
||||
ref.read(spyCallFormProvider.notifier).setIsoCode(code);
|
||||
}
|
||||
},
|
||||
width: 80,
|
||||
),
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 7)),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _phoneController,
|
||||
hint: context.translate(I18n.insertPhone),
|
||||
keyboardType: TextInputType.phone,
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
|
||||
_CallSection(onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.call();
|
||||
}),
|
||||
isCalling
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: _submit,
|
||||
text: context.translate(I18n.call),
|
||||
color: primaryColor,
|
||||
height: SizeUtils.getByScreen(small: 38, big: 36),
|
||||
radius: SizeUtils.getByScreen(small: 32, big: 34),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
context.translate(I18n.remoteListening),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 19, big: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: Icon(Icons.close, color: context.sfColors.legacyPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PhoneSection extends ConsumerWidget {
|
||||
final VoidCallback onSubmit;
|
||||
|
||||
const _PhoneSection({required this.onSubmit});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(remoteConnectionViewModelProvider.notifier);
|
||||
final isoCode = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.isoCode),
|
||||
);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CountryPrefixPicker(
|
||||
headerText: context.translate(I18n.selectYourCountry),
|
||||
initialSelection: isoCode,
|
||||
onChanged: (country) {
|
||||
final code = country.code;
|
||||
if (code != null) vm.updateCountry(code);
|
||||
},
|
||||
width: 80,
|
||||
),
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 7)),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: vm.phoneController,
|
||||
hint: context.translate(I18n.insertPhone),
|
||||
keyboardType: TextInputType.phone,
|
||||
onSubmitted: (_) => onSubmit(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CallSection extends ConsumerWidget {
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _CallSection({required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isCalling = ref.watch(
|
||||
remoteConnectionViewModelProvider.select((s) => s.isCalling),
|
||||
);
|
||||
|
||||
return isCalling
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: onPressed,
|
||||
text: context.translate(I18n.call),
|
||||
color: context.sfColors.legacyPrimary,
|
||||
height: SizeUtils.getByScreen(small: 38, big: 36),
|
||||
radius: SizeUtils.getByScreen(small: 32, big: 34),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:device_management/src/features/remote_connection/presentation/providers/spy_call_controller.dart';
|
||||
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:sf_infrastructure/sf_infrastructure.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
class MockCommandsRepository extends Mock implements CommandsRepository {}
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
SendCommandRequestModel(
|
||||
device: 'x',
|
||||
command: DeviceCommand.callCenter,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
ProviderContainer buildContainer(CommandsRepository commands) {
|
||||
return makeContainer(
|
||||
overrides: [
|
||||
commandsRepositoryProvider.overrideWithValue(commands),
|
||||
sfTrackingProvider.overrideWithValue(
|
||||
SfTrackingRepository(clients: const []),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
group('SpyCallController.call', () {
|
||||
test('sends CALL_CENTER command and transitions to AsyncData', () async {
|
||||
final commands = MockCommandsRepository();
|
||||
when(() => commands.send(request: any(named: 'request')))
|
||||
.thenAnswer((_) async {});
|
||||
|
||||
final container = buildContainer(commands);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(spyCallControllerProvider.notifier).call(
|
||||
deviceIdentificator: 'imei-1',
|
||||
phoneE164: '+34600000001',
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(spyCallControllerProvider),
|
||||
isA<AsyncData<void>>(),
|
||||
);
|
||||
final sent = verify(
|
||||
() => commands.send(request: captureAny(named: 'request')),
|
||||
).captured.single as SendCommandRequestModel;
|
||||
expect(sent.command, DeviceCommand.callCenter);
|
||||
expect(sent.device, 'imei-1');
|
||||
expect(sent.data, {'phone_number': '+34600000001'});
|
||||
});
|
||||
|
||||
test('exposes AsyncError when command fails', () async {
|
||||
final commands = MockCommandsRepository();
|
||||
when(() => commands.send(request: any(named: 'request')))
|
||||
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
|
||||
|
||||
final container = buildContainer(commands);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(spyCallControllerProvider.notifier).call(
|
||||
deviceIdentificator: 'imei-1',
|
||||
phoneE164: '+34600000001',
|
||||
);
|
||||
|
||||
expect(
|
||||
container.read(spyCallControllerProvider),
|
||||
isA<AsyncError<void>>(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user