feat: implement multi-step mood logging wizard
Adds sleep/energy/positivity/self-worth tracking columns to the database (schema v2 migration), a new 5-option StepScale widget, per-answer chips at the top of the screen, a 2×2 activity grid chip, and a full step-by-step wizard flow: mood → sleep (first entry today only) → energy → positivity → self-worth → activities → review+save.
This commit is contained in:
@@ -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<Object>);
|
||||
await m.addColumn(moodEntries,
|
||||
moodEntries.energyLevel as GeneratedColumn<Object>);
|
||||
await m.addColumn(moodEntries,
|
||||
moodEntries.positivityLevel as GeneratedColumn<Object>);
|
||||
await m.addColumn(moodEntries,
|
||||
moodEntries.selfWorthLevel as GeneratedColumn<Object>);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -43,9 +43,42 @@ class $MoodEntriesTable extends MoodEntries
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: currentDateAndTime);
|
||||
static const VerificationMeta _sleepMinutesMeta =
|
||||
const VerificationMeta('sleepMinutes');
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[id, timestamp, moodLevel, note, createdAt];
|
||||
late final GeneratedColumn<int> sleepMinutes = GeneratedColumn<int>(
|
||||
'sleep_minutes', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
static const VerificationMeta _energyLevelMeta =
|
||||
const VerificationMeta('energyLevel');
|
||||
@override
|
||||
late final GeneratedColumn<int> energyLevel = GeneratedColumn<int>(
|
||||
'energy_level', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
static const VerificationMeta _positivityLevelMeta =
|
||||
const VerificationMeta('positivityLevel');
|
||||
@override
|
||||
late final GeneratedColumn<int> positivityLevel = GeneratedColumn<int>(
|
||||
'positivity_level', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
static const VerificationMeta _selfWorthLevelMeta =
|
||||
const VerificationMeta('selfWorthLevel');
|
||||
@override
|
||||
late final GeneratedColumn<int> selfWorthLevel = GeneratedColumn<int>(
|
||||
'self_worth_level', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
@override
|
||||
List<GeneratedColumn> 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<MoodEntry> {
|
||||
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<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
@@ -129,6 +202,18 @@ class MoodEntry extends DataClass implements Insertable<MoodEntry> {
|
||||
map['note'] = Variable<String>(note);
|
||||
}
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
if (!nullToAbsent || sleepMinutes != null) {
|
||||
map['sleep_minutes'] = Variable<int>(sleepMinutes);
|
||||
}
|
||||
if (!nullToAbsent || energyLevel != null) {
|
||||
map['energy_level'] = Variable<int>(energyLevel);
|
||||
}
|
||||
if (!nullToAbsent || positivityLevel != null) {
|
||||
map['positivity_level'] = Variable<int>(positivityLevel);
|
||||
}
|
||||
if (!nullToAbsent || selfWorthLevel != null) {
|
||||
map['self_worth_level'] = Variable<int>(selfWorthLevel);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -139,6 +224,18 @@ class MoodEntry extends DataClass implements Insertable<MoodEntry> {
|
||||
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<MoodEntry> {
|
||||
moodLevel: serializer.fromJson<int>(json['moodLevel']),
|
||||
note: serializer.fromJson<String?>(json['note']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
sleepMinutes: serializer.fromJson<int?>(json['sleepMinutes']),
|
||||
energyLevel: serializer.fromJson<int?>(json['energyLevel']),
|
||||
positivityLevel: serializer.fromJson<int?>(json['positivityLevel']),
|
||||
selfWorthLevel: serializer.fromJson<int?>(json['selfWorthLevel']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -162,6 +263,10 @@ class MoodEntry extends DataClass implements Insertable<MoodEntry> {
|
||||
'moodLevel': serializer.toJson<int>(moodLevel),
|
||||
'note': serializer.toJson<String?>(note),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'sleepMinutes': serializer.toJson<int?>(sleepMinutes),
|
||||
'energyLevel': serializer.toJson<int?>(energyLevel),
|
||||
'positivityLevel': serializer.toJson<int?>(positivityLevel),
|
||||
'selfWorthLevel': serializer.toJson<int?>(selfWorthLevel),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,13 +275,25 @@ class MoodEntry extends DataClass implements Insertable<MoodEntry> {
|
||||
DateTime? timestamp,
|
||||
int? moodLevel,
|
||||
Value<String?> note = const Value.absent(),
|
||||
DateTime? createdAt}) =>
|
||||
DateTime? createdAt,
|
||||
Value<int?> sleepMinutes = const Value.absent(),
|
||||
Value<int?> energyLevel = const Value.absent(),
|
||||
Value<int?> positivityLevel = const Value.absent(),
|
||||
Value<int?> 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<MoodEntry> {
|
||||
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<MoodEntry> {
|
||||
..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<MoodEntry> {
|
||||
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<MoodEntry> {
|
||||
@@ -219,12 +356,20 @@ class MoodEntriesCompanion extends UpdateCompanion<MoodEntry> {
|
||||
final Value<int> moodLevel;
|
||||
final Value<String?> note;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<int?> sleepMinutes;
|
||||
final Value<int?> energyLevel;
|
||||
final Value<int?> positivityLevel;
|
||||
final Value<int?> 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<MoodEntry> {
|
||||
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<MoodEntry> custom({
|
||||
@@ -240,6 +389,10 @@ class MoodEntriesCompanion extends UpdateCompanion<MoodEntry> {
|
||||
Expression<int>? moodLevel,
|
||||
Expression<String>? note,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<int>? sleepMinutes,
|
||||
Expression<int>? energyLevel,
|
||||
Expression<int>? positivityLevel,
|
||||
Expression<int>? selfWorthLevel,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
@@ -247,6 +400,10 @@ class MoodEntriesCompanion extends UpdateCompanion<MoodEntry> {
|
||||
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<MoodEntry> {
|
||||
Value<DateTime>? timestamp,
|
||||
Value<int>? moodLevel,
|
||||
Value<String?>? note,
|
||||
Value<DateTime>? createdAt}) {
|
||||
Value<DateTime>? createdAt,
|
||||
Value<int?>? sleepMinutes,
|
||||
Value<int?>? energyLevel,
|
||||
Value<int?>? positivityLevel,
|
||||
Value<int?>? 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<MoodEntry> {
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (sleepMinutes.present) {
|
||||
map['sleep_minutes'] = Variable<int>(sleepMinutes.value);
|
||||
}
|
||||
if (energyLevel.present) {
|
||||
map['energy_level'] = Variable<int>(energyLevel.value);
|
||||
}
|
||||
if (positivityLevel.present) {
|
||||
map['positivity_level'] = Variable<int>(positivityLevel.value);
|
||||
}
|
||||
if (selfWorthLevel.present) {
|
||||
map['self_worth_level'] = Variable<int>(selfWorthLevel.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -293,7 +470,11 @@ class MoodEntriesCompanion extends UpdateCompanion<MoodEntry> {
|
||||
..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<String?> note,
|
||||
Value<DateTime> createdAt,
|
||||
Value<int?> sleepMinutes,
|
||||
Value<int?> energyLevel,
|
||||
Value<int?> positivityLevel,
|
||||
Value<int?> selfWorthLevel,
|
||||
});
|
||||
typedef $$MoodEntriesTableUpdateCompanionBuilder = MoodEntriesCompanion
|
||||
Function({
|
||||
@@ -1911,6 +2096,10 @@ typedef $$MoodEntriesTableUpdateCompanionBuilder = MoodEntriesCompanion
|
||||
Value<int> moodLevel,
|
||||
Value<String?> note,
|
||||
Value<DateTime> createdAt,
|
||||
Value<int?> sleepMinutes,
|
||||
Value<int?> energyLevel,
|
||||
Value<int?> positivityLevel,
|
||||
Value<int?> selfWorthLevel,
|
||||
});
|
||||
|
||||
final class $$MoodEntriesTableReferences
|
||||
@@ -1974,6 +2163,20 @@ class $$MoodEntriesTableFilterComposer
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<int> get sleepMinutes => $composableBuilder(
|
||||
column: $table.sleepMinutes, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<int> get energyLevel => $composableBuilder(
|
||||
column: $table.energyLevel, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<int> get positivityLevel => $composableBuilder(
|
||||
column: $table.positivityLevel,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<int> get selfWorthLevel => $composableBuilder(
|
||||
column: $table.selfWorthLevel,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
|
||||
Expression<bool> entryActivitiesRefs(
|
||||
Expression<bool> Function($$EntryActivitiesTableFilterComposer f) f) {
|
||||
final $$EntryActivitiesTableFilterComposer composer = $composerBuilder(
|
||||
@@ -2040,6 +2243,21 @@ class $$MoodEntriesTableOrderingComposer
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get sleepMinutes => $composableBuilder(
|
||||
column: $table.sleepMinutes,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get energyLevel => $composableBuilder(
|
||||
column: $table.energyLevel, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get positivityLevel => $composableBuilder(
|
||||
column: $table.positivityLevel,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get selfWorthLevel => $composableBuilder(
|
||||
column: $table.selfWorthLevel,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
}
|
||||
|
||||
class $$MoodEntriesTableAnnotationComposer
|
||||
@@ -2066,6 +2284,18 @@ class $$MoodEntriesTableAnnotationComposer
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get sleepMinutes => $composableBuilder(
|
||||
column: $table.sleepMinutes, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get energyLevel => $composableBuilder(
|
||||
column: $table.energyLevel, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get positivityLevel => $composableBuilder(
|
||||
column: $table.positivityLevel, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get selfWorthLevel => $composableBuilder(
|
||||
column: $table.selfWorthLevel, builder: (column) => column);
|
||||
|
||||
Expression<T> entryActivitiesRefs<T extends Object>(
|
||||
Expression<T> Function($$EntryActivitiesTableAnnotationComposer a) f) {
|
||||
final $$EntryActivitiesTableAnnotationComposer composer = $composerBuilder(
|
||||
@@ -2137,6 +2367,10 @@ class $$MoodEntriesTableTableManager extends RootTableManager<
|
||||
Value<int> moodLevel = const Value.absent(),
|
||||
Value<String?> note = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<int?> sleepMinutes = const Value.absent(),
|
||||
Value<int?> energyLevel = const Value.absent(),
|
||||
Value<int?> positivityLevel = const Value.absent(),
|
||||
Value<int?> 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<int> id = const Value.absent(),
|
||||
@@ -2151,6 +2389,10 @@ class $$MoodEntriesTableTableManager extends RootTableManager<
|
||||
required int moodLevel,
|
||||
Value<String?> note = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<int?> sleepMinutes = const Value.absent(),
|
||||
Value<int?> energyLevel = const Value.absent(),
|
||||
Value<int?> positivityLevel = const Value.absent(),
|
||||
Value<int?> 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) => (
|
||||
|
||||
@@ -74,6 +74,16 @@ class MoodDao extends DatabaseAccessor<AppDatabase> with _$MoodDaoMixin {
|
||||
..orderBy([(t) => OrderingTerm.asc(t.timestamp)]))
|
||||
.get();
|
||||
|
||||
Future<bool> 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<Map<DateTime, double>> getDailyAverages(
|
||||
DateTime start, DateTime end) async {
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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<bool>((ref) async {
|
||||
return !(await ref.read(moodDaoProvider).hasEntryToday());
|
||||
});
|
||||
|
||||
// ── Selected day (drives calendar + day detail) ───────────────────────────────
|
||||
|
||||
class SelectedDay extends Notifier<DateTime> {
|
||||
@@ -55,6 +61,10 @@ class MoodEntryNotifier extends Notifier<AsyncValue<void>> {
|
||||
List<int> activityIds = const [],
|
||||
List<int> 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<AsyncValue<void>> {
|
||||
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,
|
||||
|
||||
@@ -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<LogMoodScreen> {
|
||||
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<void> _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<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,
|
||||
),
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Activity> 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String> emojis;
|
||||
final List<String> labels;
|
||||
final List<Color> colors;
|
||||
final int? selected; // 1–5, null = nothing selected yet
|
||||
final ValueChanged<int> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user