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:
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()(); // 1–5
|
||||
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()();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
abstract class LlmService {
|
||||
Future<void> initialise();
|
||||
Stream<String> generateStream(String prompt);
|
||||
void dispose();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter_mediapipe_chat/flutter_mediapipe_chat.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'llm_service.dart';
|
||||
|
||||
class MediapipeLlmService implements LlmService {
|
||||
final _chat = FlutterMediapipeChat();
|
||||
bool _initialised = false;
|
||||
|
||||
static const _modelFileName = 'gemma-2b-it-cpu-int4.bin';
|
||||
|
||||
@override
|
||||
Future<void> initialise() async {
|
||||
if (_initialised) return;
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final modelPath = p.join(dir.path, _modelFileName);
|
||||
await _chat.loadModel(ModelConfig(path: modelPath));
|
||||
_initialised = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> generateStream(String prompt) {
|
||||
return _chat.generateResponseAsync(prompt);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_initialised = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../database/app_database.dart';
|
||||
import 'database_provider.dart';
|
||||
|
||||
// ── All activities stream ─────────────────────────────────────────────────────
|
||||
|
||||
final activitiesProvider = StreamProvider.autoDispose<List<Activity>>((ref) {
|
||||
return ref.watch(activityDaoProvider).watchAllActivities();
|
||||
});
|
||||
|
||||
// ── Activity frequency for charts ────────────────────────────────────────────
|
||||
|
||||
final activityFrequencyProvider = FutureProvider.autoDispose
|
||||
.family<Map<int, int>, ({DateTime start, DateTime end})>(
|
||||
(ref, range) =>
|
||||
ref.watch(activityDaoProvider).getActivityFrequency(range.start, range.end),
|
||||
);
|
||||
|
||||
// ── Selected activities (used by log mood screen) ─────────────────────────────
|
||||
|
||||
class SelectedActivities extends Notifier<Set<int>> {
|
||||
@override
|
||||
Set<int> build() => {};
|
||||
|
||||
void toggle(int activityId) {
|
||||
if (state.contains(activityId)) {
|
||||
state = <int>{...state}..remove(activityId);
|
||||
} else {
|
||||
state = {...state, activityId};
|
||||
}
|
||||
}
|
||||
|
||||
void clear() => state = {};
|
||||
}
|
||||
|
||||
final selectedActivitiesProvider =
|
||||
NotifierProvider.autoDispose<SelectedActivities, Set<int>>(
|
||||
SelectedActivities.new);
|
||||
|
||||
// ── Activity notifier (create / delete custom activities) ─────────────────────
|
||||
|
||||
class ActivityNotifier extends Notifier<AsyncValue<void>> {
|
||||
@override
|
||||
AsyncValue<void> build() => const AsyncData(null);
|
||||
|
||||
Future<void> addActivity({
|
||||
required String name,
|
||||
required int iconCodepoint,
|
||||
required String colourHex,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(activityDaoProvider).insertActivity(
|
||||
ActivitiesCompanion.insert(
|
||||
name: name,
|
||||
iconCodepoint: iconCodepoint,
|
||||
colourHex: colourHex,
|
||||
isCustom: const Value(true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteActivity(int id) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(activityDaoProvider).deleteActivity(id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final activityNotifierProvider =
|
||||
NotifierProvider.autoDispose<ActivityNotifier, AsyncValue<void>>(
|
||||
ActivityNotifier.new);
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../database/app_database.dart';
|
||||
import '../database/daos/mood_dao.dart';
|
||||
import '../database/daos/activity_dao.dart';
|
||||
import '../database/daos/insight_dao.dart';
|
||||
import '../database/daos/settings_dao.dart';
|
||||
|
||||
final appDatabaseProvider = Provider<AppDatabase>((ref) {
|
||||
final db = AppDatabase();
|
||||
ref.onDispose(db.close);
|
||||
return db;
|
||||
});
|
||||
|
||||
final moodDaoProvider =
|
||||
Provider<MoodDao>((ref) => ref.watch(appDatabaseProvider).moodDao);
|
||||
|
||||
final activityDaoProvider =
|
||||
Provider<ActivityDao>((ref) => ref.watch(appDatabaseProvider).activityDao);
|
||||
|
||||
final insightDaoProvider =
|
||||
Provider<InsightDao>((ref) => ref.watch(appDatabaseProvider).insightDao);
|
||||
|
||||
final settingsDaoProvider =
|
||||
Provider<SettingsDao>((ref) => ref.watch(appDatabaseProvider).settingsDao);
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../database/app_database.dart';
|
||||
import 'database_provider.dart';
|
||||
|
||||
// ── Insight type enum ─────────────────────────────────────────────────────────
|
||||
|
||||
enum InsightType { weekly, monthly, query }
|
||||
|
||||
// ── Cached insight lookup ─────────────────────────────────────────────────────
|
||||
|
||||
final cachedInsightProvider =
|
||||
FutureProvider.autoDispose.family<LlmInsight?, String>((ref, promptHash) {
|
||||
return ref.watch(insightDaoProvider).getCached(promptHash);
|
||||
});
|
||||
|
||||
// ── Insight notifier — triggers generation and caches results ─────────────────
|
||||
|
||||
class InsightNotifier extends Notifier<AsyncValue<String?>> {
|
||||
@override
|
||||
AsyncValue<String?> build() => const AsyncData(null);
|
||||
|
||||
String _hashPrompt(String prompt) =>
|
||||
sha256.convert(utf8.encode(prompt)).toString();
|
||||
|
||||
Future<void> generateInsight({
|
||||
required String prompt,
|
||||
required InsightType type,
|
||||
DateTime? rangeStart,
|
||||
DateTime? rangeEnd,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
|
||||
final hash = _hashPrompt(prompt);
|
||||
final dao = ref.read(insightDaoProvider);
|
||||
|
||||
final cached = await dao.getCached(hash);
|
||||
if (cached != null) {
|
||||
state = AsyncData(cached.responseText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generation happens in LlmNotifier (see llm_provider.dart)
|
||||
state = const AsyncData(null);
|
||||
}
|
||||
|
||||
Future<void> saveInsight({
|
||||
required String promptHash,
|
||||
required String responseText,
|
||||
required InsightType type,
|
||||
DateTime? rangeStart,
|
||||
DateTime? rangeEnd,
|
||||
}) async {
|
||||
await ref.read(insightDaoProvider).upsertInsight(
|
||||
LlmInsightsCompanion.insert(
|
||||
type: type.name,
|
||||
promptHash: promptHash,
|
||||
responseText: responseText,
|
||||
rangeStart: Value(rangeStart),
|
||||
rangeEnd: Value(rangeEnd),
|
||||
),
|
||||
);
|
||||
state = AsyncData(responseText);
|
||||
}
|
||||
|
||||
void clear() => state = const AsyncData(null);
|
||||
}
|
||||
|
||||
final insightNotifierProvider =
|
||||
NotifierProvider.autoDispose<InsightNotifier, AsyncValue<String?>>(
|
||||
InsightNotifier.new);
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../llm/llm_service.dart';
|
||||
import '../llm/mediapipe_llm_service.dart';
|
||||
|
||||
// ── Service singleton ─────────────────────────────────────────────────────────
|
||||
|
||||
final llmServiceProvider = Provider<LlmService>((ref) {
|
||||
final service = MediapipeLlmService();
|
||||
ref.onDispose(service.dispose);
|
||||
return service;
|
||||
});
|
||||
|
||||
// ── Model initialisation state ────────────────────────────────────────────────
|
||||
|
||||
final llmReadyProvider = FutureProvider<void>((ref) async {
|
||||
await ref.watch(llmServiceProvider).initialise();
|
||||
});
|
||||
|
||||
// ── Streaming response notifier ───────────────────────────────────────────────
|
||||
|
||||
class LlmResponseNotifier extends Notifier<AsyncValue<String>> {
|
||||
bool _cancelled = false;
|
||||
|
||||
@override
|
||||
AsyncValue<String> build() => const AsyncData('');
|
||||
|
||||
Future<void> generate(String prompt) async {
|
||||
_cancelled = false;
|
||||
state = const AsyncLoading();
|
||||
|
||||
final service = ref.read(llmServiceProvider);
|
||||
final buffer = StringBuffer();
|
||||
|
||||
try {
|
||||
await for (final token in service.generateStream(prompt)) {
|
||||
if (_cancelled) break;
|
||||
buffer.write(token);
|
||||
state = AsyncData(buffer.toString());
|
||||
}
|
||||
} catch (e, st) {
|
||||
state = AsyncError(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_cancelled = true;
|
||||
state = const AsyncData('');
|
||||
}
|
||||
|
||||
void clear() => state = const AsyncData('');
|
||||
}
|
||||
|
||||
final llmResponseNotifierProvider =
|
||||
NotifierProvider.autoDispose<LlmResponseNotifier, AsyncValue<String>>(
|
||||
LlmResponseNotifier.new);
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../database/app_database.dart';
|
||||
import 'database_provider.dart';
|
||||
|
||||
// ── All entries stream ────────────────────────────────────────────────────────
|
||||
|
||||
final moodEntriesProvider = StreamProvider.autoDispose<List<MoodEntry>>((ref) {
|
||||
return ref.watch(moodDaoProvider).watchAllEntries();
|
||||
});
|
||||
|
||||
// ── Entries for a specific day ────────────────────────────────────────────────
|
||||
|
||||
final moodEntriesForDayProvider =
|
||||
StreamProvider.autoDispose.family<List<MoodEntry>, DateTime>((ref, day) {
|
||||
return ref.watch(moodDaoProvider).watchEntriesForDay(day);
|
||||
});
|
||||
|
||||
// ── Daily averages for calendar heatmap ──────────────────────────────────────
|
||||
|
||||
final dailyAveragesProvider = FutureProvider.autoDispose
|
||||
.family<Map<DateTime, double>, ({DateTime start, DateTime end})>(
|
||||
(ref, range) =>
|
||||
ref.watch(moodDaoProvider).getDailyAverages(range.start, range.end),
|
||||
);
|
||||
|
||||
// ── Selected day (drives calendar + day detail) ───────────────────────────────
|
||||
|
||||
class SelectedDay extends Notifier<DateTime> {
|
||||
@override
|
||||
DateTime build() => DateTime.now();
|
||||
|
||||
void select(DateTime day) {
|
||||
state = DateTime(day.year, day.month, day.day);
|
||||
}
|
||||
|
||||
void selectToday() {
|
||||
final now = DateTime.now();
|
||||
state = DateTime(now.year, now.month, now.day);
|
||||
}
|
||||
}
|
||||
|
||||
final selectedDayProvider =
|
||||
NotifierProvider.autoDispose<SelectedDay, DateTime>(SelectedDay.new);
|
||||
|
||||
// ── Mood entry notifier (create / update / delete) ────────────────────────────
|
||||
|
||||
class MoodEntryNotifier extends Notifier<AsyncValue<void>> {
|
||||
@override
|
||||
AsyncValue<void> build() => const AsyncData(null);
|
||||
|
||||
Future<void> addEntry({
|
||||
required int moodLevel,
|
||||
String? note,
|
||||
List<int> activityIds = const [],
|
||||
List<int> tagIds = const [],
|
||||
DateTime? timestamp,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final dao = ref.read(moodDaoProvider);
|
||||
await dao.insertEntryWithActivities(
|
||||
entry: MoodEntriesCompanion.insert(
|
||||
moodLevel: moodLevel,
|
||||
timestamp: timestamp ?? DateTime.now(),
|
||||
note: Value(note),
|
||||
),
|
||||
activityIds: activityIds,
|
||||
tagIds: tagIds,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteEntry(int id) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(moodDaoProvider).deleteEntry(id),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateEntry({
|
||||
required int id,
|
||||
required int moodLevel,
|
||||
String? note,
|
||||
DateTime? timestamp,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(moodDaoProvider).updateEntry(
|
||||
MoodEntriesCompanion(
|
||||
id: Value(id),
|
||||
moodLevel: Value(moodLevel),
|
||||
note: Value(note),
|
||||
timestamp: Value(timestamp ?? DateTime.now()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final moodEntryNotifierProvider =
|
||||
NotifierProvider.autoDispose<MoodEntryNotifier, AsyncValue<void>>(
|
||||
MoodEntryNotifier.new);
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'database_provider.dart';
|
||||
|
||||
// ── Key constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
abstract class SettingsKeys {
|
||||
static const reminderEnabled = 'reminder_enabled';
|
||||
static const reminderTime = 'reminder_time'; // stored as 'HH:mm'
|
||||
static const biometricEnabled = 'biometric_enabled';
|
||||
static const themMode = 'theme_mode'; // 'system' | 'light' | 'dark'
|
||||
static const modelDownloaded = 'model_downloaded';
|
||||
static const onboardingComplete = 'onboarding_complete';
|
||||
}
|
||||
|
||||
// ── Generic setting reader ────────────────────────────────────────────────────
|
||||
|
||||
final settingProvider =
|
||||
FutureProvider.autoDispose.family<String?, String>((ref, key) {
|
||||
return ref.watch(settingsDaoProvider).get(key);
|
||||
});
|
||||
|
||||
// ── Settings notifier ─────────────────────────────────────────────────────────
|
||||
|
||||
class SettingsNotifier extends Notifier<AsyncValue<void>> {
|
||||
@override
|
||||
AsyncValue<void> build() => const AsyncData(null);
|
||||
|
||||
Future<void> set(String key, String value) async {
|
||||
await ref.read(settingsDaoProvider).set(key, value);
|
||||
ref.invalidate(settingProvider(key));
|
||||
}
|
||||
|
||||
Future<void> setReminderEnabled(bool enabled) =>
|
||||
set(SettingsKeys.reminderEnabled, enabled.toString());
|
||||
|
||||
Future<void> setReminderTime(int hour, int minute) => set(
|
||||
SettingsKeys.reminderTime,
|
||||
'${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}');
|
||||
|
||||
Future<void> setBiometricEnabled(bool enabled) =>
|
||||
set(SettingsKeys.biometricEnabled, enabled.toString());
|
||||
|
||||
Future<void> setThemeMode(String mode) => set(SettingsKeys.themMode, mode);
|
||||
|
||||
Future<void> markModelDownloaded() =>
|
||||
set(SettingsKeys.modelDownloaded, 'true');
|
||||
|
||||
Future<void> markOnboardingComplete() =>
|
||||
set(SettingsKeys.onboardingComplete, 'true');
|
||||
}
|
||||
|
||||
final settingsNotifierProvider =
|
||||
NotifierProvider.autoDispose<SettingsNotifier, AsyncValue<void>>(
|
||||
SettingsNotifier.new);
|
||||
|
||||
// ── Convenience bool readers ──────────────────────────────────────────────────
|
||||
|
||||
final isOnboardingCompleteProvider =
|
||||
FutureProvider.autoDispose<bool>((ref) async {
|
||||
final val =
|
||||
await ref.watch(settingProvider(SettingsKeys.onboardingComplete).future);
|
||||
return val == 'true';
|
||||
});
|
||||
|
||||
final isModelDownloadedProvider = FutureProvider.autoDispose<bool>((ref) async {
|
||||
final val =
|
||||
await ref.watch(settingProvider(SettingsKeys.modelDownloaded).future);
|
||||
return val == 'true';
|
||||
});
|
||||
Reference in New Issue
Block a user