Files
stefwill d1b18c5296 feat: add day detail screen with swipe-to-delete and undo
Adds DayDetailScreen showing all entries for a selected day with
swipe-to-delete and an undo snackbar. CalendarScreen gains a tappable
day header (Today / Yesterday / Month Day) that pushes the detail screen.
2026-04-26 14:29:00 +10:00

183 lines
5.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/mood_provider.dart';
import 'day_detail_screen.dart';
import 'widgets/calendar_heatmap.dart';
import 'widgets/day_entry_card.dart';
const _months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
class CalendarScreen extends ConsumerStatefulWidget {
const CalendarScreen({super.key});
@override
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
}
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
late DateTime _month;
@override
void initState() {
super.initState();
final now = DateTime.now();
_month = DateTime(now.year, now.month);
}
void _prevMonth() => setState(() => _month = DateTime(_month.year, _month.month - 1));
void _nextMonth() {
final now = DateTime.now();
final next = DateTime(_month.year, _month.month + 1);
if (!next.isAfter(DateTime(now.year, now.month))) {
setState(() => _month = next);
}
}
static String _dayLabel(DateTime day) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
if (day == today) return 'Today';
if (day == yesterday) return 'Yesterday';
return '${_months[day.month - 1]} ${day.day}';
}
bool get _canGoForward {
final now = DateTime.now();
return _month.year < now.year ||
(_month.year == now.year && _month.month < now.month);
}
@override
Widget build(BuildContext context) {
final selectedDay = ref.watch(selectedDayProvider);
final entriesAsync = ref.watch(moodEntriesForDayProvider(selectedDay));
return Scaffold(
appBar: AppBar(
title: const Text('History'),
centerTitle: false,
),
body: Column(
children: [
_MonthNav(
label: '${_months[_month.month - 1]} ${_month.year}',
onPrev: _prevMonth,
onNext: _canGoForward ? _nextMonth : null,
),
CalendarHeatmap(
month: _month,
selectedDay: selectedDay,
onDayTap: (day) {
ref.read(selectedDayProvider.notifier).select(day);
// Follow the selected day's month
final m = DateTime(day.year, day.month);
if (m != _month) setState(() => _month = m);
},
),
const Divider(height: 1),
InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => DayDetailScreen(day: selectedDay),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
Text(
_dayLabel(selectedDay),
style: Theme.of(context).textTheme.titleSmall,
),
const Spacer(),
Icon(Icons.chevron_right,
color: Theme.of(context).colorScheme.outline),
],
),
),
),
const Divider(height: 1),
Expanded(
child: entriesAsync.when(
data: (entries) => entries.isEmpty
? _EmptyDay(day: selectedDay)
: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: entries.length,
itemBuilder: (context, i) =>
DayEntryCard(entry: entries[i]),
),
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (_, __) => const SizedBox.shrink(),
),
),
],
),
);
}
}
class _MonthNav extends StatelessWidget {
const _MonthNav({
required this.label,
required this.onPrev,
required this.onNext,
});
final String label;
final VoidCallback onPrev;
final VoidCallback? onNext;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
children: [
IconButton(icon: const Icon(Icons.chevron_left), onPressed: onPrev),
Expanded(
child: Text(
label,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: onNext,
),
],
),
);
}
}
class _EmptyDay extends StatelessWidget {
const _EmptyDay({required this.day});
final DateTime day;
@override
Widget build(BuildContext context) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final label = day == today ? 'today' : '${day.day}/${day.month}/${day.year}';
return Center(
child: Text(
'No entries for $label',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Theme.of(context).colorScheme.outline),
),
);
}
}