223 lines
6.3 KiB
Swift
223 lines
6.3 KiB
Swift
import Combine
|
|
import Foundation
|
|
|
|
@MainActor
|
|
class SyncService: ObservableObject {
|
|
|
|
@Published var isSyncing = false
|
|
@Published var lastSyncTime: Date?
|
|
@Published var syncError: String?
|
|
@Published var isConnected = false
|
|
@Published var isTesting = false
|
|
@Published var isOfflineMode = false
|
|
|
|
@Published var providerType: SyncProviderType = .server {
|
|
didSet {
|
|
updateActiveProvider()
|
|
userDefaults.set(providerType.rawValue, forKey: Keys.providerType)
|
|
}
|
|
}
|
|
|
|
private var activeProvider: SyncProvider?
|
|
|
|
private let userDefaults = UserDefaults.standard
|
|
private let logTag = "SyncService"
|
|
private var syncTask: Task<Void, Never>?
|
|
|
|
private enum Keys {
|
|
static let serverURL = "sync_server_url"
|
|
static let authToken = "sync_auth_token"
|
|
static let lastSyncTime = "last_sync_time"
|
|
static let isConnected = "is_connected"
|
|
static let autoSyncEnabled = "auto_sync_enabled"
|
|
static let offlineMode = "offline_mode"
|
|
static let providerType = "sync_provider_type"
|
|
}
|
|
|
|
// Legacy properties for compatibility with SettingsView
|
|
var serverURL: String {
|
|
get { userDefaults.string(forKey: Keys.serverURL) ?? "" }
|
|
set {
|
|
userDefaults.set(newValue, forKey: Keys.serverURL)
|
|
// If active provider is server, it will pick up the change from UserDefaults
|
|
}
|
|
}
|
|
|
|
var authToken: String {
|
|
get { userDefaults.string(forKey: Keys.authToken) ?? "" }
|
|
set { userDefaults.set(newValue, forKey: Keys.authToken) }
|
|
}
|
|
|
|
var isConfigured: Bool {
|
|
return activeProvider?.isConfigured ?? false
|
|
}
|
|
|
|
var isAutoSyncEnabled: Bool {
|
|
get { userDefaults.bool(forKey: Keys.autoSyncEnabled) }
|
|
set { userDefaults.set(newValue, forKey: Keys.autoSyncEnabled) }
|
|
}
|
|
|
|
init() {
|
|
isConnected = userDefaults.bool(forKey: Keys.isConnected)
|
|
isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true
|
|
isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode)
|
|
|
|
if let savedType = userDefaults.string(forKey: Keys.providerType),
|
|
let type = SyncProviderType(rawValue: savedType) {
|
|
self.providerType = type
|
|
} else {
|
|
self.providerType = .server // Default
|
|
}
|
|
|
|
updateActiveProvider()
|
|
}
|
|
|
|
private func updateActiveProvider() {
|
|
switch providerType {
|
|
case .server:
|
|
activeProvider = ServerSyncProvider()
|
|
case .iCloud:
|
|
activeProvider = ICloudSyncProvider()
|
|
case .none:
|
|
activeProvider = nil
|
|
}
|
|
|
|
// Update status based on new provider
|
|
if let provider = activeProvider {
|
|
isConnected = provider.isConnected
|
|
lastSyncTime = provider.lastSyncTime
|
|
} else {
|
|
isConnected = false
|
|
lastSyncTime = nil
|
|
}
|
|
}
|
|
|
|
func syncWithServer(dataManager: ClimbingDataManager) async throws {
|
|
if isOfflineMode {
|
|
AppLogger.info("Sync skipped: Offline mode is enabled.", tag: logTag)
|
|
return
|
|
}
|
|
|
|
guard let provider = activeProvider else {
|
|
if providerType == .none {
|
|
return
|
|
}
|
|
throw SyncError.notConfigured
|
|
}
|
|
|
|
guard provider.isConfigured else {
|
|
throw SyncError.notConfigured
|
|
}
|
|
|
|
// For server provider, we check connection. For others, maybe not needed or different check.
|
|
if providerType == .server && !provider.isConnected {
|
|
throw SyncError.notConnected
|
|
}
|
|
|
|
isSyncing = true
|
|
syncError = nil
|
|
|
|
defer {
|
|
isSyncing = false
|
|
}
|
|
|
|
do {
|
|
try await provider.sync(dataManager: dataManager)
|
|
|
|
// Update last sync time
|
|
self.lastSyncTime = provider.lastSyncTime
|
|
} catch {
|
|
syncError = error.localizedDescription
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func testConnection() async throws {
|
|
guard let provider = activeProvider else {
|
|
AppLogger.error("Test connection failed: No active provider", tag: logTag)
|
|
throw SyncError.notConfigured
|
|
}
|
|
|
|
isTesting = true
|
|
defer { isTesting = false }
|
|
|
|
try await provider.testConnection()
|
|
|
|
isConnected = provider.isConnected
|
|
userDefaults.set(isConnected, forKey: Keys.isConnected)
|
|
}
|
|
|
|
func triggerAutoSync(dataManager: ClimbingDataManager) {
|
|
guard isConnected && isConfigured && isAutoSyncEnabled else {
|
|
if isSyncing {
|
|
isSyncing = false
|
|
}
|
|
return
|
|
}
|
|
|
|
guard !isSyncing else { return }
|
|
|
|
syncTask?.cancel()
|
|
|
|
syncTask = Task {
|
|
try? await Task.sleep(for: .seconds(2))
|
|
guard !Task.isCancelled else { return }
|
|
|
|
do {
|
|
try await syncWithServer(dataManager: dataManager)
|
|
} catch {
|
|
self.isSyncing = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func forceSyncNow(dataManager: ClimbingDataManager) {
|
|
guard isConnected && isConfigured else { return }
|
|
|
|
syncTask?.cancel()
|
|
syncTask = nil
|
|
|
|
Task {
|
|
do {
|
|
try await syncWithServer(dataManager: dataManager)
|
|
} catch {
|
|
self.isSyncing = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func disconnect() {
|
|
activeProvider?.disconnect()
|
|
|
|
syncTask?.cancel()
|
|
syncTask = nil
|
|
isSyncing = false
|
|
isConnected = false
|
|
lastSyncTime = nil
|
|
syncError = nil
|
|
|
|
// These are shared keys, so clearing them affects all providers if they use them
|
|
// But disconnect() is usually user initiated action
|
|
userDefaults.set(false, forKey: Keys.isConnected)
|
|
}
|
|
|
|
func clearConfiguration() {
|
|
serverURL = ""
|
|
authToken = ""
|
|
lastSyncTime = nil
|
|
isConnected = false
|
|
isAutoSyncEnabled = true
|
|
userDefaults.removeObject(forKey: Keys.lastSyncTime)
|
|
userDefaults.removeObject(forKey: Keys.isConnected)
|
|
userDefaults.removeObject(forKey: Keys.autoSyncEnabled)
|
|
syncTask?.cancel()
|
|
syncTask = nil
|
|
|
|
activeProvider?.disconnect()
|
|
}
|
|
|
|
deinit {
|
|
syncTask?.cancel()
|
|
}
|
|
}
|