mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
221
lib/services/database_helper.dart
Normal file
221
lib/services/database_helper.dart
Normal 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],
|
||||
);
|
||||
}
|
||||
}
|
124
lib/services/notification_service.dart
Normal file
124
lib/services/notification_service.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user