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 } return try? JSONDecoder().decode(RDEPUBLocation.self, from: data) } public func saveLocation(_ location: RDEPUBLocation, for bookIdentifier: String) { guard let data = try? JSONEncoder().encode(location) else { return } defaults.set(data, forKey: locationPrefix + bookIdentifier) } public func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark] { guard let data = defaults.data(forKey: bookmarksPrefix + bookIdentifier) else { return [] } return (try? JSONDecoder().decode([RDEPUBBookmark].self, from: data)) ?? [] } public func saveBookmarks(_ bookmarks: [RDEPUBBookmark], for bookIdentifier: String) { guard let data = try? JSONEncoder().encode(bookmarks) else { return } defaults.set(data, forKey: bookmarksPrefix + bookIdentifier) } public func loadHighlights(for bookIdentifier: String) -> [RDEPUBHighlight] { guard let data = defaults.data(forKey: highlightsPrefix + bookIdentifier) else { return [] } return (try? JSONDecoder().decode([RDEPUBHighlight].self, from: data)) ?? [] } public func saveHighlights(_ highlights: [RDEPUBHighlight], for bookIdentifier: String) { guard let data = try? JSONEncoder().encode(highlights) else { return } 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) } public func loadReaderSettings() -> RDEPUBReaderSettings? { guard let data = defaults.data(forKey: settingsKey) else { return nil } return try? JSONDecoder().decode(RDEPUBReaderSettings.self, from: data) } public func saveReaderSettings(_ settings: RDEPUBReaderSettings) { guard let data = try? JSONEncoder().encode(settings) else { return } defaults.set(data, forKey: settingsKey) } }