From a9615748d12ba448c71246fce363c22747c1bf3d Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 3 Jul 2026 22:18:42 +0800 Subject: [PATCH 1/2] chore: bump version to 6.0 --- KMReader.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/KMReader.xcodeproj/project.pbxproj b/KMReader.xcodeproj/project.pbxproj index 268694da..1c8d7b02 100644 --- a/KMReader.xcodeproj/project.pbxproj +++ b/KMReader.xcodeproj/project.pbxproj @@ -442,7 +442,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "KMReader/KMReader-macOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = M777UHWZA4; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -468,7 +468,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14; - MARKETING_VERSION = 5.3; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.everpcpc.Komga; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -500,7 +500,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "KMReader/KMReader-macOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=appletvos*]" = M777UHWZA4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = M777UHWZA4; @@ -529,7 +529,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14; - MARKETING_VERSION = 5.3; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.everpcpc.Komga; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -560,7 +560,7 @@ CODE_SIGN_ENTITLEMENTS = KMReaderWidgets/KMReaderWidgets.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = M777UHWZA4; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = KMReaderWidgets/Info.plist; @@ -572,7 +572,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.3; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.everpcpc.Komga.KMReaderWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -594,7 +594,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = M777UHWZA4; GENERATE_INFOPLIST_FILE = YES; @@ -607,7 +607,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.3; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.everpcpc.Komga.KMReaderWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From cfe3cf11c261a076976659c42d8cf9c79f4ef1ef Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 3 Jul 2026 22:31:18 +0800 Subject: [PATCH 2/2] refactor(storage): remove SwiftData legacy import Remove the SwiftData schema and legacy importer now that runtime storage is fully GRDB-backed. Startup now only opens and migrates the GRDB database, local data reset only tracks the active SQLite store, and AGENTS documents GRDB as the persistence layer. Add an append-only GRDB migration to drop the obsolete local migration marker table without mutating historical migrations. --- AGENTS.md | 14 +- .../Core/Storage/GRDB/LocalDatabase.swift | 53 +- .../Legacy/LegacySwiftDataImporter.swift | 390 ------------- .../Core/Storage/LocalDataResetService.swift | 3 - .../Schema/KMReaderMigrationPlan.swift | 552 ------------------ .../Storage/Schema/KMReaderSchemaV1.swift | 302 ---------- .../Storage/Schema/KMReaderSchemaV2.swift | 284 --------- .../Storage/Schema/KMReaderSchemaV3.swift | 257 -------- .../Storage/Schema/KMReaderSchemaV4.swift | 259 -------- .../Storage/Schema/KMReaderSchemaV5.swift | 260 --------- .../Storage/Schema/KMReaderSchemaV6.swift | 533 ----------------- KMReader/MainApp.swift | 1 - 12 files changed, 12 insertions(+), 2896 deletions(-) delete mode 100644 KMReader/Core/Storage/Legacy/LegacySwiftDataImporter.swift delete mode 100644 KMReader/Core/Storage/Schema/KMReaderMigrationPlan.swift delete mode 100644 KMReader/Core/Storage/Schema/KMReaderSchemaV1.swift delete mode 100644 KMReader/Core/Storage/Schema/KMReaderSchemaV2.swift delete mode 100644 KMReader/Core/Storage/Schema/KMReaderSchemaV3.swift delete mode 100644 KMReader/Core/Storage/Schema/KMReaderSchemaV4.swift delete mode 100644 KMReader/Core/Storage/Schema/KMReaderSchemaV5.swift delete mode 100644 KMReader/Core/Storage/Schema/KMReaderSchemaV6.swift diff --git a/AGENTS.md b/AGENTS.md index cf4c40b8..66ad39b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -176,7 +176,7 @@ Test with: - **UI**: SwiftUI, UIKit, and AppKit are all acceptable. Choose the most maintainable and platform-appropriate approach per feature. - **State**: `@Observable` pattern (not `ObservableObject`) -- **Persistence**: SwiftData for profiles/libraries/fonts/series/books/collections/read lists/dashboard caches, UserDefaults via `AppConfig` +- **Persistence**: GRDB for profiles/libraries/fonts/series/books/collections/read lists/dashboard caches, UserDefaults via `AppConfig` - **Networking**: Centralized `APIClient` with feature-specific services - **Real-time**: Server-Sent Events (SSE) via `SSEService` - **Error Handling**: Route all user-visible errors through `ErrorManager.shared` (Core/Storage/Errors/) @@ -186,7 +186,7 @@ Test with: ``` KMReader/ -├── MainApp.swift # Entry point, SwiftData setup, environment injection +├── MainApp.swift # Entry point, GRDB setup, environment injection ├── ContentView.swift # Main navigation, login/tab switching ├── MainSplitView.swift # Split view shell for macOS/iPad ├── PhoneTabView.swift # iPhone tab shell (iOS 18+) @@ -295,7 +295,7 @@ KMReader/ **App Lifecycle** -- `MainApp.swift`: Loads SwiftData schema, configures stores, registers iOS AppDelegate for background downloads +- `MainApp.swift`: Opens and migrates the GRDB store, configures stores, registers iOS AppDelegate for background downloads - `ContentView.swift`: Chooses onboarding (`LandingView`) or authenticated shells - macOS and iPad: `MainSplitView` - iPhone: `PhoneTabView` (iOS 18+) or `OldTabView` @@ -309,7 +309,7 @@ KMReader/ **State & Persistence** -- **SwiftData**: `KomgaInstance`, `KomgaLibrary`, `KomgaSeries`, `KomgaBook`, `KomgaCollection`, `KomgaReadList`, `CustomFont`, `PendingProgress` with dedicated stores +- **GRDB**: `KomgaInstance`, `KomgaLibrary`, `KomgaSeries`, `KomgaBook`, `KomgaCollection`, `KomgaReadList`, `CustomFont`, `PendingProgress` with dedicated stores - **AppConfig**: Centralizes UserDefaults (server URL, tokens, SSE toggles, reader preferences, cache budgets, API timeout/retry settings) - **Caches**: Multi-tier caching scoped per Komga instance via `CacheNamespace` (managed by `CacheManager`) - Use `@AppStorage` in views, `AppConfig` elsewhere @@ -320,7 +320,7 @@ KMReader/ - `APIClient.swift`: Authenticated requests, JSON decoding, OSLog logging, configurable timeout and retry - Feature services mirror `openapi.json` endpoints with pagination/filtering - Services organized by domain: `AuthService`, `BookService`, `SeriesService`, `LibraryService`, etc. -- Authentication: `AuthService` + SwiftData `KomgaInstance` stores + `AppConfig` +- Authentication: `AuthService` + GRDB `KomgaInstance` stores + `AppConfig` **Real-time Updates** @@ -343,7 +343,7 @@ KMReader/ **Sync & Initialization** -- `SyncService`: Syncs data between server and local SwiftData +- `SyncService`: Syncs data between server and local GRDB storage - `ProgressSyncService`: Syncs read progress to server - `SyncViewModel`: Exposes synchronization state to SwiftUI and delegates work to `SyncWorker` - `SyncWorker`: Runs synchronization, pagination, reconciliation, and persistence off the main actor @@ -376,7 +376,7 @@ Additional patterns: - Pass shared object dependencies explicitly at split/tab roots, `NavigationStack` roots, sheets, full-screen covers, scene boundaries, and any `UIHostingController`/`NSHostingController` boundary; do not assume outer environment inheritance is stable during snapshot, rotation, or scene transitions - For architecture-level bugs, prefer replacing the confused layer instead of adding compensating state around it. Small patches are acceptable only when the underlying ownership model is already sound. - SSE callbacks are single-assignment closures; implement dispatchers if multiple components need the same event -- Clearing caches/server data must go through `CacheManager` and SwiftData stores +- Clearing caches/server data must go through `CacheManager` and GRDB stores - New API endpoints belong in appropriate service; keep request-building out of views - Dashboard/library selections stored via `LibraryManager` and related managers - All logging goes through `AppLogger` with OSLog subsystems and categories diff --git a/KMReader/Core/Storage/GRDB/LocalDatabase.swift b/KMReader/Core/Storage/GRDB/LocalDatabase.swift index 5b0974d8..2795c476 100644 --- a/KMReader/Core/Storage/GRDB/LocalDatabase.swift +++ b/KMReader/Core/Storage/GRDB/LocalDatabase.swift @@ -6,45 +6,8 @@ import Foundation import GRDB -enum LegacyImportMarkerState: String, Codable, Sendable { - case missing - case completed - case failed -} - -nonisolated struct LocalMigrationMarker: Codable, Sendable { - static let databaseTableName = "local_migration_markers" - - var key: String - var state: LegacyImportMarkerState - var message: String? - var updatedAt: Date - - init( - key: String, - state: LegacyImportMarkerState, - message: String? = nil, - updatedAt: Date = Date() - ) { - self.key = key - self.state = state - self.message = message - self.updatedAt = updatedAt - } - - enum CodingKeys: String, CodingKey { - case key - case state - case message - case updatedAt = "updated_at" - } -} - -nonisolated extension LocalMigrationMarker: FetchableRecord, MutablePersistableRecord {} - nonisolated enum LocalDatabase { static let fileName = "KMReader.sqlite" - static let legacyImportMarkerKey = "swiftdata_v6_import" static func open() throws -> DatabaseQueue { let url = try databaseURL() @@ -100,6 +63,10 @@ nonisolated enum LocalDatabase { } } + migrator.registerMigration("00006_drop_migration_markers") { db in + try db.execute(sql: "DROP TABLE IF EXISTS local_migration_markers") + } + try migrator.migrate(writer) } @@ -108,18 +75,8 @@ nonisolated enum LocalDatabase { return supportDirectory.appendingPathComponent(fileName) } - static func legacyStoreCandidates(fileManager: FileManager = .default) -> [URL] { - var storeDirectories = AppStorageDirectory.supportDirectoryCandidates(fileManager: fileManager) - - if let sharedContainer = WidgetDataStore.sharedContainerURL { - storeDirectories.append(sharedContainer.appendingPathComponent("Library/Application Support", isDirectory: true)) - } - - return storeDirectories.map { $0.appendingPathComponent("default.store") } - } - private static nonisolated func createMarkerTable(_ db: Database) throws { - try db.create(table: LocalMigrationMarker.databaseTableName, ifNotExists: true) { table in + try db.create(table: "local_migration_markers", ifNotExists: true) { table in table.column("key", .text).primaryKey() table.column("state", .text).notNull() table.column("message", .text) diff --git a/KMReader/Core/Storage/Legacy/LegacySwiftDataImporter.swift b/KMReader/Core/Storage/Legacy/LegacySwiftDataImporter.swift deleted file mode 100644 index 3257b1b8..00000000 --- a/KMReader/Core/Storage/Legacy/LegacySwiftDataImporter.swift +++ /dev/null @@ -1,390 +0,0 @@ -// -// LegacySwiftDataImporter.swift -// -// - -import Foundation -import GRDB -import SwiftData - -nonisolated enum LegacySwiftDataImporter { - static func importIfNeeded(into dbQueue: DatabaseQueue) throws { - let marker = try dbQueue.read { db in - try LocalMigrationMarker.fetchOne(db, key: LocalDatabase.legacyImportMarkerKey) - } - - if marker?.state == .completed || marker?.state == .missing { - return - } - - guard legacyStoreExists() else { - try writeMarker(.missing, message: nil, into: dbQueue) - return - } - - do { - let container = try makeLegacyContainer() - let context = ModelContext(container) - try importAll(from: context, into: dbQueue) - try writeMarker(.completed, message: nil, into: dbQueue) - } catch { - try? writeMarker(.failed, message: String(describing: error), into: dbQueue) - throw error - } - } - - private static func legacyStoreExists(fileManager: FileManager = .default) -> Bool { - LocalDatabase.legacyStoreCandidates(fileManager: fileManager).contains { url in - fileManager.fileExists(atPath: url.path) - } - } - - private static func makeLegacyContainer() throws -> ModelContainer { - let schema = Schema(versionedSchema: KMReaderSchemaV6.self) - let configuration = ModelConfiguration(schema: schema) - do { - return try ModelContainer( - for: schema, - migrationPlan: KMReaderMigrationPlan.self, - configurations: [configuration] - ) - } catch { - AppLogger(.database).warning( - "Staged SwiftData legacy migration failed; retrying inferred migration for import only: \(String(describing: error))" - ) - return try ModelContainer(for: schema, configurations: [configuration]) - } - } - - private static func writeMarker( - _ state: LegacyImportMarkerState, - message: String?, - into dbQueue: DatabaseQueue - ) throws { - try dbQueue.write { db in - var marker = LocalMigrationMarker( - key: LocalDatabase.legacyImportMarkerKey, - state: state, - message: message - ) - try marker.save(db) - } - } - - private static func importAll(from context: ModelContext, into dbQueue: DatabaseQueue) throws { - try importBatches( - KMReaderSchemaV6.KomgaInstance.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, instance in - var record = KomgaInstance( - id: instance.id, - name: instance.name, - serverURL: instance.serverURL, - username: instance.username, - authToken: instance.authToken, - isAdmin: instance.isAdmin, - authMethod: instance.authMethod ?? .basicAuth, - createdAt: instance.createdAt, - lastUsedAt: instance.lastUsedAt, - seriesLastSyncedAt: instance.seriesLastSyncedAt, - booksLastSyncedAt: instance.booksLastSyncedAt - ) - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.KomgaLibrary.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, library in - var record = KomgaLibrary( - id: library.id, - instanceId: library.instanceId, - libraryId: library.libraryId, - name: library.name, - createdAt: library.createdAt, - fileSize: library.fileSize, - booksCount: library.booksCount, - seriesCount: library.seriesCount, - sidecarsCount: library.sidecarsCount, - collectionsCount: library.collectionsCount, - readlistsCount: library.readlistsCount - ) - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.KomgaSeries.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, item in - let metadata = RawCodableStore.decode(SeriesMetadata.self, from: item.metadataRaw) ?? .empty - let booksMetadata = RawCodableStore.decode(SeriesBooksMetadata.self, from: item.booksMetadataRaw) ?? .empty - var record = KomgaSeries( - id: item.id, - seriesId: item.seriesId, - libraryId: item.libraryId, - instanceId: item.instanceId, - name: item.name, - url: item.url, - created: item.created, - lastModified: item.lastModified, - booksCount: item.booksCount, - booksReadCount: item.booksReadCount, - booksUnreadCount: item.booksUnreadCount, - booksInProgressCount: item.booksInProgressCount, - metadata: metadata, - booksMetadata: booksMetadata, - isUnavailable: item.isUnavailable, - oneshot: item.oneshot, - downloadedBooks: item.downloadedBooks, - pendingBooks: item.pendingBooks, - downloadedSize: item.downloadedSize, - offlinePolicy: OfflinePolicy(storageValue: item.offlinePolicyRaw), - offlinePolicyLimit: item.offlinePolicyLimit - ) - record.metadataRaw = item.metadataRaw - record.booksMetadataRaw = item.booksMetadataRaw - record.metaTitle = item.metaTitle - record.metaTitleSort = item.metaTitleSort - record.metaPublisherIndex = item.metaPublisherIndex - record.metaAuthorsIndex = item.metaAuthorsIndex - record.metaGenresIndex = item.metaGenresIndex - record.metaTagsIndex = item.metaTagsIndex - record.metaLanguageIndex = item.metaLanguageIndex - record.downloadStatusRaw = item.downloadStatusRaw - record.downloadError = item.downloadError - record.downloadAt = item.downloadAt - record.collectionIdsRaw = item.collectionIdsRaw - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.KomgaBook.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, item in - let media = RawCodableStore.decode(Media.self, from: item.mediaRaw) ?? .empty - let metadata = RawCodableStore.decode(BookMetadata.self, from: item.metadataRaw) ?? .empty - let readProgress = RawCodableStore.decode(ReadProgress.self, from: item.readProgressRaw) - var record = KomgaBook( - id: item.id, - bookId: item.bookId, - seriesId: item.seriesId, - libraryId: item.libraryId, - instanceId: item.instanceId, - name: item.name, - url: item.url, - number: item.number, - created: item.created, - lastModified: item.lastModified, - sizeBytes: item.sizeBytes, - size: item.size, - media: media, - metadata: metadata, - readProgress: readProgress, - isUnavailable: item.isUnavailable, - oneshot: item.oneshot, - seriesTitle: item.seriesTitle, - downloadedSize: item.downloadedSize - ) - record.mediaRaw = item.mediaRaw - record.metadataRaw = item.metadataRaw - record.readProgressRaw = item.readProgressRaw - record.mediaPagesCount = item.mediaPagesCount - record.mediaProfile = item.mediaProfile - record.metaTitle = item.metaTitle - record.metaNumber = item.metaNumber - record.metaNumberSort = item.metaNumberSort - record.metaReleaseDate = item.metaReleaseDate - record.progressPage = item.progressPage - record.progressCompleted = item.progressCompleted - record.progressReadDate = item.progressReadDate - record.metaAuthorsIndex = item.metaAuthorsIndex - record.metaTagsIndex = item.metaTagsIndex - record.pagesRaw = item.pagesRaw - record.tocRaw = item.tocRaw - record.webPubManifestRaw = item.webPubManifestRaw - record.epubProgressionRaw = item.epubProgressionRaw - record.downloadStatusRaw = item.downloadStatusRaw - record.downloadError = item.downloadError - record.downloadAt = item.downloadAt - record.readListIdsRaw = item.readListIdsRaw - record.isolatePagesRaw = item.isolatePagesRaw - record.pageRotationsRaw = item.pageRotationsRaw - record.epubPreferencesRaw = item.epubPreferencesRaw - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.KomgaCollection.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, item in - let seriesIds = item.seriesIdsRaw.flatMap { try? JSONDecoder().decode([String].self, from: $0) } ?? [] - var record = KomgaCollection( - id: item.id, - collectionId: item.collectionId, - instanceId: item.instanceId, - name: item.name, - ordered: item.ordered, - createdDate: item.createdDate, - lastModifiedDate: item.lastModifiedDate, - filtered: item.filtered, - isPinned: item.isPinned, - seriesIds: seriesIds - ) - record.seriesIdsRaw = item.seriesIdsRaw - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.KomgaReadList.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, item in - let bookIds = item.bookIdsRaw.flatMap { try? JSONDecoder().decode([String].self, from: $0) } ?? [] - var record = KomgaReadList( - id: item.id, - readListId: item.readListId, - instanceId: item.instanceId, - name: item.name, - summary: item.summary, - ordered: item.ordered, - createdDate: item.createdDate, - lastModifiedDate: item.lastModifiedDate, - filtered: item.filtered, - isPinned: item.isPinned, - bookIds: bookIds, - downloadedBooks: item.downloadedBooks, - pendingBooks: item.pendingBooks, - downloadedSize: item.downloadedSize - ) - record.bookIdsRaw = item.bookIdsRaw - record.downloadStatusRaw = item.downloadStatusRaw - record.downloadError = item.downloadError - record.downloadAt = item.downloadAt - try record.save(db) - try db.execute( - sql: """ - DELETE FROM \(ReadListBookMembership.databaseTableName) - WHERE instance_id = ? - AND read_list_id = ? - """, - arguments: [record.instanceId, record.readListId] - ) - for (position, bookId) in bookIds.enumerated() { - try ReadListBookMembership( - instanceId: record.instanceId, - readListId: record.readListId, - bookId: bookId, - position: position - ).insert(db) - } - } - - try importBatches( - KMReaderSchemaV6.CustomFontV1.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.name, order: .forward)] - ) { db, item in - var record = CustomFont( - name: item.name, - path: item.path, - fileName: item.fileName, - fileSize: item.fileSize, - createdAt: item.createdAt - ) - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.PendingProgress.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, item in - var record = PendingProgress( - instanceId: item.instanceId, - bookId: item.bookId, - page: item.page, - completed: item.completed, - progressionData: item.progressionData, - createdAt: item.createdAt - ) - record.id = item.id - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.SavedFilterV1.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, item in - let filterType = SavedFilterType(rawValue: item.filterTypeRaw) ?? .series - var record = SavedFilter( - id: item.id, - name: item.name, - filterType: filterType, - filterDataJSON: item.filterDataJSON, - createdAt: item.createdAt, - updatedAt: item.updatedAt - ) - record.filterTypeRaw = item.filterTypeRaw - try record.save(db) - } - - try importBatches( - KMReaderSchemaV6.EpubThemePresetV1.self, - from: context, - into: dbQueue, - sortBy: [SortDescriptor(\.id, order: .forward)] - ) { db, item in - var record = EpubThemePreset( - id: item.id, - name: item.name, - preferencesJSON: item.preferencesJSON, - createdAt: item.createdAt, - updatedAt: item.updatedAt - ) - try record.save(db) - } - } - - private static func importBatches( - _ modelType: LegacyModel.Type, - from context: ModelContext, - into dbQueue: DatabaseQueue, - sortBy: [SortDescriptor], - importRecord: (Database, LegacyModel) throws -> Void - ) throws { - _ = modelType - var offset = 0 - - while true { - var descriptor = FetchDescriptor(sortBy: sortBy) - descriptor.fetchLimit = DatabaseOperator.recordFetchChunkSize - descriptor.fetchOffset = offset - - let items = try context.fetch(descriptor) - guard !items.isEmpty else { return } - - try dbQueue.write { db in - for item in items { - try importRecord(db, item) - } - } - - offset += items.count - } - } -} diff --git a/KMReader/Core/Storage/LocalDataResetService.swift b/KMReader/Core/Storage/LocalDataResetService.swift index 9ff130bb..c1b6060f 100644 --- a/KMReader/Core/Storage/LocalDataResetService.swift +++ b/KMReader/Core/Storage/LocalDataResetService.swift @@ -97,9 +97,6 @@ enum LocalDataResetService { } let storeFileNames = [ - "default.store", - "default.store-shm", - "default.store-wal", LocalDatabase.fileName, "\(LocalDatabase.fileName)-shm", "\(LocalDatabase.fileName)-wal", diff --git a/KMReader/Core/Storage/Schema/KMReaderMigrationPlan.swift b/KMReader/Core/Storage/Schema/KMReaderMigrationPlan.swift deleted file mode 100644 index 29fbeaaa..00000000 --- a/KMReader/Core/Storage/Schema/KMReaderMigrationPlan.swift +++ /dev/null @@ -1,552 +0,0 @@ -// -// KMReaderMigrationPlan.swift -// -// - -import Foundation -import SwiftData - -nonisolated private struct MigrationBookSnapshot: Codable { - let id: String - let media: Media - let metadata: BookMetadata - let readProgress: ReadProgress? -} - -nonisolated private struct MigrationSeriesSnapshot: Codable { - let id: String - let metadata: SeriesMetadata - let booksMetadata: SeriesBooksMetadata -} - -nonisolated private struct MigrationBookRawSnapshot: Codable { - let id: String - let mediaRaw: Data? - let metadataRaw: Data? - let readProgressRaw: Data? -} - -nonisolated private struct MigrationSeriesRawSnapshot: Codable { - let id: String - let metadataRaw: Data? - let booksMetadataRaw: Data? -} - -nonisolated private enum MigrationSnapshotStore { - private static let directoryName = "kmreader_swiftdata_v1_v2_migration" - private static let booksPrefix = "books" - private static let seriesPrefix = "series" - private static let rawBooksPrefix = "raw-books" - private static let rawSeriesPrefix = "raw-series" - - private static var directoryURL: URL { - let baseURL = (try? AppStorageDirectory.supportDirectory()) ?? FileManager.default.temporaryDirectory - return baseURL.appendingPathComponent(directoryName, isDirectory: true) - } - - static func prepare() throws { - clear() - try AppStorageDirectory.ensureDirectoryExists( - at: directoryURL, - fileManager: .default - ) - } - - static func writeBookChunk(_ chunk: [MigrationBookSnapshot], index: Int) throws { - try writeChunk(chunk, prefix: booksPrefix, index: index) - } - - static func writeSeriesChunk(_ chunk: [MigrationSeriesSnapshot], index: Int) throws { - try writeChunk(chunk, prefix: seriesPrefix, index: index) - } - - static func writeRawBookChunk(_ chunk: [MigrationBookRawSnapshot], index: Int) throws { - try writeChunk(chunk, prefix: rawBooksPrefix, index: index) - } - - static func writeRawSeriesChunk(_ chunk: [MigrationSeriesRawSnapshot], index: Int) throws { - try writeChunk(chunk, prefix: rawSeriesPrefix, index: index) - } - - static func readBookChunk(at url: URL) throws -> [MigrationBookSnapshot] { - try readChunk(at: url) - } - - static func readSeriesChunk(at url: URL) throws -> [MigrationSeriesSnapshot] { - try readChunk(at: url) - } - - static func readRawBookChunk(at url: URL) throws -> [MigrationBookRawSnapshot] { - try readChunk(at: url) - } - - static func readRawSeriesChunk(at url: URL) throws -> [MigrationSeriesRawSnapshot] { - try readChunk(at: url) - } - - static func bookChunkURLs() -> [URL] { - chunkURLs(prefix: booksPrefix) - } - - static func seriesChunkURLs() -> [URL] { - chunkURLs(prefix: seriesPrefix) - } - - static func rawBookChunkURLs() -> [URL] { - chunkURLs(prefix: rawBooksPrefix) - } - - static func rawSeriesChunkURLs() -> [URL] { - chunkURLs(prefix: rawSeriesPrefix) - } - - private static func writeChunk( - _ chunk: [T], - prefix: String, - index: Int - ) throws { - guard !chunk.isEmpty else { return } - let data = try JSONEncoder().encode(chunk) - try data.write(to: chunkURL(prefix: prefix, index: index), options: .atomic) - } - - private static func readChunk(at url: URL) throws -> [T] { - let data = try Data(contentsOf: url) - return try JSONDecoder().decode([T].self, from: data) - } - - private static func chunkURL(prefix: String, index: Int) -> URL { - let filename = "\(prefix)-\(String(format: "%05d", index)).json" - return directoryURL.appendingPathComponent(filename) - } - - private static func chunkURLs(prefix: String) -> [URL] { - guard - let urls = try? FileManager.default.contentsOfDirectory( - at: directoryURL, - includingPropertiesForKeys: nil - ) - else { - return [] - } - return - urls - .filter { $0.lastPathComponent.hasPrefix("\(prefix)-") } - .sorted { $0.lastPathComponent < $1.lastPathComponent } - } - - static func clear() { - try? FileManager.default.removeItem(at: directoryURL) - } -} - -enum KMReaderMigrationPlan: SchemaMigrationPlan { - private static let migrationBatchSize = 500 - - static var schemas: [any VersionedSchema.Type] { - [ - KMReaderSchemaV1.self, - KMReaderSchemaV2.self, - KMReaderSchemaV3.self, - KMReaderSchemaV4.self, - KMReaderSchemaV6.self, - ] - } - - static var stages: [MigrationStage] { - [ - migrateV1toV2, - migrateV2toV3, - migrateV3toV4, - migrateV4toV6, - ] - } - - static let migrateV1toV2 = MigrationStage.custom( - fromVersion: KMReaderSchemaV1.self, - toVersion: KMReaderSchemaV2.self, - willMigrate: { context in - try MigrationSnapshotStore.prepare() - try snapshotBooks(context: context) - try snapshotSeries(context: context) - }, - didMigrate: { context in - defer { MigrationSnapshotStore.clear() } - - try applyBookSnapshots(context: context) - try applySeriesSnapshots(context: context) - - if context.hasChanges { - try context.save() - } - } - ) - - static let migrateV2toV3 = MigrationStage.lightweight( - fromVersion: KMReaderSchemaV2.self, - toVersion: KMReaderSchemaV3.self - ) - - static let migrateV3toV4 = MigrationStage.custom( - fromVersion: KMReaderSchemaV3.self, - toVersion: KMReaderSchemaV4.self, - willMigrate: { context in - try MigrationSnapshotStore.prepare() - try snapshotRawBooks(context: context) - try snapshotRawSeries(context: context) - }, - didMigrate: { context in - defer { MigrationSnapshotStore.clear() } - - try applyRawBookSnapshots(context: context) - try applyRawSeriesSnapshots(context: context) - - if context.hasChanges { - try context.save() - } - } - ) - - private static func snapshotBooks(context: ModelContext) throws { - var offset = 0 - var chunkIndex = 0 - - while true { - var descriptor = FetchDescriptor( - sortBy: [SortDescriptor(\KMReaderSchemaV1.KomgaBook.id, order: .forward)] - ) - descriptor.fetchOffset = offset - descriptor.fetchLimit = migrationBatchSize - - let books = try context.fetch(descriptor) - guard !books.isEmpty else { break } - - let chunk = books.map(makeBookSnapshot) - try MigrationSnapshotStore.writeBookChunk(chunk, index: chunkIndex) - - offset += books.count - chunkIndex += 1 - } - } - - private static func snapshotSeries(context: ModelContext) throws { - var offset = 0 - var chunkIndex = 0 - - while true { - var descriptor = FetchDescriptor( - sortBy: [SortDescriptor(\KMReaderSchemaV1.KomgaSeries.id, order: .forward)] - ) - descriptor.fetchOffset = offset - descriptor.fetchLimit = migrationBatchSize - - let series = try context.fetch(descriptor) - guard !series.isEmpty else { break } - - let chunk = series.map(makeSeriesSnapshot) - try MigrationSnapshotStore.writeSeriesChunk(chunk, index: chunkIndex) - - offset += series.count - chunkIndex += 1 - } - } - - private static func applyBookSnapshots(context: ModelContext) throws { - for chunkURL in MigrationSnapshotStore.bookChunkURLs() { - let snapshots = try MigrationSnapshotStore.readBookChunk(at: chunkURL) - guard !snapshots.isEmpty else { continue } - - let byID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) }) - let ids = Set(byID.keys) - let descriptor = FetchDescriptor( - predicate: #Predicate { ids.contains($0.id) } - ) - let books = try context.fetch(descriptor) - - for book in books { - guard let snapshot = byID[book.id] else { continue } - book.applyContent( - media: snapshot.media, - metadata: snapshot.metadata, - readProgress: snapshot.readProgress - ) - } - - if context.hasChanges { - try context.save() - } - } - } - - private static func applySeriesSnapshots(context: ModelContext) throws { - for chunkURL in MigrationSnapshotStore.seriesChunkURLs() { - let snapshots = try MigrationSnapshotStore.readSeriesChunk(at: chunkURL) - guard !snapshots.isEmpty else { continue } - - let byID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) }) - let ids = Set(byID.keys) - let descriptor = FetchDescriptor( - predicate: #Predicate { ids.contains($0.id) } - ) - let seriesList = try context.fetch(descriptor) - - for series in seriesList { - guard let snapshot = byID[series.id] else { continue } - series.applyContent( - metadata: snapshot.metadata, - booksMetadata: snapshot.booksMetadata - ) - } - - if context.hasChanges { - try context.save() - } - } - } - - private static func snapshotRawBooks(context: ModelContext) throws { - var offset = 0 - var chunkIndex = 0 - - while true { - var descriptor = FetchDescriptor( - sortBy: [SortDescriptor(\KMReaderSchemaV3.KomgaBook.id, order: .forward)] - ) - descriptor.fetchOffset = offset - descriptor.fetchLimit = migrationBatchSize - - let books = try context.fetch(descriptor) - guard !books.isEmpty else { break } - - let chunk = books.map(makeRawBookSnapshot) - try MigrationSnapshotStore.writeRawBookChunk(chunk, index: chunkIndex) - - offset += books.count - chunkIndex += 1 - } - } - - private static func snapshotRawSeries(context: ModelContext) throws { - var offset = 0 - var chunkIndex = 0 - - while true { - var descriptor = FetchDescriptor( - sortBy: [SortDescriptor(\KMReaderSchemaV3.KomgaSeries.id, order: .forward)] - ) - descriptor.fetchOffset = offset - descriptor.fetchLimit = migrationBatchSize - - let series = try context.fetch(descriptor) - guard !series.isEmpty else { break } - - let chunk = series.map(makeRawSeriesSnapshot) - try MigrationSnapshotStore.writeRawSeriesChunk(chunk, index: chunkIndex) - - offset += series.count - chunkIndex += 1 - } - } - - private static func applyRawBookSnapshots(context: ModelContext) throws { - for chunkURL in MigrationSnapshotStore.rawBookChunkURLs() { - let snapshots = try MigrationSnapshotStore.readRawBookChunk(at: chunkURL) - guard !snapshots.isEmpty else { continue } - - let byID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) }) - let ids = Set(byID.keys) - let descriptor = FetchDescriptor( - predicate: #Predicate { ids.contains($0.id) } - ) - let books = try context.fetch(descriptor) - - for book in books { - guard let snapshot = byID[book.id] else { continue } - book.mediaRaw = snapshot.mediaRaw - book.metadataRaw = snapshot.metadataRaw - book.readProgressRaw = snapshot.readProgressRaw - } - - if context.hasChanges { - try context.save() - } - } - } - - private static func applyRawSeriesSnapshots(context: ModelContext) throws { - for chunkURL in MigrationSnapshotStore.rawSeriesChunkURLs() { - let snapshots = try MigrationSnapshotStore.readRawSeriesChunk(at: chunkURL) - guard !snapshots.isEmpty else { continue } - - let byID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) }) - let ids = Set(byID.keys) - let descriptor = FetchDescriptor( - predicate: #Predicate { ids.contains($0.id) } - ) - let seriesList = try context.fetch(descriptor) - - for series in seriesList { - guard let snapshot = byID[series.id] else { continue } - series.metadataRaw = snapshot.metadataRaw - series.booksMetadataRaw = snapshot.booksMetadataRaw - } - - if context.hasChanges { - try context.save() - } - } - } - - private static func makeBookSnapshot(_ book: KMReaderSchemaV1.KomgaBook) -> MigrationBookSnapshot { - let media = Media( - status: book.mediaStatus.isEmpty ? MediaStatus.unknown.rawValue : book.mediaStatus, - mediaType: book.mediaType, - pagesCount: book.mediaPagesCount, - comment: book.mediaComment, - mediaProfile: book.mediaProfile, - epubDivinaCompatible: book.mediaEpubDivinaCompatible, - epubIsKepub: book.mediaEpubIsKepub - ) - - let metadata = BookMetadata( - created: book.metaCreated, - lastModified: book.metaLastModified, - title: book.metaTitle, - titleLock: book.metaTitleLock, - summary: book.metaSummary, - summaryLock: book.metaSummaryLock, - number: book.metaNumber, - numberLock: book.metaNumberLock, - numberSort: book.metaNumberSort, - numberSortLock: book.metaNumberSortLock, - releaseDate: book.metaReleaseDate, - releaseDateLock: book.metaReleaseDateLock, - authors: decode([Author].self, from: book.metaAuthorsRaw), - authorsLock: book.metaAuthorsLock, - tags: decode([String].self, from: book.metaTagsRaw), - tagsLock: book.metaTagsLock, - isbn: book.metaIsbn, - isbnLock: book.metaIsbnLock, - links: decode([WebLink].self, from: book.metaLinksRaw), - linksLock: book.metaLinksLock - ) - - let readProgress: ReadProgress? - if let page = book.progressPage, - let completed = book.progressCompleted, - let readDate = book.progressReadDate, - let lastModified = book.progressLastModified - { - let created = book.progressCreated ?? readDate - readProgress = ReadProgress( - page: page, - completed: completed, - readDate: readDate, - created: created, - lastModified: lastModified - ) - } else { - readProgress = nil - } - - return MigrationBookSnapshot( - id: book.id, - media: media, - metadata: metadata, - readProgress: readProgress - ) - } - - private static func makeRawBookSnapshot( - _ book: KMReaderSchemaV3.KomgaBook - ) -> MigrationBookRawSnapshot { - MigrationBookRawSnapshot( - id: book.id, - mediaRaw: RawCodableStore.encode(book.media ?? Media.empty), - metadataRaw: RawCodableStore.encode(book.metadata ?? BookMetadata.empty), - readProgressRaw: RawCodableStore.encodeOptional(book.readProgress) - ) - } - - private static func makeSeriesSnapshot(_ series: KMReaderSchemaV1.KomgaSeries) -> MigrationSeriesSnapshot { - let metadata = SeriesMetadata( - status: series.metaStatus, - statusLock: series.metaStatusLock, - created: series.metaCreated, - lastModified: series.metaLastModified, - title: series.metaTitle, - titleLock: series.metaTitleLock, - titleSort: series.metaTitleSort, - titleSortLock: series.metaTitleSortLock, - summary: series.metaSummary, - summaryLock: series.metaSummaryLock, - readingDirection: series.metaReadingDirection, - readingDirectionLock: series.metaReadingDirectionLock, - publisher: series.metaPublisher, - publisherLock: series.metaPublisherLock, - ageRating: series.metaAgeRating, - ageRatingLock: series.metaAgeRatingLock, - language: series.metaLanguage, - languageLock: series.metaLanguageLock, - genres: decode([String].self, from: series.metaGenresRaw), - genresLock: series.metaGenresLock, - tags: decode([String].self, from: series.metaTagsRaw), - tagsLock: series.metaTagsLock, - totalBookCount: series.metaTotalBookCount, - totalBookCountLock: series.metaTotalBookCountLock, - sharingLabels: decode([String].self, from: series.metaSharingLabelsRaw), - sharingLabelsLock: series.metaSharingLabelsLock, - links: decode([WebLink].self, from: series.metaLinksRaw), - linksLock: series.metaLinksLock, - alternateTitles: decode([AlternateTitle].self, from: series.metaAlternateTitlesRaw), - alternateTitlesLock: series.metaAlternateTitlesLock - ) - - let booksMetadata = SeriesBooksMetadata( - created: series.booksMetaCreated, - lastModified: series.booksMetaLastModified, - authors: decode([Author].self, from: series.booksMetaAuthorsRaw), - tags: decode([String].self, from: series.booksMetaTagsRaw), - releaseDate: series.booksMetaReleaseDate, - summary: series.booksMetaSummary, - summaryNumber: series.booksMetaSummaryNumber - ) - - return MigrationSeriesSnapshot( - id: series.id, - metadata: metadata, - booksMetadata: booksMetadata - ) - } - - private static func makeRawSeriesSnapshot( - _ series: KMReaderSchemaV3.KomgaSeries - ) -> MigrationSeriesRawSnapshot { - MigrationSeriesRawSnapshot( - id: series.id, - metadataRaw: RawCodableStore.encode(series.metadata ?? SeriesMetadata.empty), - booksMetadataRaw: RawCodableStore.encode(series.booksMetadata ?? SeriesBooksMetadata.empty) - ) - } - - private static func decode(_ type: T.Type, from data: Data?) -> T? { - guard let data else { - return nil - } - return try? JSONDecoder().decode(type, from: data) - } - - // V4 → V6: bridge the historical snapshot chain to the schema-owned active - // model graph. Runtime-backed V4/V5 stores from shipped builds do not match - // these snapshot source schemas and are handled by MainApp's inferred fallback. - static let migrateV4toV6 = MigrationStage.custom( - fromVersion: KMReaderSchemaV4.self, - toVersion: KMReaderSchemaV6.self, - willMigrate: { _ in }, - didMigrate: { context in - if context.hasChanges { - try context.save() - } - } - ) -} diff --git a/KMReader/Core/Storage/Schema/KMReaderSchemaV1.swift b/KMReader/Core/Storage/Schema/KMReaderSchemaV1.swift deleted file mode 100644 index b0b7cefa..00000000 --- a/KMReader/Core/Storage/Schema/KMReaderSchemaV1.swift +++ /dev/null @@ -1,302 +0,0 @@ -// -// KMReaderSchemaV1.swift -// -// - -import Foundation -import SwiftData - -enum KMReaderSchemaV1: VersionedSchema { - static var versionIdentifier: Schema.Version { - Schema.Version(1, 0, 0) - } - - static var models: [any PersistentModel.Type] { - [ - KMReaderSchemaV1.KomgaInstance.self, - KMReaderSchemaV1.KomgaLibrary.self, - KMReaderSchemaV1.KomgaSeries.self, - KMReaderSchemaV1.KomgaBook.self, - KMReaderSchemaV1.KomgaCollection.self, - KMReaderSchemaV1.KomgaReadList.self, - KMReaderSchemaV1.CustomFont.self, - KMReaderSchemaV1.PendingProgress.self, - KMReaderSchemaV1.SavedFilter.self, - KMReaderSchemaV1.EpubThemePreset.self, - ] - } - - @Model - final class KomgaInstance { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var serverURL: String = "" - var username: String = "" - var authToken: String = "" - var isAdmin: Bool = false - var authMethod: AuthenticationMethod? = AuthenticationMethod.basicAuth - var createdAt: Date = Date(timeIntervalSince1970: 0) - var lastUsedAt: Date = Date(timeIntervalSince1970: 0) - var seriesLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - var booksLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class KomgaLibrary { - @Attribute(.unique) var id: UUID = UUID() - var instanceId: String = "" - var libraryId: String = "" - var name: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - - var fileSize: Double? - var booksCount: Double? - var seriesCount: Double? - var sidecarsCount: Double? - var collectionsCount: Double? - var readlistsCount: Double? - - init() {} - } - - @Model - final class KomgaBook { - @Attribute(.unique) var id: String = "" - - var bookId: String = "" - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var number: Double = 0 - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - var sizeBytes: Int64 = 0 - var size: String = "" - - var mediaStatus: String = "" - var mediaType: String = "" - var mediaPagesCount: Int = 0 - var mediaComment: String? - var mediaProfile: String? - var mediaEpubDivinaCompatible: Bool? - var mediaEpubIsKepub: Bool? - - var metaCreated: String? - var metaLastModified: String? - var metaTitle: String = "" - var metaTitleLock: Bool? - var metaSummary: String? - var metaSummaryLock: Bool? - var metaNumber: String = "" - var metaNumberLock: Bool? - var metaNumberSort: Double = 0 - var metaNumberSortLock: Bool? - var metaReleaseDate: String? - var metaReleaseDateLock: Bool? - var metaAuthorsRaw: Data? - var metaAuthorsLock: Bool? - var metaTagsRaw: Data? - var metaTagsLock: Bool? - var metaIsbn: String? - var metaIsbnLock: Bool? - var metaLinksRaw: Data? - var metaLinksLock: Bool? - - var progressPage: Int? - var progressCompleted: Bool? - var progressReadDate: Date? - var progressCreated: Date? - var progressLastModified: Date? - - var isUnavailable: Bool = false - var oneshot: Bool = false - var seriesTitle: String = "" - - var pagesRaw: Data? - var tocRaw: Data? - var webPubManifestRaw: Data? - var epubProgressionRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - - var readListIdsRaw: Data? - - var isolatePagesRaw: Data? - var epubPreferencesRaw: String? - - init() {} - } - - @Model - final class KomgaSeries { - @Attribute(.unique) var id: String = "" - - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - - var booksCount: Int = 0 - var booksReadCount: Int = 0 - var booksUnreadCount: Int = 0 - var booksInProgressCount: Int = 0 - - var metaStatus: String? - var metaStatusLock: Bool? - var metaCreated: String? - var metaLastModified: String? - var metaTitle: String = "" - var metaTitleLock: Bool? - var metaTitleSort: String = "" - var metaTitleSortLock: Bool? - var metaSummary: String? - var metaSummaryLock: Bool? - var metaReadingDirection: String? - var metaReadingDirectionLock: Bool? - var metaPublisher: String? - var metaPublisherLock: Bool? - var metaAgeRating: Int? - var metaAgeRatingLock: Bool? - var metaLanguage: String? - var metaLanguageLock: Bool? - var metaGenresRaw: Data? - var metaGenresLock: Bool? - var metaTagsRaw: Data? - var metaTagsLock: Bool? - var metaTotalBookCount: Int? - var metaTotalBookCountLock: Bool? - var metaSharingLabelsRaw: Data? - var metaSharingLabelsLock: Bool? - var metaLinksRaw: Data? - var metaLinksLock: Bool? - var metaAlternateTitlesRaw: Data? - var metaAlternateTitlesLock: Bool? - - var booksMetaCreated: String? - var booksMetaLastModified: String? - var booksMetaAuthorsRaw: Data? - var booksMetaTagsRaw: Data? - var booksMetaReleaseDate: String? - var booksMetaSummary: String? - var booksMetaSummaryNumber: String? - - var isUnavailable: Bool = false - var oneshot: Bool = false - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - var offlinePolicyRaw: String = "manual" - var offlinePolicyLimit: Int = 0 - - var collectionIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaCollection { - @Attribute(.unique) var id: String = "" - - var collectionId: String = "" - var instanceId: String = "" - - var name: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - - var seriesIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaReadList { - @Attribute(.unique) var id: String = "" - - var readListId: String = "" - var instanceId: String = "" - - var name: String = "" - var summary: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - - var bookIdsRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - - init() {} - } - - @Model - final class CustomFont { - @Attribute(.unique) var name: String = "" - var path: String? - var fileName: String? - var fileSize: Int64? - var createdAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class PendingProgress { - @Attribute(.unique) var id: String = "" - var instanceId: String = "" - var bookId: String = "" - var page: Int = 0 - var completed: Bool = false - var createdAt: Date = Date(timeIntervalSince1970: 0) - var progressionData: Data? - - init() {} - } - - @Model - final class SavedFilter { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var filterTypeRaw: String = "" - var filterDataJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class EpubThemePreset { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var preferencesJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } -} diff --git a/KMReader/Core/Storage/Schema/KMReaderSchemaV2.swift b/KMReader/Core/Storage/Schema/KMReaderSchemaV2.swift deleted file mode 100644 index 93410e2b..00000000 --- a/KMReader/Core/Storage/Schema/KMReaderSchemaV2.swift +++ /dev/null @@ -1,284 +0,0 @@ -// -// KMReaderSchemaV2.swift -// -// - -import Foundation -import SwiftData - -enum KMReaderSchemaV2: VersionedSchema { - static var versionIdentifier: Schema.Version { - Schema.Version(2, 0, 0) - } - - static var models: [any PersistentModel.Type] { - [ - KMReaderSchemaV2.KomgaInstance.self, - KMReaderSchemaV2.KomgaLibrary.self, - KMReaderSchemaV2.KomgaSeries.self, - KMReaderSchemaV2.KomgaBook.self, - KMReaderSchemaV2.KomgaCollection.self, - KMReaderSchemaV2.KomgaReadList.self, - KMReaderSchemaV2.CustomFont.self, - KMReaderSchemaV2.PendingProgress.self, - KMReaderSchemaV2.SavedFilter.self, - KMReaderSchemaV2.EpubThemePreset.self, - ] - } - - @Model - final class KomgaInstance { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var serverURL: String = "" - var username: String = "" - var authToken: String = "" - var isAdmin: Bool = false - var authMethod: AuthenticationMethod? = AuthenticationMethod.basicAuth - var createdAt: Date = Date(timeIntervalSince1970: 0) - var lastUsedAt: Date = Date(timeIntervalSince1970: 0) - var seriesLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - var booksLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class KomgaLibrary { - @Attribute(.unique) var id: UUID = UUID() - var instanceId: String = "" - var libraryId: String = "" - var name: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - - var fileSize: Double? - var booksCount: Double? - var seriesCount: Double? - var sidecarsCount: Double? - var collectionsCount: Double? - var readlistsCount: Double? - - init() {} - } - - @Model - final class KomgaBook { - @Attribute(.unique) var id: String = "" - - var bookId: String = "" - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var number: Double = 0 - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - var sizeBytes: Int64 = 0 - var size: String = "" - - var media: Media? - var metadata: BookMetadata? - var readProgress: ReadProgress? - - var mediaPagesCount: Int = 0 - var mediaProfile: String? - var metaTitle: String = "" - var metaNumber: String = "" - var metaNumberSort: Double = 0 - var metaReleaseDate: String? - var progressPage: Int? - var progressCompleted: Bool? - var progressReadDate: Date? - var metaAuthorsIndex: String = "|" - var metaTagsIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - var seriesTitle: String = "" - - var pagesRaw: Data? - var tocRaw: Data? - var webPubManifestRaw: Data? - var epubProgressionRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - - var readListIdsRaw: Data? - - var isolatePagesRaw: Data? - var epubPreferencesRaw: String? - - init() {} - - func applyContent(media: Media, metadata: BookMetadata, readProgress: ReadProgress?) { - self.media = media - self.metadata = metadata - self.readProgress = readProgress - mediaPagesCount = media.pagesCount - mediaProfile = media.mediaProfile - metaTitle = metadata.title - metaNumber = metadata.number - metaNumberSort = metadata.numberSort - metaReleaseDate = metadata.releaseDate - progressPage = readProgress?.page - progressCompleted = readProgress?.completed - progressReadDate = readProgress?.readDate - metaAuthorsIndex = MetadataIndex.encode(values: metadata.authors?.map(\.name) ?? []) - metaTagsIndex = MetadataIndex.encode(values: metadata.tags ?? []) - } - } - - @Model - final class KomgaSeries { - @Attribute(.unique) var id: String = "" - - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - - var booksCount: Int = 0 - var booksReadCount: Int = 0 - var booksUnreadCount: Int = 0 - var booksInProgressCount: Int = 0 - - var metadata: SeriesMetadata? - var booksMetadata: SeriesBooksMetadata? - - var metaTitle: String = "" - var metaTitleSort: String = "" - var metaPublisherIndex: String = "|" - var metaAuthorsIndex: String = "|" - var metaGenresIndex: String = "|" - var metaTagsIndex: String = "|" - var metaLanguageIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - var offlinePolicyRaw: String = "manual" - var offlinePolicyLimit: Int = 0 - - var collectionIdsRaw: Data? - - init() {} - - func applyContent(metadata: SeriesMetadata, booksMetadata: SeriesBooksMetadata) { - self.metadata = metadata - self.booksMetadata = booksMetadata - metaTitle = metadata.title - metaTitleSort = metadata.titleSort - metaPublisherIndex = MetadataIndex.encode(value: metadata.publisher) - metaAuthorsIndex = MetadataIndex.encode(values: booksMetadata.authors?.map(\.name) ?? []) - metaGenresIndex = MetadataIndex.encode(values: metadata.genres ?? []) - metaTagsIndex = MetadataIndex.encode(values: metadata.tags ?? []) - metaLanguageIndex = MetadataIndex.encode(value: metadata.language) - } - } - - @Model - final class KomgaCollection { - @Attribute(.unique) var id: String = "" - - var collectionId: String = "" - var instanceId: String = "" - - var name: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - - var seriesIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaReadList { - @Attribute(.unique) var id: String = "" - - var readListId: String = "" - var instanceId: String = "" - - var name: String = "" - var summary: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - - var bookIdsRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - - init() {} - } - - @Model - final class CustomFont { - @Attribute(.unique) var name: String = "" - var path: String? - var fileName: String? - var fileSize: Int64? - var createdAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class PendingProgress { - @Attribute(.unique) var id: String = "" - var instanceId: String = "" - var bookId: String = "" - var page: Int = 0 - var completed: Bool = false - var createdAt: Date = Date(timeIntervalSince1970: 0) - var progressionData: Data? - - init() {} - } - - @Model - final class SavedFilter { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var filterTypeRaw: String = "" - var filterDataJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class EpubThemePreset { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var preferencesJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } -} diff --git a/KMReader/Core/Storage/Schema/KMReaderSchemaV3.swift b/KMReader/Core/Storage/Schema/KMReaderSchemaV3.swift deleted file mode 100644 index 05e6b610..00000000 --- a/KMReader/Core/Storage/Schema/KMReaderSchemaV3.swift +++ /dev/null @@ -1,257 +0,0 @@ -// -// KMReaderSchemaV3.swift -// -// - -import Foundation -import SwiftData - -enum KMReaderSchemaV3: VersionedSchema { - static var versionIdentifier: Schema.Version { - Schema.Version(3, 0, 0) - } - - static var models: [any PersistentModel.Type] { - [ - KMReaderSchemaV3.KomgaInstance.self, - KMReaderSchemaV3.KomgaLibrary.self, - KMReaderSchemaV3.KomgaSeries.self, - KMReaderSchemaV3.KomgaBook.self, - KMReaderSchemaV3.KomgaCollection.self, - KMReaderSchemaV3.KomgaReadList.self, - KMReaderSchemaV3.CustomFont.self, - KMReaderSchemaV3.PendingProgress.self, - KMReaderSchemaV3.SavedFilter.self, - KMReaderSchemaV3.EpubThemePreset.self, - ] - } - - @Model - final class KomgaInstance { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var serverURL: String = "" - var username: String = "" - var authToken: String = "" - var isAdmin: Bool = false - var authMethod: AuthenticationMethod? = AuthenticationMethod.basicAuth - var createdAt: Date = Date(timeIntervalSince1970: 0) - var lastUsedAt: Date = Date(timeIntervalSince1970: 0) - var seriesLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - var booksLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class KomgaLibrary { - @Attribute(.unique) var id: UUID = UUID() - var instanceId: String = "" - var libraryId: String = "" - var name: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - - var fileSize: Double? - var booksCount: Double? - var seriesCount: Double? - var sidecarsCount: Double? - var collectionsCount: Double? - var readlistsCount: Double? - - init() {} - } - - @Model - final class KomgaBook { - @Attribute(.unique) var id: String = "" - - var bookId: String = "" - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var number: Double = 0 - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - var sizeBytes: Int64 = 0 - var size: String = "" - - var media: Media? - var metadata: BookMetadata? - var readProgress: ReadProgress? - - var mediaPagesCount: Int = 0 - var mediaProfile: String? - var metaTitle: String = "" - var metaNumber: String = "" - var metaNumberSort: Double = 0 - var metaReleaseDate: String? - var progressPage: Int? - var progressCompleted: Bool? - var progressReadDate: Date? - var metaAuthorsIndex: String = "|" - var metaTagsIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - var seriesTitle: String = "" - - var pagesRaw: Data? - var tocRaw: Data? - var webPubManifestRaw: Data? - var epubProgressionRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - - var readListIdsRaw: Data? - - var isolatePagesRaw: Data? - var epubPreferencesRaw: String? - - init() {} - } - - @Model - final class KomgaSeries { - @Attribute(.unique) var id: String = "" - - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - - var booksCount: Int = 0 - var booksReadCount: Int = 0 - var booksUnreadCount: Int = 0 - var booksInProgressCount: Int = 0 - - var metadata: SeriesMetadata? - var booksMetadata: SeriesBooksMetadata? - - var metaTitle: String = "" - var metaTitleSort: String = "" - var metaPublisherIndex: String = "|" - var metaAuthorsIndex: String = "|" - var metaGenresIndex: String = "|" - var metaTagsIndex: String = "|" - var metaLanguageIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - var offlinePolicyRaw: String = "manual" - var offlinePolicyLimit: Int = 0 - - var collectionIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaCollection { - @Attribute(.unique) var id: String = "" - - var collectionId: String = "" - var instanceId: String = "" - - var name: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - var isPinned: Bool = false - - var seriesIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaReadList { - @Attribute(.unique) var id: String = "" - - var readListId: String = "" - var instanceId: String = "" - - var name: String = "" - var summary: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - var isPinned: Bool = false - - var bookIdsRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - - init() {} - } - - @Model - final class CustomFont { - @Attribute(.unique) var name: String = "" - var path: String? - var fileName: String? - var fileSize: Int64? - var createdAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class PendingProgress { - @Attribute(.unique) var id: String = "" - var instanceId: String = "" - var bookId: String = "" - var page: Int = 0 - var completed: Bool = false - var createdAt: Date = Date(timeIntervalSince1970: 0) - var progressionData: Data? - - init() {} - } - - @Model - final class SavedFilter { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var filterTypeRaw: String = "" - var filterDataJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class EpubThemePreset { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var preferencesJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } -} diff --git a/KMReader/Core/Storage/Schema/KMReaderSchemaV4.swift b/KMReader/Core/Storage/Schema/KMReaderSchemaV4.swift deleted file mode 100644 index f027e5ca..00000000 --- a/KMReader/Core/Storage/Schema/KMReaderSchemaV4.swift +++ /dev/null @@ -1,259 +0,0 @@ -// -// KMReaderSchemaV4.swift -// -// - -import Foundation -import SwiftData - -enum KMReaderSchemaV4: VersionedSchema { - static var versionIdentifier: Schema.Version { - Schema.Version(4, 0, 0) - } - - static var models: [any PersistentModel.Type] { - [ - KMReaderSchemaV4.KomgaInstance.self, - KMReaderSchemaV4.KomgaLibrary.self, - KMReaderSchemaV4.KomgaSeries.self, - KMReaderSchemaV4.KomgaBook.self, - KMReaderSchemaV4.KomgaCollection.self, - KMReaderSchemaV4.KomgaReadList.self, - KMReaderSchemaV4.CustomFont.self, - KMReaderSchemaV4.PendingProgress.self, - KMReaderSchemaV4.SavedFilter.self, - KMReaderSchemaV4.EpubThemePreset.self, - ] - } - - @Model - final class KomgaInstance { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var serverURL: String = "" - var username: String = "" - var authToken: String = "" - var isAdmin: Bool = false - var authMethod: AuthenticationMethod? = AuthenticationMethod.basicAuth - var createdAt: Date = Date(timeIntervalSince1970: 0) - var lastUsedAt: Date = Date(timeIntervalSince1970: 0) - var seriesLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - var booksLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class KomgaLibrary { - @Attribute(.unique) var id: UUID = UUID() - var instanceId: String = "" - var libraryId: String = "" - var name: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - - var fileSize: Double? - var booksCount: Double? - var seriesCount: Double? - var sidecarsCount: Double? - var collectionsCount: Double? - var readlistsCount: Double? - - init() {} - } - - @Model - final class KomgaBook { - @Attribute(.unique) var id: String = "" - - var bookId: String = "" - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var number: Double = 0 - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - var sizeBytes: Int64 = 0 - var size: String = "" - - // API-aligned raw storage - var mediaRaw: Data? - var metadataRaw: Data? - var readProgressRaw: Data? - - var mediaPagesCount: Int = 0 - var mediaProfile: String? - var metaTitle: String = "" - var metaNumber: String = "" - var metaNumberSort: Double = 0 - var metaReleaseDate: String? - var progressPage: Int? - var progressCompleted: Bool? - var progressReadDate: Date? - var metaAuthorsIndex: String = "|" - var metaTagsIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - var seriesTitle: String = "" - - var pagesRaw: Data? - var tocRaw: Data? - var webPubManifestRaw: Data? - var epubProgressionRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - - var readListIdsRaw: Data? - - var isolatePagesRaw: Data? - var epubPreferencesRaw: String? - - init() {} - } - - @Model - final class KomgaSeries { - @Attribute(.unique) var id: String = "" - - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - - var booksCount: Int = 0 - var booksReadCount: Int = 0 - var booksUnreadCount: Int = 0 - var booksInProgressCount: Int = 0 - - // API-aligned raw storage - var metadataRaw: Data? - var booksMetadataRaw: Data? - - var metaTitle: String = "" - var metaTitleSort: String = "" - var metaPublisherIndex: String = "|" - var metaAuthorsIndex: String = "|" - var metaGenresIndex: String = "|" - var metaTagsIndex: String = "|" - var metaLanguageIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - var offlinePolicyRaw: String = "manual" - var offlinePolicyLimit: Int = 0 - - var collectionIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaCollection { - @Attribute(.unique) var id: String = "" - - var collectionId: String = "" - var instanceId: String = "" - - var name: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - var isPinned: Bool = false - - var seriesIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaReadList { - @Attribute(.unique) var id: String = "" - - var readListId: String = "" - var instanceId: String = "" - - var name: String = "" - var summary: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - var isPinned: Bool = false - - var bookIdsRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - - init() {} - } - - @Model - final class CustomFont { - @Attribute(.unique) var name: String = "" - var path: String? - var fileName: String? - var fileSize: Int64? - var createdAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class PendingProgress { - @Attribute(.unique) var id: String = "" - var instanceId: String = "" - var bookId: String = "" - var page: Int = 0 - var completed: Bool = false - var createdAt: Date = Date(timeIntervalSince1970: 0) - var progressionData: Data? - - init() {} - } - - @Model - final class SavedFilter { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var filterTypeRaw: String = "" - var filterDataJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class EpubThemePreset { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var preferencesJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } -} diff --git a/KMReader/Core/Storage/Schema/KMReaderSchemaV5.swift b/KMReader/Core/Storage/Schema/KMReaderSchemaV5.swift deleted file mode 100644 index 78023ba3..00000000 --- a/KMReader/Core/Storage/Schema/KMReaderSchemaV5.swift +++ /dev/null @@ -1,260 +0,0 @@ -// -// KMReaderSchemaV5.swift -// -// - -import Foundation -import SwiftData - -enum KMReaderSchemaV5: VersionedSchema { - static var versionIdentifier: Schema.Version { - Schema.Version(5, 0, 0) - } - - static var models: [any PersistentModel.Type] { - [ - KMReaderSchemaV5.KomgaInstance.self, - KMReaderSchemaV5.KomgaLibrary.self, - KMReaderSchemaV5.KomgaSeries.self, - KMReaderSchemaV5.KomgaBook.self, - KMReaderSchemaV5.KomgaCollection.self, - KMReaderSchemaV5.KomgaReadList.self, - KMReaderSchemaV5.CustomFont.self, - KMReaderSchemaV5.PendingProgress.self, - KMReaderSchemaV5.SavedFilter.self, - KMReaderSchemaV5.EpubThemePreset.self, - ] - } - - @Model - final class KomgaInstance { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var serverURL: String = "" - var username: String = "" - var authToken: String = "" - var isAdmin: Bool = false - var authMethod: AuthenticationMethod? = AuthenticationMethod.basicAuth - var createdAt: Date = Date(timeIntervalSince1970: 0) - var lastUsedAt: Date = Date(timeIntervalSince1970: 0) - var seriesLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - var booksLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class KomgaLibrary { - @Attribute(.unique) var id: UUID = UUID() - var instanceId: String = "" - var libraryId: String = "" - var name: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - - var fileSize: Double? - var booksCount: Double? - var seriesCount: Double? - var sidecarsCount: Double? - var collectionsCount: Double? - var readlistsCount: Double? - - init() {} - } - - @Model - final class KomgaBook { - @Attribute(.unique) var id: String = "" - - var bookId: String = "" - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var number: Double = 0 - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - var sizeBytes: Int64 = 0 - var size: String = "" - - // API-aligned raw storage - var mediaRaw: Data? - var metadataRaw: Data? - var readProgressRaw: Data? - - var mediaPagesCount: Int = 0 - var mediaProfile: String? - var metaTitle: String = "" - var metaNumber: String = "" - var metaNumberSort: Double = 0 - var metaReleaseDate: String? - var progressPage: Int? - var progressCompleted: Bool? - var progressReadDate: Date? - var metaAuthorsIndex: String = "|" - var metaTagsIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - var seriesTitle: String = "" - - var pagesRaw: Data? - var tocRaw: Data? - var webPubManifestRaw: Data? - var epubProgressionRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - - var readListIdsRaw: Data? - - var isolatePagesRaw: Data? - var pageRotationsRaw: Data? - var epubPreferencesRaw: String? - - init() {} - } - - @Model - final class KomgaSeries { - @Attribute(.unique) var id: String = "" - - var seriesId: String = "" - var libraryId: String = "" - var instanceId: String = "" - - var name: String = "" - var url: String = "" - var created: Date = Date(timeIntervalSince1970: 0) - var lastModified: Date = Date(timeIntervalSince1970: 0) - - var booksCount: Int = 0 - var booksReadCount: Int = 0 - var booksUnreadCount: Int = 0 - var booksInProgressCount: Int = 0 - - // API-aligned raw storage - var metadataRaw: Data? - var booksMetadataRaw: Data? - - var metaTitle: String = "" - var metaTitleSort: String = "" - var metaPublisherIndex: String = "|" - var metaAuthorsIndex: String = "|" - var metaGenresIndex: String = "|" - var metaTagsIndex: String = "|" - var metaLanguageIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool = false - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - var offlinePolicyRaw: String = "manual" - var offlinePolicyLimit: Int = 0 - - var collectionIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaCollection { - @Attribute(.unique) var id: String = "" - - var collectionId: String = "" - var instanceId: String = "" - - var name: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - var isPinned: Bool = false - - var seriesIdsRaw: Data? - - init() {} - } - - @Model - final class KomgaReadList { - @Attribute(.unique) var id: String = "" - - var readListId: String = "" - var instanceId: String = "" - - var name: String = "" - var summary: String = "" - var ordered: Bool = false - var createdDate: Date = Date(timeIntervalSince1970: 0) - var lastModifiedDate: Date = Date(timeIntervalSince1970: 0) - var filtered: Bool = false - var isPinned: Bool = false - - var bookIdsRaw: Data? - - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - - init() {} - } - - @Model - final class CustomFont { - @Attribute(.unique) var name: String = "" - var path: String? - var fileName: String? - var fileSize: Int64? - var createdAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class PendingProgress { - @Attribute(.unique) var id: String = "" - var instanceId: String = "" - var bookId: String = "" - var page: Int = 0 - var completed: Bool = false - var createdAt: Date = Date(timeIntervalSince1970: 0) - var progressionData: Data? - - init() {} - } - - @Model - final class SavedFilter { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var filterTypeRaw: String = "" - var filterDataJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } - - @Model - final class EpubThemePreset { - @Attribute(.unique) var id: UUID = UUID() - var name: String = "" - var preferencesJSON: String = "" - var createdAt: Date = Date(timeIntervalSince1970: 0) - var updatedAt: Date = Date(timeIntervalSince1970: 0) - - init() {} - } -} diff --git a/KMReader/Core/Storage/Schema/KMReaderSchemaV6.swift b/KMReader/Core/Storage/Schema/KMReaderSchemaV6.swift deleted file mode 100644 index c395eb6d..00000000 --- a/KMReader/Core/Storage/Schema/KMReaderSchemaV6.swift +++ /dev/null @@ -1,533 +0,0 @@ -// -// KMReaderSchemaV6.swift -// -// - -import Foundation -import SwiftData - -enum KMReaderSchemaV6: VersionedSchema { - static var versionIdentifier: Schema.Version { - Schema.Version(6, 0, 0) - } - - static var models: [any PersistentModel.Type] { - [ - KMReaderSchemaV6.KomgaInstance.self, - KMReaderSchemaV6.KomgaLibrary.self, - KMReaderSchemaV6.KomgaSeries.self, - KMReaderSchemaV6.KomgaBook.self, - KMReaderSchemaV6.KomgaCollection.self, - KMReaderSchemaV6.KomgaReadList.self, - KMReaderSchemaV6.CustomFontV1.self, - KMReaderSchemaV6.PendingProgress.self, - KMReaderSchemaV6.SavedFilterV1.self, - KMReaderSchemaV6.EpubThemePresetV1.self, - ] - } - - @Model - final class KomgaInstance { - @Attribute(.unique) var id: UUID - var name: String - var serverURL: String - var username: String - var authToken: String - var isAdmin: Bool - var authMethod: AuthenticationMethod? = AuthenticationMethod.basicAuth - var createdAt: Date - var lastUsedAt: Date - var seriesLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - var booksLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - - init( - id: UUID = UUID(), - name: String, - serverURL: String, - username: String, - authToken: String, - isAdmin: Bool, - authMethod: AuthenticationMethod = .basicAuth, - createdAt: Date = Date(), - lastUsedAt: Date = Date(), - seriesLastSyncedAt: Date = Date(timeIntervalSince1970: 0), - booksLastSyncedAt: Date = Date(timeIntervalSince1970: 0) - ) { - self.id = id - self.name = name - self.serverURL = serverURL - self.username = username - self.authToken = authToken - self.isAdmin = isAdmin - self.authMethod = authMethod - self.createdAt = createdAt - self.lastUsedAt = lastUsedAt - self.seriesLastSyncedAt = seriesLastSyncedAt - self.booksLastSyncedAt = booksLastSyncedAt - } - } - - @Model - final class KomgaLibrary { - static let allLibrariesId = "__all_libraries__" - - @Attribute(.unique) var id: UUID - var instanceId: String - var libraryId: String - var name: String - var createdAt: Date - - // Metrics - var fileSize: Double? - var booksCount: Double? - var seriesCount: Double? - var sidecarsCount: Double? - var collectionsCount: Double? - var readlistsCount: Double? - - init( - id: UUID = UUID(), - instanceId: String, - libraryId: String, - name: String, - createdAt: Date = Date(), - fileSize: Double? = nil, - booksCount: Double? = nil, - seriesCount: Double? = nil, - sidecarsCount: Double? = nil, - collectionsCount: Double? = nil, - readlistsCount: Double? = nil - ) { - self.id = id - self.instanceId = instanceId - self.libraryId = libraryId - self.name = name - self.createdAt = createdAt - self.fileSize = fileSize - self.booksCount = booksCount - self.seriesCount = seriesCount - self.sidecarsCount = sidecarsCount - self.collectionsCount = collectionsCount - self.readlistsCount = readlistsCount - } - } - - @Model - final class KomgaSeries { - @Attribute(.unique) var id: String // Composite: CompositeID.generate - - var seriesId: String - var libraryId: String - var instanceId: String - - var name: String - var url: String - var created: Date - var lastModified: Date - - var booksCount: Int - var booksReadCount: Int - var booksUnreadCount: Int - var booksInProgressCount: Int - - // API-aligned raw storage - var metadataRaw: Data? - var booksMetadataRaw: Data? - - // Query fields - var metaTitle: String - var metaTitleSort: String - var metaPublisherIndex: String = "|" - var metaAuthorsIndex: String = "|" - var metaGenresIndex: String = "|" - var metaTagsIndex: String = "|" - var metaLanguageIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool - - // Track offline download status (managed locally) - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - var offlinePolicyRaw: String = "manual" - var offlinePolicyLimit: Int = 0 - - // Cached collection IDs containing this series - var collectionIdsRaw: Data? - - init( - id: String? = nil, - seriesId: String, - libraryId: String, - instanceId: String, - name: String, - url: String, - created: Date, - lastModified: Date, - booksCount: Int, - booksReadCount: Int, - booksUnreadCount: Int, - booksInProgressCount: Int, - metadata: SeriesMetadata, - booksMetadata: SeriesBooksMetadata, - isUnavailable: Bool, - oneshot: Bool, - downloadedBooks: Int = 0, - pendingBooks: Int = 0, - downloadedSize: Int64 = 0, - offlinePolicy: OfflinePolicy = .manual, - offlinePolicyLimit: Int = 0 - ) { - self.id = id ?? CompositeID.generate(instanceId: instanceId, id: seriesId) - self.seriesId = seriesId - self.libraryId = libraryId - self.instanceId = instanceId - self.name = name - self.url = url - self.created = created - self.lastModified = lastModified - self.booksCount = booksCount - self.booksReadCount = booksReadCount - self.booksUnreadCount = booksUnreadCount - self.booksInProgressCount = booksInProgressCount - - self.metadataRaw = RawCodableStore.encode(metadata) - self.booksMetadataRaw = RawCodableStore.encode(booksMetadata) - self.metaTitle = metadata.title - self.metaTitleSort = metadata.titleSort - self.metaPublisherIndex = MetadataIndex.encode(value: metadata.publisher) - self.metaAuthorsIndex = MetadataIndex.encode(values: booksMetadata.authors?.map(\.name) ?? []) - self.metaGenresIndex = MetadataIndex.encode(values: metadata.genres ?? []) - self.metaTagsIndex = MetadataIndex.encode(values: metadata.tags ?? []) - self.metaLanguageIndex = MetadataIndex.encode(value: metadata.language) - - self.isUnavailable = isUnavailable - self.oneshot = oneshot - self.downloadedBooks = downloadedBooks - self.pendingBooks = pendingBooks - self.downloadedSize = downloadedSize - self.offlinePolicyRaw = offlinePolicy.storageValue - self.offlinePolicyLimit = offlinePolicyLimit - self.collectionIdsRaw = try? JSONEncoder().encode([] as [String]) - } - } - - @Model - final class KomgaBook { - @Attribute(.unique) var id: String // Composite: CompositeID.generate - - var bookId: String - var seriesId: String - var libraryId: String - var instanceId: String - - var name: String - var url: String - var number: Double - var created: Date - var lastModified: Date - var sizeBytes: Int64 - var size: String - - // API-aligned raw storage - var mediaRaw: Data? - var metadataRaw: Data? - var readProgressRaw: Data? - - // Query fields - var mediaPagesCount: Int - var mediaProfile: String? - var metaTitle: String - var metaNumber: String - var metaNumberSort: Double - var metaReleaseDate: String? - var progressPage: Int? - var progressCompleted: Bool? - var progressReadDate: Date? - var metaAuthorsIndex: String = "|" - var metaTagsIndex: String = "|" - - var isUnavailable: Bool = false - var oneshot: Bool - var seriesTitle: String = "" - - // Metadata storage - var pagesRaw: Data? - var tocRaw: Data? - var webPubManifestRaw: Data? - var epubProgressionRaw: Data? - - // Track offline download status (managed locally) - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - - // Cached read list IDs containing this book - var readListIdsRaw: Data? - - var isolatePagesRaw: Data? - var pageRotationsRaw: Data? - var epubPreferencesRaw: String? - - init( - id: String? = nil, - bookId: String, - seriesId: String, - libraryId: String, - instanceId: String, - name: String, - url: String, - number: Double, - created: Date, - lastModified: Date, - sizeBytes: Int64, - size: String, - media: Media, - metadata: BookMetadata, - readProgress: ReadProgress?, - isUnavailable: Bool, - oneshot: Bool, - seriesTitle: String = "", - downloadedSize: Int64 = 0 - ) { - self.id = id ?? CompositeID.generate(instanceId: instanceId, id: bookId) - self.bookId = bookId - self.seriesId = seriesId - self.libraryId = libraryId - self.instanceId = instanceId - self.name = name - self.url = url - self.number = number - self.created = created - self.lastModified = lastModified - self.sizeBytes = sizeBytes - self.size = size - - self.mediaRaw = RawCodableStore.encode(media) - self.metadataRaw = RawCodableStore.encode(metadata) - self.readProgressRaw = RawCodableStore.encodeOptional(readProgress) - - self.mediaPagesCount = media.pagesCount - self.mediaProfile = media.mediaProfile - self.metaTitle = metadata.title - self.metaNumber = metadata.number - self.metaNumberSort = metadata.numberSort - self.metaReleaseDate = metadata.releaseDate - self.progressPage = readProgress?.page - self.progressCompleted = readProgress?.completed - self.progressReadDate = readProgress?.readDate - self.metaAuthorsIndex = MetadataIndex.encode(values: metadata.authors?.map(\.name) ?? []) - self.metaTagsIndex = MetadataIndex.encode(values: metadata.tags ?? []) - - self.seriesTitle = seriesTitle - self.isUnavailable = isUnavailable - self.oneshot = oneshot - self.downloadedSize = downloadedSize - self.readListIdsRaw = try? JSONEncoder().encode([] as [String]) - self.isolatePagesRaw = try? JSONEncoder().encode([] as [Int]) - self.pageRotationsRaw = nil - self.epubPreferencesRaw = nil - self.epubProgressionRaw = nil - } - } - - @Model - final class KomgaCollection { - @Attribute(.unique) var id: String // Composite: CompositeID.generate - - var collectionId: String - var instanceId: String - - var name: String - var ordered: Bool - var createdDate: Date - var lastModifiedDate: Date - var filtered: Bool - var isPinned: Bool = false - - var seriesIdsRaw: Data? - - init( - id: String? = nil, - collectionId: String, - instanceId: String, - name: String, - ordered: Bool, - createdDate: Date, - lastModifiedDate: Date, - filtered: Bool, - isPinned: Bool = false, - seriesIds: [String] = [] - ) { - self.id = id ?? CompositeID.generate(instanceId: instanceId, id: collectionId) - self.collectionId = collectionId - self.instanceId = instanceId - self.name = name - self.ordered = ordered - self.createdDate = createdDate - self.lastModifiedDate = lastModifiedDate - self.filtered = filtered - self.isPinned = isPinned - self.seriesIdsRaw = try? JSONEncoder().encode(seriesIds) - } - } - - @Model - final class KomgaReadList { - @Attribute(.unique) var id: String // Composite: CompositeID.generate - - var readListId: String - var instanceId: String - - var name: String - var summary: String - var ordered: Bool - var createdDate: Date - var lastModifiedDate: Date - var filtered: Bool - var isPinned: Bool = false - - var bookIdsRaw: Data? - - // Track offline download status (managed locally, manual only) - var downloadStatusRaw: String = "notDownloaded" - var downloadError: String? - var downloadAt: Date? - var downloadedSize: Int64 = 0 - var downloadedBooks: Int = 0 - var pendingBooks: Int = 0 - - init( - id: String? = nil, - readListId: String, - instanceId: String, - name: String, - summary: String, - ordered: Bool, - createdDate: Date, - lastModifiedDate: Date, - filtered: Bool, - isPinned: Bool = false, - bookIds: [String] = [], - downloadedBooks: Int = 0, - pendingBooks: Int = 0, - downloadedSize: Int64 = 0 - ) { - self.id = id ?? CompositeID.generate(instanceId: instanceId, id: readListId) - self.readListId = readListId - self.instanceId = instanceId - self.name = name - self.summary = summary - self.ordered = ordered - self.createdDate = createdDate - self.lastModifiedDate = lastModifiedDate - self.filtered = filtered - self.isPinned = isPinned - self.bookIdsRaw = try? JSONEncoder().encode(bookIds) - self.downloadedBooks = downloadedBooks - self.pendingBooks = pendingBooks - self.downloadedSize = downloadedSize - } - } - - @Model - final class CustomFontV1 { - @Attribute(.unique) var name: String - var path: String? // File path for imported fonts, nil for system/manual fonts - var fileName: String? // Original file name for imported fonts - var fileSize: Int64? // File size in bytes for imported fonts - var createdAt: Date - - init(name: String, path: String? = nil, fileName: String? = nil, fileSize: Int64? = nil, createdAt: Date = Date()) { - self.name = name - self.path = path - self.fileName = fileName - self.fileSize = fileSize - self.createdAt = createdAt - } - } - - @Model - final class PendingProgress { - @Attribute(.unique) var id: String // Composite: "instanceId_bookId" - - var instanceId: String - var bookId: String - var page: Int - var completed: Bool - var createdAt: Date - var progressionData: Data? // For EPUB R2Progression - - init( - instanceId: String, - bookId: String, - page: Int, - completed: Bool, - progressionData: Data? = nil - ) { - self.id = CompositeID.generate(instanceId: instanceId, id: bookId) - self.instanceId = instanceId - self.bookId = bookId - self.page = page - self.completed = completed - self.createdAt = Date() - self.progressionData = progressionData - } - } - - @Model - final class SavedFilterV1 { - @Attribute(.unique) var id: UUID - - var name: String - var filterTypeRaw: String - var filterDataJSON: String - var createdAt: Date - var updatedAt: Date - - init( - id: UUID = UUID(), - name: String, - filterType: SavedFilterType, - filterDataJSON: String, - createdAt: Date = Date(), - updatedAt: Date = Date() - ) { - self.id = id - self.name = name - self.filterTypeRaw = filterType.rawValue - self.filterDataJSON = filterDataJSON - self.createdAt = createdAt - self.updatedAt = updatedAt - } - } - - @Model - final class EpubThemePresetV1 { - @Attribute(.unique) var id: UUID - - var name: String - var preferencesJSON: String - var createdAt: Date - var updatedAt: Date - - init( - id: UUID = UUID(), - name: String, - preferencesJSON: String, - createdAt: Date = Date(), - updatedAt: Date = Date() - ) { - self.id = id - self.name = name - self.preferencesJSON = preferencesJSON - self.createdAt = createdAt - self.updatedAt = updatedAt - } - } -} diff --git a/KMReader/MainApp.swift b/KMReader/MainApp.swift index 7e400731..dece546d 100644 --- a/KMReader/MainApp.swift +++ b/KMReader/MainApp.swift @@ -107,7 +107,6 @@ struct MainApp: App { private nonisolated static func makePreparedDatabaseQueue() throws -> DatabaseQueue { let queue = try LocalDatabase.open() try LocalDatabase.migrate(queue) - try LegacySwiftDataImporter.importIfNeeded(into: queue) return queue }