import Foundation public protocol RDEPUBReaderPersistence: AnyObject { func loadLocation(for bookIdentifier: String) -> RDEPUBLocation? func saveLocation(_ location: RDEPUBLocation, for bookIdentifier: String) func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark] func saveBookmarks(_ bookmarks: [RDEPUBBookmark], for bookIdentifier: String) func loadHighlights(for bookIdentifier: String) -> [RDEPUBHighlight] func saveHighlights(_ highlights: [RDEPUBHighlight], for bookIdentifier: String) func loadReaderSettings() -> RDEPUBReaderSettings? func saveReaderSettings(_ settings: RDEPUBReaderSettings) } public extension RDEPUBReaderPersistence { func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark] { #if DEBUG print("[RDEPUBReaderPersistence] ⚠️ loadBookmarks called on default no-op implementation for: \(bookIdentifier)") #endif return [] } func saveBookmarks(_ bookmarks: [RDEPUBBookmark], for bookIdentifier: String) { #if DEBUG print("[RDEPUBReaderPersistence] ⚠️ saveBookmarks(\(bookmarks.count) items) called on default no-op implementation for: \(bookIdentifier)") #endif } func loadReaderSettings() -> RDEPUBReaderSettings? { #if DEBUG print("[RDEPUBReaderPersistence] ⚠️ loadReaderSettings called on default no-op implementation") #endif return nil } func saveReaderSettings(_ settings: RDEPUBReaderSettings) { #if DEBUG print("[RDEPUBReaderPersistence] ⚠️ saveReaderSettings called on default no-op implementation") #endif } } public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence { private let defaults: UserDefaults private let locationPrefix: String private let bookmarksPrefix: String private let highlightsPrefix: String private let settingsKey: String public init( defaults: UserDefaults = .standard, locationPrefix: String = "ssreader.epub.location.", bookmarksPrefix: String = "ssreader.epub.bookmarks.", highlightsPrefix: String = "ssreader.epub.highlights.", settingsKey: String = "ssreader.epub.settings" ) { self.defaults = defaults self.locationPrefix = locationPrefix self.bookmarksPrefix = bookmarksPrefix self.highlightsPrefix = highlightsPrefix self.settingsKey = settingsKey } public func loadLocation(for bookIdentifier: String) -> RDEPUBLocation? { guard let data = defaults.data(forKey: locationPrefix + bookIdentifier) else { return nil } do { return try JSONDecoder().decode(RDEPUBLocation.self, from: data) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to decode location for '\(bookIdentifier)': \(error)") #endif return nil } } public func saveLocation(_ location: RDEPUBLocation, for bookIdentifier: String) { do { let data = try JSONEncoder().encode(location) defaults.set(data, forKey: locationPrefix + bookIdentifier) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to encode location for '\(bookIdentifier)': \(error)") #endif } } public func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark] { guard let data = defaults.data(forKey: bookmarksPrefix + bookIdentifier) else { return [] } do { return try JSONDecoder().decode([RDEPUBBookmark].self, from: data) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to decode bookmarks for '\(bookIdentifier)': \(error)") #endif return [] } } public func saveBookmarks(_ bookmarks: [RDEPUBBookmark], for bookIdentifier: String) { do { let data = try JSONEncoder().encode(bookmarks) defaults.set(data, forKey: bookmarksPrefix + bookIdentifier) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to encode bookmarks for '\(bookIdentifier)': \(error)") #endif } } public func loadHighlights(for bookIdentifier: String) -> [RDEPUBHighlight] { guard let data = defaults.data(forKey: highlightsPrefix + bookIdentifier) else { return [] } do { return try JSONDecoder().decode([RDEPUBHighlight].self, from: data) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to decode highlights for '\(bookIdentifier)': \(error)") #endif return [] } } public func saveHighlights(_ highlights: [RDEPUBHighlight], for bookIdentifier: String) { do { let data = try JSONEncoder().encode(highlights) if data.count > 1_048_576 { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ saveHighlights data size (\(data.count) bytes) exceeds 1MB for: \(bookIdentifier)") #endif } defaults.set(data, forKey: highlightsPrefix + bookIdentifier) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to encode highlights for '\(bookIdentifier)': \(error)") #endif } } public func loadReaderSettings() -> RDEPUBReaderSettings? { guard let data = defaults.data(forKey: settingsKey) else { return nil } do { return try JSONDecoder().decode(RDEPUBReaderSettings.self, from: data) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to decode reader settings: \(error)") #endif return nil } } public func saveReaderSettings(_ settings: RDEPUBReaderSettings) { do { let data = try JSONEncoder().encode(settings) defaults.set(data, forKey: settingsKey) } catch { #if DEBUG print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to encode reader settings: \(error)") #endif } } }