feat: add journey stats and history detail
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user