Fix offline reading bugs and improve network monitoring (Phase 5)
Bugfixes: - Add toggle for offline mode simulation (DEBUG only) - Fix VPN false-positives with interface count check - Add detailed error logging for download failures - Fix last sync timestamp display - Translate all strings to English Network Monitoring: - Add NetworkMonitorRepository with NWPathMonitor - Check path.status AND availableInterfaces for reliability - Add manual reportConnectionFailure/Success methods - Auto-load cached bookmarks when offline - Visual debug banner (green=online, red=offline) Architecture: - Clean architecture with Repository → UseCase → ViewModel - Network status in AppSettings for global access - Combine publishers for reactive updates
This commit is contained in:
parent
fdc6b3a6b6
commit
e4657aa281
102
readeck/Data/Repository/NetworkMonitorRepository.swift
Normal file
102
readeck/Data/Repository/NetworkMonitorRepository.swift
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// NetworkMonitorRepository.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Claude on 18.11.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import Combine
|
||||
|
||||
// MARK: - Protocol
|
||||
|
||||
protocol PNetworkMonitorRepository {
|
||||
var isConnected: AnyPublisher<Bool, Never> { get }
|
||||
func startMonitoring()
|
||||
func stopMonitoring()
|
||||
func reportConnectionFailure()
|
||||
func reportConnectionSuccess()
|
||||
}
|
||||
|
||||
// MARK: - Implementation
|
||||
|
||||
final class NetworkMonitorRepository: PNetworkMonitorRepository {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let monitor = NWPathMonitor()
|
||||
private let queue = DispatchQueue(label: "com.readeck.networkmonitor")
|
||||
private let _isConnectedSubject = CurrentValueSubject<Bool, Never>(true)
|
||||
private var hasPathConnection = true
|
||||
private var hasRealConnection = true
|
||||
|
||||
var isConnected: AnyPublisher<Bool, Never> {
|
||||
_isConnectedSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
// Repository just manages the monitor, doesn't start it automatically
|
||||
}
|
||||
|
||||
deinit {
|
||||
monitor.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
func startMonitoring() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
guard let self = self else { return }
|
||||
|
||||
// More sophisticated check: path must be satisfied AND have actual interfaces
|
||||
let hasInterfaces = path.availableInterfaces.count > 0
|
||||
let isConnected = path.status == .satisfied && hasInterfaces
|
||||
|
||||
self.hasPathConnection = isConnected
|
||||
self.updateConnectionState()
|
||||
|
||||
// Log network changes with details
|
||||
if path.status == .satisfied {
|
||||
if hasInterfaces {
|
||||
Logger.network.info("📡 Network path available (interfaces: \(path.availableInterfaces.count))")
|
||||
} else {
|
||||
Logger.network.warning("⚠️ Network path satisfied but no interfaces (VPN?)")
|
||||
}
|
||||
} else {
|
||||
Logger.network.warning("📡 Network path unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
monitor.start(queue: queue)
|
||||
Logger.network.debug("Network monitoring started")
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
monitor.cancel()
|
||||
Logger.network.debug("Network monitoring stopped")
|
||||
}
|
||||
|
||||
func reportConnectionFailure() {
|
||||
hasRealConnection = false
|
||||
updateConnectionState()
|
||||
Logger.network.warning("⚠️ Real connection failure reported (VPN/unreachable server)")
|
||||
}
|
||||
|
||||
func reportConnectionSuccess() {
|
||||
hasRealConnection = true
|
||||
updateConnectionState()
|
||||
Logger.network.info("✅ Real connection success reported")
|
||||
}
|
||||
|
||||
private func updateConnectionState() {
|
||||
// Only connected if BOTH path is available AND real connection works
|
||||
let isConnected = hasPathConnection && hasRealConnection
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self._isConnectedSubject.send(isConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
readeck/Domain/UseCase/NetworkMonitorUseCase.swift
Normal file
58
readeck/Domain/UseCase/NetworkMonitorUseCase.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// NetworkMonitorUseCase.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Claude on 18.11.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
// MARK: - Protocol
|
||||
|
||||
protocol PNetworkMonitorUseCase {
|
||||
var isConnected: AnyPublisher<Bool, Never> { get }
|
||||
func startMonitoring()
|
||||
func stopMonitoring()
|
||||
func reportConnectionFailure()
|
||||
func reportConnectionSuccess()
|
||||
}
|
||||
|
||||
// MARK: - Implementation
|
||||
|
||||
final class NetworkMonitorUseCase: PNetworkMonitorUseCase {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private let repository: PNetworkMonitorRepository
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var isConnected: AnyPublisher<Bool, Never> {
|
||||
repository.isConnected
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(repository: PNetworkMonitorRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
func startMonitoring() {
|
||||
repository.startMonitoring()
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
repository.stopMonitoring()
|
||||
}
|
||||
|
||||
func reportConnectionFailure() {
|
||||
repository.reportConnectionFailure()
|
||||
}
|
||||
|
||||
func reportConnectionSuccess() {
|
||||
repository.reportConnectionSuccess()
|
||||
}
|
||||
}
|
||||
@ -92,13 +92,13 @@ final class OfflineCacheSyncUseCase: POfflineCacheSyncUseCase {
|
||||
if offlineCacheRepository.hasCachedArticle(id: bookmark.id) {
|
||||
Logger.sync.debug("⏭️ Skipping '\(bookmark.title)' (already cached)")
|
||||
skippedCount += 1
|
||||
_syncProgressSubject.send("⏭️ Artikel \(progress) bereits gecacht...")
|
||||
_syncProgressSubject.send("⏭️ Article \(progress) already cached...")
|
||||
continue
|
||||
}
|
||||
|
||||
// Update progress
|
||||
let imagesSuffix = settings.saveImages ? " + Bilder" : ""
|
||||
_syncProgressSubject.send("📥 Artikel \(progress)\(imagesSuffix)...")
|
||||
let imagesSuffix = settings.saveImages ? " + images" : ""
|
||||
_syncProgressSubject.send("📥 Article \(progress)\(imagesSuffix)...")
|
||||
Logger.sync.info("📥 Caching '\(bookmark.title)'")
|
||||
|
||||
do {
|
||||
@ -116,7 +116,15 @@ final class OfflineCacheSyncUseCase: POfflineCacheSyncUseCase {
|
||||
Logger.sync.info("✅ Cached '\(bookmark.title)'")
|
||||
} catch {
|
||||
errorCount += 1
|
||||
Logger.sync.error("❌ Failed to cache '\(bookmark.title)': \(error.localizedDescription)")
|
||||
|
||||
// Detailed error logging
|
||||
if let urlError = error as? URLError {
|
||||
Logger.sync.error("❌ Failed to cache '\(bookmark.title)' - Network error: \(urlError.code.rawValue) (\(urlError.localizedDescription))")
|
||||
} else if let decodingError = error as? DecodingError {
|
||||
Logger.sync.error("❌ Failed to cache '\(bookmark.title)' - Decoding error: \(decodingError)")
|
||||
} else {
|
||||
Logger.sync.error("❌ Failed to cache '\(bookmark.title)' - Error: \(error.localizedDescription) (Type: \(type(of: error)))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,7 +137,7 @@ final class OfflineCacheSyncUseCase: POfflineCacheSyncUseCase {
|
||||
try await settingsRepository.saveOfflineSettings(updatedSettings)
|
||||
|
||||
// Final status
|
||||
let statusMessage = "✅ Synchronisiert: \(successCount), Übersprungen: \(skippedCount), Fehler: \(errorCount)"
|
||||
let statusMessage = "✅ Synced: \(successCount), Skipped: \(skippedCount), Errors: \(errorCount)"
|
||||
Logger.sync.info(statusMessage)
|
||||
_syncProgressSubject.send(statusMessage)
|
||||
|
||||
@ -139,7 +147,7 @@ final class OfflineCacheSyncUseCase: POfflineCacheSyncUseCase {
|
||||
|
||||
} catch {
|
||||
Logger.sync.error("❌ Offline sync failed: \(error.localizedDescription)")
|
||||
_syncProgressSubject.send("❌ Synchronisierung fehlgeschlagen")
|
||||
_syncProgressSubject.send("❌ Sync failed")
|
||||
|
||||
// Clear error message after 5 seconds
|
||||
try? await Task.sleep(nanoseconds: 5_000_000_000)
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
@ -14,17 +15,22 @@ class AppViewModel {
|
||||
private let settingsRepository = SettingsRepository()
|
||||
private let factory: UseCaseFactory
|
||||
private let syncTagsUseCase: PSyncTagsUseCase
|
||||
let networkMonitorUseCase: PNetworkMonitorUseCase
|
||||
|
||||
var hasFinishedSetup: Bool = true
|
||||
var isServerReachable: Bool = false
|
||||
var isNetworkConnected: Bool = true
|
||||
|
||||
private var lastAppStartTagSyncTime: Date?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
||||
self.factory = factory
|
||||
self.syncTagsUseCase = factory.makeSyncTagsUseCase()
|
||||
setupNotificationObservers()
|
||||
self.networkMonitorUseCase = factory.makeNetworkMonitorUseCase()
|
||||
|
||||
setupNotificationObservers()
|
||||
setupNetworkMonitoring()
|
||||
loadSetupStatus()
|
||||
}
|
||||
|
||||
@ -63,6 +69,28 @@ class AppViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNetworkMonitoring() {
|
||||
// Start monitoring network status
|
||||
networkMonitorUseCase.startMonitoring()
|
||||
|
||||
// Bind network status to our published property
|
||||
networkMonitorUseCase.isConnected
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isNetworkConnected, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func bindNetworkStatus(to appSettings: AppSettings) {
|
||||
// Bind network status to AppSettings for global access
|
||||
networkMonitorUseCase.isConnected
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { isConnected in
|
||||
Logger.viewModel.info("🌐 Network status changed: \(isConnected ? "Connected" : "Disconnected")")
|
||||
appSettings.isNetworkConnected = isConnected
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func loadSetupStatus() {
|
||||
hasFinishedSetup = settingsRepository.hasFinishedSetup
|
||||
}
|
||||
|
||||
@ -5,22 +5,23 @@ import SwiftUI
|
||||
struct BookmarksView: View {
|
||||
|
||||
// MARK: States
|
||||
|
||||
|
||||
@State private var viewModel: BookmarksViewModel
|
||||
@State private var showingAddBookmark = false
|
||||
@State private var selectedBookmarkId: String?
|
||||
@State private var showingAddBookmarkFromShare = false
|
||||
@State private var shareURL = ""
|
||||
@State private var shareTitle = ""
|
||||
|
||||
|
||||
let state: BookmarkState
|
||||
let type: [BookmarkType]
|
||||
@Binding var selectedBookmark: Bookmark?
|
||||
@EnvironmentObject var playerUIState: PlayerUIState
|
||||
@EnvironmentObject var appSettings: AppSettings
|
||||
let tag: String?
|
||||
|
||||
|
||||
// MARK: Environments
|
||||
|
||||
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
@ -37,8 +38,13 @@ struct BookmarksView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(spacing: 0) {
|
||||
#if DEBUG
|
||||
// Debug: Network status indicator
|
||||
debugNetworkStatusBanner
|
||||
#endif
|
||||
|
||||
// Offline banner
|
||||
if viewModel.isNetworkError && (viewModel.bookmarks?.bookmarks.isEmpty == false) {
|
||||
if !appSettings.isNetworkConnected && (viewModel.bookmarks?.bookmarks.isEmpty == false) {
|
||||
offlineBanner
|
||||
}
|
||||
|
||||
@ -86,7 +92,21 @@ struct BookmarksView: View {
|
||||
Task {
|
||||
// Wait a bit for the server to process the new bookmark
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
|
||||
|
||||
await viewModel.refreshBookmarks()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: appSettings.isNetworkConnected) { oldValue, newValue in
|
||||
// Network status changed
|
||||
if !newValue && oldValue {
|
||||
// Lost network connection - load cached bookmarks
|
||||
Task {
|
||||
await viewModel.loadCachedBookmarksFromUI()
|
||||
}
|
||||
} else if newValue && !oldValue {
|
||||
// Regained network connection - refresh from server
|
||||
Task {
|
||||
await viewModel.refreshBookmarks()
|
||||
}
|
||||
}
|
||||
@ -285,6 +305,31 @@ struct BookmarksView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var debugNetworkStatusBanner: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: appSettings.isNetworkConnected ? "wifi" : "wifi.slash")
|
||||
.font(.body)
|
||||
.foregroundColor(appSettings.isNetworkConnected ? .green : .red)
|
||||
|
||||
Text("DEBUG: Network \(appSettings.isNetworkConnected ? "Connected ✓" : "Disconnected ✗")")
|
||||
.font(.caption)
|
||||
.foregroundColor(appSettings.isNetworkConnected ? .green : .red)
|
||||
.bold()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.background(appSettings.isNetworkConnected ? Color.green.opacity(0.1) : Color.red.opacity(0.1))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(appSettings.isNetworkConnected ? Color.green : Color.red),
|
||||
alignment: .bottom
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var offlineBanner: some View {
|
||||
HStack(spacing: 12) {
|
||||
@ -292,7 +337,7 @@ struct BookmarksView: View {
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Offline-Modus – Zeige gespeicherte Artikel")
|
||||
Text("Offline Mode – Showing cached articles")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ class BookmarksViewModel {
|
||||
private let deleteBookmarkUseCase: PDeleteBookmarkUseCase
|
||||
private let loadCardLayoutUseCase: PLoadCardLayoutUseCase
|
||||
private let offlineCacheRepository: POfflineCacheRepository
|
||||
|
||||
|
||||
var bookmarks: BookmarksPage?
|
||||
var isLoading = false
|
||||
var isInitialLoading = true
|
||||
@ -19,7 +19,7 @@ class BookmarksViewModel {
|
||||
var currentType = [BookmarkType.article]
|
||||
var currentTag: String? = nil
|
||||
var cardLayoutStyle: CardLayoutStyle = .magazine
|
||||
|
||||
|
||||
var showingAddBookmarkFromShare = false
|
||||
var shareURL = ""
|
||||
var shareTitle = ""
|
||||
@ -29,8 +29,7 @@ class BookmarksViewModel {
|
||||
|
||||
// Prevent concurrent updates
|
||||
private var isUpdating = false
|
||||
|
||||
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var limit = 50
|
||||
private var offset = 0
|
||||
@ -69,7 +68,7 @@ class BookmarksViewModel {
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
// Listen for
|
||||
NotificationCenter.default
|
||||
.publisher(for: .addBookmarkFromShare)
|
||||
@ -179,6 +178,13 @@ class BookmarksViewModel {
|
||||
Logger.viewModel.error("Failed to load cached bookmarks: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func loadCachedBookmarksFromUI() async {
|
||||
isNetworkError = true
|
||||
errorMessage = "No internet connection"
|
||||
await loadCachedBookmarks()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func loadMoreBookmarks() async {
|
||||
|
||||
@ -27,6 +27,7 @@ protocol UseCaseFactory {
|
||||
func makeDeleteAnnotationUseCase() -> PDeleteAnnotationUseCase
|
||||
func makeSettingsRepository() -> PSettingsRepository
|
||||
func makeOfflineCacheSyncUseCase() -> POfflineCacheSyncUseCase
|
||||
func makeNetworkMonitorUseCase() -> PNetworkMonitorUseCase
|
||||
}
|
||||
|
||||
|
||||
@ -41,6 +42,7 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
private lazy var serverInfoRepository: PServerInfoRepository = ServerInfoRepository(apiClient: infoApiClient)
|
||||
private lazy var annotationsRepository: PAnnotationsRepository = AnnotationsRepository(api: api)
|
||||
private let offlineCacheRepository: POfflineCacheRepository = OfflineCacheRepository()
|
||||
private let networkMonitorRepository: PNetworkMonitorRepository = NetworkMonitorRepository()
|
||||
|
||||
static let shared = DefaultUseCaseFactory()
|
||||
|
||||
@ -159,4 +161,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
settingsRepository: settingsRepository
|
||||
)
|
||||
}
|
||||
|
||||
func makeNetworkMonitorUseCase() -> PNetworkMonitorUseCase {
|
||||
return NetworkMonitorUseCase(repository: networkMonitorRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,6 +112,10 @@ class MockUseCaseFactory: UseCaseFactory {
|
||||
func makeOfflineCacheSyncUseCase() -> POfflineCacheSyncUseCase {
|
||||
MockOfflineCacheSyncUseCase()
|
||||
}
|
||||
|
||||
func makeNetworkMonitorUseCase() -> PNetworkMonitorUseCase {
|
||||
MockNetworkMonitorUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -331,6 +335,45 @@ class MockOfflineCacheSyncUseCase: POfflineCacheSyncUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
class MockNetworkMonitorRepository: PNetworkMonitorRepository {
|
||||
var isConnected: AnyPublisher<Bool, Never> {
|
||||
Just(true).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func startMonitoring() {}
|
||||
func stopMonitoring() {}
|
||||
func reportConnectionFailure() {}
|
||||
func reportConnectionSuccess() {}
|
||||
}
|
||||
|
||||
class MockNetworkMonitorUseCase: PNetworkMonitorUseCase {
|
||||
private let repository: PNetworkMonitorRepository
|
||||
|
||||
init(repository: PNetworkMonitorRepository = MockNetworkMonitorRepository()) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
var isConnected: AnyPublisher<Bool, Never> {
|
||||
repository.isConnected
|
||||
}
|
||||
|
||||
func startMonitoring() {
|
||||
repository.startMonitoring()
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
repository.stopMonitoring()
|
||||
}
|
||||
|
||||
func reportConnectionFailure() {
|
||||
repository.reportConnectionFailure()
|
||||
}
|
||||
|
||||
func reportConnectionSuccess() {
|
||||
repository.reportConnectionSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
extension Bookmark {
|
||||
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)
|
||||
|
||||
@ -18,6 +18,7 @@ import Combine
|
||||
|
||||
class AppSettings: ObservableObject {
|
||||
@Published var settings: Settings?
|
||||
@Published var isNetworkConnected: Bool = true
|
||||
|
||||
var enableTTS: Bool {
|
||||
settings?.enableTTS ?? false
|
||||
|
||||
@ -9,19 +9,20 @@ import SwiftUI
|
||||
|
||||
struct OfflineSettingsView: View {
|
||||
@State private var viewModel = OfflineSettingsViewModel()
|
||||
@EnvironmentObject var appSettings: AppSettings
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Toggle("Offline-Reading aktivieren", isOn: $viewModel.offlineSettings.enabled)
|
||||
Toggle("Enable Offline Reading", isOn: $viewModel.offlineSettings.enabled)
|
||||
.onChange(of: viewModel.offlineSettings.enabled) {
|
||||
Task {
|
||||
await viewModel.saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
Text("Lade automatisch Artikel für die Offline-Nutzung herunter.")
|
||||
Text("Automatically download articles for offline use.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 2)
|
||||
@ -31,7 +32,7 @@ struct OfflineSettingsView: View {
|
||||
// Max articles slider
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Maximale Artikel")
|
||||
Text("Maximum Articles")
|
||||
Spacer()
|
||||
Text("\(viewModel.offlineSettings.maxUnreadArticlesInt)")
|
||||
.font(.caption)
|
||||
@ -43,7 +44,7 @@ struct OfflineSettingsView: View {
|
||||
in: 0...100,
|
||||
step: 10
|
||||
) {
|
||||
Text("Max. Artikel offline")
|
||||
Text("Max. Articles Offline")
|
||||
}
|
||||
.onChange(of: viewModel.offlineSettings.maxUnreadArticles) {
|
||||
Task {
|
||||
@ -54,14 +55,14 @@ struct OfflineSettingsView: View {
|
||||
|
||||
// Save images toggle
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Toggle("Bilder speichern", isOn: $viewModel.offlineSettings.saveImages)
|
||||
Toggle("Save Images", isOn: $viewModel.offlineSettings.saveImages)
|
||||
.onChange(of: viewModel.offlineSettings.saveImages) {
|
||||
Task {
|
||||
await viewModel.saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
Text("Lädt auch Bilder für die Offline-Nutzung herunter.")
|
||||
Text("Also download images for offline use.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 2)
|
||||
@ -83,7 +84,7 @@ struct OfflineSettingsView: View {
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Jetzt synchronisieren")
|
||||
Text("Sync Now")
|
||||
.foregroundColor(viewModel.isSyncing ? .secondary : .blue)
|
||||
|
||||
if let progress = viewModel.syncProgress {
|
||||
@ -91,7 +92,7 @@ struct OfflineSettingsView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else if let lastSync = viewModel.offlineSettings.lastSyncDate {
|
||||
Text("Zuletzt: \(lastSync.formatted(.relative(presentation: .named)))")
|
||||
Text("Last synced: \(lastSync.formatted(.relative(presentation: .named)))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@ -106,8 +107,8 @@ struct OfflineSettingsView: View {
|
||||
if viewModel.cachedArticlesCount > 0 {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Gespeicherte Artikel")
|
||||
Text("\(viewModel.cachedArticlesCount) Artikel (\(viewModel.cacheSize))")
|
||||
Text("Cached Articles")
|
||||
Text("\(viewModel.cachedArticlesCount) articles (\(viewModel.cacheSize))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@ -116,43 +117,36 @@ struct OfflineSettingsView: View {
|
||||
}
|
||||
|
||||
#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)
|
||||
// Debug: Toggle offline mode simulation
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Toggle(isOn: Binding(
|
||||
get: { !appSettings.isNetworkConnected },
|
||||
set: { isOffline in
|
||||
appSettings.isNetworkConnected = !isOffline
|
||||
}
|
||||
)) {
|
||||
HStack {
|
||||
Image(systemName: "airplane")
|
||||
.foregroundColor(.orange)
|
||||
|
||||
Spacer()
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Simulate Offline Mode")
|
||||
.foregroundColor(.orange)
|
||||
Text("DEBUG: Toggle network status")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} header: {
|
||||
Text("Offline-Reading")
|
||||
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
|
||||
}
|
||||
|
||||
@ -78,6 +78,8 @@ class OfflineSettingsViewModel {
|
||||
func syncNow() async {
|
||||
Logger.viewModel.info("Manual sync triggered")
|
||||
await offlineCacheSyncUseCase.syncOfflineArticles(settings: offlineSettings)
|
||||
// Reload settings to get updated lastSyncDate
|
||||
await loadSettings()
|
||||
updateCacheStats()
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ struct readeckApp: App {
|
||||
Task {
|
||||
await loadAppSettings()
|
||||
}
|
||||
appViewModel.bindNetworkStatus(to: appSettings)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .settingsChanged)) { _ in
|
||||
Task {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user