Refactor settings to use Clean Architecture with ViewModels
- Add cache settings UseCases (get/update size, clear cache) - Create CacheSettingsViewModel and OfflineSettingsViewModel - Replace direct UserDefaults access with repository pattern - Add CachedArticlesPreviewView for viewing offline articles - Integrate offline settings into main SettingsContainerView - Wire up new UseCases in factory pattern
This commit is contained in:
parent
d3e15c6352
commit
4fd55ef5d0
@ -76,6 +76,35 @@ class CoreDataManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
func resetDatabase() throws {
|
||||||
|
logger.warning("⚠️ Resetting Core Data database - ALL DATA WILL BE DELETED")
|
||||||
|
|
||||||
|
guard let store = persistentContainer.persistentStoreCoordinator.persistentStores.first else {
|
||||||
|
throw NSError(domain: "CoreDataManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No persistent store found"])
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let storeURL = store.url else {
|
||||||
|
throw NSError(domain: "CoreDataManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Store URL not found"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the persistent store
|
||||||
|
try persistentContainer.persistentStoreCoordinator.remove(store)
|
||||||
|
|
||||||
|
// Delete the store files
|
||||||
|
try FileManager.default.removeItem(at: storeURL)
|
||||||
|
|
||||||
|
// Also delete related files (-wal, -shm)
|
||||||
|
let walURL = storeURL.deletingPathExtension().appendingPathExtension("sqlite-wal")
|
||||||
|
let shmURL = storeURL.deletingPathExtension().appendingPathExtension("sqlite-shm")
|
||||||
|
|
||||||
|
try? FileManager.default.removeItem(at: walURL)
|
||||||
|
try? FileManager.default.removeItem(at: shmURL)
|
||||||
|
|
||||||
|
logger.info("Core Data database files deleted successfully")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private func setupInMemoryStore(container: NSPersistentContainer) {
|
private func setupInMemoryStore(container: NSPersistentContainer) {
|
||||||
logger.warning("Setting up in-memory Core Data store as fallback")
|
logger.warning("Setting up in-memory Core Data store as fallback")
|
||||||
isInMemoryStore = true
|
isInMemoryStore = true
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct OfflineSettings: Codable {
|
struct OfflineSettings: Codable {
|
||||||
var enabled: Bool = true
|
var enabled: Bool = false
|
||||||
var maxUnreadArticles: Double = 20 // Double für Slider (Default: 20 Artikel)
|
var maxUnreadArticles: Double = 20 // Double für Slider (Default: 20 Artikel)
|
||||||
var saveImages: Bool = false
|
var saveImages: Bool = false
|
||||||
var lastSyncDate: Date?
|
var lastSyncDate: Date?
|
||||||
|
|||||||
17
readeck/Domain/UseCase/ClearCacheUseCase.swift
Normal file
17
readeck/Domain/UseCase/ClearCacheUseCase.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol PClearCacheUseCase {
|
||||||
|
func execute() async throws
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClearCacheUseCase: PClearCacheUseCase {
|
||||||
|
private let settingsRepository: PSettingsRepository
|
||||||
|
|
||||||
|
init(settingsRepository: PSettingsRepository) {
|
||||||
|
self.settingsRepository = settingsRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute() async throws {
|
||||||
|
try await settingsRepository.clearCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
17
readeck/Domain/UseCase/GetCacheSizeUseCase.swift
Normal file
17
readeck/Domain/UseCase/GetCacheSizeUseCase.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol PGetCacheSizeUseCase {
|
||||||
|
func execute() async throws -> UInt
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetCacheSizeUseCase: PGetCacheSizeUseCase {
|
||||||
|
private let settingsRepository: PSettingsRepository
|
||||||
|
|
||||||
|
init(settingsRepository: PSettingsRepository) {
|
||||||
|
self.settingsRepository = settingsRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute() async throws -> UInt {
|
||||||
|
return try await settingsRepository.getCacheSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
17
readeck/Domain/UseCase/GetMaxCacheSizeUseCase.swift
Normal file
17
readeck/Domain/UseCase/GetMaxCacheSizeUseCase.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol PGetMaxCacheSizeUseCase {
|
||||||
|
func execute() async throws -> UInt
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetMaxCacheSizeUseCase: PGetMaxCacheSizeUseCase {
|
||||||
|
private let settingsRepository: PSettingsRepository
|
||||||
|
|
||||||
|
init(settingsRepository: PSettingsRepository) {
|
||||||
|
self.settingsRepository = settingsRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute() async throws -> UInt {
|
||||||
|
return try await settingsRepository.getMaxCacheSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
17
readeck/Domain/UseCase/UpdateMaxCacheSizeUseCase.swift
Normal file
17
readeck/Domain/UseCase/UpdateMaxCacheSizeUseCase.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol PUpdateMaxCacheSizeUseCase {
|
||||||
|
func execute(sizeInBytes: UInt) async throws
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateMaxCacheSizeUseCase: PUpdateMaxCacheSizeUseCase {
|
||||||
|
private let settingsRepository: PSettingsRepository
|
||||||
|
|
||||||
|
init(settingsRepository: PSettingsRepository) {
|
||||||
|
self.settingsRepository = settingsRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(sizeInBytes: UInt) async throws {
|
||||||
|
try await settingsRepository.updateMaxCacheSize(sizeInBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,6 +31,10 @@ protocol UseCaseFactory {
|
|||||||
func makeGetCachedBookmarksUseCase() -> PGetCachedBookmarksUseCase
|
func makeGetCachedBookmarksUseCase() -> PGetCachedBookmarksUseCase
|
||||||
func makeGetCachedArticleUseCase() -> PGetCachedArticleUseCase
|
func makeGetCachedArticleUseCase() -> PGetCachedArticleUseCase
|
||||||
func makeCreateAnnotationUseCase() -> PCreateAnnotationUseCase
|
func makeCreateAnnotationUseCase() -> PCreateAnnotationUseCase
|
||||||
|
func makeGetCacheSizeUseCase() -> PGetCacheSizeUseCase
|
||||||
|
func makeGetMaxCacheSizeUseCase() -> PGetMaxCacheSizeUseCase
|
||||||
|
func makeUpdateMaxCacheSizeUseCase() -> PUpdateMaxCacheSizeUseCase
|
||||||
|
func makeClearCacheUseCase() -> PClearCacheUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -180,4 +184,20 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
func makeCreateAnnotationUseCase() -> PCreateAnnotationUseCase {
|
func makeCreateAnnotationUseCase() -> PCreateAnnotationUseCase {
|
||||||
return CreateAnnotationUseCase(repository: annotationsRepository)
|
return CreateAnnotationUseCase(repository: annotationsRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeGetCacheSizeUseCase() -> PGetCacheSizeUseCase {
|
||||||
|
return GetCacheSizeUseCase(settingsRepository: settingsRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGetMaxCacheSizeUseCase() -> PGetMaxCacheSizeUseCase {
|
||||||
|
return GetMaxCacheSizeUseCase(settingsRepository: settingsRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUpdateMaxCacheSizeUseCase() -> PUpdateMaxCacheSizeUseCase {
|
||||||
|
return UpdateMaxCacheSizeUseCase(settingsRepository: settingsRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeClearCacheUseCase() -> PClearCacheUseCase {
|
||||||
|
return ClearCacheUseCase(settingsRepository: settingsRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,18 @@ import Foundation
|
|||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class MockUseCaseFactory: UseCaseFactory {
|
class MockUseCaseFactory: UseCaseFactory {
|
||||||
|
func makeGetCachedBookmarksUseCase() -> any PGetCachedBookmarksUseCase {
|
||||||
|
MockGetCachedBookmarksUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGetCachedArticleUseCase() -> any PGetCachedArticleUseCase {
|
||||||
|
MockGetCachedArticleUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCreateAnnotationUseCase() -> any PCreateAnnotationUseCase {
|
||||||
|
MockCreateAnnotationUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
func makeCheckServerReachabilityUseCase() -> any PCheckServerReachabilityUseCase {
|
func makeCheckServerReachabilityUseCase() -> any PCheckServerReachabilityUseCase {
|
||||||
MockCheckServerReachabilityUseCase()
|
MockCheckServerReachabilityUseCase()
|
||||||
}
|
}
|
||||||
@ -116,6 +128,22 @@ class MockUseCaseFactory: UseCaseFactory {
|
|||||||
func makeNetworkMonitorUseCase() -> PNetworkMonitorUseCase {
|
func makeNetworkMonitorUseCase() -> PNetworkMonitorUseCase {
|
||||||
MockNetworkMonitorUseCase()
|
MockNetworkMonitorUseCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeGetCacheSizeUseCase() -> PGetCacheSizeUseCase {
|
||||||
|
MockGetCacheSizeUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGetMaxCacheSizeUseCase() -> PGetMaxCacheSizeUseCase {
|
||||||
|
MockGetMaxCacheSizeUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUpdateMaxCacheSizeUseCase() -> PUpdateMaxCacheSizeUseCase {
|
||||||
|
MockUpdateMaxCacheSizeUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeClearCacheUseCase() -> PClearCacheUseCase {
|
||||||
|
MockClearCacheUseCase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -313,6 +341,10 @@ class MockSettingsRepository: PSettingsRepository {
|
|||||||
return OfflineSettings()
|
return OfflineSettings()
|
||||||
}
|
}
|
||||||
func saveOfflineSettings(_ settings: OfflineSettings) async throws {}
|
func saveOfflineSettings(_ settings: OfflineSettings) async throws {}
|
||||||
|
func getCacheSize() async throws -> UInt { return 0 }
|
||||||
|
func getMaxCacheSize() async throws -> UInt { return 200 * 1024 * 1024 }
|
||||||
|
func updateMaxCacheSize(_ sizeInBytes: UInt) async throws {}
|
||||||
|
func clearCache() async throws {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockOfflineCacheSyncUseCase: POfflineCacheSyncUseCase {
|
class MockOfflineCacheSyncUseCase: POfflineCacheSyncUseCase {
|
||||||
@ -374,8 +406,53 @@ class MockNetworkMonitorUseCase: PNetworkMonitorUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MockGetCachedBookmarksUseCase: PGetCachedBookmarksUseCase {
|
||||||
|
func execute() async throws -> [Bookmark] {
|
||||||
|
return [Bookmark.mock]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockGetCachedArticleUseCase: PGetCachedArticleUseCase {
|
||||||
|
func execute(id: String) -> String? {
|
||||||
|
let path = Bundle.main.path(forResource: "article", ofType: "html")
|
||||||
|
return try? String(contentsOfFile: path!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockCreateAnnotationUseCase: PCreateAnnotationUseCase {
|
||||||
|
func execute(bookmarkId: String, color: String, startOffset: Int, endOffset: Int, startSelector: String, endSelector: String) async throws -> Annotation {
|
||||||
|
return Annotation(id: "", text: "", created: "", startOffset: 0, endOffset: 1, startSelector: "", endSelector: "")
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(bookmarkId: String, text: String, startOffset: Int, endOffset: Int, startSelector: String, endSelector: String) async throws {
|
||||||
|
// Mock implementation - do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Bookmark {
|
extension Bookmark {
|
||||||
static let mock: Bookmark = .init(
|
static let mock: Bookmark = .init(
|
||||||
id: "123", title: "title", url: "https://example.com", href: "https://example.com", description: "description", authors: ["Tom"], created: "", published: "", updated: "", siteName: "example.com", site: "https://example.com", readingTime: 2, wordCount: 20, hasArticle: true, isArchived: false, isDeleted: false, isMarked: true, labels: ["Test"], lang: "EN", loaded: false, readProgress: 0, documentType: "", state: 0, textDirection: "ltr", type: "", resources: .init(article: nil, icon: nil, image: nil, log: nil, props: nil, thumbnail: nil)
|
id: "123", title: "title", url: "https://example.com", href: "https://example.com", description: "description", authors: ["Tom"], created: "", published: "", updated: "", siteName: "example.com", site: "https://example.com", readingTime: 2, wordCount: 20, hasArticle: true, isArchived: false, isDeleted: false, isMarked: true, labels: ["Test"], lang: "EN", loaded: false, readProgress: 0, documentType: "", state: 0, textDirection: "ltr", type: "", resources: .init(article: nil, icon: nil, image: nil, log: nil, props: nil, thumbnail: nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MockGetCacheSizeUseCase: PGetCacheSizeUseCase {
|
||||||
|
func execute() async throws -> UInt {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockGetMaxCacheSizeUseCase: PGetMaxCacheSizeUseCase {
|
||||||
|
func execute() async throws -> UInt {
|
||||||
|
return 200 * 1024 * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockUpdateMaxCacheSizeUseCase: PUpdateMaxCacheSizeUseCase {
|
||||||
|
func execute(sizeInBytes: UInt) async throws {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockClearCacheUseCase: PClearCacheUseCase {
|
||||||
|
func execute() async throws {}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,24 +1,22 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Kingfisher
|
|
||||||
|
|
||||||
struct CacheSettingsView: View {
|
struct CacheSettingsView: View {
|
||||||
@State private var cacheSize: String = "0 MB"
|
@State private var viewModel = CacheSettingsViewModel()
|
||||||
@State private var maxCacheSize: Double = 200
|
|
||||||
@State private var isClearing: Bool = false
|
|
||||||
@State private var showClearAlert: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Current Cache Size")
|
Text("Current Cache Size")
|
||||||
Text("\(cacheSize) / \(Int(maxCacheSize)) MB max")
|
Text("\(viewModel.cacheSize) / \(Int(viewModel.maxCacheSize)) MB max")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Refresh") {
|
Button("Refresh") {
|
||||||
updateCacheSize()
|
Task {
|
||||||
|
await viewModel.updateCacheSize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
@ -28,24 +26,26 @@ struct CacheSettingsView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text("Max Cache Size")
|
Text("Max Cache Size")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(Int(maxCacheSize)) MB")
|
Text("\(Int(viewModel.maxCacheSize)) MB")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider(value: $maxCacheSize, in: 50...1200, step: 50) {
|
Slider(value: $viewModel.maxCacheSize, in: 50...1200, step: 50) {
|
||||||
Text("Max Cache Size")
|
Text("Max Cache Size")
|
||||||
}
|
}
|
||||||
.onChange(of: maxCacheSize) { _, newValue in
|
.onChange(of: viewModel.maxCacheSize) { _, newValue in
|
||||||
updateMaxCacheSize(newValue)
|
Task {
|
||||||
|
await viewModel.updateMaxCacheSize(newValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showClearAlert = true
|
viewModel.showClearAlert = true
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
if isClearing {
|
if viewModel.isClearing {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.scaleEffect(0.8)
|
.scaleEffect(0.8)
|
||||||
} else {
|
} else {
|
||||||
@ -55,7 +55,7 @@ struct CacheSettingsView: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Clear Cache")
|
Text("Clear Cache")
|
||||||
.foregroundColor(isClearing ? .secondary : .red)
|
.foregroundColor(viewModel.isClearing ? .secondary : .red)
|
||||||
Text("Remove all cached images")
|
Text("Remove all cached images")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
@ -64,69 +64,24 @@ struct CacheSettingsView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(isClearing)
|
.disabled(viewModel.isClearing)
|
||||||
} header: {
|
} header: {
|
||||||
Text("Cache Settings")
|
Text("Cache Settings")
|
||||||
}
|
}
|
||||||
.onAppear {
|
.task {
|
||||||
updateCacheSize()
|
await viewModel.loadCacheSettings()
|
||||||
loadMaxCacheSize()
|
|
||||||
}
|
}
|
||||||
.alert("Clear Cache", isPresented: $showClearAlert) {
|
.alert("Clear Cache", isPresented: $viewModel.showClearAlert) {
|
||||||
Button("Cancel", role: .cancel) { }
|
Button("Cancel", role: .cancel) { }
|
||||||
Button("Clear", role: .destructive) {
|
Button("Clear", role: .destructive) {
|
||||||
clearCache()
|
Task {
|
||||||
|
await viewModel.clearCache()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text("This will remove all cached images. They will be downloaded again when needed.")
|
Text("This will remove all cached images. They will be downloaded again when needed.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCacheSize() {
|
|
||||||
KingfisherManager.shared.cache.calculateDiskStorageSize { result in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
switch result {
|
|
||||||
case .success(let size):
|
|
||||||
let mbSize = Double(size) / (1024 * 1024)
|
|
||||||
self.cacheSize = String(format: "%.1f MB", mbSize)
|
|
||||||
case .failure:
|
|
||||||
self.cacheSize = "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadMaxCacheSize() {
|
|
||||||
let savedSize = UserDefaults.standard.object(forKey: "KingfisherMaxCacheSize") as? UInt
|
|
||||||
if let savedSize = savedSize {
|
|
||||||
maxCacheSize = Double(savedSize) / (1024 * 1024)
|
|
||||||
KingfisherManager.shared.cache.diskStorage.config.sizeLimit = savedSize
|
|
||||||
} else {
|
|
||||||
maxCacheSize = 200
|
|
||||||
let defaultBytes = UInt(200 * 1024 * 1024)
|
|
||||||
KingfisherManager.shared.cache.diskStorage.config.sizeLimit = defaultBytes
|
|
||||||
UserDefaults.standard.set(defaultBytes, forKey: "KingfisherMaxCacheSize")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateMaxCacheSize(_ newSize: Double) {
|
|
||||||
let bytes = UInt(newSize * 1024 * 1024)
|
|
||||||
KingfisherManager.shared.cache.diskStorage.config.sizeLimit = bytes
|
|
||||||
UserDefaults.standard.set(bytes, forKey: "KingfisherMaxCacheSize")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clearCache() {
|
|
||||||
isClearing = true
|
|
||||||
|
|
||||||
KingfisherManager.shared.cache.clearDiskCache {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isClearing = false
|
|
||||||
self.updateCacheSize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KingfisherManager.shared.cache.clearMemoryCache()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
92
readeck/UI/Settings/CacheSettingsViewModel.swift
Normal file
92
readeck/UI/Settings/CacheSettingsViewModel.swift
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// CacheSettingsViewModel.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Claude on 01.12.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class CacheSettingsViewModel {
|
||||||
|
|
||||||
|
// MARK: - Dependencies
|
||||||
|
|
||||||
|
private let getCacheSizeUseCase: PGetCacheSizeUseCase
|
||||||
|
private let getMaxCacheSizeUseCase: PGetMaxCacheSizeUseCase
|
||||||
|
private let updateMaxCacheSizeUseCase: PUpdateMaxCacheSizeUseCase
|
||||||
|
private let clearCacheUseCase: PClearCacheUseCase
|
||||||
|
|
||||||
|
// MARK: - Published State
|
||||||
|
|
||||||
|
var cacheSize: String = "0 MB"
|
||||||
|
var maxCacheSize: Double = 200 // in MB
|
||||||
|
var isClearing: Bool = false
|
||||||
|
var showClearAlert: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
||||||
|
self.getCacheSizeUseCase = factory.makeGetCacheSizeUseCase()
|
||||||
|
self.getMaxCacheSizeUseCase = factory.makeGetMaxCacheSizeUseCase()
|
||||||
|
self.updateMaxCacheSizeUseCase = factory.makeUpdateMaxCacheSizeUseCase()
|
||||||
|
self.clearCacheUseCase = factory.makeClearCacheUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func loadCacheSettings() async {
|
||||||
|
await updateCacheSize()
|
||||||
|
await loadMaxCacheSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func updateCacheSize() async {
|
||||||
|
do {
|
||||||
|
let sizeInBytes = try await getCacheSizeUseCase.execute()
|
||||||
|
let mbSize = Double(sizeInBytes) / (1024 * 1024)
|
||||||
|
cacheSize = String(format: "%.1f MB", mbSize)
|
||||||
|
Logger.viewModel.debug("Cache size: \(cacheSize)")
|
||||||
|
} catch {
|
||||||
|
cacheSize = "Unknown"
|
||||||
|
Logger.viewModel.error("Failed to get cache size: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func loadMaxCacheSize() async {
|
||||||
|
do {
|
||||||
|
let sizeInBytes = try await getMaxCacheSizeUseCase.execute()
|
||||||
|
maxCacheSize = Double(sizeInBytes) / (1024 * 1024)
|
||||||
|
Logger.viewModel.debug("Max cache size: \(maxCacheSize) MB")
|
||||||
|
} catch {
|
||||||
|
Logger.viewModel.error("Failed to load max cache size: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func updateMaxCacheSize(_ newSize: Double) async {
|
||||||
|
let bytes = UInt(newSize * 1024 * 1024)
|
||||||
|
do {
|
||||||
|
try await updateMaxCacheSizeUseCase.execute(sizeInBytes: bytes)
|
||||||
|
Logger.viewModel.info("Updated max cache size to \(newSize) MB")
|
||||||
|
} catch {
|
||||||
|
Logger.viewModel.error("Failed to update max cache size: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func clearCache() async {
|
||||||
|
isClearing = true
|
||||||
|
do {
|
||||||
|
try await clearCacheUseCase.execute()
|
||||||
|
await updateCacheSize()
|
||||||
|
Logger.viewModel.info("Cache cleared successfully")
|
||||||
|
} catch {
|
||||||
|
Logger.viewModel.error("Failed to clear cache: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
isClearing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
200
readeck/UI/Settings/CachedArticlesPreviewView.swift
Normal file
200
readeck/UI/Settings/CachedArticlesPreviewView.swift
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
//
|
||||||
|
// CachedArticlesPreviewView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Claude on 30.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CachedArticlesPreviewView: View {
|
||||||
|
|
||||||
|
// MARK: - State
|
||||||
|
|
||||||
|
@State private var viewModel = CachedArticlesPreviewViewModel()
|
||||||
|
@State private var selectedBookmarkId: String?
|
||||||
|
@EnvironmentObject var appSettings: AppSettings
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
if viewModel.isLoading && viewModel.cachedBookmarks.isEmpty {
|
||||||
|
loadingView
|
||||||
|
} else if let errorMessage = viewModel.errorMessage {
|
||||||
|
errorView(message: errorMessage)
|
||||||
|
} else if viewModel.cachedBookmarks.isEmpty {
|
||||||
|
emptyStateView
|
||||||
|
} else {
|
||||||
|
cachedBookmarksList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Cached Articles")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationDestination(
|
||||||
|
item: Binding<String?>(
|
||||||
|
get: { selectedBookmarkId },
|
||||||
|
set: { selectedBookmarkId = $0 }
|
||||||
|
)
|
||||||
|
) { bookmarkId in
|
||||||
|
BookmarkDetailView(bookmarkId: bookmarkId)
|
||||||
|
.toolbar(.hidden, for: .tabBar)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
await viewModel.loadCachedBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - View Components
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var cachedBookmarksList: some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
ForEach(viewModel.cachedBookmarks, id: \.id) { bookmark in
|
||||||
|
Button(action: {
|
||||||
|
selectedBookmarkId = bookmark.id
|
||||||
|
}) {
|
||||||
|
BookmarkCardView(
|
||||||
|
bookmark: bookmark,
|
||||||
|
currentState: .unread,
|
||||||
|
layout: .magazine,
|
||||||
|
onArchive: { _ in },
|
||||||
|
onDelete: { _ in },
|
||||||
|
onToggleFavorite: { _ in }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.listRowInsets(EdgeInsets(
|
||||||
|
top: 12,
|
||||||
|
leading: 16,
|
||||||
|
bottom: 12,
|
||||||
|
trailing: 16
|
||||||
|
))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color(R.color.bookmark_list_bg))
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.font(.caption)
|
||||||
|
Text("\(viewModel.cachedBookmarks.count) articles cached")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.textCase(nil)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
} footer: {
|
||||||
|
Text("These articles are available offline. You can read them without an internet connection.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.background(Color(R.color.bookmark_list_bg))
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.refreshable {
|
||||||
|
await viewModel.refreshList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var loadingView: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(1.3)
|
||||||
|
.tint(.accentColor)
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Text("Loading Cached Articles")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text("Please wait...")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color(R.color.bookmark_list_bg))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func errorView(message: String) -> some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.font(.system(size: 48))
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Text("Unable to load cached articles")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text(message)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Try Again") {
|
||||||
|
Task {
|
||||||
|
await viewModel.loadCachedBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(.large)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color(R.color.bookmark_list_bg))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var emptyStateView: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Image(systemName: "tray")
|
||||||
|
.font(.system(size: 64))
|
||||||
|
.foregroundColor(.secondary.opacity(0.5))
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Text("No Cached Articles")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text("Enable offline reading and sync to cache articles for offline access")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hint
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
|
.font(.caption)
|
||||||
|
Text("Use 'Sync Now' to download articles")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color.accentColor.opacity(0.1))
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color(R.color.bookmark_list_bg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
CachedArticlesPreviewView()
|
||||||
|
.environmentObject(AppSettings())
|
||||||
|
}
|
||||||
|
}
|
||||||
53
readeck/UI/Settings/CachedArticlesPreviewViewModel.swift
Normal file
53
readeck/UI/Settings/CachedArticlesPreviewViewModel.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// CachedArticlesPreviewViewModel.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Claude on 30.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class CachedArticlesPreviewViewModel {
|
||||||
|
|
||||||
|
// MARK: - Dependencies
|
||||||
|
|
||||||
|
private let getCachedBookmarksUseCase: PGetCachedBookmarksUseCase
|
||||||
|
|
||||||
|
// MARK: - Published State
|
||||||
|
|
||||||
|
var cachedBookmarks: [Bookmark] = []
|
||||||
|
var isLoading = false
|
||||||
|
var errorMessage: String?
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
||||||
|
self.getCachedBookmarksUseCase = factory.makeGetCachedBookmarksUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func loadCachedBookmarks() async {
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
Logger.viewModel.info("📱 CachedArticlesPreviewViewModel: Loading cached bookmarks...")
|
||||||
|
cachedBookmarks = try await getCachedBookmarksUseCase.execute()
|
||||||
|
Logger.viewModel.info("✅ Loaded \(cachedBookmarks.count) cached bookmarks for preview")
|
||||||
|
} catch {
|
||||||
|
Logger.viewModel.error("❌ Failed to load cached bookmarks: \(error.localizedDescription)")
|
||||||
|
errorMessage = "Failed to load cached articles"
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func refreshList() async {
|
||||||
|
await loadCachedBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// OfflineSettingsView.swift
|
// OfflineReadingDetailView.swift
|
||||||
// readeck
|
// readeck
|
||||||
//
|
//
|
||||||
// Created by Claude on 17.11.25.
|
// Created by Claude on 17.11.25.
|
||||||
@ -7,12 +7,12 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct OfflineSettingsView: View {
|
struct OfflineReadingDetailView: View {
|
||||||
@State private var viewModel = OfflineSettingsViewModel()
|
@State private var viewModel = OfflineSettingsViewModel()
|
||||||
@EnvironmentObject var appSettings: AppSettings
|
@EnvironmentObject var appSettings: AppSettings
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
List {
|
||||||
Section {
|
Section {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Toggle("Enable Offline Reading", isOn: $viewModel.offlineSettings.enabled)
|
Toggle("Enable Offline Reading", isOn: $viewModel.offlineSettings.enabled)
|
||||||
@ -67,7 +67,21 @@ struct OfflineSettingsView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.top, 2)
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Settings")
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.textCase(nil)
|
||||||
|
} footer: {
|
||||||
|
Text("VPN connections are detected as active internet connections.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.offlineSettings.enabled {
|
||||||
|
Section {
|
||||||
// Sync button
|
// Sync button
|
||||||
Button(action: {
|
Button(action: {
|
||||||
Task {
|
Task {
|
||||||
@ -103,20 +117,27 @@ struct OfflineSettingsView: View {
|
|||||||
}
|
}
|
||||||
.disabled(viewModel.isSyncing)
|
.disabled(viewModel.isSyncing)
|
||||||
|
|
||||||
// Cache stats
|
// Cache stats with preview link
|
||||||
if viewModel.cachedArticlesCount > 0 {
|
if viewModel.cachedArticlesCount > 0 {
|
||||||
HStack {
|
SettingsRowNavigationLink(
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
icon: "doc.text.magnifyingglass",
|
||||||
Text("Cached Articles")
|
iconColor: .green,
|
||||||
Text("\(viewModel.cachedArticlesCount) articles (\(viewModel.cacheSize))")
|
title: "Preview Cached Articles",
|
||||||
.font(.caption)
|
subtitle: "\(viewModel.cachedArticlesCount) articles (\(viewModel.cacheSize))"
|
||||||
.foregroundColor(.secondary)
|
) {
|
||||||
}
|
CachedArticlesPreviewView()
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Synchronization")
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.textCase(nil)
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
Section {
|
||||||
// Debug: Toggle offline mode simulation
|
// Debug: Toggle offline mode simulation
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Toggle(isOn: Binding(
|
Toggle(isOn: Binding(
|
||||||
@ -139,12 +160,19 @@ struct OfflineSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
} header: {
|
||||||
|
Text("Debug")
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.textCase(nil)
|
||||||
}
|
}
|
||||||
} header: {
|
#endif
|
||||||
Text("Offline Reading")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.navigationTitle("Offline Reading")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadSettings()
|
await viewModel.loadSettings()
|
||||||
}
|
}
|
||||||
@ -8,6 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsContainerView: View {
|
struct SettingsContainerView: View {
|
||||||
|
@State private var offlineViewModel = OfflineSettingsViewModel()
|
||||||
|
|
||||||
private var appVersion: String {
|
private var appVersion: String {
|
||||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
|
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
|
||||||
@ -19,13 +20,67 @@ struct SettingsContainerView: View {
|
|||||||
List {
|
List {
|
||||||
AppearanceSettingsView()
|
AppearanceSettingsView()
|
||||||
|
|
||||||
ReadingSettingsView()
|
Section {
|
||||||
|
Toggle("Enable Offline Reading", isOn: $offlineViewModel.offlineSettings.enabled)
|
||||||
|
.onChange(of: offlineViewModel.offlineSettings.enabled) {
|
||||||
|
Task {
|
||||||
|
await offlineViewModel.saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if offlineViewModel.offlineSettings.enabled {
|
||||||
|
Button(action: {
|
||||||
|
Task {
|
||||||
|
await offlineViewModel.syncNow()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if offlineViewModel.isSyncing {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Sync Now")
|
||||||
|
.foregroundColor(offlineViewModel.isSyncing ? .secondary : .blue)
|
||||||
|
|
||||||
|
if let progress = offlineViewModel.syncProgress {
|
||||||
|
Text(progress)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else if let lastSync = offlineViewModel.offlineSettings.lastSyncDate {
|
||||||
|
Text("Last synced: \(lastSync.formatted(.relative(presentation: .named)))")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(offlineViewModel.isSyncing)
|
||||||
|
|
||||||
|
SettingsRowNavigationLink(
|
||||||
|
icon: "arrow.down.circle.fill",
|
||||||
|
iconColor: .blue,
|
||||||
|
title: "Offline Reading",
|
||||||
|
subtitle: offlineViewModel.cachedArticlesCount > 0 ? "\(offlineViewModel.cachedArticlesCount) articles cached" : nil
|
||||||
|
) {
|
||||||
|
OfflineReadingDetailView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Offline Reading")
|
||||||
|
} footer: {
|
||||||
|
Text("Automatically download articles for offline use. VPN connections are detected as active internet connections.")
|
||||||
|
}
|
||||||
|
|
||||||
CacheSettingsView()
|
CacheSettingsView()
|
||||||
|
|
||||||
SyncSettingsView()
|
ReadingSettingsView()
|
||||||
|
|
||||||
OfflineSettingsView()
|
|
||||||
|
|
||||||
SettingsServerView()
|
SettingsServerView()
|
||||||
|
|
||||||
@ -44,11 +99,23 @@ struct SettingsContainerView: View {
|
|||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.task {
|
||||||
|
await offlineViewModel.loadSettings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var debugSettingsSection: some View {
|
private var debugSettingsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
|
SettingsRowNavigationLink(
|
||||||
|
icon: "wrench.and.screwdriver.fill",
|
||||||
|
iconColor: .orange,
|
||||||
|
title: "Debug Menu",
|
||||||
|
subtitle: "Network simulation, data management & more"
|
||||||
|
) {
|
||||||
|
DebugMenuView()
|
||||||
|
}
|
||||||
|
|
||||||
SettingsRowNavigationLink(
|
SettingsRowNavigationLink(
|
||||||
icon: "list.bullet.rectangle",
|
icon: "list.bullet.rectangle",
|
||||||
iconColor: .blue,
|
iconColor: .blue,
|
||||||
|
|||||||
@ -46,15 +46,6 @@ struct SettingsGeneralView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Section {
|
|
||||||
Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled)
|
|
||||||
if viewModel.autoSyncEnabled {
|
|
||||||
Stepper("Sync interval: \(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60)
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Sync Settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
|
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
|
||||||
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
|
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user