Files
impact/lib/data/database/database_helper.dart
2026-01-18 13:38:09 +01:00

255 lines
6.6 KiB
Dart

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/session.dart';
import '../models/shot.dart';
import '../../core/constants/app_constants.dart';
class DatabaseHelper {
static DatabaseHelper? _instance;
static Database? _database;
DatabaseHelper._internal();
factory DatabaseHelper() {
_instance ??= DatabaseHelper._internal();
return _instance!;
}
Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, AppConstants.databaseName);
return await openDatabase(
path,
version: AppConstants.databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE ${AppConstants.sessionsTable} (
id TEXT PRIMARY KEY,
target_type TEXT NOT NULL,
image_path TEXT NOT NULL,
total_score INTEGER NOT NULL,
grouping_diameter REAL,
grouping_center_x REAL,
grouping_center_y REAL,
created_at TEXT NOT NULL,
notes TEXT,
target_center_x REAL,
target_center_y REAL,
target_radius REAL
)
''');
await db.execute('''
CREATE TABLE ${AppConstants.shotsTable} (
id TEXT PRIMARY KEY,
x REAL NOT NULL,
y REAL NOT NULL,
score INTEGER NOT NULL,
session_id TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES ${AppConstants.sessionsTable}(id) ON DELETE CASCADE
)
''');
await db.execute('''
CREATE INDEX idx_shots_session_id ON ${AppConstants.shotsTable}(session_id)
''');
await db.execute('''
CREATE INDEX idx_sessions_created_at ON ${AppConstants.sessionsTable}(created_at DESC)
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// Handle future database migrations here
}
// Session operations
Future<int> insertSession(Session session) async {
final db = await database;
return await db.transaction((txn) async {
await txn.insert(
AppConstants.sessionsTable,
session.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
for (final shot in session.shots) {
// Ensure shot has correct session_id
final shotWithSessionId = shot.copyWith(sessionId: session.id);
await txn.insert(
AppConstants.shotsTable,
shotWithSessionId.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
return 1;
});
}
Future<Session?> getSession(String id) async {
final db = await database;
final sessionMaps = await db.query(
AppConstants.sessionsTable,
where: 'id = ?',
whereArgs: [id],
);
if (sessionMaps.isEmpty) return null;
final shotMaps = await db.query(
AppConstants.shotsTable,
where: 'session_id = ?',
whereArgs: [id],
);
final shots = shotMaps.map((map) => Shot.fromMap(map)).toList();
return Session.fromMap(sessionMaps.first, shots);
}
Future<List<Session>> getAllSessions({
String? targetType,
int? limit,
int? offset,
}) async {
final db = await database;
String? whereClause;
List<dynamic>? whereArgs;
if (targetType != null) {
whereClause = 'target_type = ?';
whereArgs = [targetType];
}
final sessionMaps = await db.query(
AppConstants.sessionsTable,
where: whereClause,
whereArgs: whereArgs,
orderBy: 'created_at DESC',
limit: limit,
offset: offset,
);
// First, check if there are orphaned shots (with empty session_id)
// and only one session - if so, assign them to that session
if (sessionMaps.length == 1) {
final orphanedShots = await db.query(
AppConstants.shotsTable,
where: 'session_id = ?',
whereArgs: [''],
);
if (orphanedShots.isNotEmpty) {
await db.update(
AppConstants.shotsTable,
{'session_id': sessionMaps.first['id']},
where: 'session_id = ?',
whereArgs: [''],
);
}
}
final sessions = <Session>[];
for (final sessionMap in sessionMaps) {
final sessionId = sessionMap['id'] as String;
final shotMaps = await db.query(
AppConstants.shotsTable,
where: 'session_id = ?',
whereArgs: [sessionId],
);
final shots = shotMaps.map((map) => Shot.fromMap(map)).toList();
sessions.add(Session.fromMap(sessionMap, shots));
}
return sessions;
}
Future<int> updateSession(Session session) async {
final db = await database;
return await db.transaction((txn) async {
await txn.update(
AppConstants.sessionsTable,
session.toMap(),
where: 'id = ?',
whereArgs: [session.id],
);
// Delete existing shots and insert new ones
await txn.delete(
AppConstants.shotsTable,
where: 'session_id = ?',
whereArgs: [session.id],
);
for (final shot in session.shots) {
// Ensure shot has correct session_id
final shotWithSessionId = shot.copyWith(sessionId: session.id);
await txn.insert(
AppConstants.shotsTable,
shotWithSessionId.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
return 1;
});
}
Future<int> deleteSession(String id) async {
final db = await database;
return await db.delete(
AppConstants.sessionsTable,
where: 'id = ?',
whereArgs: [id],
);
}
Future<Map<String, dynamic>> getStatistics() async {
final db = await database;
final totalSessions = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM ${AppConstants.sessionsTable}'),
) ?? 0;
final totalShots = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM ${AppConstants.shotsTable}'),
) ?? 0;
final avgScore = (await db.rawQuery(
'SELECT AVG(total_score) as avg FROM ${AppConstants.sessionsTable}',
)).first['avg'] as double? ?? 0.0;
final bestScore = Sqflite.firstIntValue(
await db.rawQuery(
'SELECT MAX(total_score) FROM ${AppConstants.sessionsTable}',
),
) ?? 0;
return {
'totalSessions': totalSessions,
'totalShots': totalShots,
'averageScore': avgScore,
'bestScore': bestScore,
};
}
Future<void> close() async {
final db = await database;
await db.close();
_database = null;
}
}