Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand All @@ -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+)
Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand All @@ -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**

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions KMReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = "";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = "";
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 = "";
Expand Down
53 changes: 5 additions & 48 deletions KMReader/Core/Storage/GRDB/LocalDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
Expand Down
Loading