ReadKeep/readeck/Data/Repository/OfflineSyncManager.swift
Ilyas Hallak df8a7b64b2 feat: Add Kingfisher caching, card layouts, dynamic tag layout, and undo delete
- Integrate Kingfisher for image caching with CachedAsyncImage component
- Add CacheSettingsView for managing image cache size and clearing cache
- Implement three card layout styles: compact, magazine (default), natural
- Add AppearanceSettingsView with visual layout previews and theme settings
- Create Clean Architecture for card layout with domain models and use cases
- Implement FlowLayout for dynamic label width calculation
- Add skeleton loading animation for initial bookmark loads
- Replace delete confirmation dialogs with immediate delete + 3-second undo
- Support multiple simultaneous undo operations with individual progress bars
- Add grayed-out visual feedback for pending deletions
- Centralize notification names in dedicated NotificationNames file
- Remove pagination logic from label management (replaced with FlowLayout)
- Update AsyncImage usage across BookmarkCardView, BookmarkDetailView, ImageViewerView
- Improve UI consistency and spacing throughout the app
2025-09-04 10:43:27 +02:00

136 lines
4.1 KiB
Swift

import Foundation
import CoreData
import SwiftUI
class OfflineSyncManager: ObservableObject {
static let shared = OfflineSyncManager()
@Published var isSyncing = false
@Published var syncStatus: String?
private let coreDataManager = CoreDataManager.shared
private let api: PAPI
init(api: PAPI = API()) {
self.api = api
}
// MARK: - Sync Methods
func syncOfflineBookmarks() async {
// First check if server is reachable
guard await ServerConnectivity.isServerReachable() else {
await MainActor.run {
isSyncing = false
syncStatus = "Server not reachable. Cannot sync."
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.syncStatus = nil
}
return
}
await MainActor.run {
isSyncing = true
syncStatus = "Syncing bookmarks with server..."
}
let offlineBookmarks = getOfflineBookmarks()
guard !offlineBookmarks.isEmpty else {
await MainActor.run {
isSyncing = false
syncStatus = "No bookmarks to sync"
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.syncStatus = nil
}
return
}
var successCount = 0
var failedCount = 0
for bookmark in offlineBookmarks {
guard let url = bookmark.url else {
failedCount += 1
continue
}
let tags = bookmark.tags?.components(separatedBy: ",").filter { !$0.isEmpty } ?? []
let title = bookmark.title ?? ""
do {
// Try to upload via API
let dto = CreateBookmarkRequestDto(url: url, title: title, labels: tags.isEmpty ? nil : tags)
_ = try await api.createBookmark(createRequest: dto)
// If successful, delete from offline storage
deleteOfflineBookmark(bookmark)
successCount += 1
await MainActor.run {
syncStatus = "Synced \(successCount) bookmarks..."
}
} catch {
print("Failed to sync bookmark: \(url) - \(error)")
failedCount += 1
}
}
await MainActor.run {
isSyncing = false
if failedCount == 0 {
syncStatus = "✅ Successfully synced \(successCount) bookmarks"
} else {
syncStatus = "⚠️ Synced \(successCount), failed \(failedCount) bookmarks"
}
}
// Clear status after a few seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.syncStatus = nil
}
}
func getOfflineBookmarksCount() -> Int {
return getOfflineBookmarks().count
}
private func getOfflineBookmarks() -> [ArticleURLEntity] {
let fetchRequest: NSFetchRequest<ArticleURLEntity> = ArticleURLEntity.fetchRequest()
do {
return try coreDataManager.context.fetch(fetchRequest)
} catch {
print("Failed to fetch offline bookmarks: \(error)")
return []
}
}
private func deleteOfflineBookmark(_ entity: ArticleURLEntity) {
coreDataManager.context.delete(entity)
coreDataManager.save()
}
// MARK: - Auto Sync on Server Connectivity Changes
func startAutoSync() {
// Monitor server connectivity and auto-sync when server becomes reachable
NotificationCenter.default.addObserver(
forName: .serverDidBecomeAvailable,
object: nil,
queue: .main
) { [weak self] _ in
Task {
await self?.syncOfflineBookmarks()
}
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}