Files
valuecards/lib/screens/reflection_screen.dart
2026-09-18 00:01:42 +02:00

299 lines
10 KiB
Dart

/// Deep reflection on top values, with optional full-screen journal sheet.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
class ReflectionScreen extends StatelessWidget {
const ReflectionScreen({super.key});
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final cores = sort.coreValues;
final activeId = sort.activeReflectionValueId ??
(cores.isEmpty ? null : cores.first.id);
final active = activeId == null ? null : sort.cardById(activeId);
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return PracticeScaffold(
header: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 0),
child: Row(
children: [
IconButton(
onPressed: sort.goToHub,
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: Text(
'Step 5 · Deep Reflection',
style: text.labelMedium,
),
),
],
),
),
child: cores.isEmpty || active == null
? Center(
child: Text(
'Rank at least one core value to reflect.',
style: text.bodyLarge,
),
)
: ListView(
children: [
Text('Your Core Values', style: text.displaySmall),
const SizedBox(height: 4),
Text(
'${cores.length} core anchors · '
'${sort.totalAnsweredReflections} of ${sort.totalReflectionSlots} answered',
style: text.bodyMedium,
),
const SizedBox(height: 14),
SizedBox(
height: 88,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: cores.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final card = cores[index];
final selected = card.id == active.id;
final answered = sort.answeredPromptCount(card.id);
final rankColor =
selected ? colors.onPrimary : colors.secondary;
final titleColor =
selected ? colors.onPrimary : colors.onSurface;
final metaColor = selected
? colors.onPrimary.withValues(alpha: 0.85)
: colors.onSurfaceVariant;
return InkWell(
onTap: () => sort.selectReflectionValue(card.id),
borderRadius: BorderRadius.circular(16),
child: Container(
width: 120,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: selected
? colors.primary
: colors.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selected
? colors.primary
: colors.outlineVariant,
width: selected ? 2 : 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'#${index + 1}',
style: text.labelSmall?.copyWith(
color: rankColor,
fontWeight: FontWeight.w700,
),
),
Text(
card.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: text.titleMedium?.copyWith(
color: titleColor,
fontWeight: selected
? FontWeight.w700
: FontWeight.w500,
),
),
Text(
'$answered/${kReflectionPrompts.length}',
style: text.labelSmall?.copyWith(
color: metaColor,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 18),
Text(active.title, style: text.headlineMedium),
const SizedBox(height: 4),
Text(
'"${active.description}"',
style: text.bodyMedium?.copyWith(fontStyle: FontStyle.italic),
),
const SizedBox(height: 16),
...List.generate(kReflectionPrompts.length, (index) {
final answer = sort.reflectionFor(active.id, index);
final answered = answer.isNotEmpty;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Material(
color: colors.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(color: colors.outlineVariant),
),
child: ListTile(
leading: Icon(
answered ? Icons.check_circle : Icons.circle_outlined,
color: answered
? colors.tertiary
: colors.onSurfaceVariant,
),
title: Text(
kReflectionPrompts[index],
style: text.titleMedium,
),
subtitle: answered
? Text(
answer,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: null,
trailing: Icon(
answered ? Icons.edit_outlined : Icons.open_in_full,
),
onTap: () => _openJournal(
context,
sort,
active.id,
index,
),
),
),
);
}),
],
),
);
}
Future<void> _openJournal(
BuildContext context,
ValuesSortController sort,
String valueId,
int promptIndex,
) async {
sort.selectPrompt(promptIndex);
final card = sort.cardById(valueId);
if (card == null) return;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (ctx) => _ReflectionJournalSheet(
cardTitle: card.title,
prompt: kReflectionPrompts[promptIndex],
initialText: sort.reflectionFor(valueId, promptIndex),
onSave: (text) {
sort.setReflection(valueId, promptIndex, text);
},
),
);
}
}
class _ReflectionJournalSheet extends StatefulWidget {
const _ReflectionJournalSheet({
required this.cardTitle,
required this.prompt,
required this.initialText,
required this.onSave,
});
final String cardTitle;
final String prompt;
final String initialText;
final ValueChanged<String> onSave;
@override
State<_ReflectionJournalSheet> createState() =>
_ReflectionJournalSheetState();
}
class _ReflectionJournalSheetState extends State<_ReflectionJournalSheet> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialText);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _saveAndClose() {
widget.onSave(_controller.text);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
final bottom = MediaQuery.viewInsetsOf(context).bottom;
return Padding(
padding: EdgeInsets.fromLTRB(20, 8, 20, bottom + 20),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
'Reflection · ${widget.cardTitle}',
style: text.titleLarge,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),
Align(
alignment: Alignment.centerLeft,
child: Text(
widget.prompt,
style: text.bodyLarge?.copyWith(color: colors.onSurfaceVariant),
),
),
const SizedBox(height: 12),
Expanded(
child: TextField(
controller: _controller,
expands: true,
maxLines: null,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
hintText: 'Write freely. This stays on your device.',
),
),
),
const SizedBox(height: 12),
PrimaryPillButton(
label: 'Save reflection',
onPressed: _saveAndClose,
),
],
),
);
}
}