- 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
136 lines
4.1 KiB
Swift
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)
|
|
}
|
|
}
|