screen-locker/stronglift_replacement/workout_app/lib/widgets/calendar_widget.dart
Krzysztof kuhy Rudnicki d8062a601f feat: overhaul workout app with progress tracking and UX improvements
- Remove automatic rest timer after each set
- Add inline threshold controls (success/fail streaks) on each exercise card
  during an active workout
- Settings: auto-save on change with 600 ms debounce; replace Save button
  with Reset to defaults
- Fix weight display asymmetry in settings (fixed 72 px width, centred)
- Progress screen (renamed from History): per-exercise view shows streak
  counters, weight chart (kg Y-axis, date X-axis, rolling-2 avg for total
  volume), exercise-filtered calendar, and per-exercise session tiles
- Total view shows rolling-2-session average volume chart + full calendar
  + all-session list
- Add WorkoutCalendar widget with monthly navigation
- Store warmupDone in ExerciseResult JSON; surface warmup per session tile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 11:39:25 +02:00

139 lines
4.5 KiB
Dart

/// Monthly calendar showing which days had workouts.
library;
import 'package:flutter/material.dart';
class WorkoutCalendar extends StatelessWidget {
const WorkoutCalendar({
super.key,
required this.workoutDates,
required this.month,
required this.onPrevMonth,
required this.onNextMonth,
});
final Set<String> workoutDates;
/// Only the year and month of this DateTime are used.
final DateTime month;
final VoidCallback onPrevMonth;
final VoidCallback onNextMonth;
static const _weekHeaders = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
static const _monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
String _dateKey(int year, int m, int day) =>
'$year-${m.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
@override
Widget build(BuildContext context) {
final year = month.year;
final m = month.month;
final daysInMonth = DateTime(year, m + 1, 0).day;
// weekday: 1=Mon..7=Sun → offset 0..6
final firstWeekday = DateTime(year, m, 1).weekday - 1;
final totalCells = firstWeekday + daysInMonth;
final rows = (totalCells / 7).ceil();
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
// Month navigation header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.chevron_left, color: Colors.white70),
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
onPressed: onPrevMonth,
),
Text(
'${_monthNames[m - 1]} $year',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
IconButton(
icon: const Icon(Icons.chevron_right, color: Colors.white70),
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
onPressed: onNextMonth,
),
],
),
const SizedBox(height: 6),
// Day-of-week headers
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _weekHeaders
.map(
(h) => SizedBox(
width: 30,
child: Text(
h,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white38,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
)
.toList(),
),
const SizedBox(height: 4),
// Day grid
...List.generate(rows, (row) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(7, (col) {
final cell = row * 7 + col;
final day = cell - firstWeekday + 1;
if (day < 1 || day > daysInMonth) {
return const SizedBox(width: 30, height: 30);
}
final key = _dateKey(year, m, day);
final worked = workoutDates.contains(key);
return Container(
width: 30,
height: 30,
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: worked
? BoxDecoration(
shape: BoxShape.circle,
color: Colors.green.shade700,
)
: null,
alignment: Alignment.center,
child: Text(
'$day',
style: TextStyle(
color: worked ? Colors.white : Colors.white38,
fontSize: 12,
fontWeight:
worked ? FontWeight.bold : FontWeight.normal,
),
),
);
}),
);
}),
],
),
);
}
}