feat: add journey stats and history detail

This commit is contained in:
2026-06-12 01:26:19 +08:00
parent b287a56731
commit eb8b209c0c
10 changed files with 701 additions and 1 deletions
@@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import '../../application/focus_seed_controller.dart';
import '../../l10n/l10n.dart';
import '../../models/focus_session.dart';
import '../../models/reflection_entry.dart';
import '../../widgets/surface_panel.dart';
enum _HistoryView { focus, notes }
class HistoryScreen extends StatefulWidget {
const HistoryScreen({required this.controller, super.key});
final FocusSeedController controller;
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
_HistoryView _view = _HistoryView.focus;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(title: Text(l10n.historyTitle)),
body: SafeArea(
top: false,
child: AnimatedBuilder(
animation: widget.controller,
builder: (context, _) {
final state = widget.controller.state!;
final focusSessions = state.sessions.reversed.toList();
final reflections = state.reflections.reversed.toList();
return ListView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 28),
children: [
SizedBox(
width: double.infinity,
child: SegmentedButton<_HistoryView>(
showSelectedIcon: false,
segments: [
ButtonSegment(
value: _HistoryView.focus,
icon: const Icon(Icons.timer_outlined),
label: Text(l10n.focusHistory),
),
ButtonSegment(
value: _HistoryView.notes,
icon: const Icon(Icons.edit_note_rounded),
label: Text(l10n.notesHistory),
),
],
selected: {_view},
onSelectionChanged:
(selection) => setState(() => _view = selection.first),
),
),
const SizedBox(height: 14),
AnimatedSwitcher(
duration: const Duration(milliseconds: 220),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
child:
_view == _HistoryView.focus
? _FocusHistoryList(
key: const ValueKey('focus-history'),
sessions: focusSessions,
)
: _ReflectionHistoryList(
key: const ValueKey('reflection-history'),
reflections: reflections,
),
),
],
);
},
),
),
);
}
}
class _FocusHistoryList extends StatelessWidget {
const _FocusHistoryList({required this.sessions, super.key});
final List<FocusSession> sessions;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
if (sessions.isEmpty) {
return _EmptyHistory(
title: l10n.emptyFocusHistoryTitle,
body: l10n.emptyFocusHistoryBody,
);
}
return SurfacePanel(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
child: Column(
children: [
for (var index = 0; index < sessions.length; index++) ...[
if (index > 0) const Divider(),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.timer_outlined),
title: Text(l10n.focusMinutes(sessions[index].minutes)),
subtitle: Text(
localizedReadableDate(context, sessions[index].completedAt),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
localizedTime(context, sessions[index].completedAt),
),
),
],
],
),
);
}
}
class _ReflectionHistoryList extends StatelessWidget {
const _ReflectionHistoryList({required this.reflections, super.key});
final List<ReflectionEntry> reflections;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
if (reflections.isEmpty) {
return _EmptyHistory(
title: l10n.emptyNotesHistoryTitle,
body: l10n.emptyNotesHistoryBody,
);
}
return SurfacePanel(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
child: Column(
children: [
for (var index = 0; index < reflections.length; index++) ...[
if (index > 0) const Divider(),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(reflections[index].mood.icon),
title: Text(
reflections[index].note,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(reflections[index].dateKey),
),
],
],
),
);
}
}
class _EmptyHistory extends StatelessWidget {
const _EmptyHistory({required this.title, required this.body});
final String title;
final String body;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SurfacePanel(
padding: const EdgeInsets.all(22),
child: Column(
children: [
Icon(
Icons.auto_graph_rounded,
size: 38,
color: theme.colorScheme.primary,
),
const SizedBox(height: 10),
Text(
title,
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
Text(
body,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}