220 lines
7.5 KiB
Swift
220 lines
7.5 KiB
Swift
//
|
|
// GRDBDatabaseManager.swift
|
|
// FluxRead
|
|
//
|
|
// Created on 16/12/2025.
|
|
//
|
|
|
|
import Foundation
|
|
import GRDB
|
|
|
|
class GRDBDatabaseManager {
|
|
|
|
private var dbQueue: DatabaseQueue?
|
|
|
|
init() {
|
|
do {
|
|
try setupDatabase()
|
|
} catch {
|
|
print("❌ Failed to setup GRDB database: \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Database Setup
|
|
|
|
private func setupDatabase() throws {
|
|
let fileManager = FileManager.default
|
|
let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
let dbPath = documentsPath.appendingPathComponent("fluxread_grdb.sqlite").path
|
|
|
|
// Create database queue
|
|
dbQueue = try DatabaseQueue(path: dbPath)
|
|
|
|
// Create tables
|
|
try dbQueue?.write { db in
|
|
try createTables(db)
|
|
}
|
|
}
|
|
|
|
private func createTables(_ db: Database) throws {
|
|
|
|
/*
|
|
try db.create(table: GRDBComicBookFile.databaseTableName, ifNotExists: true) { t in
|
|
|
|
t.column("fileHash", .text).primaryKey()
|
|
t.column("name", .text).notNull()
|
|
t.column("fileSize", .integer).notNull()
|
|
t.column("dateAdded", .datetime).notNull()
|
|
t.column("filePath", .text).notNull()
|
|
t.column("numberOfPages", .integer).notNull()
|
|
|
|
t.column("pageCount", .integer)
|
|
|
|
t.column("lastReadDate", .datetime).indexed()
|
|
t.column("issueDate", .datetime)
|
|
t.column("isRead", .boolean).notNull().defaults(to: false).indexed()
|
|
t.column("readCompletion", .integer).notNull().defaults(to: 0).indexed()
|
|
t.column("syncDate", .datetime).indexed()
|
|
t.column("estimatedProcessingDate", .datetime)
|
|
|
|
t.column("providerId", .text)
|
|
t.column("providerPath", .text)
|
|
t.column("isAvailableLocally", .boolean).indexed()
|
|
t.column("parentFileHash", .text).indexed()
|
|
|
|
}
|
|
|
|
try db.create(table: GRDBComicBookPage.databaseTableName, ifNotExists: true) { t in
|
|
t.column("comicBookId", .text).notNull()
|
|
.indexed()
|
|
|
|
t.column("fileHash", .text).notNull()
|
|
t.column("pageNumber", .integer).notNull().indexed()
|
|
t.column("syncDate", .datetime).indexed()
|
|
t.primaryKey(["comicBookId", "fileHash"])
|
|
|
|
}
|
|
|
|
try db.create(table: GRDBComicBookPagePanel.databaseTableName, ifNotExists: true) { t in
|
|
t.column("comicBookId", .text).notNull()
|
|
.indexed()
|
|
t.column("comicBookPageId", .text).notNull()
|
|
.indexed()
|
|
t.column("panelNumberInPage", .integer).notNull()
|
|
t.column("pageNumber", .integer).notNull()
|
|
t.column("left", .integer).notNull()
|
|
t.column("top", .integer).notNull()
|
|
t.column("right", .integer).notNull()
|
|
t.column("bottom", .integer).notNull()
|
|
t.column("syncDate", .datetime).indexed()
|
|
t.primaryKey(["comicBookId", "comicBookPageId", "panelNumberInPage", "pageNumber"])
|
|
}
|
|
|
|
try db.create(table: GRDBComicBookTitle.databaseTableName, ifNotExists: true) { t in
|
|
t.column("id", .text).primaryKey()
|
|
t.column("name", .text).notNull()
|
|
t.column("age", .integer).notNull()
|
|
}
|
|
|
|
try db.create(table: GRDBComicBookPageReadingStat.databaseTableName, ifNotExists: true) { t in
|
|
t.column("comicBookId", .text).notNull().indexed()
|
|
t.column("comicBookPageId", .text).notNull().indexed()
|
|
t.column("comicBookPageNumber", .integer).notNull().indexed()
|
|
t.column("startDate", .datetime).notNull()
|
|
t.column("endDate", .datetime).notNull()
|
|
t.column("numberOfSeconds", .integer).notNull()
|
|
t.column("syncDate", .datetime).indexed()
|
|
t.primaryKey(["comicBookId", "comicBookPageId", "comicBookPageNumber", "startDate"])
|
|
}
|
|
|
|
try db.create(table: LibraryImportProvider.databaseTableName, ifNotExists: true) { t in
|
|
t.column("providerId", .text).primaryKey()
|
|
t.column("connectionName", .text).notNull()
|
|
t.column("providerType", .integer).notNull()
|
|
t.column("hostName", .text).notNull()
|
|
t.column("port", .integer).notNull()
|
|
t.column("username", .text).notNull()
|
|
t.column("path", .text).notNull()
|
|
t.column("authenticationType", .integer).notNull()
|
|
|
|
}
|
|
try db.create(table: ComicBookMetaDataAttribute.databaseTableName, ifNotExists: true) { t in
|
|
t.column("attributeId", .integer).notNull()
|
|
t.column("attributeValue", .text).notNull()
|
|
t.column("attributeType", .integer).notNull()
|
|
|
|
}
|
|
|
|
try db.create(table: ComicBookMetaDataAttributeRelation.databaseTableName, ifNotExists: true) { t in
|
|
t.column("attributeId", .integer).notNull()
|
|
t.column("comicBookId", .text).notNull()
|
|
t.primaryKey(["attributeId", "comicBookId"])
|
|
|
|
}
|
|
|
|
try db.create(table: ComicBookMetadata.databaseTableName, ifNotExists: true) { t in
|
|
t.column("comicBookId", .text).primaryKey()
|
|
t.column("creationDate", .datetime).notNull()
|
|
|
|
t.column("metadata", .text).notNull()
|
|
|
|
}
|
|
|
|
try db.create(table: GRDBComicBookPagePanelText.databaseTableName, ifNotExists: true) { t in
|
|
|
|
t.column("comicBookId", .text).notNull()
|
|
t.column("comicBookPageId", .integer).notNull()
|
|
t.column("textBlockNumber", .integer).notNull()
|
|
t.column("textBlock", .text).notNull()
|
|
t.column("left", .double).notNull()
|
|
t.column("top", .double).notNull()
|
|
t.column("right", .double).notNull()
|
|
t.column("bottom", .double).notNull()
|
|
t.primaryKey(["comicBookId", "comicBookPageId", "textBlockNumber"])
|
|
|
|
}
|
|
|
|
try db.create(table: UserActivity.databaseTableName, ifNotExists: true) { t in
|
|
|
|
t.column("activityId", .integer).primaryKey()
|
|
|
|
t.column("activityType", .integer).notNull()
|
|
t.column("creationDate", .datetime).notNull()
|
|
t.column("activityDescription", .text)
|
|
t.column("comicId", .integer)
|
|
t.column("activityDetail", .text)
|
|
|
|
}
|
|
|
|
try db.create(table: SavedSearch.databaseTableName, ifNotExists: true) { t in
|
|
|
|
t.column("id", .text).primaryKey()
|
|
t.column("query", .text).notNull()
|
|
t.column("attributeId", .integer).notNull().indexed()
|
|
|
|
}
|
|
|
|
try db.create(table: SavedSearchComic.databaseTableName, ifNotExists: true) { t in
|
|
|
|
t.column("id", .text).primaryKey()
|
|
t.column("savedSearchId", .text).notNull().indexed()
|
|
t.column("comicId", .text).notNull()
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
}
|
|
|
|
// MARK: - Public Access
|
|
|
|
func getDatabase() -> DatabaseQueue? {
|
|
return dbQueue
|
|
}
|
|
|
|
func write<T>(_ updates: (Database) throws -> T) throws -> T {
|
|
guard let dbQueue = dbQueue else {
|
|
throw DatabaseError.notInitialized
|
|
}
|
|
return try dbQueue.write(updates)
|
|
}
|
|
|
|
func read<T>(_ value: (Database) throws -> T) throws -> T {
|
|
guard let dbQueue = dbQueue else {
|
|
throw DatabaseError.notInitialized
|
|
}
|
|
return try dbQueue.read(value)
|
|
}
|
|
|
|
// MARK: - Error Types
|
|
|
|
enum DatabaseError: Error {
|
|
case notInitialized
|
|
case recordNotFound
|
|
case insertFailed
|
|
case updateFailed
|
|
case deleteFailed
|
|
}
|
|
}
|