iOS 1.2.1 - Better auto sync and sync indicator
This commit is contained in:
@@ -396,7 +396,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -439,7 +439,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -459,7 +459,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -481,7 +481,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -492,7 +492,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 12;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,5 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class SyncService: ObservableObject {
|
class SyncService: ObservableObject {
|
||||||
@@ -455,7 +454,7 @@ class SyncService: ObservableObject {
|
|||||||
let zipData = try createMinimalZipFromBackup(updatedBackup)
|
let zipData = try createMinimalZipFromBackup(updatedBackup)
|
||||||
|
|
||||||
// Use existing import method which properly handles data restoration
|
// Use existing import method which properly handles data restoration
|
||||||
try dataManager.importData(from: zipData)
|
try dataManager.importData(from: zipData, showSuccessMessage: false)
|
||||||
|
|
||||||
// Update local data state to match imported data timestamp
|
// Update local data state to match imported data timestamp
|
||||||
DataStateManager.shared.setLastModified(backup.exportedAt)
|
DataStateManager.shared.setLastModified(backup.exportedAt)
|
||||||
@@ -735,180 +734,29 @@ class SyncService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func triggerAutoSync(dataManager: ClimbingDataManager) {
|
func triggerAutoSync(dataManager: ClimbingDataManager) {
|
||||||
guard isConnected && isConfigured && isAutoSyncEnabled else { return }
|
// Early exit if sync cannot proceed - don't set isSyncing
|
||||||
|
guard isConnected && isConfigured && isAutoSyncEnabled else {
|
||||||
|
// Ensure isSyncing is false when sync is not possible
|
||||||
|
if isSyncing {
|
||||||
|
isSyncing = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent multiple simultaneous syncs
|
||||||
|
guard !isSyncing else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await syncWithServer(dataManager: dataManager)
|
try await syncWithServer(dataManager: dataManager)
|
||||||
} catch {
|
} catch {
|
||||||
print("Auto-sync failed: \(error)")
|
await MainActor.run {
|
||||||
// Don't show UI errors for auto-sync failures
|
self.isSyncing = false
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPRECATED: Complex merge logic replaced with simple timestamp-based sync
|
|
||||||
// These methods are no longer used but kept for reference
|
|
||||||
@available(*, deprecated, message: "Use simple timestamp-based sync instead")
|
|
||||||
private func performIntelligentMerge(local: ClimbDataBackup, server: ClimbDataBackup) throws
|
|
||||||
-> ClimbDataBackup
|
|
||||||
{
|
|
||||||
print("Merging data - preserving all entities to prevent data loss")
|
|
||||||
|
|
||||||
// Merge gyms by ID, keeping most recently updated
|
|
||||||
let mergedGyms = mergeGyms(local: local.gyms, server: server.gyms)
|
|
||||||
|
|
||||||
// Merge problems by ID, keeping most recently updated
|
|
||||||
let mergedProblems = mergeProblems(local: local.problems, server: server.problems)
|
|
||||||
|
|
||||||
// Merge sessions by ID, keeping most recently updated
|
|
||||||
let mergedSessions = mergeSessions(local: local.sessions, server: server.sessions)
|
|
||||||
|
|
||||||
// Merge attempts by ID, keeping most recently updated
|
|
||||||
let mergedAttempts = mergeAttempts(local: local.attempts, server: server.attempts)
|
|
||||||
|
|
||||||
print(
|
|
||||||
"Merge results: gyms=\(mergedGyms.count), problems=\(mergedProblems.count), sessions=\(mergedSessions.count), attempts=\(mergedAttempts.count)"
|
|
||||||
)
|
|
||||||
|
|
||||||
return ClimbDataBackup(
|
|
||||||
exportedAt: ISO8601DateFormatter().string(from: Date()),
|
|
||||||
version: "2.0",
|
|
||||||
formatVersion: "2.0",
|
|
||||||
gyms: mergedGyms,
|
|
||||||
problems: mergedProblems,
|
|
||||||
sessions: mergedSessions,
|
|
||||||
attempts: mergedAttempts
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func mergeGyms(local: [BackupGym], server: [BackupGym]) -> [BackupGym] {
|
|
||||||
var merged: [String: BackupGym] = [:]
|
|
||||||
|
|
||||||
// Add all local gyms
|
|
||||||
for gym in local {
|
|
||||||
merged[gym.id] = gym
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add server gyms, replacing if newer
|
|
||||||
for serverGym in server {
|
|
||||||
if let localGym = merged[serverGym.id] {
|
|
||||||
// Keep the most recently updated
|
|
||||||
if isNewerThan(serverGym.updatedAt, localGym.updatedAt) {
|
|
||||||
merged[serverGym.id] = serverGym
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// New gym from server
|
|
||||||
merged[serverGym.id] = serverGym
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array(merged.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func mergeProblems(local: [BackupProblem], server: [BackupProblem]) -> [BackupProblem] {
|
|
||||||
var merged: [String: BackupProblem] = [:]
|
|
||||||
|
|
||||||
// Add all local problems
|
|
||||||
for problem in local {
|
|
||||||
merged[problem.id] = problem
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add server problems, replacing if newer or merging image paths
|
|
||||||
for serverProblem in server {
|
|
||||||
if let localProblem = merged[serverProblem.id] {
|
|
||||||
// Merge image paths from both sources
|
|
||||||
let localImages = Set(localProblem.imagePaths ?? [])
|
|
||||||
let serverImages = Set(serverProblem.imagePaths ?? [])
|
|
||||||
let mergedImages = Array(localImages.union(serverImages))
|
|
||||||
|
|
||||||
// Use most recently updated problem data but with merged images
|
|
||||||
let newerProblem =
|
|
||||||
isNewerThan(serverProblem.updatedAt, localProblem.updatedAt)
|
|
||||||
? serverProblem : localProblem
|
|
||||||
merged[serverProblem.id] = BackupProblem(
|
|
||||||
id: newerProblem.id,
|
|
||||||
gymId: newerProblem.gymId,
|
|
||||||
name: newerProblem.name,
|
|
||||||
description: newerProblem.description,
|
|
||||||
climbType: newerProblem.climbType,
|
|
||||||
difficulty: newerProblem.difficulty,
|
|
||||||
tags: newerProblem.tags,
|
|
||||||
location: newerProblem.location,
|
|
||||||
imagePaths: mergedImages.isEmpty ? nil : mergedImages,
|
|
||||||
isActive: newerProblem.isActive,
|
|
||||||
dateSet: newerProblem.dateSet,
|
|
||||||
notes: newerProblem.notes,
|
|
||||||
createdAt: newerProblem.createdAt,
|
|
||||||
updatedAt: newerProblem.updatedAt
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// New problem from server
|
|
||||||
merged[serverProblem.id] = serverProblem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(merged.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func mergeSessions(local: [BackupClimbSession], server: [BackupClimbSession])
|
|
||||||
-> [BackupClimbSession]
|
|
||||||
{
|
|
||||||
var merged: [String: BackupClimbSession] = [:]
|
|
||||||
|
|
||||||
// Add all local sessions
|
|
||||||
for session in local {
|
|
||||||
merged[session.id] = session
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add server sessions, replacing if newer
|
|
||||||
for serverSession in server {
|
|
||||||
if let localSession = merged[serverSession.id] {
|
|
||||||
// Keep the most recently updated
|
|
||||||
if isNewerThan(serverSession.updatedAt, localSession.updatedAt) {
|
|
||||||
merged[serverSession.id] = serverSession
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// New session from server
|
|
||||||
merged[serverSession.id] = serverSession
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(merged.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func mergeAttempts(local: [BackupAttempt], server: [BackupAttempt]) -> [BackupAttempt] {
|
|
||||||
var merged: [String: BackupAttempt] = [:]
|
|
||||||
|
|
||||||
// Add all local attempts
|
|
||||||
for attempt in local {
|
|
||||||
merged[attempt.id] = attempt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add server attempts, replacing if newer
|
|
||||||
for serverAttempt in server {
|
|
||||||
if let localAttempt = merged[serverAttempt.id] {
|
|
||||||
// Keep the most recently created (attempts don't typically get updated)
|
|
||||||
if isNewerThan(serverAttempt.createdAt, localAttempt.createdAt) {
|
|
||||||
merged[serverAttempt.id] = serverAttempt
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// New attempt from server
|
|
||||||
merged[serverAttempt.id] = serverAttempt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(merged.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func isNewerThan(_ dateString1: String, _ dateString2: String) -> Bool {
|
|
||||||
let formatter = ISO8601DateFormatter()
|
|
||||||
guard let date1 = formatter.date(from: dateString1),
|
|
||||||
let date2 = formatter.date(from: dateString2)
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return date1 > date2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
@@ -931,8 +779,6 @@ class SyncService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed SyncTrigger enum - now using simple auto sync on any data change
|
|
||||||
|
|
||||||
enum SyncError: LocalizedError {
|
enum SyncError: LocalizedError {
|
||||||
case notConfigured
|
case notConfigured
|
||||||
case notConnected
|
case notConnected
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
// Sync service for automatic syncing
|
// Sync service for automatic syncing
|
||||||
let syncService = SyncService()
|
let syncService = SyncService()
|
||||||
|
|
||||||
|
// Published property to propagate sync state changes
|
||||||
|
@Published var isSyncing = false
|
||||||
|
|
||||||
private enum Keys {
|
private enum Keys {
|
||||||
static let gyms = "openclimb_gyms"
|
static let gyms = "openclimb_gyms"
|
||||||
static let problems = "openclimb_problems"
|
static let problems = "openclimb_problems"
|
||||||
@@ -67,6 +70,10 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
migrateImagePaths()
|
migrateImagePaths()
|
||||||
setupLiveActivityNotifications()
|
setupLiveActivityNotifications()
|
||||||
|
|
||||||
|
// Keep our published isSyncing in sync with syncService.isSyncing
|
||||||
|
syncService.$isSyncing
|
||||||
|
.assign(to: &$isSyncing)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
||||||
await performImageMaintenance()
|
await performImageMaintenance()
|
||||||
@@ -206,6 +213,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Gym added successfully"
|
successMessage = "Gym added successfully"
|
||||||
clearMessageAfterDelay()
|
clearMessageAfterDelay()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateGym(_ gym: Gym) {
|
func updateGym(_ gym: Gym) {
|
||||||
@@ -215,6 +225,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Gym updated successfully"
|
successMessage = "Gym updated successfully"
|
||||||
clearMessageAfterDelay()
|
clearMessageAfterDelay()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +250,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Gym deleted successfully"
|
successMessage = "Gym deleted successfully"
|
||||||
clearMessageAfterDelay()
|
clearMessageAfterDelay()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func gym(withId id: UUID) -> Gym? {
|
func gym(withId id: UUID) -> Gym? {
|
||||||
@@ -261,6 +277,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Problem updated successfully"
|
successMessage = "Problem updated successfully"
|
||||||
clearMessageAfterDelay()
|
clearMessageAfterDelay()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +295,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
problems.removeAll { $0.id == problem.id }
|
problems.removeAll { $0.id == problem.id }
|
||||||
saveProblems()
|
saveProblems()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func problem(withId id: UUID) -> Problem? {
|
func problem(withId id: UUID) -> Problem? {
|
||||||
@@ -291,7 +313,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startSession(gymId: UUID, notes: String? = nil) {
|
func startSession(gymId: UUID, notes: String? = nil) {
|
||||||
|
// End any currently active session
|
||||||
if let currentActive = activeSession {
|
if let currentActive = activeSession {
|
||||||
endSession(currentActive.id)
|
endSession(currentActive.id)
|
||||||
}
|
}
|
||||||
@@ -314,6 +336,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
for: newSession, gymName: gym.name)
|
for: newSession, gymName: gym.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func endSession(_ sessionId: UUID) {
|
func endSession(_ sessionId: UUID) {
|
||||||
@@ -358,8 +383,11 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
successMessage = "Session updated successfully"
|
successMessage = "Session updated successfully"
|
||||||
clearMessageAfterDelay()
|
clearMessageAfterDelay()
|
||||||
|
|
||||||
// Update Live Activity when session updates
|
// Update Live Activity when session is updated
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +396,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
attempts.removeAll { $0.sessionId == session.id }
|
attempts.removeAll { $0.sessionId == session.id }
|
||||||
saveAttempts()
|
saveAttempts()
|
||||||
|
|
||||||
// Remove from active session if it's the current one
|
// If this is the active session, clear it
|
||||||
if activeSession?.id == session.id {
|
if activeSession?.id == session.id {
|
||||||
activeSession = nil
|
activeSession = nil
|
||||||
saveActiveSession()
|
saveActiveSession()
|
||||||
@@ -380,6 +408,12 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Session deleted successfully"
|
successMessage = "Session deleted successfully"
|
||||||
clearMessageAfterDelay()
|
clearMessageAfterDelay()
|
||||||
|
|
||||||
|
// Update Live Activity when session is deleted
|
||||||
|
updateLiveActivityForActiveSession()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func session(withId id: UUID) -> ClimbSession? {
|
func session(withId id: UUID) -> ClimbSession? {
|
||||||
@@ -421,6 +455,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
|
|
||||||
// Update Live Activity when attempt is updated
|
// Update Live Activity when attempt is updated
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,6 +470,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
|
|
||||||
// Update Live Activity when attempt is deleted
|
// Update Live Activity when attempt is deleted
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
|
|
||||||
|
// Trigger auto-sync if enabled
|
||||||
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func attempts(forSession sessionId: UUID) -> [Attempt] {
|
func attempts(forSession sessionId: UUID) -> [Attempt] {
|
||||||
@@ -476,7 +516,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
return gym(withId: mostUsedGymId)
|
return gym(withId: mostUsedGymId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetAllData() {
|
func resetAllData(showSuccessMessage: Bool = true) {
|
||||||
gyms.removeAll()
|
gyms.removeAll()
|
||||||
problems.removeAll()
|
problems.removeAll()
|
||||||
sessions.removeAll()
|
sessions.removeAll()
|
||||||
@@ -490,8 +530,11 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
userDefaults.removeObject(forKey: Keys.activeSession)
|
userDefaults.removeObject(forKey: Keys.activeSession)
|
||||||
|
|
||||||
DataStateManager.shared.reset()
|
DataStateManager.shared.reset()
|
||||||
successMessage = "All data has been reset"
|
|
||||||
clearMessageAfterDelay()
|
if showSuccessMessage {
|
||||||
|
successMessage = "All data has been reset"
|
||||||
|
clearMessageAfterDelay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportData() -> Data? {
|
func exportData() -> Data? {
|
||||||
@@ -530,7 +573,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func importData(from data: Data) throws {
|
func importData(from data: Data, showSuccessMessage: Bool = true) throws {
|
||||||
do {
|
do {
|
||||||
let importResult = try ZipUtils.extractImportZip(data: data)
|
let importResult = try ZipUtils.extractImportZip(data: data)
|
||||||
|
|
||||||
@@ -566,7 +609,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
|
|
||||||
try validateImportData(importData)
|
try validateImportData(importData)
|
||||||
|
|
||||||
resetAllData()
|
resetAllData(showSuccessMessage: showSuccessMessage)
|
||||||
|
|
||||||
let updatedProblems = updateProblemImagePaths(
|
let updatedProblems = updateProblemImagePaths(
|
||||||
problems: importData.problems,
|
problems: importData.problems,
|
||||||
@@ -586,9 +629,11 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
// Update data state to current time since we just imported new data
|
// Update data state to current time since we just imported new data
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
|
|
||||||
successMessage =
|
if showSuccessMessage {
|
||||||
"Data imported successfully with \(importResult.imagePathMapping.count) images"
|
successMessage =
|
||||||
clearMessageAfterDelay()
|
"Data imported successfully with \(importResult.imagePathMapping.count) images"
|
||||||
|
clearMessageAfterDelay()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError("Import failed: \(error.localizedDescription)")
|
setError("Import failed: \(error.localizedDescription)")
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -20,6 +20,27 @@ struct AnalyticsView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.navigationTitle("Analytics")
|
.navigationTitle("Analytics")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
if dataManager.isSyncing {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
||||||
|
.scaleEffect(0.6)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
.animation(
|
||||||
|
.easeInOut(duration: 0.2), value: dataManager.isSyncing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,25 @@ struct GymsView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle("Gyms")
|
.navigationTitle("Gyms")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
if dataManager.isSyncing {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
||||||
|
.scaleEffect(0.6)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
.animation(
|
||||||
|
.easeInOut(duration: 0.2), value: dataManager.isSyncing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Button("Add") {
|
Button("Add") {
|
||||||
showingAddGym = true
|
showingAddGym = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
//
|
//
|
||||||
// LiveActivityDebugView.swift
|
// LiveActivityDebugView.swift
|
||||||
// OpenClimb
|
|
||||||
//
|
|
||||||
// Created by Assistant on 2025-09-15.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,25 @@ struct ProblemsView: View {
|
|||||||
.navigationTitle("Problems")
|
.navigationTitle("Problems")
|
||||||
.searchable(text: $searchText, prompt: "Search problems...")
|
.searchable(text: $searchText, prompt: "Search problems...")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
if dataManager.isSyncing {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
||||||
|
.scaleEffect(0.6)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
.animation(
|
||||||
|
.easeInOut(duration: 0.2), value: dataManager.isSyncing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if !dataManager.gyms.isEmpty {
|
if !dataManager.gyms.isEmpty {
|
||||||
Button("Add") {
|
Button("Add") {
|
||||||
showingAddProblem = true
|
showingAddProblem = true
|
||||||
|
|||||||
@@ -17,7 +17,25 @@ struct SessionsView: View {
|
|||||||
.navigationTitle("Sessions")
|
.navigationTitle("Sessions")
|
||||||
.navigationBarTitleDisplayMode(.automatic)
|
.navigationBarTitleDisplayMode(.automatic)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
if dataManager.isSyncing {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
||||||
|
.scaleEffect(0.6)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
.animation(
|
||||||
|
.easeInOut(duration: 0.2), value: dataManager.isSyncing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if dataManager.gyms.isEmpty {
|
if dataManager.gyms.isEmpty {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} else if dataManager.activeSession == nil {
|
} else if dataManager.activeSession == nil {
|
||||||
|
|||||||
@@ -22,6 +22,27 @@ struct SettingsView: View {
|
|||||||
AppInfoSection()
|
AppInfoSection()
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
if dataManager.isSyncing {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
|
||||||
|
.scaleEffect(0.6)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
.animation(
|
||||||
|
.easeInOut(duration: 0.2), value: dataManager.isSyncing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.sheet(
|
.sheet(
|
||||||
item: Binding<SheetType?>(
|
item: Binding<SheetType?>(
|
||||||
get: { activeSheet },
|
get: { activeSheet },
|
||||||
@@ -436,6 +457,7 @@ struct SyncSection: View {
|
|||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
.padding(.leading, 24)
|
.padding(.leading, 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingSyncSettings) {
|
.sheet(isPresented: $showingSyncSettings) {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
//
|
//
|
||||||
// AppIntent.swift
|
// AppIntent.swift
|
||||||
// SessionStatusLive
|
|
||||||
//
|
|
||||||
// Created by Atridad Lahiji on 2025-09-15.
|
|
||||||
//
|
|
||||||
|
|
||||||
import WidgetKit
|
|
||||||
import AppIntents
|
import AppIntents
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
||||||
static var title: LocalizedStringResource { "Configuration" }
|
static var title: LocalizedStringResource { "Configuration" }
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
//
|
//
|
||||||
// SessionStatusLive.swift
|
// SessionStatusLive.swift
|
||||||
// SessionStatusLive
|
|
||||||
//
|
|
||||||
// Created by Atridad Lahiji on 2025-09-15.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
//
|
//
|
||||||
// SessionStatusLiveBundle.swift
|
// SessionStatusLiveBundle.swift
|
||||||
// SessionStatusLive
|
|
||||||
//
|
|
||||||
// Created by Atridad Lahiji on 2025-09-15.
|
|
||||||
//
|
|
||||||
|
|
||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct SessionStatusLiveBundle: WidgetBundle {
|
struct SessionStatusLiveBundle: WidgetBundle {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
//
|
//
|
||||||
// SessionStatusLiveControl.swift
|
// SessionStatusLiveControl.swift
|
||||||
// SessionStatusLive
|
|
||||||
//
|
|
||||||
// Created by Atridad Lahiji on 2025-09-15.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppIntents
|
import AppIntents
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@@ -42,8 +38,9 @@ extension SessionStatusLiveControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func currentValue(configuration: TimerConfiguration) async throws -> Value {
|
func currentValue(configuration: TimerConfiguration) async throws -> Value {
|
||||||
let isRunning = true // Check if the timer is running
|
let isRunning = true // Check if the timer is running
|
||||||
return SessionStatusLiveControl.Value(isRunning: isRunning, name: configuration.timerName)
|
return SessionStatusLiveControl.Value(
|
||||||
|
isRunning: isRunning, name: configuration.timerName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
//
|
//
|
||||||
// SessionStatusLiveLiveActivity.swift
|
// SessionStatusLiveLiveActivity.swift
|
||||||
// SessionStatusLive
|
|
||||||
//
|
|
||||||
// Created by Atridad Lahiji on 2025-09-15.
|
|
||||||
//
|
|
||||||
|
|
||||||
import ActivityKit
|
import ActivityKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|||||||
Reference in New Issue
Block a user