mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 13:23:13 +02:00
- 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>
139 lines
4.5 KiB
Dart
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,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|