Files

681 lines
22 KiB
Dart
Raw Permalink Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/activity_provider.dart';
import '../../core/providers/mood_provider.dart';
import '../../shared/widgets/activity_grid.dart';
import '../../shared/widgets/mood_selector.dart';
import 'widgets/activities_chip.dart';
import 'widgets/answer_chip.dart';
import 'widgets/step_scale.dart';
// ── Emoji / label data ────────────────────────────────────────────────────────
const _moodEmojis = ['😞', '😔', '😐', '🙂', '😄'];
const _moodLabels = ['Awful', 'Bad', 'Okay', 'Good', 'Great'];
const _energyEmojis = ['💤', '🥱', '⚡', '🔥', '🚀'];
const _energyLabels = ['Very low', 'Low', 'OK', 'High', 'Very high'];
const _positivityEmojis = ['🌧', '☁️', '🌤', '☀️', '🌈'];
const _positivityLabels = ['Very neg', 'Negative', 'Neutral', 'Positive', 'Very pos'];
const _selfWorthEmojis = ['💔', '😕', '😐', '💪', '⭐'];
const _selfWorthLabels = ['Very low', 'Low', 'Moderate', 'High', 'Very high'];
const _scaleColors = [
Color(0xFFE57373),
Color(0xFFFFB74D),
Color(0xFFFFD54F),
Color(0xFF81C784),
Color(0xFF4DB6AC),
];
// ── Step enum ─────────────────────────────────────────────────────────────────
enum _Step { mood, sleep, energy, positivity, selfWorth, activities }
// ── Screen ────────────────────────────────────────────────────────────────────
class LogMoodScreen extends ConsumerStatefulWidget {
const LogMoodScreen({super.key});
@override
ConsumerState<LogMoodScreen> createState() => _LogMoodScreenState();
}
class _LogMoodScreenState extends ConsumerState<LogMoodScreen> {
// answers
int? _mood;
int _sleepHours = 7;
int _sleepMinutesVal = 30; // 0/5/10…55
int? _energy;
int? _positivity;
int? _selfWorth;
// navigation state
int _stepIndex = 0;
bool _inReview = false;
bool _editingFromReview = false;
// sleep scroll controllers
late final FixedExtentScrollController _hoursCtrl;
late final FixedExtentScrollController _minsCtrl;
// computed step list (set once isFirstToday is known)
List<_Step>? _steps;
@override
void initState() {
super.initState();
_hoursCtrl = FixedExtentScrollController(initialItem: _sleepHours);
_minsCtrl =
FixedExtentScrollController(initialItem: _sleepMinutesVal ~/ 5);
}
@override
void dispose() {
_hoursCtrl.dispose();
_minsCtrl.dispose();
super.dispose();
}
// ── Helpers ─────────────────────────────────────────────────────────────────
List<_Step> _buildStepList(bool isFirstToday) {
return [
_Step.mood,
if (isFirstToday) _Step.sleep,
_Step.energy,
_Step.positivity,
_Step.selfWorth,
_Step.activities,
];
}
_Step get _currentStep => _steps![_stepIndex];
String _sleepLabel() {
final mm = _sleepMinutesVal.toString().padLeft(2, '0');
return '$_sleepHours:$mm';
}
int _sleepTotalMinutes() => _sleepHours * 60 + _sleepMinutesVal;
Color _colorFor(int level) => _scaleColors[level - 1];
// ── Navigation ───────────────────────────────────────────────────────────────
void _advance() {
if (_editingFromReview) {
setState(() {
_editingFromReview = false;
_inReview = true;
_stepIndex = _steps!.length; // back to review index
});
return;
}
final next = _stepIndex + 1;
if (next >= _steps!.length) {
setState(() {
_inReview = true;
_stepIndex = _steps!.length;
});
} else {
setState(() => _stepIndex = next);
}
}
void _editStepFromReview(int idx) {
setState(() {
_editingFromReview = true;
_stepIndex = idx;
_inReview = false;
});
}
// ── Save ─────────────────────────────────────────────────────────────────────
Future<void> _save() async {
final selectedActivities = ref.read(selectedActivitiesProvider);
await ref.read(moodEntryNotifierProvider.notifier).addEntry(
moodLevel: _mood ?? 3,
activityIds: selectedActivities.toList(),
sleepMinutes:
_steps!.contains(_Step.sleep) ? _sleepTotalMinutes() : null,
energyLevel: _energy,
positivityLevel: _positivity,
selfWorthLevel: _selfWorth,
);
if (!mounted) return;
ref.read(selectedActivitiesProvider.notifier).clear();
ref.invalidate(isFirstEntryTodayProvider);
setState(() {
_mood = null;
_sleepHours = 7;
_sleepMinutesVal = 30;
_energy = null;
_positivity = null;
_selfWorth = null;
_stepIndex = 0;
_inReview = false;
_editingFromReview = false;
_steps = null;
});
// reset scroll controllers
_hoursCtrl.jumpToItem(7);
_minsCtrl.jumpToItem(6); // 30 / 5 = 6
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Mood logged!')),
);
}
// ── Answer chip row ──────────────────────────────────────────────────────────
Widget _buildChipRow() {
final steps = _steps;
if (steps == null) return const SizedBox.shrink();
final chips = <Widget>[];
for (int i = 0; i < steps.length; i++) {
final step = steps[i];
final answered = _stepIndex > i || _inReview;
final isLiveSleep =
step == _Step.sleep && _stepIndex == i && !_inReview;
if (!answered && !isLiveSleep) continue;
Widget chip;
final tappable = _inReview ? () => _editStepFromReview(i) : null;
switch (step) {
case _Step.mood:
if (_mood == null) continue;
chip = AnswerChip(
label: _moodEmojis[_mood! - 1],
backgroundColor: _colorFor(_mood!),
onTap: tappable,
);
case _Step.sleep:
chip = AnswerChip(
label: _sleepLabel(),
backgroundColor: const Color(0xFF42A5F5),
onTap: tappable,
);
case _Step.energy:
if (_energy == null) continue;
chip = AnswerChip(
label: _energyEmojis[_energy! - 1],
backgroundColor: _colorFor(_energy!),
onTap: tappable,
);
case _Step.positivity:
if (_positivity == null) continue;
chip = AnswerChip(
label: _positivityEmojis[_positivity! - 1],
backgroundColor: _colorFor(_positivity!),
onTap: tappable,
);
case _Step.selfWorth:
if (_selfWorth == null) continue;
chip = AnswerChip(
label: _selfWorthEmojis[_selfWorth! - 1],
backgroundColor: _colorFor(_selfWorth!),
onTap: tappable,
);
case _Step.activities:
chip = ActivitiesChip(
backgroundColor: const Color(0xFF78909C),
onTap: tappable,
);
}
chips.add(
Padding(
padding: const EdgeInsets.only(right: 8),
child: chip,
),
);
}
if (chips.isEmpty) return const SizedBox(height: 60);
return SizedBox(
height: 60,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 24),
children: chips,
),
);
}
// ── Step builders ─────────────────────────────────────────────────────────────
Widget _buildMoodStep() {
final theme = Theme.of(context);
return Column(
children: [
Text(
'How are you feeling?',
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
MoodSelector(
selected: _mood ?? 3,
emojis: _moodEmojis,
labels: _moodLabels,
colors: _scaleColors,
onChanged: (v) {
setState(() => _mood = v);
Future.delayed(const Duration(milliseconds: 250), _advance);
},
),
const SizedBox(height: 16),
if (_mood != null)
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
_moodLabels[_mood! - 1],
key: ValueKey(_mood),
style: theme.textTheme.titleLarge?.copyWith(
color: _colorFor(_mood!),
fontWeight: FontWeight.w500,
),
),
),
],
);
}
Widget _buildSleepStep() {
final theme = Theme.of(context);
const minuteOptions = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
return Column(
children: [
Text(
'How long did you sleep?',
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// live circle
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: Color(0xFF42A5F5),
shape: BoxShape.circle,
),
child: Center(
child: Text(
_sleepLabel(),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Hours wheel
Column(
children: [
Text('Hours', style: theme.textTheme.labelLarge),
const SizedBox(height: 8),
SizedBox(
width: 80,
height: 160,
child: ListWheelScrollView.useDelegate(
controller: _hoursCtrl,
itemExtent: 48,
perspective: 0.003,
diameterRatio: 1.4,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (i) =>
setState(() => _sleepHours = i),
childDelegate: ListWheelChildBuilderDelegate(
childCount: 13, // 012
builder: (ctx, i) => Center(
child: Text(
'$i',
style: theme.textTheme.titleLarge,
),
),
),
),
),
],
),
const SizedBox(width: 16),
Text(':', style: theme.textTheme.headlineMedium),
const SizedBox(width: 16),
// Minutes wheel
Column(
children: [
Text('Minutes', style: theme.textTheme.labelLarge),
const SizedBox(height: 8),
SizedBox(
width: 80,
height: 160,
child: ListWheelScrollView.useDelegate(
controller: _minsCtrl,
itemExtent: 48,
perspective: 0.003,
diameterRatio: 1.4,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (i) =>
setState(() => _sleepMinutesVal = minuteOptions[i]),
childDelegate: ListWheelChildBuilderDelegate(
childCount: minuteOptions.length,
builder: (ctx, i) => Center(
child: Text(
minuteOptions[i].toString().padLeft(2, '0'),
style: theme.textTheme.titleLarge,
),
),
),
),
),
],
),
],
),
const SizedBox(height: 32),
FilledButton(
onPressed: _advance,
child: const Text('Next →'),
),
],
);
}
Widget _buildScaleStep({
required String title,
required List<String> emojis,
required List<String> labels,
required int? current,
required ValueChanged<int> onSelected,
}) {
final theme = Theme.of(context);
return Column(
children: [
Text(
title,
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
StepScale(
emojis: emojis,
labels: labels,
colors: _scaleColors,
selected: current,
onSelected: (v) {
onSelected(v);
Future.delayed(const Duration(milliseconds: 250), _advance);
},
),
const SizedBox(height: 16),
if (current != null)
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
labels[current - 1],
key: ValueKey(current),
style: theme.textTheme.titleLarge?.copyWith(
color: _colorFor(current),
fontWeight: FontWeight.w500,
),
),
),
],
);
}
Widget _buildActivitiesStep() {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
'What have you been doing?',
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 32),
const ActivityGrid(),
const SizedBox(height: 32),
Center(
child: FilledButton(
onPressed: _advance,
child: const Text('Done →'),
),
),
],
);
}
Widget _buildReviewStep() {
final theme = Theme.of(context);
final steps = _steps!;
return Column(
children: [
Text(
'Ready to save?',
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
for (int i = 0; i < steps.length; i++)
_reviewChipFor(steps[i], i),
],
),
const SizedBox(height: 48),
// Save FAB
GestureDetector(
onTap: _save,
child: Container(
width: 72,
height: 72,
decoration: const BoxDecoration(
color: Color(0xFF4CAF50),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Color(0x554CAF50),
blurRadius: 12,
offset: Offset(0, 4),
)
],
),
child: const Icon(Icons.check, color: Colors.white, size: 36),
),
),
],
);
}
Widget _reviewChipFor(_Step step, int idx) {
switch (step) {
case _Step.mood:
return _reviewChipTile(
label: _mood != null ? _moodLabels[_mood! - 1] : '—',
emoji: _mood != null ? _moodEmojis[_mood! - 1] : '?',
color: _mood != null ? _colorFor(_mood!) : Colors.grey,
onTap: () => _editStepFromReview(idx),
);
case _Step.sleep:
return _reviewChipTile(
label: 'Sleep',
emoji: _sleepLabel(),
color: const Color(0xFF42A5F5),
onTap: () => _editStepFromReview(idx),
);
case _Step.energy:
return _reviewChipTile(
label: _energy != null ? _energyLabels[_energy! - 1] : '—',
emoji: _energy != null ? _energyEmojis[_energy! - 1] : '?',
color: _energy != null ? _colorFor(_energy!) : Colors.grey,
onTap: () => _editStepFromReview(idx),
);
case _Step.positivity:
return _reviewChipTile(
label: _positivity != null ? _positivityLabels[_positivity! - 1] : '—',
emoji: _positivity != null ? _positivityEmojis[_positivity! - 1] : '?',
color: _positivity != null ? _colorFor(_positivity!) : Colors.grey,
onTap: () => _editStepFromReview(idx),
);
case _Step.selfWorth:
return _reviewChipTile(
label: _selfWorth != null ? _selfWorthLabels[_selfWorth! - 1] : '—',
emoji: _selfWorth != null ? _selfWorthEmojis[_selfWorth! - 1] : '?',
color: _selfWorth != null ? _colorFor(_selfWorth!) : Colors.grey,
onTap: () => _editStepFromReview(idx),
);
case _Step.activities:
return GestureDetector(
onTap: () => _editStepFromReview(idx),
child: ActivitiesChip(
backgroundColor: const Color(0xFF78909C),
onTap: () => _editStepFromReview(idx),
),
);
}
}
Widget _reviewChipTile({
required String label,
required String emoji,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: color.withValues(alpha: 0.5), width: 1.5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(emoji, style: const TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(width: 4),
Icon(Icons.edit, size: 14, color: color.withValues(alpha: 0.7)),
],
),
),
);
}
// ── Build ─────────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
final firstEntryAsync = ref.watch(isFirstEntryTodayProvider);
return firstEntryAsync.when(
loading: () => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
error: (e, _) => Scaffold(
body: Center(child: Text('Error: $e')),
),
data: (isFirstToday) {
// Build (or reuse) the step list once resolved
_steps ??= _buildStepList(isFirstToday);
return Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 16),
// ── Answer chip row ──────────────────────────────────
_buildChipRow(),
const SizedBox(height: 24),
// ── Step content ─────────────────────────────────────
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: _buildCurrentStep(),
),
),
],
),
),
);
},
);
}
Widget _buildCurrentStep() {
if (_inReview) return _buildReviewStep();
final step = _currentStep;
switch (step) {
case _Step.mood:
return _buildMoodStep();
case _Step.sleep:
return _buildSleepStep();
case _Step.energy:
return _buildScaleStep(
title: 'How is your energy level?',
emojis: _energyEmojis,
labels: _energyLabels,
current: _energy,
onSelected: (v) => setState(() => _energy = v),
);
case _Step.positivity:
return _buildScaleStep(
title: 'How positive do you feel?',
emojis: _positivityEmojis,
labels: _positivityLabels,
current: _positivity,
onSelected: (v) => setState(() => _positivity = v),
);
case _Step.selfWorth:
return _buildScaleStep(
title: 'How is your self-worth?',
emojis: _selfWorthEmojis,
labels: _selfWorthLabels,
current: _selfWorth,
onSelected: (v) => setState(() => _selfWorth = v),
);
case _Step.activities:
return _buildActivitiesStep();
}
}
}