do not disturb ui and state

This commit is contained in:
2026-03-18 11:02:03 +01:00
parent fa36037aac
commit 2ea6b5875d
21 changed files with 1521 additions and 27 deletions

View File

@@ -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,
),
],
),
],

View File

@@ -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';
export 'src/features/apps_use/apps_use_builder.dart';
export 'src/features/do_not_disturb/do_not_disturb_builder.dart';

View File

@@ -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),

View File

@@ -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<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: DoNotDisturbScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -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<bool> 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';
}
}

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$TimeRangeEntity {
String get id; String get deviceId; bool get active; TimeOfDay get startTime; TimeOfDay get endTime; List<bool> 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<TimeRangeEntity> get copyWith => _$TimeRangeEntityCopyWithImpl<TimeRangeEntity>(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<bool> 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<bool>,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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List<bool> 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 extends Object?>(TResult Function( String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List<bool> 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 extends Object?>(TResult? Function( String id, String deviceId, bool active, TimeOfDay startTime, TimeOfDay endTime, List<bool> 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<bool> 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<bool> _days;
@override@JsonKey() List<bool> 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<bool> 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<bool>,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

View File

@@ -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<Widget>.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<bool> days) {
const weekDayI18nKeys = [
I18n.monday,
I18n.tuesday,
I18n.wednesday,
I18n.thursday,
I18n.friday,
I18n.saturday,
I18n.sunday,
];
final daysLabels = days.indexed.fold<List<String>>([], (List<String> 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),
),
),
);
}
}

View File

@@ -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<DoNotDisturbViewState> {
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<void> selectTimeRange(TimeRangeEntity range) async {
if (range == state.timeRange) return;
state = state.copyWith(timeRange: range, isLoading: true);
}
Future<void> _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<void> updateTimeRange({
required String timeRangeId,
required List<bool> days,
required TimeOfDay startTime,
required TimeOfDay endTime
}) async {
if (_identificator == null) return;
try {
List<TimeRangeEntity> 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<void> createTimeRange({
required TimeOfDay startTime,
required TimeOfDay endTime,
required List<bool> days,
}) async {
if (_identificator == null) return;
final newTimeRange = TimeRangeEntity(
id: '',
deviceId: _identificator!,
active: true,
startTime: startTime,
endTime: endTime,
);
await sendCommand(timeRange: newTimeRange);
List<TimeRangeEntity> timeRanges = state.timeRanges.toList();
timeRanges.add(newTimeRange);
state = state.copyWith(
timeRanges: timeRanges,
successMessage: I18n.timePeriodCreated,
);
}
Future<void> 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 <TimeRangeEntity> timeRanges = state.timeRanges.toList();
timeRanges[i] = timeRange.copyWith(active: value);
state = state.copyWith(
timeRanges: timeRanges,
);
}
}

View File

@@ -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<TimeRangeEntity> 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;
}

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$DoNotDisturbViewState {
List<TimeRangeEntity> 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<DoNotDisturbViewState> get copyWith => _$DoNotDisturbViewStateCopyWithImpl<DoNotDisturbViewState>(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<TimeRangeEntity> 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<TimeRangeEntity>,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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( List<TimeRangeEntity> 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 extends Object?>(TResult Function( List<TimeRangeEntity> 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 extends Object?>(TResult? Function( List<TimeRangeEntity> 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<TimeRangeEntity> 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<TimeRangeEntity> _timeRanges;
@override@JsonKey() List<TimeRangeEntity> 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<TimeRangeEntity> 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<TimeRangeEntity>,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

View File

@@ -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<ActivityFormSheet> createState() => _ActivityFormSheetState();
}
class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
late List<bool> _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<TimeOfDay> 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));
},
),
);
}
}

View File

@@ -13,26 +13,22 @@ class CommandsRemoteDatasourceImpl implements CommandsRemoteDatasource {
Future<void> send({
required SendCommandRequestModel request
}) async {
try{
final response = await safeCall(
() => _repository.post<Map<String, dynamic>>(
'/commands',
body: request.toJson(),
),
'Error in command ${request.command}',
);
final response = await safeCall(
() => _repository.post<Map<String, dynamic>>(
'/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);¡
}
}

View File

@@ -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')

View File

@@ -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

View File

@@ -7,6 +7,6 @@
<versions>
<version>2.6.4</version>
</versions>
<lastUpdated>20260316000000</lastUpdated>
<lastUpdated>20260318000000</lastUpdated>
</versioning>
</metadata>

View File

@@ -1 +1 @@
a0ed8b315dd3aaa92839422686f00f9d
f9e85f64806f37132f0c0cc4ef8a67ec

View File

@@ -1 +1 @@
4888c373e3701bd965ce8ff0daaa19071f7d9a9e
b7a72907f1f917f7b7d0cd57cb170901965b4113

View File

@@ -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';

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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';
}