feat: sync device settings after updates and improve remote connection features

- Add DeviceSettingsSync extension on Ref to centralize device provider
    updates after settings changes (sound, volume, language, timezone,
    battery, disable functions, alerts, pedometer, heart rate freq,
    location freq, background image)
  - Add photo capture countdown with Lottie animation in remote camera
  - Replace Image.network with Image.memory for photo display
  - Fix commands datasource to handle text/html responses (post<dynamic>)
  - Add typed error/success events to RemoteConnectionViewModel
  - Add background image active indicator and backgroundImageId to device settings
  - Change photos endpoint from /devices/identificator/:id/photos/files to /photos/files
  - Remove dead deviceId param from getBackgroundImage chain
  - Relax PictureEntity required fields to @Default for API compatibility
  - Fix LocationViewModel rebuild crash (ref.watch → ref.read)
  - Use .select() in RemoteCameraScreen for optimized rebuilds
  - Increase health measure countdown to 60s
This commit is contained in:
2026-03-25 13:51:48 +01:00
parent a05c167f30
commit b7614a39f1
46 changed files with 489 additions and 232 deletions

View File

@@ -1,9 +1,7 @@
import '../models/get_background_image_response_model.dart';
abstract class BackgroundImageRemoteDatasource {
Future<GetBackgroundImageResponseModel> getBackgroundImage({
required String deviceId,
});
Future<GetBackgroundImageResponseModel> getBackgroundImage();
Future<String> uploadImage({
required String path,

View File

@@ -6,17 +6,16 @@ import 'package:sf_infrastructure/configure_dependencies.dart';
import '../models/get_background_image_response_model.dart';
import 'background_image_remote_datasource.dart';
class BackgroundImageRemoteDatasourceImpl implements BackgroundImageRemoteDatasource {
class BackgroundImageRemoteDatasourceImpl
implements BackgroundImageRemoteDatasource {
BackgroundImageRemoteDatasourceImpl(this._repository);
final QuestiaRepository _repository;
@override
Future<GetBackgroundImageResponseModel> getBackgroundImage({required String deviceId}) async {
Future<GetBackgroundImageResponseModel> getBackgroundImage() async {
final response = await safeCall(
() => _repository.get<dynamic>(
'/devices/identificator/$deviceId/photos/files',
),
() => _repository.get<dynamic>('/photos/files'),
'Error getting background image',
);
@@ -39,10 +38,7 @@ class BackgroundImageRemoteDatasourceImpl implements BackgroundImageRemoteDataso
final dio = GetIt.I<Dio>();
dio.options.headers.remove('content-type');
try {
final response = await dio.post<dynamic>(
'/photos',
data: formData,
);
final response = await dio.post<dynamic>('/photos', data: formData);
final data = response.data;
if (data == null) {
@@ -74,4 +70,4 @@ class BackgroundImageRemoteDatasourceImpl implements BackgroundImageRemoteDataso
'Error setting background image',
);
}
}
}

View File

@@ -22,13 +22,13 @@ abstract class GetPicturesItemResponseModel
with _$GetPicturesItemResponseModel {
const factory GetPicturesItemResponseModel({
required String id,
required String deviceIdentificator,
@Default('') String deviceIdentificator,
String? imgType,
String? timestamp,
required String fileId,
@Default('') String fileId,
String? fileName,
String? contentType,
required int createdAt,
@Default(0) int createdAt,
Map<String, dynamic>? file,
}) = _GetPicturesItemResponseModel;

View File

@@ -486,17 +486,17 @@ return $default(_that.id,_that.deviceIdentificator,_that.imgType,_that.timestamp
@JsonSerializable()
class _GetPicturesItemResponseModel implements GetPicturesItemResponseModel {
const _GetPicturesItemResponseModel({required this.id, required this.deviceIdentificator, this.imgType, this.timestamp, required this.fileId, this.fileName, this.contentType, required this.createdAt, final Map<String, dynamic>? file}): _file = file;
const _GetPicturesItemResponseModel({required this.id, this.deviceIdentificator = '', this.imgType, this.timestamp, this.fileId = '', this.fileName, this.contentType, this.createdAt = 0, final Map<String, dynamic>? file}): _file = file;
factory _GetPicturesItemResponseModel.fromJson(Map<String, dynamic> json) => _$GetPicturesItemResponseModelFromJson(json);
@override final String id;
@override final String deviceIdentificator;
@override@JsonKey() final String deviceIdentificator;
@override final String? imgType;
@override final String? timestamp;
@override final String fileId;
@override@JsonKey() final String fileId;
@override final String? fileName;
@override final String? contentType;
@override final int createdAt;
@override@JsonKey() final int createdAt;
final Map<String, dynamic>? _file;
@override Map<String, dynamic>? get file {
final value = _file;

View File

@@ -24,13 +24,13 @@ _GetPicturesItemResponseModel _$GetPicturesItemResponseModelFromJson(
Map<String, dynamic> json,
) => _GetPicturesItemResponseModel(
id: json['id'] as String,
deviceIdentificator: json['deviceIdentificator'] as String,
deviceIdentificator: json['deviceIdentificator'] as String? ?? '',
imgType: json['imgType'] as String?,
timestamp: json['timestamp'] as String?,
fileId: json['fileId'] as String,
fileId: json['fileId'] as String? ?? '',
fileName: json['fileName'] as String?,
contentType: json['contentType'] as String?,
createdAt: (json['createdAt'] as num).toInt(),
createdAt: (json['createdAt'] as num?)?.toInt() ?? 0,
file: json['file'] as Map<String, dynamic>?,
);

View File

@@ -11,8 +11,8 @@ class BackgroundImageRepositoryImpl implements BackgroundImageRepository {
final BackgroundImageRemoteDatasource _remote;
@override
Future<List<PictureEntity>> getPhotos({required String deviceId}) async {
final model = await _remote.getBackgroundImage(deviceId: deviceId);
Future<List<PictureEntity>> getPhotos() async {
final model = await _remote.getBackgroundImage();
return model.toEntities();
}

View File

@@ -2,7 +2,7 @@
import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart';
abstract class BackgroundImageRepository {
Future<List<PictureEntity>> getPhotos({required String deviceId});
Future<List<PictureEntity>> getPhotos();
Future<String> uploadImage({required String path});
Future<void> setBackgroundImage({required String deviceId, required String photoId});
}

View File

@@ -255,6 +255,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
updatedSettings: updatedSettings,
);
if (!ref.mounted) return false;
ref.syncDeviceSettings(device, updatedSettings);
return true;
} catch (e) {
if (!ref.mounted) return false;

View File

@@ -113,7 +113,7 @@ class BackgroundImageScreen extends ConsumerWidget {
)
: _PhotoGrid(
state: state,
vm: vm,
onPhotoTap: vm.setAsBackground,
primaryColor: primaryColor,
),
),
@@ -160,12 +160,12 @@ class _EmptyState extends StatelessWidget {
class _PhotoGrid extends StatelessWidget {
final BackgroundImageViewState state;
final BackgroundImageViewModel vm;
final ValueChanged<String> onPhotoTap;
final Color primaryColor;
const _PhotoGrid({
required this.state,
required this.vm,
required this.onPhotoTap,
required this.primaryColor,
});
@@ -192,28 +192,53 @@ class _PhotoGrid extends StatelessWidget {
itemCount: state.photos.length,
itemBuilder: (context, index) {
final photo = state.photos[index];
final isActive = state.currentBackgroundId == photo.id;
return GestureDetector(
onTap: () => vm.setAsBackground(photo.id),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade300,
),
),
clipBehavior: Clip.antiAlias,
child: photo.fileBytes != null
? Image.memory(
photo.fileBytes!,
fit: BoxFit.cover,
)
: Center(
child: Icon(
Icons.image_outlined,
size: 48,
color: Colors.grey.shade400,
onTap: () => onPhotoTap(photo.id),
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isActive ? primaryColor : Colors.grey.shade300,
width: isActive ? 3 : 1,
),
),
clipBehavior: Clip.antiAlias,
child: photo.fileBytes != null
? Image.memory(
photo.fileBytes!,
fit: BoxFit.cover,
)
: Center(
child: Icon(
Icons.image_outlined,
size: 48,
color: Colors.grey.shade400,
),
),
),
),
if (isActive)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
size: 16,
),
),
),
],
),
);
},

View File

@@ -26,12 +26,14 @@ class BackgroundImageViewModel extends Notifier<BackgroundImageViewState> {
final device = ref.read(selectedDeviceProvider);
if (device == null) return;
final photos = await _repository.getPhotos(
deviceId: device.identificator,
);
final photos = await _repository.getPhotos();
if (!ref.mounted) return;
state = state.copyWith(photos: photos, isLoading: false);
state = state.copyWith(
photos: photos,
currentBackgroundId: device.settings.backgroundImageId,
isLoading: false,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
@@ -57,14 +59,29 @@ class BackgroundImageViewModel extends Notifier<BackgroundImageViewState> {
);
if (image == null) return;
final device = ref.read(selectedDeviceProvider);
if (device == null) return;
state = state.copyWith(isSaving: true, errorEvent: null, successEvent: null);
try {
await _repository.uploadImage(path: image.path);
final photoId = await _repository.uploadImage(path: image.path);
if (!ref.mounted) return;
await _repository.setBackgroundImage(
deviceId: device.id,
photoId: photoId,
);
if (!ref.mounted) return;
ref.syncDeviceSettings(
device,
device.settings.copyWith(backgroundImageId: photoId),
);
state = state.copyWith(
isSaving: false,
currentBackgroundId: photoId,
successEvent: BackgroundImageSuccessEvent.uploaded,
);
@@ -91,8 +108,14 @@ class BackgroundImageViewModel extends Notifier<BackgroundImageViewState> {
);
if (!ref.mounted) return;
ref.syncDeviceSettings(
device,
device.settings.copyWith(backgroundImageId: photoId),
);
state = state.copyWith(
isSaving: false,
currentBackgroundId: photoId,
successEvent: BackgroundImageSuccessEvent.backgroundSet,
);
} catch (e) {

View File

@@ -12,6 +12,7 @@ abstract class BackgroundImageViewState with _$BackgroundImageViewState {
@Default([]) List<PictureEntity> photos,
@Default(true) bool isLoading,
@Default(false) bool isSaving,
String? currentBackgroundId,
BackgroundImageSuccessEvent? successEvent,
BackgroundImageErrorEvent? errorEvent,
}) = _BackgroundImageViewState;

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BackgroundImageViewState {
List<PictureEntity> get photos; bool get isLoading; bool get isSaving; BackgroundImageSuccessEvent? get successEvent; BackgroundImageErrorEvent? get errorEvent;
List<PictureEntity> get photos; bool get isLoading; bool get isSaving; String? get currentBackgroundId; BackgroundImageSuccessEvent? get successEvent; BackgroundImageErrorEvent? get errorEvent;
/// Create a copy of BackgroundImageViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $BackgroundImageViewStateCopyWith<BackgroundImageViewState> get copyWith => _$Ba
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackgroundImageViewState&&const DeepCollectionEquality().equals(other.photos, photos)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackgroundImageViewState&&const DeepCollectionEquality().equals(other.photos, photos)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.currentBackgroundId, currentBackgroundId) || other.currentBackgroundId == currentBackgroundId)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(photos),isLoading,isSaving,successEvent,errorEvent);
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(photos),isLoading,isSaving,currentBackgroundId,successEvent,errorEvent);
@override
String toString() {
return 'BackgroundImageViewState(photos: $photos, isLoading: $isLoading, isSaving: $isSaving, successEvent: $successEvent, errorEvent: $errorEvent)';
return 'BackgroundImageViewState(photos: $photos, isLoading: $isLoading, isSaving: $isSaving, currentBackgroundId: $currentBackgroundId, successEvent: $successEvent, errorEvent: $errorEvent)';
}
@@ -45,7 +45,7 @@ abstract mixin class $BackgroundImageViewStateCopyWith<$Res> {
factory $BackgroundImageViewStateCopyWith(BackgroundImageViewState value, $Res Function(BackgroundImageViewState) _then) = _$BackgroundImageViewStateCopyWithImpl;
@useResult
$Res call({
List<PictureEntity> photos, bool isLoading, bool isSaving, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent
List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent
});
@@ -62,12 +62,13 @@ class _$BackgroundImageViewStateCopyWithImpl<$Res>
/// Create a copy of BackgroundImageViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? photos = null,Object? isLoading = null,Object? isSaving = null,Object? successEvent = freezed,Object? errorEvent = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? photos = null,Object? isLoading = null,Object? isSaving = null,Object? currentBackgroundId = freezed,Object? successEvent = freezed,Object? errorEvent = freezed,}) {
return _then(_self.copyWith(
photos: null == photos ? _self.photos : photos // ignore: cast_nullable_to_non_nullable
as List<PictureEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
as bool,currentBackgroundId: freezed == currentBackgroundId ? _self.currentBackgroundId : currentBackgroundId // ignore: cast_nullable_to_non_nullable
as String?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageSuccessEvent?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageErrorEvent?,
));
@@ -154,10 +155,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<PictureEntity> photos, bool isLoading, bool isSaving, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _BackgroundImageViewState() when $default != null:
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.successEvent,_that.errorEvent);case _:
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.currentBackgroundId,_that.successEvent,_that.errorEvent);case _:
return orElse();
}
@@ -175,10 +176,10 @@ return $default(_that.photos,_that.isLoading,_that.isSaving,_that.successEvent,_
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<PictureEntity> photos, bool isLoading, bool isSaving, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent) $default,) {final _that = this;
switch (_that) {
case _BackgroundImageViewState():
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.successEvent,_that.errorEvent);case _:
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.currentBackgroundId,_that.successEvent,_that.errorEvent);case _:
throw StateError('Unexpected subclass');
}
@@ -195,10 +196,10 @@ return $default(_that.photos,_that.isLoading,_that.isSaving,_that.successEvent,_
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<PictureEntity> photos, bool isLoading, bool isSaving, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent)? $default,) {final _that = this;
switch (_that) {
case _BackgroundImageViewState() when $default != null:
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.successEvent,_that.errorEvent);case _:
return $default(_that.photos,_that.isLoading,_that.isSaving,_that.currentBackgroundId,_that.successEvent,_that.errorEvent);case _:
return null;
}
@@ -210,7 +211,7 @@ return $default(_that.photos,_that.isLoading,_that.isSaving,_that.successEvent,_
class _BackgroundImageViewState implements BackgroundImageViewState {
const _BackgroundImageViewState({final List<PictureEntity> photos = const [], this.isLoading = true, this.isSaving = false, this.successEvent, this.errorEvent}): _photos = photos;
const _BackgroundImageViewState({final List<PictureEntity> photos = const [], this.isLoading = true, this.isSaving = false, this.currentBackgroundId, this.successEvent, this.errorEvent}): _photos = photos;
final List<PictureEntity> _photos;
@@ -222,6 +223,7 @@ class _BackgroundImageViewState implements BackgroundImageViewState {
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isSaving;
@override final String? currentBackgroundId;
@override final BackgroundImageSuccessEvent? successEvent;
@override final BackgroundImageErrorEvent? errorEvent;
@@ -235,16 +237,16 @@ _$BackgroundImageViewStateCopyWith<_BackgroundImageViewState> get copyWith => __
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackgroundImageViewState&&const DeepCollectionEquality().equals(other._photos, _photos)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackgroundImageViewState&&const DeepCollectionEquality().equals(other._photos, _photos)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.currentBackgroundId, currentBackgroundId) || other.currentBackgroundId == currentBackgroundId)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_photos),isLoading,isSaving,successEvent,errorEvent);
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_photos),isLoading,isSaving,currentBackgroundId,successEvent,errorEvent);
@override
String toString() {
return 'BackgroundImageViewState(photos: $photos, isLoading: $isLoading, isSaving: $isSaving, successEvent: $successEvent, errorEvent: $errorEvent)';
return 'BackgroundImageViewState(photos: $photos, isLoading: $isLoading, isSaving: $isSaving, currentBackgroundId: $currentBackgroundId, successEvent: $successEvent, errorEvent: $errorEvent)';
}
@@ -255,7 +257,7 @@ abstract mixin class _$BackgroundImageViewStateCopyWith<$Res> implements $Backgr
factory _$BackgroundImageViewStateCopyWith(_BackgroundImageViewState value, $Res Function(_BackgroundImageViewState) _then) = __$BackgroundImageViewStateCopyWithImpl;
@override @useResult
$Res call({
List<PictureEntity> photos, bool isLoading, bool isSaving, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent
List<PictureEntity> photos, bool isLoading, bool isSaving, String? currentBackgroundId, BackgroundImageSuccessEvent? successEvent, BackgroundImageErrorEvent? errorEvent
});
@@ -272,12 +274,13 @@ class __$BackgroundImageViewStateCopyWithImpl<$Res>
/// Create a copy of BackgroundImageViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? photos = null,Object? isLoading = null,Object? isSaving = null,Object? successEvent = freezed,Object? errorEvent = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? photos = null,Object? isLoading = null,Object? isSaving = null,Object? currentBackgroundId = freezed,Object? successEvent = freezed,Object? errorEvent = freezed,}) {
return _then(_BackgroundImageViewState(
photos: null == photos ? _self._photos : photos // ignore: cast_nullable_to_non_nullable
as List<PictureEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
as bool,currentBackgroundId: freezed == currentBackgroundId ? _self.currentBackgroundId : currentBackgroundId // ignore: cast_nullable_to_non_nullable
as String?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageSuccessEvent?,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as BackgroundImageErrorEvent?,
));

