Add comprehensive offline bookmark support with sync capabilities: - Implement offline bookmark storage using Core Data with App Group sharing - Add Share Extension support for saving bookmarks when server unavailable - Create LocalBookmarksSyncView for managing offline bookmark queue - Add OfflineSyncManager for automatic and manual sync operations - Implement ServerConnectivity monitoring and status handling - Add badge notifications on More tab for pending offline bookmarks - Fix tag pagination in Share Extension with unique IDs for proper rendering - Update PhoneTabView with event-based badge count updates - Add App Group entitlements for data sharing between main app and extension The offline system provides seamless bookmark saving when disconnected, with automatic sync when connection is restored and manual sync options.
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: NSNotification.Name("ServerDidBecomeAvailable"),
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
Task {
|
|
await self?.syncOfflineBookmarks()
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
}
|