From 2ea6b5875dd3b50f3e060a306fa88b8d5abdef9b Mon Sep 17 00:00:00 2001 From: aitorarana Date: Wed, 18 Mar 2026 11:02:03 +0100 Subject: [PATCH] do not disturb ui and state --- .../mobile_app/lib/navigation/app_router.dart | 5 + .../lib/device_management.dart | 3 +- .../device_management_screen.dart | 2 +- .../do_not_disturb_builder.dart | 19 + .../domain/entities/time_range_entity.dart | 31 ++ .../entities/time_range_entity.freezed.dart | 290 +++++++++++++++ .../presentation/do_not_disturb_screen.dart | 335 ++++++++++++++++++ .../state/do_not_disturb_view_model.dart | 140 ++++++++ .../state/do_not_disturb_view_state.dart | 19 + .../do_not_disturb_view_state.freezed.dart | 319 +++++++++++++++++ .../presentation/widgets/set_time_dialog.dart | 324 +++++++++++++++++ .../commands_remote_datasource_impl.dart | 34 +- .../models/send_command_request_model.dart | 2 + .../lib/src/widgets/layouts/page_layout.dart | 2 +- .../antelop/antelop/maven-metadata.xml | 2 +- .../antelop/antelop/maven-metadata.xml.md5 | 2 +- .../antelop/antelop/maven-metadata.xml.sha1 | 2 +- packages/navigation/lib/app_routes.dart | 1 + packages/sf_localizations/assets/l10n/en.json | 6 +- packages/sf_localizations/assets/l10n/es.json | 6 +- .../lib/src/generated/i18n.dart | 4 + 21 files changed, 1521 insertions(+), 27 deletions(-) create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/do_not_disturb_builder.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.freezed.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_model.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.freezed.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/widgets/set_time_dialog.dart diff --git a/apps/mobile_app/lib/navigation/app_router.dart b/apps/mobile_app/lib/navigation/app_router.dart index abfe569a..5a19318b 100644 --- a/apps/mobile_app/lib/navigation/app_router.dart +++ b/apps/mobile_app/lib/navigation/app_router.dart @@ -141,6 +141,11 @@ void configureAppRouter() { name: 'apps_use', pageBuilder: const AppsUseBuilder().buildPage, ), + GoRoute( + path: 'do_not_disturb', + name: 'do_not_disturb', + pageBuilder: const DoNotDisturbBuilder().buildPage, + ), ], ), ], diff --git a/modules/legacy/modules/device_management/lib/device_management.dart b/modules/legacy/modules/device_management/lib/device_management.dart index e8fa9498..3fa720da 100644 --- a/modules/legacy/modules/device_management/lib/device_management.dart +++ b/modules/legacy/modules/device_management/lib/device_management.dart @@ -9,4 +9,5 @@ export 'src/features/locate_device/locate_device_builder.dart'; export 'src/features/health/health_builder.dart'; export 'src/features/rewards/rewards_builder.dart'; export 'src/features/activity_meter/activity_meter_builder.dart'; -export 'src/features/apps_use/apps_use_builder.dart'; \ No newline at end of file +export 'src/features/apps_use/apps_use_builder.dart'; +export 'src/features/do_not_disturb/do_not_disturb_builder.dart'; \ No newline at end of file diff --git a/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart b/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart index 8b68f252..7ae9a376 100644 --- a/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/device_management/device_management_screen.dart @@ -54,7 +54,7 @@ class DeviceManagementScreen extends ConsumerWidget { SizedBox(height: SizeUtils.getByScreen(small: 16, big: 15)), AppMenuButton( color: theme.getColorFor(ThemeCode.legacyPrimary), - onPressed: () {}, + onPressed: () => navigationContract.pushTo(AppRoutes.doNotDisturb), icon: SFIcons.doNotDisturbCircle, negativeIcon: true, text: context.translate(I18n.doNotDisturb), diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/do_not_disturb_builder.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/do_not_disturb_builder.dart new file mode 100644 index 00000000..f98e5d1a --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/do_not_disturb_builder.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:get_it/get_it.dart'; +import 'package:navigation/navigation.dart'; + +import 'presentation/do_not_disturb_screen.dart'; + +class DoNotDisturbBuilder { + const DoNotDisturbBuilder(); + + Page buildPage(BuildContext context, GoRouterState state) { + final NavigationContract navigationContract = GetIt.I(); + + return MaterialPage( + key: state.pageKey, + child: DoNotDisturbScreen(navigationContract: navigationContract), + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.dart new file mode 100644 index 00000000..59303e8b --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'time_range_entity.freezed.dart'; + +@freezed +abstract class TimeRangeEntity with _$TimeRangeEntity { + const TimeRangeEntity._(); + + const factory TimeRangeEntity({ + required String id, + required String deviceId, + required bool active, + required TimeOfDay startTime, + required TimeOfDay endTime, + @Default([false, false, false, false, false, false, false]) + List days, + int? createdAt, + int? updatedAt, + }) = _TimeRangeEntity; + + @override + String toString() { + final sh = startTime.hour.toString().padLeft(2, '0'); + final sm = startTime.minute.toString().padLeft(2, '0'); + final eh = endTime.hour.toString().padLeft(2, '0'); + final em = endTime.minute.toString().padLeft(2, '0'); + + return '$sh:$sm - $eh:$em'; + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.freezed.dart new file mode 100644 index 00000000..bf62d927 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/domain/entities/time_range_entity.freezed.dart @@ -0,0 +1,290 @@ +// 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 'time_range_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$TimeRangeEntity { + + String get id; String get deviceId; bool get active; TimeOfDay get startTime; TimeOfDay get endTime; List get days; int? get createdAt; int? get updatedAt; +/// Create a copy of TimeRangeEntity +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TimeRangeEntityCopyWith get copyWith => _$TimeRangeEntityCopyWithImpl(this as TimeRangeEntity, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is TimeRangeEntity&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.active, active) || other.active == active)&&(identical(other.startTime, startTime) || other.startTime == startTime)&&(identical(other.endTime, endTime) || other.endTime == endTime)&&const DeepCollectionEquality().equals(other.days, days)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); +} + + +@override +int get hashCode => Object.hash(runtimeType,id,deviceId,active,startTime,endTime,const DeepCollectionEquality().hash(days),createdAt,updatedAt); + + + +} + +/// @nodoc +abstract mixin class $TimeRangeEntityCopyWith<$Res> { + factory $TimeRangeEntityCopyWith(TimeRangeEntity value, $Res Function(TimeRangeEntity) _then) = _$TimeRangeEntityCopyWithImpl; +@useResult +$Res call({ + String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List days, int? createdAt, int? updatedAt +}); + + + + +} +/// @nodoc +class _$TimeRangeEntityCopyWithImpl<$Res> + implements $TimeRangeEntityCopyWith<$Res> { + _$TimeRangeEntityCopyWithImpl(this._self, this._then); + + final TimeRangeEntity _self; + final $Res Function(TimeRangeEntity) _then; + +/// Create a copy of TimeRangeEntity +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? active = null,Object? startTime = null,Object? endTime = null,Object? days = null,Object? createdAt = freezed,Object? updatedAt = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable +as String,active: null == active ? _self.active : active // ignore: cast_nullable_to_non_nullable +as bool,startTime: null == startTime ? _self.startTime : startTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,endTime: null == endTime ? _self.endTime : endTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,days: null == days ? _self.days : days // ignore: cast_nullable_to_non_nullable +as List,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as int?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [TimeRangeEntity]. +extension TimeRangeEntityPatterns on TimeRangeEntity { +/// 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 Function( _TimeRangeEntity value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _TimeRangeEntity() 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 Function( _TimeRangeEntity value) $default,){ +final _that = this; +switch (_that) { +case _TimeRangeEntity(): +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? Function( _TimeRangeEntity value)? $default,){ +final _that = this; +switch (_that) { +case _TimeRangeEntity() 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 Function( String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List days, int? createdAt, int? updatedAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _TimeRangeEntity() when $default != null: +return $default(_that.id,_that.deviceId,_that.active,_that.startTime,_that.endTime,_that.days,_that.createdAt,_that.updatedAt);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 Function( String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List days, int? createdAt, int? updatedAt) $default,) {final _that = this; +switch (_that) { +case _TimeRangeEntity(): +return $default(_that.id,_that.deviceId,_that.active,_that.startTime,_that.endTime,_that.days,_that.createdAt,_that.updatedAt);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? Function( String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List days, int? createdAt, int? updatedAt)? $default,) {final _that = this; +switch (_that) { +case _TimeRangeEntity() when $default != null: +return $default(_that.id,_that.deviceId,_that.active,_that.startTime,_that.endTime,_that.days,_that.createdAt,_that.updatedAt);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _TimeRangeEntity extends TimeRangeEntity { + const _TimeRangeEntity({required this.id, required this.deviceId, required this.active, required this.startTime, required this.endTime, final List days = const [false, false, false, false, false, false, false], this.createdAt, this.updatedAt}): _days = days,super._(); + + +@override final String id; +@override final String deviceId; +@override final bool active; +@override final TimeOfDay startTime; +@override final TimeOfDay endTime; + final List _days; +@override@JsonKey() List get days { + if (_days is EqualUnmodifiableListView) return _days; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_days); +} + +@override final int? createdAt; +@override final int? updatedAt; + +/// Create a copy of TimeRangeEntity +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$TimeRangeEntityCopyWith<_TimeRangeEntity> get copyWith => __$TimeRangeEntityCopyWithImpl<_TimeRangeEntity>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TimeRangeEntity&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.active, active) || other.active == active)&&(identical(other.startTime, startTime) || other.startTime == startTime)&&(identical(other.endTime, endTime) || other.endTime == endTime)&&const DeepCollectionEquality().equals(other._days, _days)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); +} + + +@override +int get hashCode => Object.hash(runtimeType,id,deviceId,active,startTime,endTime,const DeepCollectionEquality().hash(_days),createdAt,updatedAt); + + + +} + +/// @nodoc +abstract mixin class _$TimeRangeEntityCopyWith<$Res> implements $TimeRangeEntityCopyWith<$Res> { + factory _$TimeRangeEntityCopyWith(_TimeRangeEntity value, $Res Function(_TimeRangeEntity) _then) = __$TimeRangeEntityCopyWithImpl; +@override @useResult +$Res call({ + String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List days, int? createdAt, int? updatedAt +}); + + + + +} +/// @nodoc +class __$TimeRangeEntityCopyWithImpl<$Res> + implements _$TimeRangeEntityCopyWith<$Res> { + __$TimeRangeEntityCopyWithImpl(this._self, this._then); + + final _TimeRangeEntity _self; + final $Res Function(_TimeRangeEntity) _then; + +/// Create a copy of TimeRangeEntity +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? active = null,Object? startTime = null,Object? endTime = null,Object? days = null,Object? createdAt = freezed,Object? updatedAt = freezed,}) { + return _then(_TimeRangeEntity( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable +as String,active: null == active ? _self.active : active // ignore: cast_nullable_to_non_nullable +as bool,startTime: null == startTime ? _self.startTime : startTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,endTime: null == endTime ? _self.endTime : endTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,days: null == days ? _self._days : days // ignore: cast_nullable_to_non_nullable +as List,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as int?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + + +} + +// dart format on diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart new file mode 100644 index 00000000..f21e11f4 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart @@ -0,0 +1,335 @@ +import 'package:design_system/design_system.dart'; +import 'package:device_management/src/features/do_not_disturb/domain/entities/time_range_entity.dart'; +import 'package:device_management/src/features/do_not_disturb/presentation/state/do_not_disturb_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_shared/legacy_shared.dart'; +import 'package:navigation/navigation.dart'; +import 'package:sf_localizations/sf_localizations.dart'; +import 'package:utils/utils.dart'; + +import 'widgets/set_time_dialog.dart'; + +class DoNotDisturbScreen extends ConsumerWidget { + final NavigationContract navigationContract; + + const DoNotDisturbScreen({ + super.key, + required this.navigationContract, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + final state = ref.watch(doNotDisturbViewModelProvider); + final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); + + ref.listen( + doNotDisturbViewModelProvider.select((s) => s.errorMessage), + (_, errorMessage) { + if (errorMessage.isNotEmpty) { + showTopSnackbar(context, message: errorMessage, type: MessageType.error); + } + }, + ); + + ref.listen( + doNotDisturbViewModelProvider.select((s) => s.successMessage), + (_, successMessage) { + if (successMessage.isNotEmpty) { + showTopSnackbar(context, message: context.translate(successMessage), type: MessageType.success); + ref.read(doNotDisturbViewModelProvider.notifier).clearSuccess(); + } + }, + ); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0, + centerTitle: true, + automaticallyImplyLeading: false, + leading: IconButton( + onPressed: () => navigationContract.goBack(), + icon: Icon( + Icons.adaptive.arrow_back, + color: primaryColor, + size: SizeUtils.getByScreen(small: 32, big: 28), + ), + ), + title: Text( + context.translate(I18n.doNotDisturb).toUpperCase(), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 20, big: 19), + fontWeight: FontWeight.w500, + letterSpacing: 0, + color: primaryColor, + ), + ), + ), + body: SafeArea( + top: false, + child: state.isLoading + ? const Center(child: CircularProgressIndicator()) + : const _Body(), + ), + ); + } +} + +class _Body extends ConsumerWidget { + + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final primaryColor = ref.read(themePortProvider) + .getColorFor(ThemeCode.legacyPrimary); + + return Padding( + padding: SizeUtils.getByScreen( + small: EdgeInsets.symmetric(horizontal: 22, vertical: 10), + big: EdgeInsets.symmetric(horizontal: 21, vertical: 8), + ), + child: Column( + children: [ + Icon(SFIcons.doNotDisturbCircle, + size: 140, + color: primaryColor, + ), + SizedBox(height: 12), + Align( + alignment: Alignment.bottomLeft, + child: Text(context.translate(I18n.doNotDisturbMessage)), + ), + SizedBox(height: 12), + _TimeRangesList(onEdit: (timeRange) => _openForm(context, timeRange: timeRange)) + ], + ) + ); + } + + void _openForm(BuildContext context, {TimeRangeEntity? timeRange}) { + showTimeRangeForm(context, timeRange: timeRange); + } + +} + +class _TimeRangesList extends ConsumerWidget { + final void Function(TimeRangeEntity timeRange) onEdit; + + const _TimeRangesList({required this.onEdit}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final timeRanges = ref.watch( + doNotDisturbViewModelProvider.select((s) => s.timeRanges), + ); + + return SingleChildScrollView( + child: Column( + children: List.generate(DoNotDisturbViewModel.maxIntervals, (int i) { + final timeRange = timeRanges.elementAtOrNull(i); + + if (timeRange == null) { + return _EmptyTimeRangeCard(); + } else { + return _TimeRangeCard( + index: i, + timeRange: timeRange, + onEdit: () => onEdit(timeRange), + ); + } + }).toList(), + ), + ); + } +} + +class _EmptyTimeRangeCard extends ConsumerWidget { + + const _EmptyTimeRangeCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.read(themePortProvider); + final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); + + return Padding( + padding: EdgeInsets.only( + bottom: SizeUtils.getByScreen(small: 12, big: 10), + ), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: SizeUtils.getByScreen(small: 16, big: 14), + vertical: SizeUtils.getByScreen(small: 14, big: 12), + ), + decoration: BoxDecoration( + color: theme.getColorFor(ThemeCode.backgroundSecondary), + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.translate(I18n.addTimeRange), + style: TextStyle( + fontWeight: FontWeight.w500, + color: theme.getColorFor(ThemeCode.textPrimary), + fontSize: SizeUtils.getByScreen(small: 18, big: 18), + ), + ), + SizedBox(width: 8), + DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: primaryColor + ), + child: IconButton( + onPressed: (){showTimeRangeForm(context);}, + color: theme.getColorFor(ThemeCode.textSecondary), + icon: Icon(Icons.add), + ), + ), + ], + ), + ], + ), + ), + ); + } + +} + +class _TimeRangeCard extends ConsumerWidget { + final TimeRangeEntity timeRange; + final VoidCallback onEdit; + final int index; + + const _TimeRangeCard({ + required this.index, + required this.timeRange, + required this.onEdit, + }); + + String weekDayShortLabel(BuildContext context, String i18nKey) { + final fullName = context.translate(i18nKey); + return fullName.length > 3 ? fullName.substring(0, 3) : fullName; + } + + String weekDaysLabel(BuildContext context, List days) { + + const weekDayI18nKeys = [ + I18n.monday, + I18n.tuesday, + I18n.wednesday, + I18n.thursday, + I18n.friday, + I18n.saturday, + I18n.sunday, + ]; + + final daysLabels = days.indexed.fold>([], (List labels, (int i, bool d) day){ + if (day.$2) { + final label = weekDayShortLabel(context, weekDayI18nKeys[day.$1]); + labels.add(label); + } + return labels; + }).join(', '); + + return daysLabels; + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); + final sh = timeRange.startTime.hour.toString().padLeft(2, '0'); + final sm = timeRange.startTime.minute.toString().padLeft(2, '0'); + final eh = timeRange.endTime.hour.toString().padLeft(2, '0'); + final em = timeRange.endTime.minute.toString().padLeft(2, '0'); + + final vm = ref.read(doNotDisturbViewModelProvider.notifier); + + return GestureDetector( + onTap: (){showTimeRangeForm(context, timeRange: timeRange);}, + child: Padding( + padding: EdgeInsets.only( + bottom: SizeUtils.getByScreen(small: 12, big: 10), + ), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: SizeUtils.getByScreen(small: 16, big: 14), + vertical: SizeUtils.getByScreen(small: 14, big: 12), + ), + decoration: BoxDecoration( + color: theme.getColorFor(ThemeCode.backgroundSecondary), + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$sh:$sm - $eh:$em', + style: TextStyle( + fontWeight: FontWeight.bold, + color: theme.getColorFor(ThemeCode.textPrimary), + fontSize: SizeUtils.getByScreen(small: 24, big: 26), + ), + ), + Switch( + value: timeRange.active, + onChanged: (value){vm.setTimeRangeActive(index, value);}, + activeThumbColor: primaryColor, + ) + ], + ), + Text( + weekDaysLabel(context, timeRange.days), + style: TextStyle( + color: theme.getColorFor(ThemeCode.textTertiary) + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ActionButton extends StatelessWidget { + final IconData icon; + final Color color; + final VoidCallback onPressed; + + const _ActionButton({ + required this.icon, + required this.color, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: EdgeInsets.all(SizeUtils.getByScreen(small: 6, big: 8)), + child: Icon( + icon, + color: color, + size: SizeUtils.getByScreen(small: 22, big: 24), + ), + ), + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_model.dart new file mode 100644 index 00000000..f6bf2539 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_model.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_shared/legacy_shared.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +import '../../domain/entities/time_range_entity.dart'; +import 'do_not_disturb_view_state.dart'; + +final doNotDisturbViewModelProvider = NotifierProvider.autoDispose< + DoNotDisturbViewModel, DoNotDisturbViewState>( + DoNotDisturbViewModel.new, +); + +class DoNotDisturbViewModel extends Notifier { + late final CommandsRepository _repository; + + static const int maxIntervals = 4; + + @override + DoNotDisturbViewState build() { + _repository = ref.read(commandsRepositoryProvider); + Future.microtask(_load); + return const DoNotDisturbViewState(); + } + + String? get _identificator => + ref.read(selectedDeviceProvider)?.identificator; + + Future selectTimeRange(TimeRangeEntity range) async { + if (range == state.timeRange) return; + state = state.copyWith(timeRange: range, isLoading: true); + } + + Future _load() async { + try { + if (_identificator == null) return; + + // final timeRanges = await _repository.getDoNotDisturb(deviceId: _identificator); + final timeRanges = [ + TimeRangeEntity(id: '1', deviceId: '1111', active: true, startTime: TimeOfDay(hour: 0, minute: 0), endTime: TimeOfDay(hour: 6, minute: 0)) + ]; + state = state.copyWith(timeRanges: timeRanges, isLoading: false); + } catch (e) { + state = state.copyWith( + isLoading: false, + errorMessage: formatErrorMessage(e), + ); + } + } + + Future updateTimeRange({ + required String timeRangeId, + required List days, + required TimeOfDay startTime, + required TimeOfDay endTime + }) async { + if (_identificator == null) return; + + try { + List timeRanges = state.timeRanges.toList(); + final index = timeRanges.indexWhere((t)=>t.id == timeRangeId); + TimeRangeEntity timeRange = timeRanges[index]; + + timeRange = timeRange.copyWith( + days: days, + startTime: startTime, + endTime: endTime, + ); + + await sendCommand(timeRange: timeRange); + + timeRanges[index] = timeRange; + + state = state.copyWith( + timeRanges: timeRanges, + successMessage: I18n.timePeriodUpdated, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + errorMessage: e.toString(), + ); + } + } + + Future createTimeRange({ + required TimeOfDay startTime, + required TimeOfDay endTime, + required List days, + }) async { + if (_identificator == null) return; + + final newTimeRange = TimeRangeEntity( + id: '', + deviceId: _identificator!, + active: true, + startTime: startTime, + endTime: endTime, + ); + + await sendCommand(timeRange: newTimeRange); + + List timeRanges = state.timeRanges.toList(); + timeRanges.add(newTimeRange); + + state = state.copyWith( + timeRanges: timeRanges, + successMessage: I18n.timePeriodCreated, + ); + } + + Future sendCommand({required TimeRangeEntity timeRange}) async { + final request = SendCommandRequestModel( + device: _identificator!, + command: DeviceCommand.setDoNotDisturb, + // data: {}, + ); + await _repository.send(request: request); + } + + void clearSuccess() { + state = state.copyWith(successMessage: ''); + } + + String _formatError(Object e) { + final msg = e.toString(); + return msg.startsWith('Exception: ') ? msg.substring(11) : msg; + } + + void setTimeRangeActive(int i, bool value) { + + final timeRange = state.timeRanges[i]; + List timeRanges = state.timeRanges.toList(); + timeRanges[i] = timeRange.copyWith(active: value); + + state = state.copyWith( + timeRanges: timeRanges, + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.dart new file mode 100644 index 00000000..236b4cc6 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../domain/entities/time_range_entity.dart'; + +part 'do_not_disturb_view_state.freezed.dart'; + +@freezed +abstract class DoNotDisturbViewState with _$DoNotDisturbViewState { + const factory DoNotDisturbViewState({ + @Default([]) List timeRanges, + TimeRangeEntity? timeRange, + @Default(TimeOfDay(hour: 0, minute: 0)) TimeOfDay startTime, + @Default(TimeOfDay(hour: 0, minute: 0)) TimeOfDay endTime, + @Default(true) bool isLoading, + @Default('') String errorMessage, + @Default('') String successMessage, + }) = _DoNotDisturbViewState; +} diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.freezed.dart new file mode 100644 index 00000000..2f34dcdd --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/state/do_not_disturb_view_state.freezed.dart @@ -0,0 +1,319 @@ +// 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 'do_not_disturb_view_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$DoNotDisturbViewState { + + List get timeRanges; TimeRangeEntity? get timeRange; TimeOfDay get startTime; TimeOfDay get endTime; bool get isLoading; String get errorMessage; String get successMessage; +/// Create a copy of DoNotDisturbViewState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DoNotDisturbViewStateCopyWith get copyWith => _$DoNotDisturbViewStateCopyWithImpl(this as DoNotDisturbViewState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DoNotDisturbViewState&&const DeepCollectionEquality().equals(other.timeRanges, timeRanges)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.startTime, startTime) || other.startTime == startTime)&&(identical(other.endTime, endTime) || other.endTime == endTime)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(timeRanges),timeRange,startTime,endTime,isLoading,errorMessage,successMessage); + +@override +String toString() { + return 'DoNotDisturbViewState(timeRanges: $timeRanges, timeRange: $timeRange, startTime: $startTime, endTime: $endTime, isLoading: $isLoading, errorMessage: $errorMessage, successMessage: $successMessage)'; +} + + +} + +/// @nodoc +abstract mixin class $DoNotDisturbViewStateCopyWith<$Res> { + factory $DoNotDisturbViewStateCopyWith(DoNotDisturbViewState value, $Res Function(DoNotDisturbViewState) _then) = _$DoNotDisturbViewStateCopyWithImpl; +@useResult +$Res call({ + List timeRanges, TimeRangeEntity? timeRange, TimeOfDay startTime, TimeOfDay endTime, bool isLoading, String errorMessage, String successMessage +}); + + +$TimeRangeEntityCopyWith<$Res>? get timeRange; + +} +/// @nodoc +class _$DoNotDisturbViewStateCopyWithImpl<$Res> + implements $DoNotDisturbViewStateCopyWith<$Res> { + _$DoNotDisturbViewStateCopyWithImpl(this._self, this._then); + + final DoNotDisturbViewState _self; + final $Res Function(DoNotDisturbViewState) _then; + +/// Create a copy of DoNotDisturbViewState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? timeRanges = null,Object? timeRange = freezed,Object? startTime = null,Object? endTime = null,Object? isLoading = null,Object? errorMessage = null,Object? successMessage = null,}) { + return _then(_self.copyWith( +timeRanges: null == timeRanges ? _self.timeRanges : timeRanges // ignore: cast_nullable_to_non_nullable +as List,timeRange: freezed == timeRange ? _self.timeRange : timeRange // ignore: cast_nullable_to_non_nullable +as TimeRangeEntity?,startTime: null == startTime ? _self.startTime : startTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,endTime: null == endTime ? _self.endTime : endTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,isLoading: null == isLoading ? _self.isLoading : isLoading // 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, + )); +} +/// Create a copy of DoNotDisturbViewState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$TimeRangeEntityCopyWith<$Res>? get timeRange { + if (_self.timeRange == null) { + return null; + } + + return $TimeRangeEntityCopyWith<$Res>(_self.timeRange!, (value) { + return _then(_self.copyWith(timeRange: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [DoNotDisturbViewState]. +extension DoNotDisturbViewStatePatterns on DoNotDisturbViewState { +/// 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 Function( _DoNotDisturbViewState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DoNotDisturbViewState() 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 Function( _DoNotDisturbViewState value) $default,){ +final _that = this; +switch (_that) { +case _DoNotDisturbViewState(): +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? Function( _DoNotDisturbViewState value)? $default,){ +final _that = this; +switch (_that) { +case _DoNotDisturbViewState() 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 Function( List timeRanges, TimeRangeEntity? timeRange, TimeOfDay startTime, TimeOfDay endTime, bool isLoading, String errorMessage, String successMessage)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _DoNotDisturbViewState() when $default != null: +return $default(_that.timeRanges,_that.timeRange,_that.startTime,_that.endTime,_that.isLoading,_that.errorMessage,_that.successMessage);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 Function( List timeRanges, TimeRangeEntity? timeRange, TimeOfDay startTime, TimeOfDay endTime, bool isLoading, String errorMessage, String successMessage) $default,) {final _that = this; +switch (_that) { +case _DoNotDisturbViewState(): +return $default(_that.timeRanges,_that.timeRange,_that.startTime,_that.endTime,_that.isLoading,_that.errorMessage,_that.successMessage);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? Function( List timeRanges, TimeRangeEntity? timeRange, TimeOfDay startTime, TimeOfDay endTime, bool isLoading, String errorMessage, String successMessage)? $default,) {final _that = this; +switch (_that) { +case _DoNotDisturbViewState() when $default != null: +return $default(_that.timeRanges,_that.timeRange,_that.startTime,_that.endTime,_that.isLoading,_that.errorMessage,_that.successMessage);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _DoNotDisturbViewState implements DoNotDisturbViewState { + const _DoNotDisturbViewState({final List timeRanges = const [], this.timeRange, this.startTime = const TimeOfDay(hour: 0, minute: 0), this.endTime = const TimeOfDay(hour: 0, minute: 0), this.isLoading = true, this.errorMessage = '', this.successMessage = ''}): _timeRanges = timeRanges; + + + final List _timeRanges; +@override@JsonKey() List get timeRanges { + if (_timeRanges is EqualUnmodifiableListView) return _timeRanges; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_timeRanges); +} + +@override final TimeRangeEntity? timeRange; +@override@JsonKey() final TimeOfDay startTime; +@override@JsonKey() final TimeOfDay endTime; +@override@JsonKey() final bool isLoading; +@override@JsonKey() final String errorMessage; +@override@JsonKey() final String successMessage; + +/// Create a copy of DoNotDisturbViewState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DoNotDisturbViewStateCopyWith<_DoNotDisturbViewState> get copyWith => __$DoNotDisturbViewStateCopyWithImpl<_DoNotDisturbViewState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DoNotDisturbViewState&&const DeepCollectionEquality().equals(other._timeRanges, _timeRanges)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.startTime, startTime) || other.startTime == startTime)&&(identical(other.endTime, endTime) || other.endTime == endTime)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_timeRanges),timeRange,startTime,endTime,isLoading,errorMessage,successMessage); + +@override +String toString() { + return 'DoNotDisturbViewState(timeRanges: $timeRanges, timeRange: $timeRange, startTime: $startTime, endTime: $endTime, isLoading: $isLoading, errorMessage: $errorMessage, successMessage: $successMessage)'; +} + + +} + +/// @nodoc +abstract mixin class _$DoNotDisturbViewStateCopyWith<$Res> implements $DoNotDisturbViewStateCopyWith<$Res> { + factory _$DoNotDisturbViewStateCopyWith(_DoNotDisturbViewState value, $Res Function(_DoNotDisturbViewState) _then) = __$DoNotDisturbViewStateCopyWithImpl; +@override @useResult +$Res call({ + List timeRanges, TimeRangeEntity? timeRange, TimeOfDay startTime, TimeOfDay endTime, bool isLoading, String errorMessage, String successMessage +}); + + +@override $TimeRangeEntityCopyWith<$Res>? get timeRange; + +} +/// @nodoc +class __$DoNotDisturbViewStateCopyWithImpl<$Res> + implements _$DoNotDisturbViewStateCopyWith<$Res> { + __$DoNotDisturbViewStateCopyWithImpl(this._self, this._then); + + final _DoNotDisturbViewState _self; + final $Res Function(_DoNotDisturbViewState) _then; + +/// Create a copy of DoNotDisturbViewState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? timeRanges = null,Object? timeRange = freezed,Object? startTime = null,Object? endTime = null,Object? isLoading = null,Object? errorMessage = null,Object? successMessage = null,}) { + return _then(_DoNotDisturbViewState( +timeRanges: null == timeRanges ? _self._timeRanges : timeRanges // ignore: cast_nullable_to_non_nullable +as List,timeRange: freezed == timeRange ? _self.timeRange : timeRange // ignore: cast_nullable_to_non_nullable +as TimeRangeEntity?,startTime: null == startTime ? _self.startTime : startTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,endTime: null == endTime ? _self.endTime : endTime // ignore: cast_nullable_to_non_nullable +as TimeOfDay,isLoading: null == isLoading ? _self.isLoading : isLoading // 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, + )); +} + +/// Create a copy of DoNotDisturbViewState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$TimeRangeEntityCopyWith<$Res>? get timeRange { + if (_self.timeRange == null) { + return null; + } + + return $TimeRangeEntityCopyWith<$Res>(_self.timeRange!, (value) { + return _then(_self.copyWith(timeRange: value)); + }); +} +} + +// dart format on diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/widgets/set_time_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/widgets/set_time_dialog.dart new file mode 100644 index 00000000..32fb80c6 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/widgets/set_time_dialog.dart @@ -0,0 +1,324 @@ +import 'package:design_system/design_system.dart'; +import 'package:device_management/src/features/do_not_disturb/domain/entities/time_range_entity.dart'; +import 'package:device_management/src/features/do_not_disturb/presentation/state/do_not_disturb_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_shared/legacy_shared.dart'; +import 'package:sf_localizations/sf_localizations.dart'; +import 'package:utils/utils.dart'; + +void showTimeRangeForm( + BuildContext context, { + TimeRangeEntity? timeRange, + int? weekDay, + }) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ActivityFormSheet(timeRange: timeRange, weekDay: weekDay), + ); +} + +class ActivityFormSheet extends ConsumerStatefulWidget { + final TimeRangeEntity? timeRange; + final int? weekDay; + + const ActivityFormSheet({super.key, this.timeRange, this.weekDay}); + + @override + ConsumerState createState() => _ActivityFormSheetState(); +} + +class _ActivityFormSheetState extends ConsumerState { + late List _days; + late TimeOfDay _startTime; + late TimeOfDay _endTime; + bool _showStartPicker = false; + bool _showEndPicker = false; + + bool get _isEditing => widget.timeRange != null; + + bool get _isFormValid { + return _timeToMinutes(_startTime) < _timeToMinutes(_endTime); + } + + @override + void initState() { + super.initState(); + final timeRange = widget.timeRange; + _days = timeRange?.days.toList() ?? [false, false, false, false, false, false, false]; + + if (timeRange != null) { + _startTime = timeRange.startTime; + _endTime = timeRange.endTime; + } else { + _startTime = const TimeOfDay(hour: 0, minute: 0); + _endTime = const TimeOfDay(hour: 0, minute: 0); + } + } + + + int _timeToMinutes(TimeOfDay time) => time.hour * 60 + time.minute; + + void _togglePicker({required bool isStart}) { + setState(() { + if (isStart) { + _showStartPicker = !_showStartPicker; + _showEndPicker = false; + } else { + _showEndPicker = !_showEndPicker; + _showStartPicker = false; + } + }); + } + + void _submit() { + if (!_isFormValid) return; + + final vm = ref.read(doNotDisturbViewModelProvider.notifier); + + if (_isEditing) { + vm.updateTimeRange( + timeRangeId: widget.timeRange!.id, + days: _days, + startTime: _startTime, + endTime: _endTime, + ); + } else { + vm.createTimeRange( + startTime: _startTime, + endTime: _endTime, + days: _days, + ); + } + + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final theme = ref.watch(themePortProvider); + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + return Padding( + padding: EdgeInsets.only(bottom: bottomInset), + child: Container( + decoration: BoxDecoration( + color: theme.getColorFor(ThemeCode.backgroundPrimary), + borderRadius: BorderRadius.vertical( + top: Radius.circular(SizeUtils.getByScreen(small: 20, big: 18)), + ), + ), + child: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.all(SizeUtils.getByScreen(small: 22, big: 20)), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + width: 40, + height: 4, + margin: EdgeInsets.only( + bottom: SizeUtils.getByScreen(small: 16, big: 14), + ), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Text( + context.translate( + _isEditing + ? I18n.scheduledActivityEditTitle + : I18n.scheduledActivityNewTitle, + ), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 20, big: 19), + fontWeight: FontWeight.bold, + color: theme.getColorFor(ThemeCode.legacyPrimary), + ), + ), + SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)), + WeekDayChips.multi( + selectedDays: _days, + theme: theme, + onToggle: (index) => + setState(() => _days[index] = !_days[index]), + ), + SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)), + Row( + children: [ + Expanded( + child: _TimeSelector( + label: context.translate( + I18n.scheduledActivityStartTime, + ), + time: _startTime, + isExpanded: _showStartPicker, + onTap: () => _togglePicker(isStart: true), + theme: theme, + ), + ), + SizedBox(width: SizeUtils.getByScreen(small: 12, big: 10)), + Expanded( + child: _TimeSelector( + label: context.translate(I18n.scheduledActivityEndTime), + time: _endTime, + isExpanded: _showEndPicker, + onTap: () => _togglePicker(isStart: false), + theme: theme, + ), + ), + ], + ), + if (_showStartPicker) + _InlineTimePicker( + initialTime: _startTime, + onChanged: (time) => setState(() => _startTime = time), + ), + if (_showEndPicker) + _InlineTimePicker( + initialTime: _endTime, + onChanged: (time) => setState(() => _endTime = time), + ), + SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), + SizedBox( + height: SizeUtils.getByScreen(small: 48, big: 46), + child: ElevatedButton( + onPressed: _isFormValid ? _submit : null, + style: ElevatedButton.styleFrom( + backgroundColor: theme.getColorFor( + ThemeCode.legacyPrimary, + ), + disabledBackgroundColor: Colors.grey[300], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SizeUtils.getByScreen(small: 12, big: 10), + ), + ), + ), + child: Text( + context.translate(I18n.save), + style: TextStyle( + color: _isFormValid ? Colors.white : Colors.grey, + fontSize: SizeUtils.getByScreen(small: 16, big: 15), + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _TimeSelector extends StatelessWidget { + final String label; + final TimeOfDay time; + final bool isExpanded; + final VoidCallback onTap; + final ThemePort theme; + + const _TimeSelector({ + required this.label, + required this.time, + required this.isExpanded, + required this.onTap, + required this.theme, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: SizeUtils.getByScreen(small: 12, big: 10), + vertical: SizeUtils.getByScreen(small: 14, big: 12), + ), + decoration: BoxDecoration( + border: Border.all( + color: isExpanded + ? theme.getColorFor(ThemeCode.legacyPrimary) + : Colors.grey, + width: isExpanded ? 2 : 1, + ), + borderRadius: BorderRadius.circular(4), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 12, big: 11), + color: theme + .getColorFor(ThemeCode.textPrimary) + .withValues(alpha: 0.5), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + time.format(context), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 16, big: 15), + fontWeight: FontWeight.w500, + ), + ), + Icon( + isExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + size: SizeUtils.getByScreen(small: 20, big: 18), + color: theme + .getColorFor(ThemeCode.textPrimary) + .withValues(alpha: 0.5), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _InlineTimePicker extends StatelessWidget { + final TimeOfDay initialTime; + final ValueChanged onChanged; + + const _InlineTimePicker({required this.initialTime, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return Container( + height: SizeUtils.getByScreen(small: 180, big: 160), + margin: EdgeInsets.only(top: SizeUtils.getByScreen(small: 8, big: 6)), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: true, + initialDateTime: DateTime( + 0, + 1, + 1, + initialTime.hour, + initialTime.minute, + ), + onDateTimeChanged: (dateTime) { + onChanged(TimeOfDay(hour: dateTime.hour, minute: dateTime.minute)); + }, + ), + ); + } +} diff --git a/modules/legacy/packages/legacy_shared/lib/src/data/datasources/commands_remote_datasource_impl.dart b/modules/legacy/packages/legacy_shared/lib/src/data/datasources/commands_remote_datasource_impl.dart index d1662657..1024b6e8 100644 --- a/modules/legacy/packages/legacy_shared/lib/src/data/datasources/commands_remote_datasource_impl.dart +++ b/modules/legacy/packages/legacy_shared/lib/src/data/datasources/commands_remote_datasource_impl.dart @@ -13,26 +13,22 @@ class CommandsRemoteDatasourceImpl implements CommandsRemoteDatasource { Future send({ required SendCommandRequestModel request }) async { - try{ - final response = await safeCall( - () => _repository.post>( - '/commands', - body: request.toJson(), - ), - 'Error in command ${request.command}', - ); + final response = await safeCall( + () => _repository.post>( + '/commands', + body: request.toJson(), + ), + 'Error in command ${request.command}', + ); - final data = response.data; - if (data == null || data.isEmpty) { - throw Exception('Empty response from command ${request.command}'); - } - if (response.statusCode == 500) { - throw Exception('Server error from command ${request.command}'); - } - - // return CommandsResponseModel.fromJson(data); - } catch(e) { - return; + final data = response.data; + if (data == null || data.isEmpty) { + throw Exception('Empty response from command ${request.command}'); } + if (response.statusCode == 500) { + throw Exception('Server error from command ${request.command}'); + } + + // return CommandsResponseModel.fromJson(data);¡ } } diff --git a/modules/legacy/packages/legacy_shared/lib/src/data/models/send_command_request_model.dart b/modules/legacy/packages/legacy_shared/lib/src/data/models/send_command_request_model.dart index 358627ae..4bc2c613 100644 --- a/modules/legacy/packages/legacy_shared/lib/src/data/models/send_command_request_model.dart +++ b/modules/legacy/packages/legacy_shared/lib/src/data/models/send_command_request_model.dart @@ -4,6 +4,8 @@ part 'send_command_request_model.freezed.dart'; part 'send_command_request_model.g.dart'; enum DeviceCommand { + @JsonValue('SET_DO_NOT_DISTURB') + setDoNotDisturb, @JsonValue('FACTORY') factory, @JsonValue('FIND_DEVICE') diff --git a/modules/legacy/packages/legacy_shared/lib/src/widgets/layouts/page_layout.dart b/modules/legacy/packages/legacy_shared/lib/src/widgets/layouts/page_layout.dart index 905562e8..1e83f4e3 100644 --- a/modules/legacy/packages/legacy_shared/lib/src/widgets/layouts/page_layout.dart +++ b/modules/legacy/packages/legacy_shared/lib/src/widgets/layouts/page_layout.dart @@ -15,13 +15,13 @@ class LegacyPageLayout extends StatelessWidget { const LegacyPageLayout({ super.key, + required this.theme, required this.title, required this.body, this.footer, this.showBack = true, this.showEdit = false, this.onEditChange, - required this.theme, }); @override diff --git a/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml b/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml index 28301667..a264b49f 100644 --- a/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml +++ b/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml @@ -7,6 +7,6 @@ 2.6.4 - 20260316000000 + 20260318000000 diff --git a/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.md5 b/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.md5 index 9d12d556..66e064a1 100644 --- a/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.md5 +++ b/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.md5 @@ -1 +1 @@ -a0ed8b315dd3aaa92839422686f00f9d \ No newline at end of file +f9e85f64806f37132f0c0cc4ef8a67ec \ No newline at end of file diff --git a/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.sha1 b/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.sha1 index a2a39a3d..d0a4a597 100644 --- a/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.sha1 +++ b/packages/flutter_treezor_entrust_sdk_bridge/android/build/com/entrust/antelop/antelop/maven-metadata.xml.sha1 @@ -1 +1 @@ -4888c373e3701bd965ce8ff0daaa19071f7d9a9e \ No newline at end of file +b7a72907f1f917f7b7d0cd57cb170901965b4113 \ No newline at end of file diff --git a/packages/navigation/lib/app_routes.dart b/packages/navigation/lib/app_routes.dart index 41d266d0..4afd8d23 100644 --- a/packages/navigation/lib/app_routes.dart +++ b/packages/navigation/lib/app_routes.dart @@ -63,6 +63,7 @@ class AppRoutes { static const rewards = '$deviceManagement/rewards'; static const activityMeter = '$deviceManagement/activity_meter'; static const appsUse = '$deviceManagement/apps_use'; + static const doNotDisturb = '$deviceManagement/do_not_disturb'; static const legacyLogin = '$legacy/login'; static const legacySignup = '$legacy/signup'; diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index 3d2e121e..6d1059c8 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -705,5 +705,9 @@ "wifiBssidHint": "e.g. 0c:80:63:e4:cb:e1", "editChildProfile": "Edit profile", "editChildProfileSaveSuccess": "Child profile updated successfully", - "editChildProfileTitle": "Edit child profile" + "editChildProfileTitle": "Edit child profile", + "doNotDisturbMessage": "Set the time period, all the functions except for SOS and GPS will be disabled.", + "addTimeRange": "Add time period", + "timePeriodUpdated": "Time period updated", + "timePeriodCreated": "Time period created" } \ No newline at end of file diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index f2f5f9b9..45c779c4 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -703,5 +703,9 @@ "wifiBssidHint": "ej. 0c:80:63:e4:cb:e1", "editChildProfile": "Editar perfil", "editChildProfileTitle": "Editar perfil del niño", - "editChildProfileSaveSuccess": "Perfil del niño actualizado correctamente" + "editChildProfileSaveSuccess": "Perfil del niño actualizado correctamente", + "doNotDisturbMessage": "Establece el periodo de tiempo, todas las funciones que no sean SOS y GPS se desactivarán.", + "addTimeRange": "Agregar periodo de tiempo", + "timePeriodUpdated": "Periodo de tiempo actualizado", + "timePeriodCreated": "Periodo de tiempo creado" } \ No newline at end of file diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index ba36512e..ddf2399b 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -829,4 +829,8 @@ class I18n { static const String editChildProfile = 'editChildProfile'; static const String editChildProfileTitle = 'editChildProfileTitle'; static const String editChildProfileSaveSuccess = 'editChildProfileSaveSuccess'; + static const String doNotDisturbMessage = 'doNotDisturbMessage'; + static const String addTimeRange = 'addTimeRange'; + static const String timePeriodUpdated = 'timePeriodUpdated'; + static const String timePeriodCreated = 'timePeriodCreated'; }