refactor(legacy-settings): migrate alarm CRUD to AsyncNotifier
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/state/alarm_view_model.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/state/alarm_view_state.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/widgets/alarm_form_screen.dart'
|
||||
show showAlarmForm;
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarm_controller.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarms_editor_provider.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarms_provider.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/widgets/alarm_form_screen.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class AlarmScreen extends ConsumerWidget {
|
||||
@@ -19,129 +20,223 @@ class AlarmScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(alarmViewModelProvider.notifier);
|
||||
final (alarms, isLoading, isSaving, maxAlarms) = ref.watch(
|
||||
alarmViewModelProvider.select(
|
||||
(s) => (s.alarms, s.isLoading, s.isSaving, s.maxAlarms),
|
||||
),
|
||||
);
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final maxAlarms = device?.capabilities?.alarms?.maxAlarms ?? 3;
|
||||
|
||||
ref.listen(alarmViewModelProvider.select((s) => s.errorEvent), (_, next) {
|
||||
if (next == null) return;
|
||||
final message = switch (next) {
|
||||
AlarmErrorEvent.load || AlarmErrorEvent.save => context.translate(
|
||||
I18n.alarmError,
|
||||
),
|
||||
AlarmErrorEvent.maxAlarms => context
|
||||
.translate(I18n.alarmMaxReached)
|
||||
.replaceAll('{max}', maxAlarms.toString()),
|
||||
};
|
||||
showTopSnackbar(context, message: message, type: MessageType.error);
|
||||
vm.clearError();
|
||||
});
|
||||
|
||||
ref.listen(alarmViewModelProvider.select((s) => s.saveSuccess), (
|
||||
_,
|
||||
success,
|
||||
) {
|
||||
if (success) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.alarmSaved),
|
||||
type: MessageType.success,
|
||||
);
|
||||
vm.clearSaveSuccess();
|
||||
ref.listen(alarmControllerProvider, (prev, next) {
|
||||
next.showErrorOn(context);
|
||||
if (prev != null &&
|
||||
prev.isLoading &&
|
||||
!next.isLoading &&
|
||||
!next.hasError) {
|
||||
ref.read(alarmsEditorProvider.notifier).clear();
|
||||
}
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
onPressed: () => navigationContract.goBack(),
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back,
|
||||
color: primaryColor,
|
||||
size: SizeUtils.getByScreen(small: 32, big: 28),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
context.translate(I18n.alarm).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (alarms.length < maxAlarms)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
_openForm(context, vm);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: SizeUtils.getByScreen(small: 24, big: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (device == null) {
|
||||
return Scaffold(
|
||||
appBar: _appBar(context, navigationContract, primaryColor),
|
||||
body: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
final alarmsAsync = ref.watch(alarmsProvider(device.id));
|
||||
final pendingAlarms = ref.watch(alarmsEditorProvider);
|
||||
final isSaving = ref.watch(
|
||||
alarmControllerProvider.select((s) => s.isLoading),
|
||||
);
|
||||
|
||||
return alarmsAsync.when(
|
||||
loading: () => Scaffold(
|
||||
appBar: _appBar(context, navigationContract, primaryColor),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: alarms.isEmpty
|
||||
error: (_, __) => Scaffold(
|
||||
appBar: _appBar(context, navigationContract, primaryColor),
|
||||
body: Center(child: Text(context.translate(I18n.errorGeneric))),
|
||||
),
|
||||
data: (fetchedAlarms) {
|
||||
final alarms = pendingAlarms ?? fetchedAlarms;
|
||||
final canAdd = alarms.length < maxAlarms;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
appBar: _appBar(
|
||||
context,
|
||||
navigationContract,
|
||||
primaryColor,
|
||||
trailing: canAdd
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
if (alarms.length >= maxAlarms) {
|
||||
await showInfoDialog(
|
||||
context,
|
||||
I18n.alarmMaxReached,
|
||||
args: {'max': maxAlarms.toString()},
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
_openForm(
|
||||
context,
|
||||
ref,
|
||||
fetchedAlarms: fetchedAlarms,
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: SizeUtils.getByScreen(small: 24, big: 22),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: alarms.isEmpty
|
||||
? _EmptyState(primaryColor: primaryColor)
|
||||
: _AlarmList(
|
||||
alarms: alarms,
|
||||
onEdit: (index, alarm) =>
|
||||
_openForm(context, vm, index: index, alarm: alarm),
|
||||
onDelete: vm.removeAlarm,
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: (!isLoading && alarms.isNotEmpty)
|
||||
? Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
child: isSaving
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.save();
|
||||
},
|
||||
text: context.translate(I18n.alarmSave),
|
||||
color: primaryColor,
|
||||
onEdit: (index, alarm) => _openForm(
|
||||
context,
|
||||
ref,
|
||||
fetchedAlarms: fetchedAlarms,
|
||||
index: index,
|
||||
alarm: alarm,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onDelete: (index) async {
|
||||
final confirmed = await _confirmDelete(context);
|
||||
if (confirmed != true) return;
|
||||
if (!context.mounted) return;
|
||||
final remaining = [...alarms]..removeAt(index);
|
||||
await ref
|
||||
.read(alarmControllerProvider.notifier)
|
||||
.save(deviceId: device.id, alarms: remaining);
|
||||
if (!context.mounted) return;
|
||||
final state = ref.read(alarmControllerProvider);
|
||||
if (state.hasError) return;
|
||||
await showSuccessDialog(context, I18n.alarmDeleted);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: alarms.isNotEmpty
|
||||
? Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
child: isSaving
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
await ref
|
||||
.read(alarmControllerProvider.notifier)
|
||||
.save(deviceId: device.id, alarms: alarms);
|
||||
if (!context.mounted) return;
|
||||
final state =
|
||||
ref.read(alarmControllerProvider);
|
||||
if (state.hasError) return;
|
||||
await showSuccessDialog(
|
||||
context,
|
||||
I18n.alarmSaved,
|
||||
);
|
||||
if (context.mounted) {
|
||||
navigationContract.goBack();
|
||||
}
|
||||
},
|
||||
text: context.translate(I18n.alarmSave),
|
||||
color: primaryColor,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _appBar(
|
||||
BuildContext context,
|
||||
NavigationContract nav,
|
||||
Color primaryColor, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
return AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
onPressed: () => nav.goBack(),
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back,
|
||||
color: primaryColor,
|
||||
size: SizeUtils.getByScreen(small: 32, big: 28),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
context.translate(I18n.alarm).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (trailing != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
),
|
||||
child: trailing,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool?> _confirmDelete(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
icon: Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: theme.colorScheme.error,
|
||||
size: 40,
|
||||
),
|
||||
title: Text(context.translate(I18n.deleteAlarm)),
|
||||
content: Text(context.translate(I18n.deleteAlarmConfirm)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: Text(context.translate(I18n.cancel)),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
),
|
||||
child: Text(context.translate(I18n.delete)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openForm(
|
||||
BuildContext context,
|
||||
AlarmViewModel vm, {
|
||||
WidgetRef ref, {
|
||||
required List<AlarmEntity> fetchedAlarms,
|
||||
int? index,
|
||||
AlarmEntity? alarm,
|
||||
}) {
|
||||
@@ -149,10 +244,15 @@ class AlarmScreen extends ConsumerWidget {
|
||||
context,
|
||||
alarm: alarm,
|
||||
onSave: (result) {
|
||||
final editor = ref.read(alarmsEditorProvider.notifier);
|
||||
if (index != null) {
|
||||
vm.updateAlarm(index, result);
|
||||
editor.replace(
|
||||
current: fetchedAlarms,
|
||||
index: index,
|
||||
alarm: result,
|
||||
);
|
||||
} else {
|
||||
vm.addAlarm(result);
|
||||
editor.add(current: fetchedAlarms, alarm: result);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -189,7 +289,7 @@ class _EmptyState extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AlarmList extends ConsumerWidget {
|
||||
class _AlarmList extends StatelessWidget {
|
||||
final List<AlarmEntity> alarms;
|
||||
final void Function(int index, AlarmEntity alarm) onEdit;
|
||||
final void Function(int index) onDelete;
|
||||
@@ -201,12 +301,12 @@ class _AlarmList extends ConsumerWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: SizeUtils.getByScreen(
|
||||
small: EdgeInsets.symmetric(horizontal: 22, vertical: 10),
|
||||
big: EdgeInsets.symmetric(horizontal: 21, vertical: 8),
|
||||
small: const EdgeInsets.symmetric(horizontal: 22, vertical: 10),
|
||||
big: const EdgeInsets.symmetric(horizontal: 21, vertical: 8),
|
||||
),
|
||||
child: Column(
|
||||
children: alarms.asMap().entries.map((entry) {
|
||||
@@ -222,7 +322,7 @@ class _AlarmList extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AlarmCard extends ConsumerWidget {
|
||||
class _AlarmCard extends StatelessWidget {
|
||||
final AlarmEntity alarm;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
@@ -234,7 +334,7 @@ class _AlarmCard extends ConsumerWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
|
||||
final frequencyLabel = switch (alarm.frequency) {
|
||||
@@ -254,7 +354,7 @@ class _AlarmCard extends ConsumerWidget {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -282,10 +382,10 @@ class _AlarmCard extends ConsumerWidget {
|
||||
),
|
||||
if (alarm.frequency == AlarmFrequency.custom &&
|
||||
alarm.week != null) ...[
|
||||
SizedBox(height: 6),
|
||||
const SizedBox(height: 6),
|
||||
_WeekDaysDisplay(week: alarm.week!, primaryColor: primaryColor),
|
||||
],
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:settings/src/core/providers/alarm_repository_provider.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarms_provider.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
part 'alarm_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class AlarmController extends _$AlarmController {
|
||||
@override
|
||||
FutureOr<void> build() {}
|
||||
|
||||
Future<void> save({
|
||||
required String deviceId,
|
||||
required List<AlarmEntity> alarms,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref
|
||||
.read(alarmRepositoryProvider)
|
||||
.upsertAlarms(deviceId: deviceId, alarms: alarms);
|
||||
ref.invalidate(alarmsProvider(deviceId));
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacySettingsAlarmsSaved(alarmsCount: alarms.length),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'alarm_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AlarmController)
|
||||
const alarmControllerProvider = AlarmControllerProvider._();
|
||||
|
||||
final class AlarmControllerProvider
|
||||
extends $AsyncNotifierProvider<AlarmController, void> {
|
||||
const AlarmControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'alarmControllerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$alarmControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AlarmController create() => AlarmController();
|
||||
}
|
||||
|
||||
String _$alarmControllerHash() => r'5d63182414c78dd84759e0d2fcc4801e1748f09c';
|
||||
|
||||
abstract class _$AlarmController extends $AsyncNotifier<void> {
|
||||
FutureOr<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
build();
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, void>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
|
||||
part 'alarm_form_provider.g.dart';
|
||||
|
||||
class AlarmFormState {
|
||||
const AlarmFormState({
|
||||
required this.durationMinutes,
|
||||
required this.frequency,
|
||||
required this.days,
|
||||
});
|
||||
|
||||
final int durationMinutes;
|
||||
final AlarmFrequency frequency;
|
||||
final List<bool> days;
|
||||
|
||||
AlarmFormState copyWith({
|
||||
int? durationMinutes,
|
||||
AlarmFrequency? frequency,
|
||||
List<bool>? days,
|
||||
}) {
|
||||
return AlarmFormState(
|
||||
durationMinutes: durationMinutes ?? this.durationMinutes,
|
||||
frequency: frequency ?? this.frequency,
|
||||
days: days ?? this.days,
|
||||
);
|
||||
}
|
||||
|
||||
bool get canSave {
|
||||
if (frequency == AlarmFrequency.custom && !days.contains(true)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String get timeString {
|
||||
final hour = (durationMinutes ~/ 60) % 24;
|
||||
final minute = durationMinutes % 60;
|
||||
return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String? get weekString {
|
||||
if (frequency != AlarmFrequency.custom) return null;
|
||||
return days.map((d) => d ? '1' : '0').join();
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class AlarmFormNotifier extends _$AlarmFormNotifier {
|
||||
@override
|
||||
AlarmFormState build(AlarmEntity? initialAlarm) {
|
||||
return _stateFor(initialAlarm);
|
||||
}
|
||||
|
||||
void setDurationMinutes(int minutes) =>
|
||||
state = state.copyWith(durationMinutes: minutes);
|
||||
|
||||
void setFrequency(AlarmFrequency frequency) =>
|
||||
state = state.copyWith(frequency: frequency);
|
||||
|
||||
void toggleDay(int index) {
|
||||
if (index < 0 || index >= state.days.length) return;
|
||||
final updated = [...state.days];
|
||||
updated[index] = !updated[index];
|
||||
state = state.copyWith(days: updated);
|
||||
}
|
||||
|
||||
static AlarmFormState _stateFor(AlarmEntity? alarm) {
|
||||
if (alarm == null) {
|
||||
return const AlarmFormState(
|
||||
durationMinutes: 8 * 60,
|
||||
frequency: AlarmFrequency.once,
|
||||
days: [false, false, false, false, false, false, false],
|
||||
);
|
||||
}
|
||||
final parts = alarm.time.split(':');
|
||||
final hour = int.tryParse(parts[0]) ?? 8;
|
||||
final minute = int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0;
|
||||
return AlarmFormState(
|
||||
durationMinutes: hour * 60 + minute,
|
||||
frequency: alarm.frequency,
|
||||
days: alarm.week != null
|
||||
? alarm.week!.padRight(7, '0').split('').map((c) => c == '1').toList()
|
||||
: List.filled(7, false),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'alarm_form_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AlarmFormNotifier)
|
||||
const alarmFormProvider = AlarmFormNotifierFamily._();
|
||||
|
||||
final class AlarmFormNotifierProvider
|
||||
extends $NotifierProvider<AlarmFormNotifier, AlarmFormState> {
|
||||
const AlarmFormNotifierProvider._({
|
||||
required AlarmFormNotifierFamily super.from,
|
||||
required AlarmEntity? super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'alarmFormProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$alarmFormNotifierHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'alarmFormProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AlarmFormNotifier create() => AlarmFormNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AlarmFormState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AlarmFormState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AlarmFormNotifierProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$alarmFormNotifierHash() => r'cd344fbbaecfab337e40b472e263afe88dacdb94';
|
||||
|
||||
final class AlarmFormNotifierFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
AlarmFormNotifier,
|
||||
AlarmFormState,
|
||||
AlarmFormState,
|
||||
AlarmFormState,
|
||||
AlarmEntity?
|
||||
> {
|
||||
const AlarmFormNotifierFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'alarmFormProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
AlarmFormNotifierProvider call(AlarmEntity? initialAlarm) =>
|
||||
AlarmFormNotifierProvider._(argument: initialAlarm, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'alarmFormProvider';
|
||||
}
|
||||
|
||||
abstract class _$AlarmFormNotifier extends $Notifier<AlarmFormState> {
|
||||
late final _$args = ref.$arg as AlarmEntity?;
|
||||
AlarmEntity? get initialAlarm => _$args;
|
||||
|
||||
AlarmFormState build(AlarmEntity? initialAlarm);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<AlarmFormState, AlarmFormState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AlarmFormState, AlarmFormState>,
|
||||
AlarmFormState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
|
||||
part 'alarms_editor_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class AlarmsEditor extends _$AlarmsEditor {
|
||||
@override
|
||||
List<AlarmEntity>? build() => null;
|
||||
|
||||
void add({
|
||||
required List<AlarmEntity> current,
|
||||
required AlarmEntity alarm,
|
||||
}) {
|
||||
final base = state ?? current;
|
||||
state = [...base, alarm];
|
||||
}
|
||||
|
||||
void replace({
|
||||
required List<AlarmEntity> current,
|
||||
required int index,
|
||||
required AlarmEntity alarm,
|
||||
}) {
|
||||
final base = state ?? current;
|
||||
if (index < 0 || index >= base.length) return;
|
||||
final updated = [...base];
|
||||
updated[index] = alarm;
|
||||
state = updated;
|
||||
}
|
||||
|
||||
void remove({
|
||||
required List<AlarmEntity> current,
|
||||
required int index,
|
||||
}) {
|
||||
final base = state ?? current;
|
||||
if (index < 0 || index >= base.length) return;
|
||||
state = [...base]..removeAt(index);
|
||||
}
|
||||
|
||||
void clear() => state = null;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'alarms_editor_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AlarmsEditor)
|
||||
const alarmsEditorProvider = AlarmsEditorProvider._();
|
||||
|
||||
final class AlarmsEditorProvider
|
||||
extends $NotifierProvider<AlarmsEditor, List<AlarmEntity>?> {
|
||||
const AlarmsEditorProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'alarmsEditorProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$alarmsEditorHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AlarmsEditor create() => AlarmsEditor();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(List<AlarmEntity>? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<List<AlarmEntity>?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$alarmsEditorHash() => r'3969010eeae051c6eba6e40ee1f3f4a93c38d347';
|
||||
|
||||
abstract class _$AlarmsEditor extends $Notifier<List<AlarmEntity>?> {
|
||||
List<AlarmEntity>? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<List<AlarmEntity>?, List<AlarmEntity>?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<List<AlarmEntity>?, List<AlarmEntity>?>,
|
||||
List<AlarmEntity>?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:settings/src/core/providers/alarm_repository_provider.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
|
||||
part 'alarms_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<AlarmEntity>> alarms(Ref ref, String deviceId) async {
|
||||
return ref.watch(alarmRepositoryProvider).getAlarms(deviceId: deviceId);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'alarms_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(alarms)
|
||||
const alarmsProvider = AlarmsFamily._();
|
||||
|
||||
final class AlarmsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<AlarmEntity>>,
|
||||
List<AlarmEntity>,
|
||||
FutureOr<List<AlarmEntity>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<AlarmEntity>>,
|
||||
$FutureProvider<List<AlarmEntity>> {
|
||||
const AlarmsProvider._({
|
||||
required AlarmsFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'alarmsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$alarmsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'alarmsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<AlarmEntity>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<AlarmEntity>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return alarms(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AlarmsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$alarmsHash() => r'1139a974b215a01cf1c13e33a0fdb527898a804e';
|
||||
|
||||
final class AlarmsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<AlarmEntity>>, String> {
|
||||
const AlarmsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'alarmsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
AlarmsProvider call(String deviceId) =>
|
||||
AlarmsProvider._(argument: deviceId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'alarmsProvider';
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:settings/src/core/domain/repositories/alarm_repository.dart';
|
||||
import 'package:settings/src/core/providers/alarm_repository_provider.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/state/alarm_view_state.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
final alarmViewModelProvider =
|
||||
NotifierProvider.autoDispose<AlarmViewModel, AlarmViewState>(
|
||||
AlarmViewModel.new,
|
||||
);
|
||||
|
||||
class AlarmViewModel extends Notifier<AlarmViewState> {
|
||||
late final AlarmRepository _repository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@override
|
||||
AlarmViewState build() {
|
||||
_repository = ref.read(alarmRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
final capabilities = ref.read(selectedDeviceProvider).value?.capabilities;
|
||||
final maxAlarms = capabilities?.alarms?.maxAlarms ?? 3;
|
||||
|
||||
Future.microtask(_load);
|
||||
return AlarmViewState(maxAlarms: maxAlarms);
|
||||
}
|
||||
|
||||
String? get _deviceId => ref.read(selectedDeviceProvider).value?.id;
|
||||
|
||||
Future<void> _load() async {
|
||||
final deviceId = _deviceId;
|
||||
if (deviceId == null) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final alarms = await _repository.getAlarms(deviceId: deviceId);
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(alarms: alarms, isLoading: false);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorEvent: AlarmErrorEvent.load,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addAlarm(AlarmEntity alarm) {
|
||||
if (state.alarms.length >= state.maxAlarms) {
|
||||
state = state.copyWith(errorEvent: AlarmErrorEvent.maxAlarms);
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
alarms: [...state.alarms, alarm],
|
||||
errorEvent: null,
|
||||
);
|
||||
}
|
||||
|
||||
void updateAlarm(int index, AlarmEntity alarm) {
|
||||
if (index < 0 || index >= state.alarms.length) return;
|
||||
final updated = [...state.alarms];
|
||||
updated[index] = alarm;
|
||||
state = state.copyWith(alarms: updated, errorEvent: null);
|
||||
}
|
||||
|
||||
void removeAlarm(int index) {
|
||||
if (index < 0 || index >= state.alarms.length) return;
|
||||
final updated = [...state.alarms]..removeAt(index);
|
||||
state = state.copyWith(alarms: updated, errorEvent: null);
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
final deviceId = _deviceId;
|
||||
if (deviceId == null) return;
|
||||
|
||||
state = state.copyWith(
|
||||
isSaving: true,
|
||||
saveSuccess: false,
|
||||
errorEvent: null,
|
||||
);
|
||||
|
||||
try {
|
||||
final alarms = await _repository.upsertAlarms(
|
||||
deviceId: deviceId,
|
||||
alarms: state.alarms,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(
|
||||
isSaving: false,
|
||||
alarms: alarms,
|
||||
saveSuccess: true,
|
||||
);
|
||||
|
||||
unawaited(
|
||||
_tracking.legacySettingsAlarmsSaved(alarmsCount: alarms.length),
|
||||
);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isSaving: false,
|
||||
errorEvent: AlarmErrorEvent.save,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (state.errorEvent != null) state = state.copyWith(errorEvent: null);
|
||||
}
|
||||
|
||||
void clearSaveSuccess() {
|
||||
if (state.saveSuccess) state = state.copyWith(saveSuccess: false);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
|
||||
part 'alarm_view_state.freezed.dart';
|
||||
|
||||
enum AlarmErrorEvent { load, save, maxAlarms }
|
||||
|
||||
@freezed
|
||||
abstract class AlarmViewState with _$AlarmViewState {
|
||||
const factory AlarmViewState({
|
||||
@Default([]) List<AlarmEntity> alarms,
|
||||
@Default(true) bool isLoading,
|
||||
@Default(false) bool isSaving,
|
||||
@Default(3) int maxAlarms,
|
||||
AlarmErrorEvent? errorEvent,
|
||||
@Default(false) bool saveSuccess,
|
||||
}) = _AlarmViewState;
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'alarm_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$AlarmViewState {
|
||||
|
||||
List<AlarmEntity> get alarms; bool get isLoading; bool get isSaving; int get maxAlarms; AlarmErrorEvent? get errorEvent; bool get saveSuccess;
|
||||
/// Create a copy of AlarmViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AlarmViewStateCopyWith<AlarmViewState> get copyWith => _$AlarmViewStateCopyWithImpl<AlarmViewState>(this as AlarmViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AlarmViewState&&const DeepCollectionEquality().equals(other.alarms, alarms)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.maxAlarms, maxAlarms) || other.maxAlarms == maxAlarms)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(alarms),isLoading,isSaving,maxAlarms,errorEvent,saveSuccess);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlarmViewState(alarms: $alarms, isLoading: $isLoading, isSaving: $isSaving, maxAlarms: $maxAlarms, errorEvent: $errorEvent, saveSuccess: $saveSuccess)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AlarmViewStateCopyWith<$Res> {
|
||||
factory $AlarmViewStateCopyWith(AlarmViewState value, $Res Function(AlarmViewState) _then) = _$AlarmViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<AlarmEntity> alarms, bool isLoading, bool isSaving, int maxAlarms, AlarmErrorEvent? errorEvent, bool saveSuccess
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AlarmViewStateCopyWithImpl<$Res>
|
||||
implements $AlarmViewStateCopyWith<$Res> {
|
||||
_$AlarmViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AlarmViewState _self;
|
||||
final $Res Function(AlarmViewState) _then;
|
||||
|
||||
/// Create a copy of AlarmViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? alarms = null,Object? isLoading = null,Object? isSaving = null,Object? maxAlarms = null,Object? errorEvent = freezed,Object? saveSuccess = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
alarms: null == alarms ? _self.alarms : alarms // ignore: cast_nullable_to_non_nullable
|
||||
as List<AlarmEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
|
||||
as bool,maxAlarms: null == maxAlarms ? _self.maxAlarms : maxAlarms // ignore: cast_nullable_to_non_nullable
|
||||
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as AlarmErrorEvent?,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AlarmViewState].
|
||||
extension AlarmViewStatePatterns on AlarmViewState {
|
||||
/// 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( _AlarmViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AlarmViewState() 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( _AlarmViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AlarmViewState():
|
||||
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( _AlarmViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AlarmViewState() 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<AlarmEntity> alarms, bool isLoading, bool isSaving, int maxAlarms, AlarmErrorEvent? errorEvent, bool saveSuccess)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AlarmViewState() when $default != null:
|
||||
return $default(_that.alarms,_that.isLoading,_that.isSaving,_that.maxAlarms,_that.errorEvent,_that.saveSuccess);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<AlarmEntity> alarms, bool isLoading, bool isSaving, int maxAlarms, AlarmErrorEvent? errorEvent, bool saveSuccess) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AlarmViewState():
|
||||
return $default(_that.alarms,_that.isLoading,_that.isSaving,_that.maxAlarms,_that.errorEvent,_that.saveSuccess);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<AlarmEntity> alarms, bool isLoading, bool isSaving, int maxAlarms, AlarmErrorEvent? errorEvent, bool saveSuccess)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AlarmViewState() when $default != null:
|
||||
return $default(_that.alarms,_that.isLoading,_that.isSaving,_that.maxAlarms,_that.errorEvent,_that.saveSuccess);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _AlarmViewState implements AlarmViewState {
|
||||
const _AlarmViewState({final List<AlarmEntity> alarms = const [], this.isLoading = true, this.isSaving = false, this.maxAlarms = 3, this.errorEvent, this.saveSuccess = false}): _alarms = alarms;
|
||||
|
||||
|
||||
final List<AlarmEntity> _alarms;
|
||||
@override@JsonKey() List<AlarmEntity> get alarms {
|
||||
if (_alarms is EqualUnmodifiableListView) return _alarms;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_alarms);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool isLoading;
|
||||
@override@JsonKey() final bool isSaving;
|
||||
@override@JsonKey() final int maxAlarms;
|
||||
@override final AlarmErrorEvent? errorEvent;
|
||||
@override@JsonKey() final bool saveSuccess;
|
||||
|
||||
/// Create a copy of AlarmViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$AlarmViewStateCopyWith<_AlarmViewState> get copyWith => __$AlarmViewStateCopyWithImpl<_AlarmViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AlarmViewState&&const DeepCollectionEquality().equals(other._alarms, _alarms)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.maxAlarms, maxAlarms) || other.maxAlarms == maxAlarms)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_alarms),isLoading,isSaving,maxAlarms,errorEvent,saveSuccess);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlarmViewState(alarms: $alarms, isLoading: $isLoading, isSaving: $isSaving, maxAlarms: $maxAlarms, errorEvent: $errorEvent, saveSuccess: $saveSuccess)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$AlarmViewStateCopyWith<$Res> implements $AlarmViewStateCopyWith<$Res> {
|
||||
factory _$AlarmViewStateCopyWith(_AlarmViewState value, $Res Function(_AlarmViewState) _then) = __$AlarmViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<AlarmEntity> alarms, bool isLoading, bool isSaving, int maxAlarms, AlarmErrorEvent? errorEvent, bool saveSuccess
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$AlarmViewStateCopyWithImpl<$Res>
|
||||
implements _$AlarmViewStateCopyWith<$Res> {
|
||||
__$AlarmViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _AlarmViewState _self;
|
||||
final $Res Function(_AlarmViewState) _then;
|
||||
|
||||
/// Create a copy of AlarmViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? alarms = null,Object? isLoading = null,Object? isSaving = null,Object? maxAlarms = null,Object? errorEvent = freezed,Object? saveSuccess = null,}) {
|
||||
return _then(_AlarmViewState(
|
||||
alarms: null == alarms ? _self._alarms : alarms // ignore: cast_nullable_to_non_nullable
|
||||
as List<AlarmEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
|
||||
as bool,maxAlarms: null == maxAlarms ? _self.maxAlarms : maxAlarms // ignore: cast_nullable_to_non_nullable
|
||||
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
as AlarmErrorEvent?,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarm_form_provider.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
@@ -16,78 +17,41 @@ void showAlarmForm(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _AlarmFormSheet(alarm: alarm, onSave: onSave),
|
||||
builder: (_) => _AlarmFormSheet(initialAlarm: alarm, onSave: onSave),
|
||||
);
|
||||
}
|
||||
|
||||
class _AlarmFormSheet extends ConsumerStatefulWidget {
|
||||
final AlarmEntity? alarm;
|
||||
class _AlarmFormSheet extends ConsumerWidget {
|
||||
final AlarmEntity? initialAlarm;
|
||||
final ValueChanged<AlarmEntity> onSave;
|
||||
|
||||
const _AlarmFormSheet({this.alarm, required this.onSave});
|
||||
const _AlarmFormSheet({this.initialAlarm, required this.onSave});
|
||||
|
||||
@override
|
||||
ConsumerState<_AlarmFormSheet> createState() => _AlarmFormSheetState();
|
||||
}
|
||||
|
||||
class _AlarmFormSheetState extends ConsumerState<_AlarmFormSheet> {
|
||||
late Duration _duration;
|
||||
late AlarmFrequency _frequency;
|
||||
late List<bool> _days;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.alarm != null) {
|
||||
final parts = widget.alarm!.time.split(':');
|
||||
final hour = int.tryParse(parts[0]) ?? 8;
|
||||
final minute = int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0;
|
||||
_duration = Duration(hours: hour, minutes: minute);
|
||||
_frequency = widget.alarm!.frequency;
|
||||
_days = widget.alarm!.week != null
|
||||
? widget.alarm!.week!.padRight(7, '0').split('').map((c) => c == '1').toList()
|
||||
: List.filled(7, false);
|
||||
} else {
|
||||
_duration = const Duration(hours: 8);
|
||||
_frequency = AlarmFrequency.once;
|
||||
_days = List.filled(7, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool get _canSave {
|
||||
if (_frequency == AlarmFrequency.custom && !_days.contains(true)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _save() {
|
||||
if (!_canSave) return;
|
||||
|
||||
final hour = _duration.inHours % 24;
|
||||
final minute = _duration.inMinutes % 60;
|
||||
final time =
|
||||
'${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
|
||||
|
||||
final week = _frequency == AlarmFrequency.custom
|
||||
? _days.map((day) => day ? '1' : '0').join()
|
||||
: null;
|
||||
|
||||
widget.onSave(AlarmEntity(time: time, frequency: _frequency, week: week));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
final formState = ref.watch(alarmFormProvider(initialAlarm));
|
||||
final notifier = ref.read(alarmFormProvider(initialAlarm).notifier);
|
||||
|
||||
void onSubmit() {
|
||||
if (!formState.canSave) return;
|
||||
onSave(
|
||||
AlarmEntity(
|
||||
time: formState.timeString,
|
||||
frequency: formState.frequency,
|
||||
week: formState.weekString,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: bottomInset),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
@@ -117,7 +81,7 @@ class _AlarmFormSheetState extends ConsumerState<_AlarmFormSheet> {
|
||||
children: [
|
||||
Text(
|
||||
context.translate(
|
||||
widget.alarm != null
|
||||
initialAlarm != null
|
||||
? I18n.editAlarm
|
||||
: I18n.addAlarm,
|
||||
),
|
||||
@@ -128,13 +92,16 @@ class _AlarmFormSheetState extends ConsumerState<_AlarmFormSheet> {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _canSave ? _save : null,
|
||||
onPressed: formState.canSave ? onSubmit : null,
|
||||
child: Text(
|
||||
context.translate(I18n.save),
|
||||
style: TextStyle(
|
||||
color: _canSave ? primaryColor : Colors.grey,
|
||||
color: formState.canSave
|
||||
? primaryColor
|
||||
: Colors.grey,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 17),
|
||||
fontSize:
|
||||
SizeUtils.getByScreen(small: 16, big: 17),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -144,15 +111,17 @@ class _AlarmFormSheetState extends ConsumerState<_AlarmFormSheet> {
|
||||
Container(
|
||||
height: SizeUtils.getByScreen(small: 160, big: 180),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: CupertinoTimerPicker(
|
||||
mode: CupertinoTimerPickerMode.hm,
|
||||
initialTimerDuration: _duration,
|
||||
onTimerDurationChanged: (duration) {
|
||||
setState(() => _duration = duration);
|
||||
},
|
||||
initialTimerDuration: Duration(
|
||||
minutes: formState.durationMinutes,
|
||||
),
|
||||
onTimerDurationChanged: (duration) =>
|
||||
notifier.setDurationMinutes(duration.inMinutes),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 20, big: 22)),
|
||||
@@ -163,38 +132,32 @@ class _AlarmFormSheetState extends ConsumerState<_AlarmFormSheet> {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
_RadioOption(
|
||||
label: context.translate(I18n.once),
|
||||
isSelected: _frequency == AlarmFrequency.once,
|
||||
isSelected: formState.frequency == AlarmFrequency.once,
|
||||
primaryColor: primaryColor,
|
||||
onTap: () =>
|
||||
setState(() => _frequency = AlarmFrequency.once),
|
||||
onTap: () => notifier.setFrequency(AlarmFrequency.once),
|
||||
),
|
||||
_RadioOption(
|
||||
label: context.translate(I18n.daily),
|
||||
isSelected: _frequency == AlarmFrequency.every,
|
||||
isSelected: formState.frequency == AlarmFrequency.every,
|
||||
primaryColor: primaryColor,
|
||||
onTap: () =>
|
||||
setState(() => _frequency = AlarmFrequency.every),
|
||||
onTap: () => notifier.setFrequency(AlarmFrequency.every),
|
||||
),
|
||||
_RadioOption(
|
||||
label: context.translate(I18n.selectDay),
|
||||
isSelected: _frequency == AlarmFrequency.custom,
|
||||
isSelected: formState.frequency == AlarmFrequency.custom,
|
||||
primaryColor: primaryColor,
|
||||
onTap: () =>
|
||||
setState(() => _frequency = AlarmFrequency.custom),
|
||||
onTap: () => notifier.setFrequency(AlarmFrequency.custom),
|
||||
),
|
||||
if (_frequency == AlarmFrequency.custom) ...[
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)),
|
||||
if (formState.frequency == AlarmFrequency.custom) ...[
|
||||
SizedBox(
|
||||
height: SizeUtils.getByScreen(small: 12, big: 14),
|
||||
),
|
||||
WeekDayChips.multi(
|
||||
selectedDays: _days,
|
||||
onToggle: (index) {
|
||||
setState(() {
|
||||
_days = _days.toList();
|
||||
_days[index] = !_days[index];
|
||||
});
|
||||
},
|
||||
selectedDays: formState.days,
|
||||
onToggle: notifier.toggleDay,
|
||||
),
|
||||
],
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
||||
@@ -237,7 +200,7 @@ class _RadioOption extends StatelessWidget {
|
||||
color: isSelected ? primaryColor : Colors.grey,
|
||||
size: SizeUtils.getByScreen(small: 22, big: 24),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:settings/src/core/domain/repositories/alarm_repository.dart';
|
||||
import 'package:settings/src/core/providers/alarm_repository_provider.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarm_controller.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
class MockAlarmRepository extends Mock implements AlarmRepository {}
|
||||
|
||||
const _alarm = AlarmEntity(time: '08:00', frequency: AlarmFrequency.once);
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(const <AlarmEntity>[]);
|
||||
});
|
||||
|
||||
ProviderContainer buildContainer(AlarmRepository repo) {
|
||||
return makeContainer(
|
||||
overrides: [
|
||||
alarmRepositoryProvider.overrideWithValue(repo),
|
||||
sfTrackingProvider.overrideWithValue(
|
||||
SfTrackingRepository(clients: const []),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
group('AlarmController.save', () {
|
||||
test('transitions to AsyncData and invalidates alarmsProvider', () async {
|
||||
final repo = MockAlarmRepository();
|
||||
when(
|
||||
() => repo.upsertAlarms(
|
||||
deviceId: any(named: 'deviceId'),
|
||||
alarms: any(named: 'alarms'),
|
||||
),
|
||||
).thenAnswer((_) async => const [_alarm]);
|
||||
when(() => repo.getAlarms(deviceId: any(named: 'deviceId')))
|
||||
.thenAnswer((_) async => const [_alarm]);
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(alarmControllerProvider.notifier)
|
||||
.save(deviceId: 'device-1', alarms: const [_alarm]);
|
||||
|
||||
expect(container.read(alarmControllerProvider), isA<AsyncData<void>>());
|
||||
|
||||
verify(
|
||||
() => repo.upsertAlarms(deviceId: 'device-1', alarms: [_alarm]),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exposes AsyncError when the repository fails', () async {
|
||||
final repo = MockAlarmRepository();
|
||||
when(
|
||||
() => repo.upsertAlarms(
|
||||
deviceId: any(named: 'deviceId'),
|
||||
alarms: any(named: 'alarms'),
|
||||
),
|
||||
).thenThrow(const ApiException(message: 'boom', isNetworkError: true));
|
||||
|
||||
final container = buildContainer(repo);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(alarmControllerProvider.notifier)
|
||||
.save(deviceId: 'device-1', alarms: const [_alarm]);
|
||||
|
||||
final state = container.read(alarmControllerProvider);
|
||||
expect(state, isA<AsyncError<void>>());
|
||||
expect(state.error, isA<ApiException>());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarm_form_provider.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
|
||||
void main() {
|
||||
group('AlarmFormNotifier', () {
|
||||
test('defaults to 08:00 once when no initial alarm', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final state = container.read(alarmFormProvider(null));
|
||||
expect(state.durationMinutes, 480);
|
||||
expect(state.frequency, AlarmFrequency.once);
|
||||
expect(state.days, [false, false, false, false, false, false, false]);
|
||||
expect(state.canSave, isTrue);
|
||||
expect(state.timeString, '08:00');
|
||||
expect(state.weekString, isNull);
|
||||
});
|
||||
|
||||
test('initializes from existing alarm with custom week', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
const existing = AlarmEntity(
|
||||
time: '21:45',
|
||||
frequency: AlarmFrequency.custom,
|
||||
week: '1010100',
|
||||
);
|
||||
final state = container.read(alarmFormProvider(existing));
|
||||
expect(state.durationMinutes, 21 * 60 + 45);
|
||||
expect(state.frequency, AlarmFrequency.custom);
|
||||
expect(state.days, [true, false, true, false, true, false, false]);
|
||||
expect(state.timeString, '21:45');
|
||||
expect(state.weekString, '1010100');
|
||||
});
|
||||
|
||||
test('setFrequency updates frequency', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmFormProvider(null).notifier)
|
||||
.setFrequency(AlarmFrequency.every);
|
||||
|
||||
expect(
|
||||
container.read(alarmFormProvider(null)).frequency,
|
||||
AlarmFrequency.every,
|
||||
);
|
||||
});
|
||||
|
||||
test('setDurationMinutes updates time', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmFormProvider(null).notifier)
|
||||
.setDurationMinutes(7 * 60 + 30);
|
||||
|
||||
expect(container.read(alarmFormProvider(null)).timeString, '07:30');
|
||||
});
|
||||
|
||||
test('toggleDay flips day at index', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(alarmFormProvider(null).notifier).toggleDay(2);
|
||||
|
||||
expect(
|
||||
container.read(alarmFormProvider(null)).days,
|
||||
[false, false, true, false, false, false, false],
|
||||
);
|
||||
});
|
||||
|
||||
test('canSave false when custom frequency without days selected', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmFormProvider(null).notifier)
|
||||
.setFrequency(AlarmFrequency.custom);
|
||||
|
||||
expect(container.read(alarmFormProvider(null)).canSave, isFalse);
|
||||
});
|
||||
|
||||
test('canSave true when custom frequency with at least one day', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmFormProvider(null).notifier)
|
||||
.setFrequency(AlarmFrequency.custom);
|
||||
container.read(alarmFormProvider(null).notifier).toggleDay(3);
|
||||
|
||||
expect(container.read(alarmFormProvider(null)).canSave, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart';
|
||||
import 'package:settings/src/features/alarm/presentation/providers/alarms_editor_provider.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
|
||||
const _a1 = AlarmEntity(time: '08:00', frequency: AlarmFrequency.once);
|
||||
const _a2 = AlarmEntity(time: '09:30', frequency: AlarmFrequency.every);
|
||||
const _a3 = AlarmEntity(
|
||||
time: '10:00',
|
||||
frequency: AlarmFrequency.custom,
|
||||
week: '1010101',
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('AlarmsEditor', () {
|
||||
test('defaults to null', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
expect(container.read(alarmsEditorProvider), isNull);
|
||||
});
|
||||
|
||||
test('add appends alarm to current list', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmsEditorProvider.notifier)
|
||||
.add(current: const [_a1], alarm: _a2);
|
||||
|
||||
expect(container.read(alarmsEditorProvider), [_a1, _a2]);
|
||||
});
|
||||
|
||||
test('replace swaps alarm at index', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmsEditorProvider.notifier)
|
||||
.replace(current: const [_a1, _a2], index: 1, alarm: _a3);
|
||||
|
||||
expect(container.read(alarmsEditorProvider), [_a1, _a3]);
|
||||
});
|
||||
|
||||
test('remove drops alarm at index', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmsEditorProvider.notifier)
|
||||
.remove(current: const [_a1, _a2], index: 0);
|
||||
|
||||
expect(container.read(alarmsEditorProvider), [_a2]);
|
||||
});
|
||||
|
||||
test('clear resets state to null', () {
|
||||
final container = makeContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container
|
||||
.read(alarmsEditorProvider.notifier)
|
||||
.add(current: const [], alarm: _a1);
|
||||
container.read(alarmsEditorProvider.notifier).clear();
|
||||
|
||||
expect(container.read(alarmsEditorProvider), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user