Stage 5: History, the shared daily Streak and Reminders

- Streak computed from all complete Sessions: Grace Hour (00:00-01:00
  counts for yesterday unless it is done), a Freeze per 7 Streak Days
  (max 2) spent automatically on a missed day (ADR 0006)
- Top bar flame (grey until today is done) opening a panel with current
  and longest Streak, Freezes and a month calendar; 'Day N of your
  streak' on a fresh summary
- History per Mode grouped by day, reopening each summary
- Two adjustable Reminders (19:00, 22:30) scheduled 14 days ahead with
  flutter_local_notifications, none on a done day; permission asked
  after the first complete Session or from Settings
- Settings: Reminders, the Player chime, exercise credits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-29 00:34:49 +01:00
1 parent 971ff64117
commit 1ffe6269ea
31 files changed
+1972 -50

No files matched your search

+55 -1
View File
@@ -8,17 +8,23 @@ import '../../localization/app_localizations.dart';
import '../../modes/mode.dart';
import '../../plans/models/plan.dart';
import '../../plans/plan_labels.dart';
import '../../streak/streak.dart';
import '../../streak/views/streak_views.dart';
import '../session_runner.dart';
/// Shown when a Session ends: how long it took, what was done and, for
/// Workouts, every logged set.
class SummaryView extends StatelessWidget {
const SummaryView({super.key, required this.sessionId});
const SummaryView({super.key, required this.sessionId, this.justFinished = false});
static const routeName = '/summary';
final String sessionId;
/// Straight from the Player rather than opened again from History: only
/// then is the Session's Streak Day worth pointing out.
final bool justFinished;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
@@ -50,6 +56,7 @@ class SummaryView extends StatelessWidget {
Text(session.complete ? l10n.summaryComplete : l10n.summaryIncomplete, style: text.headlineLarge),
const SizedBox(height: 4),
Text(session.planName, style: text.titleMedium?.copyWith(color: s.inkMuted)),
if (justFinished && session.complete) _StreakLine(session: session),
const SizedBox(height: 28),
Row(
children: [
@@ -100,6 +107,53 @@ class SummaryView extends StatelessWidget {
}
}
/// "Day 12 of your streak", when this Session was the first to complete its day.
class _StreakLine extends StatelessWidget {
const _StreakLine({required this.session});
final Session session;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final s = ZenStyle.of(context);
final sessions = context.read<SessionRepository>();
return StreakBuilder(
builder: (context, streak) => FutureBuilder(
future: sessions.completeEndTimes(),
builder: (context, snapshot) {
final times = snapshot.data;
if (times == null) return const SizedBox.shrink();
final before = streakDays(times.where((t) => t != session.endedAt));
final fresh = streakDays(times).length > before.length;
if (!fresh || streak.current == 0) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
children: [
Icon(Icons.local_fire_department_rounded, color: s.accent),
const SizedBox(width: 6),
Text(
l10n.summaryStreak(streak.current),
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: s.accent),
),
],
),
);
},
),
);
}
}
/// What the summary route takes.
class SummaryArguments {
const SummaryArguments(this.sessionId, {this.justFinished = false});
final String sessionId;
final bool justFinished;
}
class _Stat extends StatelessWidget {
const _Stat({required this.label, required this.value});