255 lines
6.6 KiB
Dart
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;
|
|
}
|
|
}
|