diff --git a/lib/core/database/app_database.dart b/lib/core/database/app_database.dart index 9802400..646ee18 100644 --- a/lib/core/database/app_database.dart +++ b/lib/core/database/app_database.dart @@ -32,7 +32,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 1; + int get schemaVersion => 2; @override MigrationStrategy get migration => MigrationStrategy( @@ -41,7 +41,16 @@ class AppDatabase extends _$AppDatabase { await _seedDefaultActivities(); }, onUpgrade: (m, from, to) async { - // Future migrations go here + if (from < 2) { + await m.addColumn(moodEntries, + moodEntries.sleepMinutes as GeneratedColumn); + await m.addColumn(moodEntries, + moodEntries.energyLevel as GeneratedColumn); + await m.addColumn(moodEntries, + moodEntries.positivityLevel as GeneratedColumn); + await m.addColumn(moodEntries, + moodEntries.selfWorthLevel as GeneratedColumn); + } }, ); diff --git a/lib/core/database/app_database.g.dart b/lib/core/database/app_database.g.dart index b5607ed..75323ec 100644 --- a/lib/core/database/app_database.g.dart +++ b/lib/core/database/app_database.g.dart @@ -43,9 +43,42 @@ class $MoodEntriesTable extends MoodEntries type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); + static const VerificationMeta _sleepMinutesMeta = + const VerificationMeta('sleepMinutes'); @override - List get $columns => - [id, timestamp, moodLevel, note, createdAt]; + late final GeneratedColumn sleepMinutes = GeneratedColumn( + 'sleep_minutes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _energyLevelMeta = + const VerificationMeta('energyLevel'); + @override + late final GeneratedColumn energyLevel = GeneratedColumn( + 'energy_level', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _positivityLevelMeta = + const VerificationMeta('positivityLevel'); + @override + late final GeneratedColumn positivityLevel = GeneratedColumn( + 'positivity_level', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _selfWorthLevelMeta = + const VerificationMeta('selfWorthLevel'); + @override + late final GeneratedColumn selfWorthLevel = GeneratedColumn( + 'self_worth_level', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + @override + List get $columns => [ + id, + timestamp, + moodLevel, + note, + createdAt, + sleepMinutes, + energyLevel, + positivityLevel, + selfWorthLevel + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -79,6 +112,30 @@ class $MoodEntriesTable extends MoodEntries context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } + if (data.containsKey('sleep_minutes')) { + context.handle( + _sleepMinutesMeta, + sleepMinutes.isAcceptableOrUnknown( + data['sleep_minutes']!, _sleepMinutesMeta)); + } + if (data.containsKey('energy_level')) { + context.handle( + _energyLevelMeta, + energyLevel.isAcceptableOrUnknown( + data['energy_level']!, _energyLevelMeta)); + } + if (data.containsKey('positivity_level')) { + context.handle( + _positivityLevelMeta, + positivityLevel.isAcceptableOrUnknown( + data['positivity_level']!, _positivityLevelMeta)); + } + if (data.containsKey('self_worth_level')) { + context.handle( + _selfWorthLevelMeta, + selfWorthLevel.isAcceptableOrUnknown( + data['self_worth_level']!, _selfWorthLevelMeta)); + } return context; } @@ -98,6 +155,14 @@ class $MoodEntriesTable extends MoodEntries .read(DriftSqlType.string, data['${effectivePrefix}note']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + sleepMinutes: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}sleep_minutes']), + energyLevel: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}energy_level']), + positivityLevel: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}positivity_level']), + selfWorthLevel: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}self_worth_level']), ); } @@ -113,12 +178,20 @@ class MoodEntry extends DataClass implements Insertable { final int moodLevel; final String? note; final DateTime createdAt; + final int? sleepMinutes; + final int? energyLevel; + final int? positivityLevel; + final int? selfWorthLevel; const MoodEntry( {required this.id, required this.timestamp, required this.moodLevel, this.note, - required this.createdAt}); + required this.createdAt, + this.sleepMinutes, + this.energyLevel, + this.positivityLevel, + this.selfWorthLevel}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -129,6 +202,18 @@ class MoodEntry extends DataClass implements Insertable { map['note'] = Variable(note); } map['created_at'] = Variable(createdAt); + if (!nullToAbsent || sleepMinutes != null) { + map['sleep_minutes'] = Variable(sleepMinutes); + } + if (!nullToAbsent || energyLevel != null) { + map['energy_level'] = Variable(energyLevel); + } + if (!nullToAbsent || positivityLevel != null) { + map['positivity_level'] = Variable(positivityLevel); + } + if (!nullToAbsent || selfWorthLevel != null) { + map['self_worth_level'] = Variable(selfWorthLevel); + } return map; } @@ -139,6 +224,18 @@ class MoodEntry extends DataClass implements Insertable { moodLevel: Value(moodLevel), note: note == null && nullToAbsent ? const Value.absent() : Value(note), createdAt: Value(createdAt), + sleepMinutes: sleepMinutes == null && nullToAbsent + ? const Value.absent() + : Value(sleepMinutes), + energyLevel: energyLevel == null && nullToAbsent + ? const Value.absent() + : Value(energyLevel), + positivityLevel: positivityLevel == null && nullToAbsent + ? const Value.absent() + : Value(positivityLevel), + selfWorthLevel: selfWorthLevel == null && nullToAbsent + ? const Value.absent() + : Value(selfWorthLevel), ); } @@ -151,6 +248,10 @@ class MoodEntry extends DataClass implements Insertable { moodLevel: serializer.fromJson(json['moodLevel']), note: serializer.fromJson(json['note']), createdAt: serializer.fromJson(json['createdAt']), + sleepMinutes: serializer.fromJson(json['sleepMinutes']), + energyLevel: serializer.fromJson(json['energyLevel']), + positivityLevel: serializer.fromJson(json['positivityLevel']), + selfWorthLevel: serializer.fromJson(json['selfWorthLevel']), ); } @override @@ -162,6 +263,10 @@ class MoodEntry extends DataClass implements Insertable { 'moodLevel': serializer.toJson(moodLevel), 'note': serializer.toJson(note), 'createdAt': serializer.toJson(createdAt), + 'sleepMinutes': serializer.toJson(sleepMinutes), + 'energyLevel': serializer.toJson(energyLevel), + 'positivityLevel': serializer.toJson(positivityLevel), + 'selfWorthLevel': serializer.toJson(selfWorthLevel), }; } @@ -170,13 +275,25 @@ class MoodEntry extends DataClass implements Insertable { DateTime? timestamp, int? moodLevel, Value note = const Value.absent(), - DateTime? createdAt}) => + DateTime? createdAt, + Value sleepMinutes = const Value.absent(), + Value energyLevel = const Value.absent(), + Value positivityLevel = const Value.absent(), + Value selfWorthLevel = const Value.absent()}) => MoodEntry( id: id ?? this.id, timestamp: timestamp ?? this.timestamp, moodLevel: moodLevel ?? this.moodLevel, note: note.present ? note.value : this.note, createdAt: createdAt ?? this.createdAt, + sleepMinutes: + sleepMinutes.present ? sleepMinutes.value : this.sleepMinutes, + energyLevel: energyLevel.present ? energyLevel.value : this.energyLevel, + positivityLevel: positivityLevel.present + ? positivityLevel.value + : this.positivityLevel, + selfWorthLevel: + selfWorthLevel.present ? selfWorthLevel.value : this.selfWorthLevel, ); MoodEntry copyWithCompanion(MoodEntriesCompanion data) { return MoodEntry( @@ -185,6 +302,17 @@ class MoodEntry extends DataClass implements Insertable { moodLevel: data.moodLevel.present ? data.moodLevel.value : this.moodLevel, note: data.note.present ? data.note.value : this.note, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + sleepMinutes: data.sleepMinutes.present + ? data.sleepMinutes.value + : this.sleepMinutes, + energyLevel: + data.energyLevel.present ? data.energyLevel.value : this.energyLevel, + positivityLevel: data.positivityLevel.present + ? data.positivityLevel.value + : this.positivityLevel, + selfWorthLevel: data.selfWorthLevel.present + ? data.selfWorthLevel.value + : this.selfWorthLevel, ); } @@ -195,13 +323,18 @@ class MoodEntry extends DataClass implements Insertable { ..write('timestamp: $timestamp, ') ..write('moodLevel: $moodLevel, ') ..write('note: $note, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('sleepMinutes: $sleepMinutes, ') + ..write('energyLevel: $energyLevel, ') + ..write('positivityLevel: $positivityLevel, ') + ..write('selfWorthLevel: $selfWorthLevel') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, timestamp, moodLevel, note, createdAt); + int get hashCode => Object.hash(id, timestamp, moodLevel, note, createdAt, + sleepMinutes, energyLevel, positivityLevel, selfWorthLevel); @override bool operator ==(Object other) => identical(this, other) || @@ -210,7 +343,11 @@ class MoodEntry extends DataClass implements Insertable { other.timestamp == this.timestamp && other.moodLevel == this.moodLevel && other.note == this.note && - other.createdAt == this.createdAt); + other.createdAt == this.createdAt && + other.sleepMinutes == this.sleepMinutes && + other.energyLevel == this.energyLevel && + other.positivityLevel == this.positivityLevel && + other.selfWorthLevel == this.selfWorthLevel); } class MoodEntriesCompanion extends UpdateCompanion { @@ -219,12 +356,20 @@ class MoodEntriesCompanion extends UpdateCompanion { final Value moodLevel; final Value note; final Value createdAt; + final Value sleepMinutes; + final Value energyLevel; + final Value positivityLevel; + final Value selfWorthLevel; const MoodEntriesCompanion({ this.id = const Value.absent(), this.timestamp = const Value.absent(), this.moodLevel = const Value.absent(), this.note = const Value.absent(), this.createdAt = const Value.absent(), + this.sleepMinutes = const Value.absent(), + this.energyLevel = const Value.absent(), + this.positivityLevel = const Value.absent(), + this.selfWorthLevel = const Value.absent(), }); MoodEntriesCompanion.insert({ this.id = const Value.absent(), @@ -232,6 +377,10 @@ class MoodEntriesCompanion extends UpdateCompanion { required int moodLevel, this.note = const Value.absent(), this.createdAt = const Value.absent(), + this.sleepMinutes = const Value.absent(), + this.energyLevel = const Value.absent(), + this.positivityLevel = const Value.absent(), + this.selfWorthLevel = const Value.absent(), }) : timestamp = Value(timestamp), moodLevel = Value(moodLevel); static Insertable custom({ @@ -240,6 +389,10 @@ class MoodEntriesCompanion extends UpdateCompanion { Expression? moodLevel, Expression? note, Expression? createdAt, + Expression? sleepMinutes, + Expression? energyLevel, + Expression? positivityLevel, + Expression? selfWorthLevel, }) { return RawValuesInsertable({ if (id != null) 'id': id, @@ -247,6 +400,10 @@ class MoodEntriesCompanion extends UpdateCompanion { if (moodLevel != null) 'mood_level': moodLevel, if (note != null) 'note': note, if (createdAt != null) 'created_at': createdAt, + if (sleepMinutes != null) 'sleep_minutes': sleepMinutes, + if (energyLevel != null) 'energy_level': energyLevel, + if (positivityLevel != null) 'positivity_level': positivityLevel, + if (selfWorthLevel != null) 'self_worth_level': selfWorthLevel, }); } @@ -255,13 +412,21 @@ class MoodEntriesCompanion extends UpdateCompanion { Value? timestamp, Value? moodLevel, Value? note, - Value? createdAt}) { + Value? createdAt, + Value? sleepMinutes, + Value? energyLevel, + Value? positivityLevel, + Value? selfWorthLevel}) { return MoodEntriesCompanion( id: id ?? this.id, timestamp: timestamp ?? this.timestamp, moodLevel: moodLevel ?? this.moodLevel, note: note ?? this.note, createdAt: createdAt ?? this.createdAt, + sleepMinutes: sleepMinutes ?? this.sleepMinutes, + energyLevel: energyLevel ?? this.energyLevel, + positivityLevel: positivityLevel ?? this.positivityLevel, + selfWorthLevel: selfWorthLevel ?? this.selfWorthLevel, ); } @@ -283,6 +448,18 @@ class MoodEntriesCompanion extends UpdateCompanion { if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (sleepMinutes.present) { + map['sleep_minutes'] = Variable(sleepMinutes.value); + } + if (energyLevel.present) { + map['energy_level'] = Variable(energyLevel.value); + } + if (positivityLevel.present) { + map['positivity_level'] = Variable(positivityLevel.value); + } + if (selfWorthLevel.present) { + map['self_worth_level'] = Variable(selfWorthLevel.value); + } return map; } @@ -293,7 +470,11 @@ class MoodEntriesCompanion extends UpdateCompanion { ..write('timestamp: $timestamp, ') ..write('moodLevel: $moodLevel, ') ..write('note: $note, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('sleepMinutes: $sleepMinutes, ') + ..write('energyLevel: $energyLevel, ') + ..write('positivityLevel: $positivityLevel, ') + ..write('selfWorthLevel: $selfWorthLevel') ..write(')')) .toString(); } @@ -1903,6 +2084,10 @@ typedef $$MoodEntriesTableCreateCompanionBuilder = MoodEntriesCompanion required int moodLevel, Value note, Value createdAt, + Value sleepMinutes, + Value energyLevel, + Value positivityLevel, + Value selfWorthLevel, }); typedef $$MoodEntriesTableUpdateCompanionBuilder = MoodEntriesCompanion Function({ @@ -1911,6 +2096,10 @@ typedef $$MoodEntriesTableUpdateCompanionBuilder = MoodEntriesCompanion Value moodLevel, Value note, Value createdAt, + Value sleepMinutes, + Value energyLevel, + Value positivityLevel, + Value selfWorthLevel, }); final class $$MoodEntriesTableReferences @@ -1974,6 +2163,20 @@ class $$MoodEntriesTableFilterComposer ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get sleepMinutes => $composableBuilder( + column: $table.sleepMinutes, builder: (column) => ColumnFilters(column)); + + ColumnFilters get energyLevel => $composableBuilder( + column: $table.energyLevel, builder: (column) => ColumnFilters(column)); + + ColumnFilters get positivityLevel => $composableBuilder( + column: $table.positivityLevel, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get selfWorthLevel => $composableBuilder( + column: $table.selfWorthLevel, + builder: (column) => ColumnFilters(column)); + Expression entryActivitiesRefs( Expression Function($$EntryActivitiesTableFilterComposer f) f) { final $$EntryActivitiesTableFilterComposer composer = $composerBuilder( @@ -2040,6 +2243,21 @@ class $$MoodEntriesTableOrderingComposer ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get sleepMinutes => $composableBuilder( + column: $table.sleepMinutes, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get energyLevel => $composableBuilder( + column: $table.energyLevel, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get positivityLevel => $composableBuilder( + column: $table.positivityLevel, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get selfWorthLevel => $composableBuilder( + column: $table.selfWorthLevel, + builder: (column) => ColumnOrderings(column)); } class $$MoodEntriesTableAnnotationComposer @@ -2066,6 +2284,18 @@ class $$MoodEntriesTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get sleepMinutes => $composableBuilder( + column: $table.sleepMinutes, builder: (column) => column); + + GeneratedColumn get energyLevel => $composableBuilder( + column: $table.energyLevel, builder: (column) => column); + + GeneratedColumn get positivityLevel => $composableBuilder( + column: $table.positivityLevel, builder: (column) => column); + + GeneratedColumn get selfWorthLevel => $composableBuilder( + column: $table.selfWorthLevel, builder: (column) => column); + Expression entryActivitiesRefs( Expression Function($$EntryActivitiesTableAnnotationComposer a) f) { final $$EntryActivitiesTableAnnotationComposer composer = $composerBuilder( @@ -2137,6 +2367,10 @@ class $$MoodEntriesTableTableManager extends RootTableManager< Value moodLevel = const Value.absent(), Value note = const Value.absent(), Value createdAt = const Value.absent(), + Value sleepMinutes = const Value.absent(), + Value energyLevel = const Value.absent(), + Value positivityLevel = const Value.absent(), + Value selfWorthLevel = const Value.absent(), }) => MoodEntriesCompanion( id: id, @@ -2144,6 +2378,10 @@ class $$MoodEntriesTableTableManager extends RootTableManager< moodLevel: moodLevel, note: note, createdAt: createdAt, + sleepMinutes: sleepMinutes, + energyLevel: energyLevel, + positivityLevel: positivityLevel, + selfWorthLevel: selfWorthLevel, ), createCompanionCallback: ({ Value id = const Value.absent(), @@ -2151,6 +2389,10 @@ class $$MoodEntriesTableTableManager extends RootTableManager< required int moodLevel, Value note = const Value.absent(), Value createdAt = const Value.absent(), + Value sleepMinutes = const Value.absent(), + Value energyLevel = const Value.absent(), + Value positivityLevel = const Value.absent(), + Value selfWorthLevel = const Value.absent(), }) => MoodEntriesCompanion.insert( id: id, @@ -2158,6 +2400,10 @@ class $$MoodEntriesTableTableManager extends RootTableManager< moodLevel: moodLevel, note: note, createdAt: createdAt, + sleepMinutes: sleepMinutes, + energyLevel: energyLevel, + positivityLevel: positivityLevel, + selfWorthLevel: selfWorthLevel, ), withReferenceMapper: (p0) => p0 .map((e) => ( diff --git a/lib/core/database/daos/mood_dao.dart b/lib/core/database/daos/mood_dao.dart index ce37cef..540e64c 100644 --- a/lib/core/database/daos/mood_dao.dart +++ b/lib/core/database/daos/mood_dao.dart @@ -74,6 +74,16 @@ class MoodDao extends DatabaseAccessor with _$MoodDaoMixin { ..orderBy([(t) => OrderingTerm.asc(t.timestamp)])) .get(); + Future hasEntryToday() async { + final today = DateTime.now(); + final start = DateTime(today.year, today.month, today.day); + final end = start.add(const Duration(days: 1)); + return (await (select(moodEntries) + ..where((t) => t.timestamp.isBetweenValues(start, end)) + ..limit(1)) + .getSingleOrNull()) != null; + } + // Average mood per day — used by calendar heatmap Future> getDailyAverages( DateTime start, DateTime end) async { diff --git a/lib/core/database/tables.dart b/lib/core/database/tables.dart index f6e3589..98969d3 100644 --- a/lib/core/database/tables.dart +++ b/lib/core/database/tables.dart @@ -8,6 +8,10 @@ class MoodEntries extends Table { IntColumn get moodLevel => integer()(); // 1–5 TextColumn get note => text().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get sleepMinutes => integer().nullable()(); + IntColumn get energyLevel => integer().nullable()(); + IntColumn get positivityLevel => integer().nullable()(); + IntColumn get selfWorthLevel => integer().nullable()(); } // ─── Activities ────────────────────────────────────────────────────────────── diff --git a/lib/core/providers/mood_provider.dart b/lib/core/providers/mood_provider.dart index 8a2faf8..5988a99 100644 --- a/lib/core/providers/mood_provider.dart +++ b/lib/core/providers/mood_provider.dart @@ -24,6 +24,12 @@ final dailyAveragesProvider = FutureProvider.autoDispose ref.watch(moodDaoProvider).getDailyAverages(range.start, range.end), ); +// ── Is this the first entry today? ─────────────────────────────────────────── + +final isFirstEntryTodayProvider = FutureProvider.autoDispose((ref) async { + return !(await ref.read(moodDaoProvider).hasEntryToday()); +}); + // ── Selected day (drives calendar + day detail) ─────────────────────────────── class SelectedDay extends Notifier { @@ -55,6 +61,10 @@ class MoodEntryNotifier extends Notifier> { List activityIds = const [], List tagIds = const [], DateTime? timestamp, + int? sleepMinutes, + int? energyLevel, + int? positivityLevel, + int? selfWorthLevel, }) async { state = const AsyncLoading(); final next = await AsyncValue.guard(() async { @@ -64,6 +74,10 @@ class MoodEntryNotifier extends Notifier> { moodLevel: moodLevel, timestamp: timestamp ?? DateTime.now(), note: Value(note), + sleepMinutes: Value(sleepMinutes), + energyLevel: Value(energyLevel), + positivityLevel: Value(positivityLevel), + selfWorthLevel: Value(selfWorthLevel), ), activityIds: activityIds, tagIds: tagIds, diff --git a/lib/features/log_mood/log_mood_screen.dart b/lib/features/log_mood/log_mood_screen.dart index 8f446ca..e5f06d6 100644 --- a/lib/features/log_mood/log_mood_screen.dart +++ b/lib/features/log_mood/log_mood_screen.dart @@ -1,9 +1,41 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../core/providers/mood_provider.dart'; + import '../../core/providers/activity_provider.dart'; -import '../../shared/widgets/mood_selector.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}); @@ -13,142 +45,636 @@ class LogMoodScreen extends ConsumerStatefulWidget { } class _LogMoodScreenState extends ConsumerState { - int _selectedMood = 3; - final _noteController = TextEditingController(); - bool _saving = false; + // answers + int? _mood; + int _sleepHours = 7; + int _sleepMinutesVal = 30; // 0/5/10…55 + int? _energy; + int? _positivity; + int? _selfWorth; - static const _moodLabels = ['Awful', 'Bad', 'Okay', 'Good', 'Great']; - static const _moodEmojis = ['😞', '😔', '😐', '😊', '😄']; - static const _moodColors = [ - Color(0xFFE57373), - Color(0xFFFFB74D), - Color(0xFFFFD54F), - Color(0xFF81C784), - Color(0xFF4DB6AC), - ]; + // 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() { - _noteController.dispose(); + _hoursCtrl.dispose(); + _minsCtrl.dispose(); super.dispose(); } - Future _save() async { - setState(() => _saving = true); - final selectedActivities = ref.read(selectedActivitiesProvider); - await ref.read(moodEntryNotifierProvider.notifier).addEntry( - moodLevel: _selectedMood, - note: _noteController.text.trim().isEmpty - ? null - : _noteController.text.trim(), - activityIds: selectedActivities.toList(), - ); - if (mounted) { - ref.read(selectedActivitiesProvider.notifier).clear(); - _noteController.clear(); + // ── 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(() { - _saving = false; - _selectedMood = 3; + _editingFromReview = false; + _inReview = true; + _stepIndex = _steps!.length; // back to review index }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Mood logged!')), - ); + return; + } + final next = _stepIndex + 1; + if (next >= _steps!.length) { + setState(() { + _inReview = true; + _stepIndex = _steps!.length; + }); + } else { + setState(() => _stepIndex = next); } } - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final moodColor = _moodColors[_selectedMood - 1]; + void _editStepFromReview(int idx) { + setState(() { + _editingFromReview = true; + _stepIndex = idx; + _inReview = false; + }); + } - return Scaffold( - backgroundColor: theme.colorScheme.surface, - appBar: AppBar( - title: const Text('How are you feeling?'), - centerTitle: true, - backgroundColor: Colors.transparent, - elevation: 0, + // ── Save ───────────────────────────────────────────────────────────────────── + + Future _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 = []; + + 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, ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + } + + // ── 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: [ - // ── Mood selector ────────────────────────────────────── - Center( - child: MoodSelector( - selected: _selectedMood, - emojis: _moodEmojis, - labels: _moodLabels, - colors: _moodColors, - onChanged: (v) => setState(() => _selectedMood = v), - ), - ), - const SizedBox(height: 32), - - // ── Current mood label ───────────────────────────────── - Center( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Text( - _moodLabels[_selectedMood - 1], - key: ValueKey(_selectedMood), - style: theme.textTheme.headlineMedium?.copyWith( - color: moodColor, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - const SizedBox(height: 32), - - // ── Activities ───────────────────────────────────────── - Text('Activities', style: theme.textTheme.titleMedium), - const SizedBox(height: 12), - const ActivityGrid(), - const SizedBox(height: 24), - - // ── Note ─────────────────────────────────────────────── - Text('Note', style: theme.textTheme.titleMedium), - const SizedBox(height: 8), - TextField( - controller: _noteController, - maxLines: 3, - decoration: InputDecoration( - hintText: 'Add a note (optional)...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - filled: true, - fillColor: theme.colorScheme.surfaceContainerHighest, - ), - ), - const SizedBox(height: 32), - - // ── Save button ──────────────────────────────────────── - SizedBox( - width: double.infinity, - height: 52, - child: FilledButton( - onPressed: _saving ? null : _save, - style: FilledButton.styleFrom( - backgroundColor: moodColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - child: _saving - ? const CircularProgressIndicator(color: Colors.white) - : const Text( - 'Save', - style: TextStyle( - fontSize: 16, fontWeight: FontWeight.w600), + // 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, // 0–12 + 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 emojis, + required List labels, + required int? current, + required ValueChanged 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(); + } + } } diff --git a/lib/features/log_mood/widgets/activities_chip.dart b/lib/features/log_mood/widgets/activities_chip.dart new file mode 100644 index 0000000..351f41b --- /dev/null +++ b/lib/features/log_mood/widgets/activities_chip.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/database/app_database.dart'; +import '../../../core/providers/activity_provider.dart'; + +/// 60 px chip showing selected activities: +/// - 0 selected : nothing (caller should not render) +/// - 1 selected : single Material icon centred +/// - 2–4 selected: 2×2 mini grid of 16 px icons +/// - 5+ selected : icon + count badge +class ActivitiesChip extends ConsumerWidget { + final Color backgroundColor; + final VoidCallback? onTap; + + const ActivitiesChip({ + super.key, + required this.backgroundColor, + this.onTap, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final activitiesAsync = ref.watch(activitiesProvider); + final selected = ref.watch(selectedActivitiesProvider); + + return activitiesAsync.when( + loading: () => _shell(backgroundColor, onTap, + const CircularProgressIndicator(strokeWidth: 2)), + error: (_, __) => const SizedBox.shrink(), + data: (all) { + final picked = + all.where((a) => selected.contains(a.id)).toList(); + if (picked.isEmpty) return const SizedBox(width: 60, height: 60); + return _shell( + backgroundColor, + onTap, + _iconContent(picked), + ); + }, + ); + } + + Widget _shell(Color bg, VoidCallback? tap, Widget child) { + return GestureDetector( + onTap: tap, + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration(color: bg, shape: BoxShape.circle), + child: Center(child: child), + ), + ); + } + + Widget _iconContent(List picked) { + if (picked.length == 1) { + return Text( + String.fromCharCode(picked[0].iconCodepoint), + style: const TextStyle( + fontFamily: 'MaterialIcons', + fontSize: 24, + color: Colors.white, + ), + ); + } + + final show = picked.take(4).toList(); + return Wrap( + spacing: 2, + runSpacing: 2, + children: show + .map( + (a) => Text( + String.fromCharCode(a.iconCodepoint), + style: const TextStyle( + fontFamily: 'MaterialIcons', + fontSize: 16, + color: Colors.white, + ), + ), + ) + .toList(), + ); + } +} diff --git a/lib/features/log_mood/widgets/answer_chip.dart b/lib/features/log_mood/widgets/answer_chip.dart new file mode 100644 index 0000000..7b4bf52 --- /dev/null +++ b/lib/features/log_mood/widgets/answer_chip.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +/// A single 60 px circular answer chip showing either an emoji or short text. +class AnswerChip extends StatelessWidget { + final String label; + final Color backgroundColor; + final VoidCallback? onTap; + + const AnswerChip({ + super.key, + required this.label, + required this.backgroundColor, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + width: 60, + height: 60, + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + boxShadow: onTap != null + ? [ + BoxShadow( + color: backgroundColor.withValues(alpha: 0.4), + blurRadius: 8, + offset: const Offset(0, 2), + ) + ] + : null, + ), + child: Center( + child: Text( + label, + style: const TextStyle(fontSize: 22), + textAlign: TextAlign.center, + ), + ), + ), + ); + } +} diff --git a/lib/features/log_mood/widgets/step_scale.dart b/lib/features/log_mood/widgets/step_scale.dart new file mode 100644 index 0000000..ea01380 --- /dev/null +++ b/lib/features/log_mood/widgets/step_scale.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +/// Reusable 5-option emoji scale used for Energy, Positivity, Self-worth steps. +/// Tapping an option immediately calls [onSelected]. +class StepScale extends StatelessWidget { + final List emojis; + final List labels; + final List colors; + final int? selected; // 1–5, null = nothing selected yet + final ValueChanged onSelected; + + const StepScale({ + super.key, + required this.emojis, + required this.labels, + required this.colors, + required this.selected, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(5, (i) { + final level = i + 1; + final isSelected = selected == level; + final color = colors[i]; + return GestureDetector( + onTap: () => onSelected(level), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + width: isSelected ? 64 : 52, + height: isSelected ? 64 : 52, + decoration: BoxDecoration( + color: isSelected + ? color.withValues(alpha: 0.15) + : Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? color : Colors.transparent, + width: 2, + ), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + emojis[i], + style: TextStyle(fontSize: isSelected ? 30 : 24), + ), + if (isSelected) + Text( + labels[i], + style: TextStyle( + fontSize: 9, + color: color, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + }), + ); + } +} diff --git a/lib/shared/widgets/activity_grid.dart b/lib/shared/widgets/activity_grid.dart index 6eb1d58..0c6b4f9 100644 --- a/lib/shared/widgets/activity_grid.dart +++ b/lib/shared/widgets/activity_grid.dart @@ -9,7 +9,6 @@ class ActivityGrid extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final activitiesAsync = ref.watch(activitiesProvider); final selected = ref.watch(selectedActivitiesProvider); - final theme = Theme.of(context); return activitiesAsync.when( data: (activities) => Wrap(