initial commit

Signed-off-by: Menno van Leeuwen <menno@vleeuwen.me>
This commit is contained in:
2025-08-26 01:21:26 +02:00
commit f8c19f9051
132 changed files with 7054 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart';
import 'dart:io';
import '../models/supplement.dart';
import '../models/supplement_intake.dart';
class DatabaseHelper {
static const _databaseName = 'supplements.db';
static const _databaseVersion = 2; // Increment version for schema changes
static const supplementsTable = 'supplements';
static const intakesTable = 'supplement_intakes';
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
static bool _initialized = false;
static void _initializeDatabaseFactory() {
if (!_initialized) {
// Initialize for desktop platforms
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
_initialized = true;
}
}
Future<Database> get database async {
_initializeDatabaseFactory();
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $supplementsTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
dosageAmount REAL NOT NULL,
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unit TEXT NOT NULL,
unitType TEXT NOT NULL DEFAULT 'units',
frequencyPerDay INTEGER NOT NULL,
reminderTimes TEXT NOT NULL,
notes TEXT,
createdAt TEXT NOT NULL,
isActive INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE $intakesTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
supplementId INTEGER NOT NULL,
takenAt TEXT NOT NULL,
dosageTaken REAL NOT NULL,
unitsTaken INTEGER NOT NULL DEFAULT 1,
notes TEXT,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add new columns for version 2
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN dosageAmount REAL DEFAULT 0');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN numberOfUnits INTEGER DEFAULT 1');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"');
await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken INTEGER DEFAULT 1');
// Migrate existing data
await db.execute('''
UPDATE $supplementsTable
SET dosageAmount = dosage,
numberOfUnits = 1,
unitType = 'units'
WHERE dosageAmount = 0
''');
}
}
// Supplement CRUD operations
Future<int> insertSupplement(Supplement supplement) async {
Database db = await database;
return await db.insert(supplementsTable, supplement.toMap());
}
Future<List<Supplement>> getAllSupplements() async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'isActive = ?',
whereArgs: [1],
orderBy: 'name ASC',
);
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
}
Future<Supplement?> getSupplement(int id) async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return Supplement.fromMap(maps.first);
}
return null;
}
Future<int> updateSupplement(Supplement supplement) async {
Database db = await database;
return await db.update(
supplementsTable,
supplement.toMap(),
where: 'id = ?',
whereArgs: [supplement.id],
);
}
Future<int> deleteSupplement(int id) async {
Database db = await database;
return await db.update(
supplementsTable,
{'isActive': 0},
where: 'id = ?',
whereArgs: [id],
);
}
// Supplement Intake CRUD operations
Future<int> insertIntake(SupplementIntake intake) async {
Database db = await database;
return await db.insert(intakesTable, intake.toMap());
}
Future<List<SupplementIntake>> getIntakesForDate(DateTime date) async {
Database db = await database;
String startDate = DateTime(date.year, date.month, date.day).toIso8601String();
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> maps = await db.query(
intakesTable,
where: 'takenAt >= ? AND takenAt <= ?',
whereArgs: [startDate, endDate],
orderBy: 'takenAt DESC',
);
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
}
Future<List<SupplementIntake>> getIntakesForMonth(int year, int month) async {
Database db = await database;
String startDate = DateTime(year, month, 1).toIso8601String();
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> maps = await db.query(
intakesTable,
where: 'takenAt >= ? AND takenAt <= ?',
whereArgs: [startDate, endDate],
orderBy: 'takenAt DESC',
);
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
}
Future<List<Map<String, dynamic>>> getIntakesWithSupplementsForDate(DateTime date) async {
Database db = await database;
String startDate = DateTime(date.year, date.month, date.day).toIso8601String();
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
ORDER BY i.takenAt DESC
''', [startDate, endDate]);
return result;
}
Future<List<Map<String, dynamic>>> getIntakesWithSupplementsForMonth(int year, int month) async {
Database db = await database;
String startDate = DateTime(year, month, 1).toIso8601String();
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
ORDER BY i.takenAt DESC
''', [startDate, endDate]);
return result;
}
Future<int> deleteIntake(int id) async {
Database db = await database;
return await db.delete(
intakesTable,
where: 'id = ?',
whereArgs: [id],
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
import '../models/supplement.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
tz.initializeTimeZones();
const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notifications.initialize(initSettings);
}
Future<bool> requestPermissions() async {
final androidPlugin = _notifications.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
if (androidPlugin != null) {
await androidPlugin.requestNotificationsPermission();
}
final iosPlugin = _notifications.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
if (iosPlugin != null) {
await iosPlugin.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
return true;
}
Future<void> scheduleSupplementReminders(Supplement supplement) async {
// Cancel existing notifications for this supplement
await cancelSupplementReminders(supplement.id!);
for (int i = 0; i < supplement.reminderTimes.length; i++) {
final timeStr = supplement.reminderTimes[i];
final timeParts = timeStr.split(':');
final hour = int.parse(timeParts[0]);
final minute = int.parse(timeParts[1]);
final notificationId = supplement.id! * 100 + i; // Unique ID for each reminder
await _notifications.zonedSchedule(
notificationId,
'Time for ${supplement.name}',
'Take ${supplement.dosage} ${supplement.unit}',
_nextInstanceOfTime(hour, minute),
const NotificationDetails(
android: AndroidNotificationDetails(
'supplement_reminders',
'Supplement Reminders',
channelDescription: 'Notifications for supplement intake reminders',
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
}
}
Future<void> cancelSupplementReminders(int supplementId) async {
// Cancel all notifications for this supplement (up to 10 possible reminders)
for (int i = 0; i < 10; i++) {
final notificationId = supplementId * 100 + i;
await _notifications.cancel(notificationId);
}
}
Future<void> cancelAllReminders() async {
await _notifications.cancelAll();
}
tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
tz.TZDateTime scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
return scheduledDate;
}
Future<void> showInstantNotification(String title, String body) async {
const NotificationDetails notificationDetails = NotificationDetails(
android: AndroidNotificationDetails(
'instant_notifications',
'Instant Notifications',
channelDescription: 'Instant notifications for supplement app',
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
);
await _notifications.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
notificationDetails,
);
}
}