ReadKeep/readeck/UI/Settings/OfflineSettingsView.swift
Ilyas Hallak fdc6b3a6b6 Add offline reading UI and app integration (Phase 4 & 5)
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.
2025-11-18 17:44:43 +01:00

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
}