2026-10-29 00:34:49 +01:00
|
|
|
import 'package:clock/clock.dart';
|
2026-10-28 23:18:03 +01:00
|
|
|
import 'package:flutter/material.dart';
|
2026-10-29 00:34:49 +01:00
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
|
import 'package:provider/provider.dart';
|
2026-10-28 23:18:03 +01:00
|
|
|
|
|
|
|
|
import '../../design/page_header.dart';
|
2026-10-29 00:34:49 +01:00
|
|
|
import '../../design/zen_nav_bar.dart';
|
|
|
|
|
import '../../design/zen_style.dart';
|
2026-10-28 23:18:03 +01:00
|
|
|
import '../../localization/app_localizations.dart';
|
2026-10-29 00:34:49 +01:00
|
|
|
import '../../player/views/summary_view.dart';
|
|
|
|
|
import '../../settings/settings_controller.dart';
|
|
|
|
|
import '../../streak/day.dart';
|
|
|
|
|
import '../session_repository.dart';
|
2026-10-28 23:18:03 +01:00
|
|
|
|
2026-10-29 00:34:49 +01:00
|
|
|
/// The current Mode's finished Sessions by day, the latest first; each opens
|
|
|
|
|
/// its summary again.
|
2026-10-28 23:18:03 +01:00
|
|
|
class HistoryView extends StatelessWidget {
|
|
|
|
|
const HistoryView({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final l10n = AppLocalizations.of(context);
|
2026-10-29 00:34:49 +01:00
|
|
|
final mode = context.watch<SettingsController>().mode;
|
|
|
|
|
return StreamBuilder(
|
|
|
|
|
stream: context.read<SessionRepository>().watchSessions(mode),
|
|
|
|
|
builder: (context, snapshot) {
|
|
|
|
|
final sessions = snapshot.data ?? const [];
|
|
|
|
|
if (sessions.isEmpty) {
|
|
|
|
|
return ListView(
|
|
|
|
|
children: [
|
|
|
|
|
PageHeader(title: l10n.historyTitle),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
EmptyState(icon: Icons.history_rounded, title: l10n.historyEmptyTitle, body: l10n.historyEmptyBody),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
final today = Day.of(clock.now());
|
|
|
|
|
final rows = <Widget>[PageHeader(title: l10n.historyTitle)];
|
|
|
|
|
Day? shown;
|
|
|
|
|
for (final session in sessions) {
|
|
|
|
|
final day = Day.of(session.endedAt);
|
|
|
|
|
if (day != shown) {
|
|
|
|
|
shown = day;
|
|
|
|
|
rows.add(_DayHeader(label: _dayLabel(l10n, day, today)));
|
|
|
|
|
}
|
|
|
|
|
rows.add(
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
|
|
|
|
|
child: _SessionTile(session: session),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return ListView(
|
|
|
|
|
padding: EdgeInsets.only(bottom: ZenNavBar.height + 32 + MediaQuery.paddingOf(context).bottom),
|
|
|
|
|
children: rows,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static String _dayLabel(AppLocalizations l10n, Day day, Day today) {
|
|
|
|
|
if (day == today) return l10n.historyToday;
|
|
|
|
|
if (day == today.add(-1)) return l10n.historyYesterday;
|
|
|
|
|
final date = day.at(0, 0);
|
|
|
|
|
return day.year == today.year ? DateFormat.MMMEd().format(date) : DateFormat.yMMMEd().format(date);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _DayHeader extends StatelessWidget {
|
|
|
|
|
const _DayHeader({required this.label});
|
|
|
|
|
|
|
|
|
|
final String label;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) => Padding(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
|
|
|
|
|
child: Text(label, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: ZenStyle.of(context).inkMuted)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _SessionTile extends StatelessWidget {
|
|
|
|
|
const _SessionTile({required this.session});
|
|
|
|
|
|
|
|
|
|
final Session session;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final l10n = AppLocalizations.of(context);
|
|
|
|
|
final s = ZenStyle.of(context);
|
|
|
|
|
final text = Theme.of(context).textTheme;
|
|
|
|
|
final line = l10n.historySessionLine(
|
|
|
|
|
DateFormat.Hm().format(session.endedAt),
|
|
|
|
|
l10n.durationMinutes((session.duration.inSeconds / 60).round()),
|
|
|
|
|
);
|
|
|
|
|
return Card(
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: () => Navigator.of(context).pushNamed(SummaryView.routeName, arguments: session.id),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
session.complete ? Icons.check_circle_rounded : Icons.flag_rounded,
|
|
|
|
|
color: session.complete ? s.accent : s.inkMuted,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(session.planName, style: text.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
Text(line, style: text.bodySmall?.copyWith(color: s.inkMuted)),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (!session.complete)
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: s.surfaceSoft,
|
|
|
|
|
borderRadius: BorderRadius.circular(s.controlRadius.clamp(0, 12)),
|
|
|
|
|
),
|
|
|
|
|
child: Text(l10n.historyEndedEarly, style: text.labelSmall?.copyWith(color: s.inkMuted)),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-10-28 23:18:03 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|