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.
159 lines
6.0 KiB
Swift
159 lines
6.0 KiB
Swift
//
|
|
// OfflineSettingsView.swift
|
|
// readeck
|
|
//
|
|
// Created by Claude on 17.11.25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct OfflineSettingsView: View {
|
|
@State private var viewModel = OfflineSettingsViewModel()
|
|
|
|
var body: some View {
|
|
Group {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Toggle("Offline-Reading aktivieren", isOn: $viewModel.offlineSettings.enabled)
|
|
.onChange(of: viewModel.offlineSettings.enabled) {
|
|
Task {
|
|
await viewModel.saveSettings()
|
|
}
|
|
}
|
|
|
|
Text("Lade automatisch Artikel für die Offline-Nutzung herunter.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.padding(.top, 2)
|
|
}
|
|
|
|
if viewModel.offlineSettings.enabled {
|
|
// Max articles slider
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text("Maximale Artikel")
|
|
Spacer()
|
|
Text("\(viewModel.offlineSettings.maxUnreadArticlesInt)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Slider(
|
|
value: $viewModel.offlineSettings.maxUnreadArticles,
|
|
in: 0...100,
|
|
step: 10
|
|
) {
|
|
Text("Max. Artikel offline")
|
|
}
|
|
.onChange(of: viewModel.offlineSettings.maxUnreadArticles) {
|
|
Task {
|
|
await viewModel.saveSettings()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save images toggle
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Toggle("Bilder speichern", isOn: $viewModel.offlineSettings.saveImages)
|
|
.onChange(of: viewModel.offlineSettings.saveImages) {
|
|
Task {
|
|
await viewModel.saveSettings()
|
|
}
|
|
}
|
|
|
|
Text("Lädt auch Bilder für die Offline-Nutzung herunter.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.padding(.top, 2)
|
|
}
|
|
|
|
// Sync button
|
|
Button(action: {
|
|
Task {
|
|
await viewModel.syncNow()
|
|
}
|
|
}) {
|
|
HStack {
|
|
if viewModel.isSyncing {
|
|
ProgressView()
|
|
.scaleEffect(0.8)
|
|
} else {
|
|
Image(systemName: "arrow.clockwise")
|
|
.foregroundColor(.blue)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Jetzt synchronisieren")
|
|
.foregroundColor(viewModel.isSyncing ? .secondary : .blue)
|
|
|
|
if let progress = viewModel.syncProgress {
|
|
Text(progress)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
} else if let lastSync = viewModel.offlineSettings.lastSyncDate {
|
|
Text("Zuletzt: \(lastSync.formatted(.relative(presentation: .named)))")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
.disabled(viewModel.isSyncing)
|
|
|
|
// Cache stats
|
|
if viewModel.cachedArticlesCount > 0 {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Gespeicherte Artikel")
|
|
Text("\(viewModel.cachedArticlesCount) Artikel (\(viewModel.cacheSize))")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
// Debug: Force offline mode
|
|
Button(action: {
|
|
simulateOfflineMode()
|
|
}) {
|
|
HStack {
|
|
Image(systemName: "airplane")
|
|
.foregroundColor(.orange)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Offline-Modus simulieren")
|
|
.foregroundColor(.orange)
|
|
Text("DEBUG: Netzwerk temporär deaktivieren")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
} header: {
|
|
Text("Offline-Reading")
|
|
}
|
|
}
|
|
.task {
|
|
await viewModel.loadSettings()
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
private func simulateOfflineMode() {
|
|
// Post notification to simulate offline mode
|
|
NotificationCenter.default.post(
|
|
name: Notification.Name("SimulateOfflineMode"),
|
|
object: nil
|
|
)
|
|
}
|
|
#endif
|
|
}
|