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_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -416,7 +416,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -439,7 +439,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -459,7 +459,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -481,7 +481,7 @@
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||
@@ -492,7 +492,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -511,7 +511,7 @@
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||
@@ -522,7 +522,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,5 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
class SyncService: ObservableObject {
|
||||
@@ -455,7 +454,7 @@ class SyncService: ObservableObject {
|
||||
let zipData = try createMinimalZipFromBackup(updatedBackup)
|
||||
|
||||
// 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
|
||||
DataStateManager.shared.setLastModified(backup.exportedAt)
|
||||
@@ -735,180 +734,29 @@ class SyncService: ObservableObject {
|
||||
}
|
||||
|
||||
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 {
|
||||
do {
|
||||
try await syncWithServer(dataManager: dataManager)
|
||||
} catch {
|
||||
print("Auto-sync failed: \(error)")
|
||||
// Don't show UI errors for auto-sync failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
await MainActor.run {
|
||||
self.isSyncing = false
|
||||
}
|
||||
} 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() {
|
||||
@@ -931,8 +779,6 @@ class SyncService: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Removed SyncTrigger enum - now using simple auto sync on any data change
|
||||
|
||||
enum SyncError: LocalizedError {
|
||||
case notConfigured
|
||||
case notConnected
|
||||
|
||||
@@ -32,6 +32,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
// Sync service for automatic syncing
|
||||
let syncService = SyncService()
|
||||
|
||||
// Published property to propagate sync state changes
|
||||
@Published var isSyncing = false
|
||||
|
||||
private enum Keys {
|
||||
static let gyms = "openclimb_gyms"
|
||||
static let problems = "openclimb_problems"
|
||||
@@ -67,6 +70,10 @@ class ClimbingDataManager: ObservableObject {
|
||||
migrateImagePaths()
|
||||
setupLiveActivityNotifications()
|
||||
|
||||
// Keep our published isSyncing in sync with syncService.isSyncing
|
||||
syncService.$isSyncing
|
||||
.assign(to: &$isSyncing)
|
||||
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
||||
await performImageMaintenance()
|
||||
@@ -206,6 +213,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
DataStateManager.shared.updateDataState()
|
||||
successMessage = "Gym added successfully"
|
||||
clearMessageAfterDelay()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
|
||||
func updateGym(_ gym: Gym) {
|
||||
@@ -215,6 +225,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
DataStateManager.shared.updateDataState()
|
||||
successMessage = "Gym updated successfully"
|
||||
clearMessageAfterDelay()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +250,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
DataStateManager.shared.updateDataState()
|
||||
successMessage = "Gym deleted successfully"
|
||||
clearMessageAfterDelay()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
|
||||
func gym(withId id: UUID) -> Gym? {
|
||||
@@ -261,6 +277,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
DataStateManager.shared.updateDataState()
|
||||
successMessage = "Problem updated successfully"
|
||||
clearMessageAfterDelay()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +295,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
problems.removeAll { $0.id == problem.id }
|
||||
saveProblems()
|
||||
DataStateManager.shared.updateDataState()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
|
||||
func problem(withId id: UUID) -> Problem? {
|
||||
@@ -291,7 +313,7 @@ class ClimbingDataManager: ObservableObject {
|
||||
}
|
||||
|
||||
func startSession(gymId: UUID, notes: String? = nil) {
|
||||
|
||||
// End any currently active session
|
||||
if let currentActive = activeSession {
|
||||
endSession(currentActive.id)
|
||||
}
|
||||
@@ -314,6 +336,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
for: newSession, gymName: gym.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
|
||||
func endSession(_ sessionId: UUID) {
|
||||
@@ -358,8 +383,11 @@ class ClimbingDataManager: ObservableObject {
|
||||
successMessage = "Session updated successfully"
|
||||
clearMessageAfterDelay()
|
||||
|
||||
// Update Live Activity when session updates
|
||||
// Update Live Activity when session is updated
|
||||
updateLiveActivityForActiveSession()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +396,7 @@ class ClimbingDataManager: ObservableObject {
|
||||
attempts.removeAll { $0.sessionId == session.id }
|
||||
saveAttempts()
|
||||
|
||||
// Remove from active session if it's the current one
|
||||
// If this is the active session, clear it
|
||||
if activeSession?.id == session.id {
|
||||
activeSession = nil
|
||||
saveActiveSession()
|
||||
@@ -380,6 +408,12 @@ class ClimbingDataManager: ObservableObject {
|
||||
DataStateManager.shared.updateDataState()
|
||||
successMessage = "Session deleted successfully"
|
||||
clearMessageAfterDelay()
|
||||
|
||||
// Update Live Activity when session is deleted
|
||||
updateLiveActivityForActiveSession()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
|
||||
func session(withId id: UUID) -> ClimbSession? {
|
||||
@@ -421,6 +455,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
// Update Live Activity when attempt is updated
|
||||
updateLiveActivityForActiveSession()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,6 +470,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
// Update Live Activity when attempt is deleted
|
||||
updateLiveActivityForActiveSession()
|
||||
|
||||
// Trigger auto-sync if enabled
|
||||
syncService.triggerAutoSync(dataManager: self)
|
||||
}
|
||||
|
||||
func attempts(forSession sessionId: UUID) -> [Attempt] {
|
||||
@@ -476,7 +516,7 @@ class ClimbingDataManager: ObservableObject {
|
||||
return gym(withId: mostUsedGymId)
|
||||
}
|
||||
|
||||
func resetAllData() {
|
||||
func resetAllData(showSuccessMessage: Bool = true) {
|
||||
gyms.removeAll()
|
||||
problems.removeAll()
|
||||
sessions.removeAll()
|
||||
@@ -490,8 +530,11 @@ class ClimbingDataManager: ObservableObject {
|
||||
userDefaults.removeObject(forKey: Keys.activeSession)
|
||||
|
||||
DataStateManager.shared.reset()
|
||||
successMessage = "All data has been reset"
|
||||
clearMessageAfterDelay()
|
||||
|
||||
if showSuccessMessage {
|
||||
successMessage = "All data has been reset"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let importResult = try ZipUtils.extractImportZip(data: data)
|
||||
|
||||
@@ -566,7 +609,7 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
try validateImportData(importData)
|
||||
|
||||
resetAllData()
|
||||
resetAllData(showSuccessMessage: showSuccessMessage)
|
||||
|
||||
let updatedProblems = updateProblemImagePaths(
|
||||
problems: importData.problems,
|
||||
@@ -586,9 +629,11 @@ class ClimbingDataManager: ObservableObject {
|
||||
// Update data state to current time since we just imported new data
|
||||
DataStateManager.shared.updateDataState()
|
||||
|
||||
successMessage =
|
||||
"Data imported successfully with \(importResult.imagePathMapping.count) images"
|
||||
clearMessageAfterDelay()
|
||||
if showSuccessMessage {
|
||||
successMessage =
|
||||
"Data imported successfully with \(importResult.imagePathMapping.count) images"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
} catch {
|
||||
setError("Import failed: \(error.localizedDescription)")
|
||||
throw error
|
||||
|
||||
@@ -20,6 +20,27 @@ struct AnalyticsView: View {
|
||||
.padding()
|
||||
}
|
||||
.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")
|
||||
.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") {
|
||||
showingAddGym = true
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// LiveActivityDebugView.swift
|
||||
// OpenClimb
|
||||
//
|
||||
// Created by Assistant on 2025-09-15.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@@ -62,7 +62,25 @@ struct ProblemsView: View {
|
||||
.navigationTitle("Problems")
|
||||
.searchable(text: $searchText, prompt: "Search problems...")
|
||||
.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 {
|
||||
Button("Add") {
|
||||
showingAddProblem = true
|
||||
|
||||
@@ -17,7 +17,25 @@ struct SessionsView: View {
|
||||
.navigationTitle("Sessions")
|
||||
.navigationBarTitleDisplayMode(.automatic)
|
||||
.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 {
|
||||
EmptyView()
|
||||
} else if dataManager.activeSession == nil {
|
||||
|
||||
@@ -22,6 +22,27 @@ struct SettingsView: View {
|
||||
AppInfoSection()
|
||||
}
|
||||
.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(
|
||||
item: Binding<SheetType?>(
|
||||
get: { activeSheet },
|
||||
@@ -436,6 +457,7 @@ struct SyncSection: View {
|
||||
.foregroundColor(.red)
|
||||
.padding(.leading, 24)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingSyncSettings) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
//
|
||||
// AppIntent.swift
|
||||
// SessionStatusLive
|
||||
//
|
||||
// Created by Atridad Lahiji on 2025-09-15.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource { "Configuration" }
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// SessionStatusLive.swift
|
||||
// SessionStatusLive
|
||||
//
|
||||
// Created by Atridad Lahiji on 2025-09-15.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
//
|
||||
// SessionStatusLiveBundle.swift
|
||||
// SessionStatusLive
|
||||
//
|
||||
// Created by Atridad Lahiji on 2025-09-15.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@main
|
||||
struct SessionStatusLiveBundle: WidgetBundle {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// SessionStatusLiveControl.swift
|
||||
// SessionStatusLive
|
||||
//
|
||||
// Created by Atridad Lahiji on 2025-09-15.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
@@ -42,8 +38,9 @@ extension SessionStatusLiveControl {
|
||||
}
|
||||
|
||||
func currentValue(configuration: TimerConfiguration) async throws -> Value {
|
||||
let isRunning = true // Check if the timer is running
|
||||
return SessionStatusLiveControl.Value(isRunning: isRunning, name: configuration.timerName)
|
||||
let isRunning = true // Check if the timer is running
|
||||
return SessionStatusLiveControl.Value(
|
||||
isRunning: isRunning, name: configuration.timerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//
|
||||
// SessionStatusLiveLiveActivity.swift
|
||||
// SessionStatusLive
|
||||
//
|
||||
// Created by Atridad Lahiji on 2025-09-15.
|
||||
//
|
||||
|
||||
import ActivityKit
|
||||
import SwiftUI
|
||||
|
||||
Reference in New Issue
Block a user