View File

@@ -32,7 +32,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
Timer? _measureTimer;
static const int _historyPageSize = 20;
static const int _measureDurationSeconds = 35;
static const int _measureDurationSeconds = 60;
@override
HealthViewState build() {
@@ -295,6 +295,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
updatedSettings: updatedSettings,
);
if (!ref.mounted) return false;
ref.syncDeviceSettings(device, updatedSettings);
return true;
} catch (e) {
if (!ref.mounted) return false;

View File

@@ -8,13 +8,13 @@ part 'picture_entity.freezed.dart';
abstract class PictureEntity with _$PictureEntity {
const factory PictureEntity({
required String id,
required String deviceIdentificator,
@Default('') String deviceIdentificator,
String? imgType,
String? timestamp,
required String fileId,
@Default('') String fileId,
String? fileName,
String? contentType,
required int createdAt,
@Default(0) int createdAt,
Uint8List? fileBytes,
}) = _PictureEntity;
}

View File

@@ -214,17 +214,17 @@ return $default(_that.id,_that.deviceIdentificator,_that.imgType,_that.timestamp
class _PictureEntity implements PictureEntity {
const _PictureEntity({required this.id, required this.deviceIdentificator, this.imgType, this.timestamp, required this.fileId, this.fileName, this.contentType, required this.createdAt, this.fileBytes});
const _PictureEntity({required this.id, this.deviceIdentificator = '', this.imgType, this.timestamp, this.fileId = '', this.fileName, this.contentType, this.createdAt = 0, this.fileBytes});
@override final String id;
@override final String deviceIdentificator;
@override@JsonKey() final String deviceIdentificator;
@override final String? imgType;
@override final String? timestamp;
@override final String fileId;
@override@JsonKey() final String fileId;
@override final String? fileName;
@override final String? contentType;
@override final int createdAt;
@override@JsonKey() final int createdAt;
@override final Uint8List? fileBytes;
/// Create a copy of PictureEntity

View File

@@ -1,7 +1,9 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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';
@@ -17,10 +19,33 @@ class RemoteCameraScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(
remoteConnectionViewModelProvider.select((s) => s.successMessage),
(_, successMessage) {
if (successMessage.isNotEmpty) {
showTopSnackbar(context, message: context.translate(successMessage), type: MessageType.success);
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);
}
},
);
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();
}
},
@@ -28,20 +53,38 @@ class RemoteCameraScreen extends ConsumerWidget {
final theme = ref.watch(themePortProvider);
final isLoadingPictures = ref.watch(
remoteConnectionViewModelProvider.select((s)=>s.isLoadingPictures)
final isLoading = ref.watch(
remoteConnectionViewModelProvider.select((s) => s.isLoadingPictures),
);
final isTakingPicture = ref.watch(
remoteConnectionViewModelProvider.select((s)=>s.isTakingPicture)
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),
);
Widget body;
if (isLoading || isTaking) {
body = const Center(child: CircularProgressIndicator());
} else if (isWaiting) {
body = _WaitingForPhotoOverlay(
remainingSeconds: countdown,
theme: theme,
);
} else {
body = const _GallerySection();
}
return LegacyPageLayout(
theme: theme,
title: context.translate(I18n.remoteCamera),
body: isLoadingPictures || isTakingPicture
? const Center(child: CircularProgressIndicator())
: const _GallerySection(),
footer: _TakePictureSection(),
body: body,
footer: isWaiting
? const SizedBox.shrink()
: const _TakePictureSection(),
);
}
}
@@ -87,12 +130,12 @@ class _GallerySection extends ConsumerWidget {
color: theme.getColorFor(ThemeCode.textTertiary)
))
),
child: Image.network(
pictures[index].fileId,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
const Icon(Icons.broken_image, color: Colors.grey),
)
child: pictures[index].fileBytes != null
? Image.memory(
pictures[index].fileBytes!,
fit: BoxFit.cover,
)
: const Icon(Icons.broken_image, color: Colors.grey),
)
)
),
@@ -124,5 +167,53 @@ class _TakePictureSection extends ConsumerWidget {
),
);
}
}
class _WaitingForPhotoOverlay extends StatelessWidget {
static const String _animationAsset =
'assets/shared/animations/shooting_photo.json';
final int remainingSeconds;
final ThemePort theme;
const _WaitingForPhotoOverlay({
required this.remainingSeconds,
required this.theme,
});
@override
Widget build(BuildContext context) {
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Lottie.asset(
_animationAsset,
width: 200,
height: 200,
),
const SizedBox(height: 24),
Text(
context.translate(I18n.takingPhoto),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
const SizedBox(height: 12),
Text(
'${remainingSeconds}s',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
],
),
);
}
}

