401 lines
13 KiB
Dart
401 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:relationship_saver/core/config/app_theme.dart';
|
|
import 'package:relationship_saver/features/local/local_models.dart';
|
|
import 'package:relationship_saver/features/local/local_repository.dart';
|
|
import 'package:relationship_saver/features/shared/frosted_card.dart';
|
|
|
|
class RemindersView extends ConsumerWidget {
|
|
const RemindersView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final AsyncValue<LocalDataState> localData = ref.watch(
|
|
localRepositoryProvider,
|
|
);
|
|
|
|
return localData.when(
|
|
data: (LocalDataState data) {
|
|
final List<ReminderRule> reminders = data.reminders;
|
|
final Map<String, PersonProfile> peopleById = <String, PersonProfile>{
|
|
for (final PersonProfile person in data.people) person.id: person,
|
|
};
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(28, 24, 28, 28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Row(
|
|
children: <Widget>[
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Text(
|
|
'Reminders',
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Set recurring nudges so good intentions become habits.',
|
|
style: Theme.of(context).textTheme.bodyLarge
|
|
?.copyWith(color: AppTheme.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FilledButton.icon(
|
|
onPressed: () => _addReminder(context, ref, data.people),
|
|
icon: const Icon(Icons.alarm_add_rounded),
|
|
label: const Text('Add Reminder'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Expanded(
|
|
child: ListView.separated(
|
|
itemCount: reminders.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
|
itemBuilder: (BuildContext context, int index) {
|
|
final ReminderRule reminder = reminders[index];
|
|
final PersonProfile? person = reminder.personId == null
|
|
? null
|
|
: peopleById[reminder.personId!];
|
|
|
|
return FrostedCard(
|
|
child: Row(
|
|
children: <Widget>[
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Text(
|
|
reminder.title,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${_labelForCadence(reminder.cadence)} • ${_formatDateTime(reminder.nextAt)}',
|
|
style: Theme.of(context).textTheme.bodyMedium
|
|
?.copyWith(color: AppTheme.textSecondary),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
person == null ? 'Unassigned' : person.name,
|
|
style: Theme.of(context).textTheme.labelLarge
|
|
?.copyWith(color: AppTheme.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Switch(
|
|
value: reminder.enabled,
|
|
onChanged: (_) {
|
|
ref
|
|
.read(localRepositoryProvider.notifier)
|
|
.toggleReminderEnabled(reminder.id);
|
|
},
|
|
),
|
|
IconButton(
|
|
onPressed: () => _editReminder(
|
|
context,
|
|
ref,
|
|
data.people,
|
|
reminder,
|
|
),
|
|
icon: const Icon(Icons.edit_rounded),
|
|
tooltip: 'Edit',
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
ref
|
|
.read(localRepositoryProvider.notifier)
|
|
.deleteReminder(reminder.id);
|
|
},
|
|
icon: const Icon(Icons.delete_outline_rounded),
|
|
tooltip: 'Delete',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (Object error, StackTrace stackTrace) {
|
|
return const Center(child: Text('Unable to load reminders'));
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _addReminder(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
List<PersonProfile> people,
|
|
) async {
|
|
final _ReminderDraft? draft = await showDialog<_ReminderDraft>(
|
|
context: context,
|
|
builder: (BuildContext context) =>
|
|
_ReminderEditorDialog(title: 'Add Reminder', people: people),
|
|
);
|
|
|
|
if (draft == null) {
|
|
return;
|
|
}
|
|
|
|
await ref
|
|
.read(localRepositoryProvider.notifier)
|
|
.addReminder(
|
|
title: draft.title,
|
|
cadence: draft.cadence,
|
|
nextAt: draft.nextAt,
|
|
personId: draft.personId,
|
|
);
|
|
}
|
|
|
|
Future<void> _editReminder(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
List<PersonProfile> people,
|
|
ReminderRule reminder,
|
|
) async {
|
|
final _ReminderDraft? draft = await showDialog<_ReminderDraft>(
|
|
context: context,
|
|
builder: (BuildContext context) => _ReminderEditorDialog(
|
|
title: 'Edit Reminder',
|
|
people: people,
|
|
initial: _ReminderDraft(
|
|
personId: reminder.personId,
|
|
title: reminder.title,
|
|
cadence: reminder.cadence,
|
|
nextAt: reminder.nextAt,
|
|
),
|
|
),
|
|
);
|
|
|
|
if (draft == null) {
|
|
return;
|
|
}
|
|
|
|
await ref
|
|
.read(localRepositoryProvider.notifier)
|
|
.updateReminder(
|
|
reminder.copyWith(
|
|
personId: draft.personId,
|
|
title: draft.title,
|
|
cadence: draft.cadence,
|
|
nextAt: draft.nextAt,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ReminderEditorDialog extends StatefulWidget {
|
|
const _ReminderEditorDialog({
|
|
required this.title,
|
|
required this.people,
|
|
this.initial,
|
|
});
|
|
|
|
final String title;
|
|
final List<PersonProfile> people;
|
|
final _ReminderDraft? initial;
|
|
|
|
@override
|
|
State<_ReminderEditorDialog> createState() => _ReminderEditorDialogState();
|
|
}
|
|
|
|
class _ReminderEditorDialogState extends State<_ReminderEditorDialog> {
|
|
late final TextEditingController _titleController;
|
|
late ReminderCadence _cadence;
|
|
late DateTime _nextAt;
|
|
String? _personId;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_titleController = TextEditingController(text: widget.initial?.title ?? '');
|
|
_cadence = widget.initial?.cadence ?? ReminderCadence.weekly;
|
|
_nextAt =
|
|
widget.initial?.nextAt ?? DateTime.now().add(const Duration(days: 2));
|
|
_personId = widget.initial?.personId;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text(widget.title),
|
|
content: SizedBox(
|
|
width: 420,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
TextField(
|
|
controller: _titleController,
|
|
decoration: const InputDecoration(labelText: 'Title'),
|
|
),
|
|
DropdownButtonFormField<ReminderCadence>(
|
|
initialValue: _cadence,
|
|
items: ReminderCadence.values
|
|
.map(
|
|
(ReminderCadence cadence) =>
|
|
DropdownMenuItem<ReminderCadence>(
|
|
value: cadence,
|
|
child: Text(_labelForCadence(cadence)),
|
|
),
|
|
)
|
|
.toList(growable: false),
|
|
onChanged: (ReminderCadence? value) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_cadence = value;
|
|
});
|
|
},
|
|
decoration: const InputDecoration(labelText: 'Cadence'),
|
|
),
|
|
DropdownButtonFormField<String?>(
|
|
initialValue: _personId,
|
|
items: <DropdownMenuItem<String?>>[
|
|
const DropdownMenuItem<String?>(
|
|
value: null,
|
|
child: Text('Unassigned'),
|
|
),
|
|
...widget.people.map(
|
|
(PersonProfile person) => DropdownMenuItem<String?>(
|
|
value: person.id,
|
|
child: Text(person.name),
|
|
),
|
|
),
|
|
],
|
|
onChanged: (String? value) {
|
|
setState(() {
|
|
_personId = value;
|
|
});
|
|
},
|
|
decoration: const InputDecoration(labelText: 'Person'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: <Widget>[
|
|
Expanded(
|
|
child: Text(
|
|
_formatDateTime(_nextAt),
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
),
|
|
OutlinedButton.icon(
|
|
onPressed: _pickDateTime,
|
|
icon: const Icon(Icons.schedule_rounded),
|
|
label: const Text('Pick Time'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
final String title = _titleController.text.trim();
|
|
if (title.isEmpty) {
|
|
return;
|
|
}
|
|
Navigator.of(context).pop(
|
|
_ReminderDraft(
|
|
personId: _personId,
|
|
title: title,
|
|
cadence: _cadence,
|
|
nextAt: _nextAt,
|
|
),
|
|
);
|
|
},
|
|
child: const Text('Save'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _pickDateTime() async {
|
|
final DateTime now = DateTime.now();
|
|
final DateTime? date = await showDatePicker(
|
|
context: context,
|
|
initialDate: _nextAt,
|
|
firstDate: now.subtract(const Duration(days: 1)),
|
|
lastDate: now.add(const Duration(days: 3650)),
|
|
);
|
|
|
|
if (date == null || !mounted) {
|
|
return;
|
|
}
|
|
|
|
final TimeOfDay? time = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay.fromDateTime(_nextAt),
|
|
);
|
|
if (time == null || !mounted) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_nextAt = DateTime(
|
|
date.year,
|
|
date.month,
|
|
date.day,
|
|
time.hour,
|
|
time.minute,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class _ReminderDraft {
|
|
const _ReminderDraft({
|
|
required this.personId,
|
|
required this.title,
|
|
required this.cadence,
|
|
required this.nextAt,
|
|
});
|
|
|
|
final String? personId;
|
|
final String title;
|
|
final ReminderCadence cadence;
|
|
final DateTime nextAt;
|
|
}
|
|
|
|
String _labelForCadence(ReminderCadence cadence) {
|
|
return switch (cadence) {
|
|
ReminderCadence.daily => 'Daily',
|
|
ReminderCadence.weekly => 'Weekly',
|
|
ReminderCadence.monthly => 'Monthly',
|
|
};
|
|
}
|
|
|
|
String _formatDateTime(DateTime value) {
|
|
final String month = value.month.toString().padLeft(2, '0');
|
|
final String day = value.day.toString().padLeft(2, '0');
|
|
final String hour = value.hour.toString().padLeft(2, '0');
|
|
final String minute = value.minute.toString().padLeft(2, '0');
|
|
return '$month/$day/${value.year} $hour:$minute';
|
|
}
|