feat: set up Flutter project with drift database, Riverpod providers, and LLM service layer

- Configure dependencies: drift 2.32.x, flutter_riverpod 3.3.1, fl_chart 1.2.x,
  flutter_local_notifications 21.x, local_auth 3.x, and supporting packages
- Drop riverpod_generator (incompatible analyzer version with drift_dev 2.32.x);
  convert all providers to manual Riverpod 3.x API using Notifier<T> and typed families
- Define drift schema: MoodEntries, Activities, EntryActivities, Tags, EntryTags,
  LlmInsights, UserSettings with four DAOs (mood, activity, insight, settings)
- Add LLM service abstraction (LlmService) and MediaPipe implementation
- Fix SettingsDao.delete → deleteByKey to avoid shadowing drift's inherited delete method
This commit is contained in:
2026-04-26 10:39:51 +10:00
parent 6e45e50f1a
commit 33d1713f51
149 changed files with 10338 additions and 1 deletions
+93
View File
@@ -0,0 +1,93 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'tables.dart';
import 'daos/mood_dao.dart';
import 'daos/activity_dao.dart';
import 'daos/insight_dao.dart';
import 'daos/settings_dao.dart';
part 'app_database.g.dart';
@DriftDatabase(
tables: [
MoodEntries,
Activities,
EntryActivities,
Tags,
EntryTags,
LlmInsights,
UserSettings,
],
daos: [
MoodDao,
ActivityDao,
InsightDao,
SettingsDao,
],
)
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) async {
await m.createAll();
await _seedDefaultActivities();
},
onUpgrade: (m, from, to) async {
// Future migrations go here
},
);
Future<void> _seedDefaultActivities() async {
final defaults = [
(
name: 'Exercise',
icon: 0xe567,
colour: '#FF6B6B'
), // Icons.fitness_center
(name: 'Good sleep', icon: 0xe7f5, colour: '#7B68EE'), // Icons.bedtime
(name: 'Social', icon: 0xe7ef, colour: '#FFB347'), // Icons.people
(name: 'Work', icon: 0xe8b6, colour: '#4FC3F7'), // Icons.work
(name: 'Food', icon: 0xe56c, colour: '#81C784'), // Icons.restaurant
(name: 'Reading', icon: 0xe865, colour: '#F06292'), // Icons.menu_book
(name: 'Outdoors', icon: 0xe1a1, colour: '#AED581'), // Icons.park
(
name: 'Family',
icon: 0xe8a1,
colour: '#FFD54F'
), // Icons.family_restroom
(
name: 'Meditation',
icon: 0xe7fb,
colour: '#80CBC4'
), // Icons.self_improvement
(name: 'Travel', icon: 0xe8b5, colour: '#FF8A65'), // Icons.flight
];
for (final (index, a) in defaults.indexed) {
await into(activities).insert(
ActivitiesCompanion.insert(
name: a.name,
iconCodepoint: a.icon,
colourHex: a.colour,
sortOrder: Value(index),
),
);
}
}
}
QueryExecutor _openConnection() {
return driftDatabase(
name: 'lumina_db',
native: DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
import 'package:drift/drift.dart';
import '../app_database.dart';
import '../tables.dart';
part 'activity_dao.g.dart';
@DriftAccessor(tables: [Activities, EntryActivities, MoodEntries])
class ActivityDao extends DatabaseAccessor<AppDatabase>
with _$ActivityDaoMixin {
ActivityDao(super.db);
Stream<List<Activity>> watchAllActivities() =>
(select(activities)..orderBy([(t) => OrderingTerm.asc(t.sortOrder)]))
.watch();
Future<int> insertActivity(ActivitiesCompanion activity) =>
into(activities).insert(activity);
Future<void> deleteActivity(int id) =>
(delete(activities)..where((t) => t.id.equals(id))).go();
// How many times each activity was logged — used by charts
Future<Map<int, int>> getActivityFrequency(
DateTime start, DateTime end) async {
final entries = await (select(moodEntries)
..where((t) => t.timestamp.isBetweenValues(start, end)))
.get();
final entryIds = entries.map((e) => e.id).toList();
if (entryIds.isEmpty) return {};
final rows = await (select(entryActivities)
..where((t) => t.entryId.isIn(entryIds)))
.get();
final Map<int, int> freq = {};
for (final r in rows) {
freq[r.activityId] = (freq[r.activityId] ?? 0) + 1;
}
return freq;
}
}
@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'activity_dao.dart';
// ignore_for_file: type=lint
mixin _$ActivityDaoMixin on DatabaseAccessor<AppDatabase> {
$ActivitiesTable get activities => attachedDatabase.activities;
$MoodEntriesTable get moodEntries => attachedDatabase.moodEntries;
$EntryActivitiesTable get entryActivities => attachedDatabase.entryActivities;
ActivityDaoManager get managers => ActivityDaoManager(this);
}
class ActivityDaoManager {
final _$ActivityDaoMixin _db;
ActivityDaoManager(this._db);
$$ActivitiesTableTableManager get activities =>
$$ActivitiesTableTableManager(_db.attachedDatabase, _db.activities);
$$MoodEntriesTableTableManager get moodEntries =>
$$MoodEntriesTableTableManager(_db.attachedDatabase, _db.moodEntries);
$$EntryActivitiesTableTableManager get entryActivities =>
$$EntryActivitiesTableTableManager(
_db.attachedDatabase, _db.entryActivities);
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:drift/drift.dart';
import '../app_database.dart';
import '../tables.dart';
part 'insight_dao.g.dart';
@DriftAccessor(tables: [LlmInsights])
class InsightDao extends DatabaseAccessor<AppDatabase> with _$InsightDaoMixin {
InsightDao(super.db);
Future<LlmInsight?> getCached(String promptHash) => (select(llmInsights)
..where((t) => t.promptHash.equals(promptHash))
..limit(1))
.getSingleOrNull();
Future<void> upsertInsight(LlmInsightsCompanion insight) =>
into(llmInsights).insertOnConflictUpdate(insight);
// Purge cached insights older than 7 days
Future<void> purgeStale() {
final cutoff = DateTime.now().subtract(const Duration(days: 7));
return (delete(llmInsights)
..where((t) => t.generatedAt.isSmallerThanValue(cutoff)))
.go();
}
}
+16
View File
@@ -0,0 +1,16 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'insight_dao.dart';
// ignore_for_file: type=lint
mixin _$InsightDaoMixin on DatabaseAccessor<AppDatabase> {
$LlmInsightsTable get llmInsights => attachedDatabase.llmInsights;
InsightDaoManager get managers => InsightDaoManager(this);
}
class InsightDaoManager {
final _$InsightDaoMixin _db;
InsightDaoManager(this._db);
$$LlmInsightsTableTableManager get llmInsights =>
$$LlmInsightsTableTableManager(_db.attachedDatabase, _db.llmInsights);
}
+92
View File
@@ -0,0 +1,92 @@
import 'package:drift/drift.dart';
import '../app_database.dart';
import '../tables.dart';
part 'mood_dao.g.dart';
// Joined result — entry + its activities
class MoodEntryWithActivities {
final MoodEntry entry;
final List<Activity> activities;
final List<Tag> tags;
MoodEntryWithActivities({
required this.entry,
required this.activities,
required this.tags,
});
}
@DriftAccessor(
tables: [MoodEntries, EntryActivities, Activities, EntryTags, Tags])
class MoodDao extends DatabaseAccessor<AppDatabase> with _$MoodDaoMixin {
MoodDao(super.db);
// ── Writes ──────────────────────────────────────────────────────────────────
Future<int> insertEntry(MoodEntriesCompanion entry) =>
into(moodEntries).insert(entry);
Future<void> insertEntryWithActivities({
required MoodEntriesCompanion entry,
required List<int> activityIds,
required List<int> tagIds,
}) async {
await transaction(() async {
final entryId = await into(moodEntries).insert(entry);
for (final actId in activityIds) {
await into(entryActivities).insert(
EntryActivitiesCompanion.insert(entryId: entryId, activityId: actId),
);
}
for (final tagId in tagIds) {
await into(entryTags).insert(
EntryTagsCompanion.insert(entryId: entryId, tagId: tagId),
);
}
});
}
Future<void> deleteEntry(int id) =>
(delete(moodEntries)..where((t) => t.id.equals(id))).go();
Future<void> updateEntry(MoodEntriesCompanion entry) =>
(update(moodEntries)..where((t) => t.id.equals(entry.id.value)))
.write(entry);
// ── Reads ───────────────────────────────────────────────────────────────────
Stream<List<MoodEntry>> watchAllEntries() =>
(select(moodEntries)..orderBy([(t) => OrderingTerm.desc(t.timestamp)]))
.watch();
Stream<List<MoodEntry>> watchEntriesForDay(DateTime day) {
final start = DateTime(day.year, day.month, day.day);
final end = start.add(const Duration(days: 1));
return (select(moodEntries)
..where((t) => t.timestamp.isBetweenValues(start, end))
..orderBy([(t) => OrderingTerm.desc(t.timestamp)]))
.watch();
}
Future<List<MoodEntry>> getEntriesInRange(DateTime start, DateTime end) =>
(select(moodEntries)
..where((t) => t.timestamp.isBetweenValues(start, end))
..orderBy([(t) => OrderingTerm.asc(t.timestamp)]))
.get();
// Average mood per day — used by calendar heatmap
Future<Map<DateTime, double>> getDailyAverages(
DateTime start, DateTime end) async {
final entries = await getEntriesInRange(start, end);
final Map<DateTime, List<int>> grouped = {};
for (final e in entries) {
final day =
DateTime(e.timestamp.year, e.timestamp.month, e.timestamp.day);
grouped.putIfAbsent(day, () => []).add(e.moodLevel);
}
return grouped.map(
(day, levels) =>
MapEntry(day, levels.reduce((a, b) => a + b) / levels.length),
);
}
}
+29
View File
@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'mood_dao.dart';
// ignore_for_file: type=lint
mixin _$MoodDaoMixin on DatabaseAccessor<AppDatabase> {
$MoodEntriesTable get moodEntries => attachedDatabase.moodEntries;
$ActivitiesTable get activities => attachedDatabase.activities;
$EntryActivitiesTable get entryActivities => attachedDatabase.entryActivities;
$TagsTable get tags => attachedDatabase.tags;
$EntryTagsTable get entryTags => attachedDatabase.entryTags;
MoodDaoManager get managers => MoodDaoManager(this);
}
class MoodDaoManager {
final _$MoodDaoMixin _db;
MoodDaoManager(this._db);
$$MoodEntriesTableTableManager get moodEntries =>
$$MoodEntriesTableTableManager(_db.attachedDatabase, _db.moodEntries);
$$ActivitiesTableTableManager get activities =>
$$ActivitiesTableTableManager(_db.attachedDatabase, _db.activities);
$$EntryActivitiesTableTableManager get entryActivities =>
$$EntryActivitiesTableTableManager(
_db.attachedDatabase, _db.entryActivities);
$$TagsTableTableManager get tags =>
$$TagsTableTableManager(_db.attachedDatabase, _db.tags);
$$EntryTagsTableTableManager get entryTags =>
$$EntryTagsTableTableManager(_db.attachedDatabase, _db.entryTags);
}
+27
View File
@@ -0,0 +1,27 @@
import 'package:drift/drift.dart';
import '../app_database.dart';
import '../tables.dart';
part 'settings_dao.g.dart';
@DriftAccessor(tables: [UserSettings])
class SettingsDao extends DatabaseAccessor<AppDatabase>
with _$SettingsDaoMixin {
SettingsDao(super.db);
Future<String?> get(String key) async {
final row = await (select(userSettings)
..where((t) => t.key.equals(key))
..limit(1))
.getSingleOrNull();
return row?.value;
}
Future<void> set(String key, String value) =>
into(userSettings).insertOnConflictUpdate(
UserSettingsCompanion.insert(key: key, value: value),
);
Future<void> deleteByKey(String key) =>
(delete(userSettings)..where((t) => t.key.equals(key))).go();
}
@@ -0,0 +1,16 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'settings_dao.dart';
// ignore_for_file: type=lint
mixin _$SettingsDaoMixin on DatabaseAccessor<AppDatabase> {
$UserSettingsTable get userSettings => attachedDatabase.userSettings;
SettingsDaoManager get managers => SettingsDaoManager(this);
}
class SettingsDaoManager {
final _$SettingsDaoMixin _db;
SettingsDaoManager(this._db);
$$UserSettingsTableTableManager get userSettings =>
$$UserSettingsTableTableManager(_db.attachedDatabase, _db.userSettings);
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:drift/drift.dart';
// ─── Mood Entries ────────────────────────────────────────────────────────────
class MoodEntries extends Table {
IntColumn get id => integer().autoIncrement()();
DateTimeColumn get timestamp => dateTime()();
IntColumn get moodLevel => integer()(); // 15
TextColumn get note => text().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
// ─── Activities ──────────────────────────────────────────────────────────────
class Activities extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 50)();
IntColumn get iconCodepoint => integer()(); // Icons codepoint value
TextColumn get colourHex =>
text().withLength(min: 7, max: 7)(); // e.g. #FF5733
BoolColumn get isCustom => boolean().withDefault(const Constant(false))();
IntColumn get sortOrder => integer().withDefault(const Constant(0))();
}
// ─── Entry ↔ Activity (many-to-many) ─────────────────────────────────────────
class EntryActivities extends Table {
IntColumn get entryId => integer().references(MoodEntries, #id)();
IntColumn get activityId => integer().references(Activities, #id)();
@override
Set<Column> get primaryKey => {entryId, activityId};
}
// ─── Tags ────────────────────────────────────────────────────────────────────
class Tags extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 30)();
TextColumn get colourHex => text().withLength(min: 7, max: 7)();
}
// ─── Entry ↔ Tag (many-to-many) ───────────────────────────────────────────────
class EntryTags extends Table {
IntColumn get entryId => integer().references(MoodEntries, #id)();
IntColumn get tagId => integer().references(Tags, #id)();
@override
Set<Column> get primaryKey => {entryId, tagId};
}
// ─── LLM Insight Cache ───────────────────────────────────────────────────────
class LlmInsights extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get type => text()(); // 'weekly' | 'monthly' | 'query'
TextColumn get promptHash => text()(); // SHA256 of the prompt — cache key
TextColumn get responseText => text()();
DateTimeColumn get generatedAt =>
dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get rangeStart => dateTime().nullable()();
DateTimeColumn get rangeEnd => dateTime().nullable()();
}
// ─── Settings ────────────────────────────────────────────────────────────────
class UserSettings extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get key => text().withLength(min: 1, max: 50).unique()();
TextColumn get value => text()();
}