View File

@@ -1,10 +1,11 @@
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/domain/entities/picture_entity.dart';
import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.dart';
import 'package:legacy_shared/legacy_shared.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../../../../core/domain/repositories/pictures_repository.dart';
@@ -18,8 +19,10 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
late final TextEditingController phoneController;
late final CommandsRepository _commandsRepository;
late final PicturesRepository _picturesRepository;
Timer? _photoTimer;
static final RegExp _phoneRegex = RegExp(r'^\+?\d{6,15}$');
static const int _photoWaitSeconds = 5;
@override
RemoteConnectionViewState build() {
@@ -60,12 +63,12 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
final text = phoneController.text;
if (text == state.phone) return;
state = state.copyWith(phone: text, errorMessage: '');
state = state.copyWith(phone: text, errorEvent: null);
}
void updateDialCode(String value) {
if (value == state.dialCode) return;
state = state.copyWith(dialCode: value, errorMessage: '');
state = state.copyWith(dialCode: value, errorEvent: null);
}
void prevPicture() {
@@ -95,18 +98,17 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
}
void clearSuccess() {
state = state.copyWith(
successMessage: '',
);
state = state.copyWith(successEvent: null);
}
Future<void> takePicture() async {
try {
state = state.copyWith(
isTakingPicture: true,
successMessage: '',
successEvent: null,
errorEvent: null,
);
final request = SendCommandRequestModel(
device: state.deviceId,
command: DeviceCommand.requestPhoto,
@@ -115,21 +117,59 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
await _commandsRepository.send(request: request);
if (!ref.mounted) return;
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(
isTakingPicture: false,
isWaitingForPhoto: false,
photoCountdown: 0,
pictures: pictures,
successMessage: I18n.photoTaken,
successEvent: RemoteConnectionSuccessEvent.photoTaken,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isTakingPicture: false,
errorMessage: e.toString(),
isWaitingForPhoto: false,
photoCountdown: 0,
errorEvent: RemoteConnectionErrorEvent.fetchPhotos,
);
}
}
@@ -137,12 +177,8 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
Future<void> call() async {
final phone = phoneController.text;
final dialCode = state.dialCode;
if (phone.isEmpty){
state = state.copyWith(errorMessage: I18n.errorMessagePhoneIsInvalid);
return;
}
if (!_phoneRegex.hasMatch(phone)) {
state = state.copyWith(errorMessage: I18n.errorMessagePhoneIsInvalid);
if (phone.isEmpty || !_phoneRegex.hasMatch(phone)) {
state = state.copyWith(errorEvent: RemoteConnectionErrorEvent.invalidPhone);
return;
}
@@ -163,7 +199,7 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
if (!ref.mounted) return;
state = state.copyWith(
isCalling: false,
errorMessage: e.toString(),
errorEvent: RemoteConnectionErrorEvent.call,
);
}
}

View File

@@ -3,6 +3,9 @@ import 'package:device_management/src/features/remote_connection/domain/entities
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({
@@ -13,8 +16,10 @@ abstract class RemoteConnectionViewState with _$RemoteConnectionViewState {
@Default(0) int pictureIndex,
@Default(true) bool isLoadingPictures,
@Default(false) bool isTakingPicture,
@Default(false) bool isWaitingForPhoto,
@Default(0) int photoCountdown,
@Default(false) bool isCalling,
@Default('') String errorMessage,
@Default('') String successMessage
RemoteConnectionErrorEvent? errorEvent,
RemoteConnectionSuccessEvent? successEvent,
}) = _RemoteConnectionViewState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RemoteConnectionViewState {
String get deviceId; String get dialCode; String get phone; List<PictureEntity> get pictures; int get pictureIndex; bool get isLoadingPictures; bool get isTakingPicture; bool get isCalling; String get errorMessage; String get successMessage;
String get deviceId; String get dialCode; 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)
@@ -25,16 +25,16 @@ $RemoteConnectionViewStateCopyWith<RemoteConnectionViewState> get copyWith => _$
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(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.isCalling, isCalling) || other.isCalling == isCalling)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(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,dialCode,phone,const DeepCollectionEquality().hash(pictures),pictureIndex,isLoadingPictures,isTakingPicture,isCalling,errorMessage,successMessage);
int get hashCode => Object.hash(runtimeType,deviceId,dialCode,phone,const DeepCollectionEquality().hash(pictures),pictureIndex,isLoadingPictures,isTakingPicture,isWaitingForPhoto,photoCountdown,isCalling,errorEvent,successEvent);
@override
String toString() {
return 'RemoteConnectionViewState(deviceId: $deviceId, dialCode: $dialCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isCalling: $isCalling, errorMessage: $errorMessage, successMessage: $successMessage)';
return 'RemoteConnectionViewState(deviceId: $deviceId, dialCode: $dialCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isWaitingForPhoto: $isWaitingForPhoto, photoCountdown: $photoCountdown, isCalling: $isCalling, errorEvent: $errorEvent, successEvent: $successEvent)';
}
@@ -45,7 +45,7 @@ abstract mixin class $RemoteConnectionViewStateCopyWith<$Res> {
factory $RemoteConnectionViewStateCopyWith(RemoteConnectionViewState value, $Res Function(RemoteConnectionViewState) _then) = _$RemoteConnectionViewStateCopyWithImpl;
@useResult
$Res call({
String deviceId, String dialCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isCalling, String errorMessage, String successMessage
String deviceId, String dialCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent
});
@@ -62,7 +62,7 @@ class _$RemoteConnectionViewStateCopyWithImpl<$Res>
/// 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? dialCode = null,Object? phone = null,Object? pictures = null,Object? pictureIndex = null,Object? isLoadingPictures = null,Object? isTakingPicture = null,Object? isCalling = null,Object? errorMessage = null,Object? successMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? dialCode = 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,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
@@ -71,10 +71,12 @@ as String,pictures: null == pictures ? _self.pictures : pictures // ignore: cast
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,isCalling: null == isCalling ? _self.isCalling : isCalling // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,successMessage: null == successMessage ? _self.successMessage : successMessage // ignore: cast_nullable_to_non_nullable
as String,
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?,
));
}
@@ -159,10 +161,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String deviceId, String dialCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isCalling, String errorMessage, String successMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String deviceId, String dialCode, 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.dialCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isCalling,_that.errorMessage,_that.successMessage);case _:
return $default(_that.deviceId,_that.dialCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);case _:
return orElse();
}
@@ -180,10 +182,10 @@ return $default(_that.deviceId,_that.dialCode,_that.phone,_that.pictures,_that.p
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String deviceId, String dialCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isCalling, String errorMessage, String successMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String deviceId, String dialCode, 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.dialCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isCalling,_that.errorMessage,_that.successMessage);case _:
return $default(_that.deviceId,_that.dialCode,_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');
}
@@ -200,10 +202,10 @@ return $default(_that.deviceId,_that.dialCode,_that.phone,_that.pictures,_that.p
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String deviceId, String dialCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isCalling, String errorMessage, String successMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String deviceId, String dialCode, 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.dialCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isCalling,_that.errorMessage,_that.successMessage);case _:
return $default(_that.deviceId,_that.dialCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);case _:
return null;
}
@@ -215,7 +217,7 @@ return $default(_that.deviceId,_that.dialCode,_that.phone,_that.pictures,_that.p
class _RemoteConnectionViewState implements RemoteConnectionViewState {
const _RemoteConnectionViewState({this.deviceId = '', this.dialCode = '+34', this.phone = '', final List<PictureEntity> pictures = const [], this.pictureIndex = 0, this.isLoadingPictures = true, this.isTakingPicture = false, this.isCalling = false, this.errorMessage = '', this.successMessage = ''}): _pictures = pictures;
const _RemoteConnectionViewState({this.deviceId = '', this.dialCode = '+34', 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;
@@ -231,9 +233,11 @@ class _RemoteConnectionViewState implements RemoteConnectionViewState {
@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@JsonKey() final String errorMessage;
@override@JsonKey() final String successMessage;
@override final RemoteConnectionErrorEvent? errorEvent;
@override final RemoteConnectionSuccessEvent? successEvent;
/// Create a copy of RemoteConnectionViewState
/// with the given fields replaced by the non-null parameter values.
@@ -245,16 +249,16 @@ _$RemoteConnectionViewStateCopyWith<_RemoteConnectionViewState> get copyWith =>
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(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.isCalling, isCalling) || other.isCalling == isCalling)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(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,dialCode,phone,const DeepCollectionEquality().hash(_pictures),pictureIndex,isLoadingPictures,isTakingPicture,isCalling,errorMessage,successMessage);
int get hashCode => Object.hash(runtimeType,deviceId,dialCode,phone,const DeepCollectionEquality().hash(_pictures),pictureIndex,isLoadingPictures,isTakingPicture,isWaitingForPhoto,photoCountdown,isCalling,errorEvent,successEvent);
@override
String toString() {
return 'RemoteConnectionViewState(deviceId: $deviceId, dialCode: $dialCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isCalling: $isCalling, errorMessage: $errorMessage, successMessage: $successMessage)';
return 'RemoteConnectionViewState(deviceId: $deviceId, dialCode: $dialCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isWaitingForPhoto: $isWaitingForPhoto, photoCountdown: $photoCountdown, isCalling: $isCalling, errorEvent: $errorEvent, successEvent: $successEvent)';
}
@@ -265,7 +269,7 @@ abstract mixin class _$RemoteConnectionViewStateCopyWith<$Res> implements $Remot
factory _$RemoteConnectionViewStateCopyWith(_RemoteConnectionViewState value, $Res Function(_RemoteConnectionViewState) _then) = __$RemoteConnectionViewStateCopyWithImpl;
@override @useResult
$Res call({
String deviceId, String dialCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isCalling, String errorMessage, String successMessage
String deviceId, String dialCode, String phone, List<PictureEntity> pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent
});
@@ -282,7 +286,7 @@ class __$RemoteConnectionViewStateCopyWithImpl<$Res>
/// 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? dialCode = null,Object? phone = null,Object? pictures = null,Object? pictureIndex = null,Object? isLoadingPictures = null,Object? isTakingPicture = null,Object? isCalling = null,Object? errorMessage = null,Object? successMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? dialCode = 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,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
@@ -291,10 +295,12 @@ as String,pictures: null == pictures ? _self._pictures : pictures // ignore: cas
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,isCalling: null == isCalling ? _self.isCalling : isCalling // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,successMessage: null == successMessage ? _self.successMessage : successMessage // ignore: cast_nullable_to_non_nullable
as String,
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?,
));
}

View File

@@ -45,7 +45,7 @@ class ShowPictureDialog extends ConsumerWidget {
child: Column(
children: [
Expanded(
child: _PictureSection(fileId: picture.fileId),
child: _PictureSection(picture: picture),
),
_MetadataSection(picture: picture),
_ControlsSection(
@@ -59,24 +59,22 @@ class ShowPictureDialog extends ConsumerWidget {
}
class _PictureSection extends StatelessWidget {
final String fileId;
final PictureEntity picture;
const _PictureSection({required this.fileId});
const _PictureSection({required this.picture});
@override
Widget build(BuildContext context) {
return Center(
child: Image.network(
fileId,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 64, color: Colors.grey);
},
),
if (picture.fileBytes != null) {
return Center(
child: Image.memory(
picture.fileBytes!,
fit: BoxFit.contain,
),
);
}
return const Center(
child: Icon(Icons.broken_image, size: 64, color: Colors.grey),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:design_system/design_system.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:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
@@ -13,26 +14,39 @@ class SpyCallDialog extends ConsumerWidget {
final theme = ref.watch(themePortProvider);
final vm = ref.read(remoteConnectionViewModelProvider.notifier);
ref.listen(remoteConnectionViewModelProvider.select((s) => s.errorMessage),
(_, msg) {
if (msg.isNotEmpty) {
showTopSnackbar(context,
message: context.translate(msg), type: MessageType.error);
}
});
ref.listen(remoteConnectionViewModelProvider.select((s) => s.isCalling),
(prev, isCalling) {
if (prev == true && !isCalling) {
final error = ref.read(remoteConnectionViewModelProvider).errorMessage;
if (error.isEmpty && context.mounted) {
Navigator.pop(context);
showTopSnackbar(context,
message: context.translate(I18n.remoteListening),
type: MessageType.success);
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);
}
}
});
},
);
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);
}
}
},
);
return Container(
padding: SizeUtils.getByScreen(

View File

@@ -64,6 +64,7 @@ class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
);
if (!ref.mounted) return;
ref.syncDeviceSettings(device, updatedSettings);
state = state.copyWith(isLoading: false, isComplete: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -24,7 +24,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
@override
LocationViewState build() {
_locationRepository = ref.read(locationRepositoryProvider);
final device = ref.watch(selectedDeviceProvider);
final device = ref.read(selectedDeviceProvider);
if (device != null) {
_fetchData(device.id);
}
@@ -282,6 +282,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
);
if (!ref.mounted) return false;
ref.syncDeviceSettings(device, updatedSettings);
state = state.copyWith(isSubmitting: false);
return true;
} catch (e) {

View File

@@ -566,7 +566,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
const SizedBox(height: 8),
FrequencySelector(
currentFrequency:
widget.selectedDevice!.settings.frequency,
ref.watch(selectedDeviceProvider)?.settings.frequency ?? 60,
options:
widget.selectedDevice!.capabilities!.location!.options,
onChanged: _updateFrequency,

View File

@@ -57,6 +57,7 @@ class AlertsViewModel extends Notifier<AlertsViewState> {
updatedSettings: updatedSettings,
);
if (!ref.mounted) return;
ref.syncDeviceSettings(device, updatedSettings);
state = state.copyWith(isSaving: false, saveSuccess: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -41,6 +41,7 @@ class BatteryViewModel extends Notifier<BatteryViewState> {
updatedSettings: updatedSettings,
);
if (!ref.mounted) return;
ref.syncDeviceSettings(device, updatedSettings);
state = state.copyWith(
nightMode: value,
isSaving: false,

View File

@@ -53,6 +53,7 @@ class DisableFunctionsViewModel extends Notifier<DisableFunctionsViewState> {
updatedSettings: updatedSettings,
);
if (!ref.mounted) return;
ref.syncDeviceSettings(device, updatedSettings);
state = state.copyWith(isSaving: false, saveSuccess: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -57,6 +57,10 @@ class LanguageViewModel extends Notifier<LanguageViewState> {
);
if (!ref.mounted) return;
ref.syncDeviceSettings(
device,
device.settings.copyWith(language: state.language),
);
state = state.copyWith(isLoading: false, isComplete: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -60,6 +60,7 @@ class SoundViewModel extends Notifier<SoundViewState> {
);
if (!ref.mounted) return;
ref.syncDeviceSettings(device, updatedSettings);
state = state.copyWith(isLoading: false, isComplete: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -52,6 +52,7 @@ class TimezoneViewModel extends Notifier<TimezoneViewState> {
updatedSettings: updatedSettings,
);
if (!ref.mounted) return;
ref.syncDeviceSettings(device, updatedSettings);
state = state.copyWith(isSaving: false, saveSuccess: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -14,7 +14,7 @@ class CommandsRemoteDatasourceImpl implements CommandsRemoteDatasource {
required SendCommandRequestModel request,
}) async {
await safeCall(
() => _repository.post<Map<String, dynamic>>(
() => _repository.post<dynamic>(
'/commands',
body: request.toJson(),
),

View File

@@ -16,3 +16,11 @@ class SelectedDeviceNotifier extends Notifier<DeviceEntity?> {
state = device;
}
}
extension DeviceSettingsSync on Ref {
void syncDeviceSettings(DeviceEntity device, DeviceSettingsEntity settings) {
read(selectedDeviceProvider.notifier).setSelectedDevice(
device.copyWith(settings: settings),
);
}
}