Phase 4 - Settings UI: - Add OfflineSettingsViewModel with reactive bindings - Add OfflineSettingsView with toggle, slider, sync button - Integrate into SettingsContainerView - Extend factories with offline dependencies - Add debug button to simulate offline mode (DEBUG only) Phase 5 - App Integration: - AppViewModel: Auto-sync on app start with 4h check - BookmarksViewModel: Offline fallback loading cached articles - BookmarksView: Offline banner when network unavailable - BookmarkDetailViewModel: Cache-first article loading - Fix concurrency issues with CurrentValueSubject Features: - Background sync on app start (non-blocking) - Cached bookmarks shown when offline - Instant article loading from cache - Visual offline indicator banner - Full offline reading experience All features compile and build successfully.
90 lines
2.5 KiB
Swift
90 lines
2.5 KiB
Swift
//
|
|
// OfflineSettingsViewModel.swift
|
|
// readeck
|
|
//
|
|
// Created by Claude on 17.11.25.
|
|
//
|
|
|
|
import Foundation
|
|
import Observation
|
|
import Combine
|
|
|
|
@Observable
|
|
class OfflineSettingsViewModel {
|
|
|
|
// MARK: - Dependencies
|
|
|
|
private let settingsRepository: PSettingsRepository
|
|
private let offlineCacheSyncUseCase: POfflineCacheSyncUseCase
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
// MARK: - Published State
|
|
|
|
var offlineSettings: OfflineSettings = OfflineSettings()
|
|
var isSyncing: Bool = false
|
|
var syncProgress: String?
|
|
var cachedArticlesCount: Int = 0
|
|
var cacheSize: String = "0 KB"
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
|
self.settingsRepository = factory.makeSettingsRepository()
|
|
self.offlineCacheSyncUseCase = factory.makeOfflineCacheSyncUseCase()
|
|
|
|
setupBindings()
|
|
}
|
|
|
|
// MARK: - Setup
|
|
|
|
private func setupBindings() {
|
|
// Bind isSyncing from UseCase
|
|
offlineCacheSyncUseCase.isSyncing
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: \.isSyncing, on: self)
|
|
.store(in: &cancellables)
|
|
|
|
// Bind syncProgress from UseCase
|
|
offlineCacheSyncUseCase.syncProgress
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: \.syncProgress, on: self)
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
@MainActor
|
|
func loadSettings() async {
|
|
do {
|
|
offlineSettings = try await settingsRepository.loadOfflineSettings()
|
|
updateCacheStats()
|
|
Logger.viewModel.debug("Loaded offline settings: enabled=\(offlineSettings.enabled)")
|
|
} catch {
|
|
Logger.viewModel.error("Failed to load offline settings: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func saveSettings() async {
|
|
do {
|
|
try await settingsRepository.saveOfflineSettings(offlineSettings)
|
|
Logger.viewModel.debug("Saved offline settings")
|
|
} catch {
|
|
Logger.viewModel.error("Failed to save offline settings: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func syncNow() async {
|
|
Logger.viewModel.info("Manual sync triggered")
|
|
await offlineCacheSyncUseCase.syncOfflineArticles(settings: offlineSettings)
|
|
updateCacheStats()
|
|
}
|
|
|
|
@MainActor
|
|
func updateCacheStats() {
|
|
cachedArticlesCount = offlineCacheSyncUseCase.getCachedArticlesCount()
|
|
cacheSize = offlineCacheSyncUseCase.getCacheSize()
|
|
}
|
|
}
|