ReadKeep/readeck/Data/Repository/NetworkMonitorRepository.swift
Ilyas Hallak 305b8f733e Implement offline hero image caching with custom cache keys
Major improvements to offline reading functionality:

**Hero Image Offline Support:**
- Add heroImageURL field to BookmarkEntity for persistent storage
- Implement ImageCache-based caching with custom keys (bookmark-{id}-hero)
- Update CachedAsyncImage to support offline loading via cache keys
- Hero images now work offline without URL dependency

**Offline Bookmark Loading:**
- Add proactive offline detection before API calls
- Implement automatic fallback to cached bookmarks when offline
- Fix network status initialization race condition
- Network monitor now checks status synchronously on init

**Core Data Enhancements:**
- Persist hero image URLs in BookmarkEntity.heroImageURL
- Reconstruct ImageResource from cached URLs on offline load
- Add extensive logging for debugging persistence issues

**UI Updates:**
- Update BookmarkDetailView2 to use cache keys for hero images
- Update BookmarkCardView (all 3 layouts) with cache key support
- Improve BookmarksView offline state handling with task-based loading
- Add 50ms delay for network status propagation

**Technical Details:**
- NetworkMonitorRepository: Fix initial status from hardcoded true to actual network check
- BookmarksViewModel: Inject AppSettings for offline detection
- OfflineCacheRepository: Add verification logging for save/load operations
- BookmarkEntityMapper: Sync heroImageURL on save, restore on load

This enables full offline reading with hero images visible in bookmark lists
and detail views, even after app restart.
2025-11-28 23:01:20 +01:00

111 lines
3.3 KiB
Swift

//
// 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>
private var hasPathConnection = true
private var hasRealConnection = true
var isConnected: AnyPublisher<Bool, Never> {
_isConnectedSubject.eraseToAnyPublisher()
}
// MARK: - Initialization
init() {
// Check current network status synchronously before starting monitor
let currentPath = monitor.currentPath
let hasInterfaces = currentPath.availableInterfaces.count > 0
let initialStatus = currentPath.status == .satisfied && hasInterfaces
_isConnectedSubject = CurrentValueSubject<Bool, Never>(initialStatus)
hasPathConnection = initialStatus
Logger.network.info("🌐 Initial network status: \(initialStatus ? "Connected" : "Offline")")
}
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)
}
}
}