ReadKeep/readeck/Data/Repository/SettingsRepository.swift
Ilyas Hallak df8a7b64b2 feat: Add Kingfisher caching, card layouts, dynamic tag layout, and undo delete
- Integrate Kingfisher for image caching with CachedAsyncImage component
- Add CacheSettingsView for managing image cache size and clearing cache
- Implement three card layout styles: compact, magazine (default), natural
- Add AppearanceSettingsView with visual layout previews and theme settings
- Create Clean Architecture for card layout with domain models and use cases
- Implement FlowLayout for dynamic label width calculation
- Add skeleton loading animation for initial bookmark loads
- Replace delete confirmation dialogs with immediate delete + 3-second undo
- Support multiple simultaneous undo operations with individual progress bars
- Add grayed-out visual feedback for pending deletions
- Centralize notification names in dedicated NotificationNames file
- Remove pagination logic from label management (replaced with FlowLayout)
- Update AsyncImage usage across BookmarkCardView, BookmarkDetailView, ImageViewerView
- Improve UI consistency and spacing throughout the app
2025-09-04 10:43:27 +02:00

259 lines
10 KiB
Swift

import Foundation
import CoreData
struct Settings {
var endpoint: String? = nil
var username: String? = nil
var password: String? = nil
var token: String? = nil
var fontFamily: FontFamily? = nil
var fontSize: FontSize? = nil
var hasFinishedSetup: Bool = false
var enableTTS: Bool? = nil
var theme: Theme? = nil
var cardLayoutStyle: CardLayoutStyle? = nil
var isLoggedIn: Bool {
token != nil && !token!.isEmpty
}
mutating func setToken(_ newToken: String) {
token = newToken
}
}
protocol PSettingsRepository {
func saveSettings(_ settings: Settings) async throws
func loadSettings() async throws -> Settings?
func clearSettings() async throws
func saveToken(_ token: String) async throws
func saveUsername(_ username: String) async throws
func savePassword(_ password: String) async throws
func saveHasFinishedSetup(_ hasFinishedSetup: Bool) async throws
func saveServerSettings(endpoint: String, username: String, password: String, token: String) async throws
func saveCardLayoutStyle(_ cardLayoutStyle: CardLayoutStyle) async throws
func loadCardLayoutStyle() async throws -> CardLayoutStyle
var hasFinishedSetup: Bool { get }
}
class SettingsRepository: PSettingsRepository {
private let coreDataManager = CoreDataManager.shared
private let userDefault = UserDefaults.standard
private let keychainHelper = KeychainHelper.shared
func saveSettings(_ settings: Settings) async throws {
// Save credentials to keychain
if let endpoint = settings.endpoint, !endpoint.isEmpty {
keychainHelper.saveEndpoint(endpoint)
}
if let username = settings.username, !username.isEmpty {
keychainHelper.saveUsername(username)
}
if let password = settings.password, !password.isEmpty {
keychainHelper.savePassword(password)
}
if let token = settings.token, !token.isEmpty {
keychainHelper.saveToken(token)
}
// Save UI preferences to Core Data
let context = coreDataManager.context
return try await withCheckedThrowingContinuation { continuation in
context.perform {
do {
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
let existingSettings = try context.fetch(fetchRequest).first ?? SettingEntity(context: context)
if let fontFamily = settings.fontFamily {
existingSettings.fontFamily = fontFamily.rawValue
}
if let fontSize = settings.fontSize {
existingSettings.fontSize = fontSize.rawValue
}
if let enableTTS = settings.enableTTS {
existingSettings.enableTTS = enableTTS
}
if let theme = settings.theme {
existingSettings.theme = theme.rawValue
}
if let cardLayoutStyle = settings.cardLayoutStyle {
existingSettings.cardLayoutStyle = cardLayoutStyle.rawValue
}
try context.save()
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
}
func loadSettings() async throws -> Settings? {
let context = coreDataManager.context
return try await withCheckedThrowingContinuation { continuation in
context.perform {
do {
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
fetchRequest.fetchLimit = 1
let settingEntities = try context.fetch(fetchRequest)
let settingEntity = settingEntities.first
// Load credentials from keychain only
let endpoint = self.keychainHelper.loadEndpoint()
let username = self.keychainHelper.loadUsername()
let password = self.keychainHelper.loadPassword()
let token = self.keychainHelper.loadToken()
// Load UI preferences from Core Data
let settings = Settings(
endpoint: endpoint,
username: username,
password: password,
token: token,
fontFamily: FontFamily(rawValue: settingEntity?.fontFamily ?? FontFamily.system.rawValue),
fontSize: FontSize(rawValue: settingEntity?.fontSize ?? FontSize.medium.rawValue),
enableTTS: settingEntity?.enableTTS,
theme: Theme(rawValue: settingEntity?.theme ?? Theme.system.rawValue),
cardLayoutStyle: CardLayoutStyle(rawValue: settingEntity?.cardLayoutStyle ?? CardLayoutStyle.magazine.rawValue)
)
continuation.resume(returning: settings)
} catch {
continuation.resume(throwing: error)
}
}
}
}
func clearSettings() async throws {
// Clear credentials from keychain
keychainHelper.clearCredentials()
// Also clear from Core Data
let context = coreDataManager.context
return try await withCheckedThrowingContinuation { continuation in
context.perform {
do {
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
let settingEntities = try context.fetch(fetchRequest)
for settingEntity in settingEntities {
context.delete(settingEntity)
}
try context.save()
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
}
func saveToken(_ token: String) async throws {
// Save to keychain only
keychainHelper.saveToken(token)
// Wenn ein Token gespeichert wird, Setup als abgeschlossen markieren
if !token.isEmpty {
self.hasFinishedSetup = true
// Notification senden, dass sich der Setup-Status geändert hat
DispatchQueue.main.async {
NotificationCenter.default.post(name: .setupStatusChanged, object: nil)
}
}
}
func saveServerSettings(endpoint: String, username: String, password: String, token: String) async throws {
keychainHelper.saveEndpoint(endpoint)
keychainHelper.saveUsername(username)
keychainHelper.savePassword(password)
keychainHelper.saveToken(token)
if !token.isEmpty {
self.hasFinishedSetup = true
DispatchQueue.main.async {
NotificationCenter.default.post(name: .setupStatusChanged, object: nil)
}
}
}
func saveUsername(_ username: String) async throws {
keychainHelper.saveUsername(username)
}
func savePassword(_ password: String) async throws {
keychainHelper.savePassword(password)
}
func saveHasFinishedSetup(_ hasFinishedSetup: Bool) async throws {
return try await withCheckedThrowingContinuation { continuation in
self.hasFinishedSetup = hasFinishedSetup
// Notification senden, dass sich der Setup-Status geändert hat
DispatchQueue.main.async {
NotificationCenter.default.post(name: .setupStatusChanged, object: nil)
}
continuation.resume()
}
}
var hasFinishedSetup: Bool {
get {
return userDefault.value(forKey: "hasFinishedSetup") as? Bool ?? false
}
set {
userDefault.set(newValue, forKey: "hasFinishedSetup")
}
}
func saveCardLayoutStyle(_ cardLayoutStyle: CardLayoutStyle) async throws {
let context = coreDataManager.context
return try await withCheckedThrowingContinuation { continuation in
context.perform {
do {
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
let existingSettings = try context.fetch(fetchRequest).first ?? SettingEntity(context: context)
existingSettings.cardLayoutStyle = cardLayoutStyle.rawValue
try context.save()
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
}
func loadCardLayoutStyle() async throws -> CardLayoutStyle {
let context = coreDataManager.context
return try await withCheckedThrowingContinuation { continuation in
context.perform {
do {
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
fetchRequest.fetchLimit = 1
let settingEntities = try context.fetch(fetchRequest)
let settingEntity = settingEntities.first
let cardLayoutStyle = CardLayoutStyle(rawValue: settingEntity?.cardLayoutStyle ?? CardLayoutStyle.magazine.rawValue) ?? .magazine
continuation.resume(returning: cardLayoutStyle)
} catch {
continuation.resume(throwing: error)
}
}
}
}
}