new plus button
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
/// GitHub-style contribution heatmap for one calendar month of shopping trips.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utils/dates.dart';
|
||||
import '../utils/money.dart';
|
||||
import '../utils/shop_calendar.dart';
|
||||
|
||||
class ShopHeatmap extends StatelessWidget {
|
||||
const ShopHeatmap({
|
||||
super.key,
|
||||
required this.month,
|
||||
required this.counts,
|
||||
required this.monthTotal,
|
||||
required this.monthTripCount,
|
||||
required this.canGoForward,
|
||||
this.selectedDay,
|
||||
required this.onPreviousMonth,
|
||||
required this.onNextMonth,
|
||||
required this.onSelectDay,
|
||||
});
|
||||
|
||||
final DateTime month;
|
||||
final Map<DateTime, int> counts;
|
||||
final double monthTotal;
|
||||
final int monthTripCount;
|
||||
final bool canGoForward;
|
||||
final DateTime? selectedDay;
|
||||
final VoidCallback onPreviousMonth;
|
||||
final VoidCallback onNextMonth;
|
||||
final ValueChanged<DateTime?> onSelectDay;
|
||||
|
||||
static const _weekdays = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final cells = monthGrid(month);
|
||||
final today = dateOnly(DateTime.now());
|
||||
final weeks = cells.length ~/ 7;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Previous month',
|
||||
onPressed: onPreviousMonth,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
formatMonthYear(month),
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Next month',
|
||||
onPressed: canGoForward ? onNextMonth : null,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
monthTripCount == 0
|
||||
? 'No trips this month'
|
||||
: '${monthTripCount == 1 ? '1 trip' : '$monthTripCount trips'} · ${formatEuro(monthTotal)}',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
for (final label in _weekdays)
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
for (var week = 0; week < weeks; week++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var weekday = 0; weekday < 7; weekday++)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: _HeatCell(
|
||||
date: cells[week * 7 + weekday],
|
||||
count: cells[week * 7 + weekday] == null
|
||||
? 0
|
||||
: counts[cells[week * 7 + weekday]] ?? 0,
|
||||
today: today,
|
||||
selectedDay: selectedDay,
|
||||
onSelect: onSelectDay,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text('Less', style: theme.textTheme.labelMedium),
|
||||
const SizedBox(width: 6),
|
||||
for (var level = 0; level <= 4; level++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 3),
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: _fillForLevel(scheme, level),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('More', style: theme.textTheme.labelMedium),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeatCell extends StatelessWidget {
|
||||
const _HeatCell({
|
||||
required this.date,
|
||||
required this.count,
|
||||
required this.today,
|
||||
required this.selectedDay,
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
final DateTime? date;
|
||||
final int count;
|
||||
final DateTime today;
|
||||
final DateTime? selectedDay;
|
||||
final ValueChanged<DateTime?> onSelect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (date == null) {
|
||||
return const SizedBox(height: 36);
|
||||
}
|
||||
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final level = heatmapLevel(count);
|
||||
final selected = selectedDay != null && isSameDay(date!, selectedDay!);
|
||||
final isToday = isSameDay(date!, today);
|
||||
final fill = _fillForLevel(scheme, level);
|
||||
final textColor = level >= 3
|
||||
? scheme.onPrimary
|
||||
: scheme.onSurface.withValues(alpha: count == 0 ? 0.45 : 1);
|
||||
|
||||
return Tooltip(
|
||||
message: count == 0
|
||||
? formatDate(date!)
|
||||
: '${formatDate(date!)} · ${count == 1 ? '1 trip' : '$count trips'}',
|
||||
child: InkWell(
|
||||
onTap: () => onSelect(selected ? null : date),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: fill,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? scheme.primary
|
||||
: isToday
|
||||
? scheme.outline
|
||||
: Colors.transparent,
|
||||
width: selected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'${date!.day}',
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: textColor,
|
||||
fontWeight: isToday || selected
|
||||
? FontWeight.w700
|
||||
: FontWeight.w500,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Color _fillForLevel(ColorScheme scheme, int level) {
|
||||
if (level <= 0) return scheme.surfaceContainerHighest;
|
||||
final t = (level / 4).clamp(0.28, 1.0);
|
||||
return Color.lerp(scheme.primary.withValues(alpha: 0.22), scheme.primary, t)!;
|
||||
}
|
||||
Reference in New Issue
Block a user