Compare commits

..

8 Commits

Author SHA1 Message Date
f4f4968431 Sync bug fixes across the board!
All checks were successful
Ascently - Sync Deploy / build-and-push (push) Successful in 2m15s
2026-01-09 22:48:20 -07:00
d002c703d5 Fixed a number of sync issues I noticed
All checks were successful
Ascently - Sync Deploy / build-and-push (push) Successful in 2m30s
2026-01-09 14:39:28 -07:00
afb0456692 more 2026-01-09 00:27:55 -07:00
74db155d93 New intermediate iOS build that trims some fat 2026-01-09 00:27:12 -07:00
ec63d7c58f Improve concurrency model for iOS 2026-01-08 19:18:44 -07:00
1c47dd93b0 Added alternate icon groundwork 2026-01-08 14:27:27 -07:00
ef05727cde Branding overhaul based on Icon Composer 2026-01-08 14:14:12 -07:00
452fd96372 Icon improvements for iOS 2026-01-08 13:59:49 -07:00
115 changed files with 1486 additions and 3599 deletions

View File

@@ -3,7 +3,6 @@
This is the native Android app for Ascently, built with Kotlin and Jetpack Compose.
## Project Structure
This is a standard Android Gradle project. The main code lives in `app/src/main/java/com/atridad/ascently/`.
- `data/`: Handles all the app's data.

View File

@@ -18,8 +18,8 @@ android {
applicationId = "com.atridad.ascently"
minSdk = 31
targetSdk = 36
versionCode = 50
versionName = "2.5.0"
versionCode = 51
versionName = "2.5.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -13,7 +13,6 @@ data class ClimbDataBackup(
val problems: List<BackupProblem>,
val sessions: List<BackupClimbSession>,
val attempts: List<BackupAttempt>,
val deletedItems: List<DeletedItem> = emptyList(),
)
@Serializable
@@ -34,6 +33,7 @@ data class BackupGym(
@kotlinx.serialization.SerialName("customDifficultyGrades")
val customDifficultyGrades: List<String>? = null,
val notes: String? = null,
val isDeleted: Boolean = false,
val createdAt: String,
val updatedAt: String,
) {
@@ -47,10 +47,26 @@ data class BackupGym(
difficultySystems = gym.difficultySystems,
customDifficultyGrades = gym.customDifficultyGrades.ifEmpty { null },
notes = gym.notes,
isDeleted = false,
createdAt = gym.createdAt,
updatedAt = gym.updatedAt,
)
}
fun createTombstone(id: String, deletedAt: String): BackupGym {
return BackupGym(
id = id,
name = "DELETED",
location = null,
supportedClimbTypes = emptyList(),
difficultySystems = emptyList(),
customDifficultyGrades = null,
notes = null,
isDeleted = true,
createdAt = deletedAt,
updatedAt = deletedAt,
)
}
}
fun toGym(): Gym {
@@ -83,6 +99,7 @@ data class BackupProblem(
val isActive: Boolean = true,
val dateSet: String? = null,
val notes: String? = null,
val isDeleted: Boolean = false,
val createdAt: String,
val updatedAt: String,
) {
@@ -106,10 +123,31 @@ data class BackupProblem(
isActive = problem.isActive,
dateSet = problem.dateSet,
notes = problem.notes,
isDeleted = false,
createdAt = problem.createdAt,
updatedAt = problem.updatedAt,
)
}
fun createTombstone(id: String, deletedAt: String): BackupProblem {
return BackupProblem(
id = id,
gymId = "00000000-0000-0000-0000-000000000000",
name = "DELETED",
description = null,
climbType = ClimbType.values().first(),
difficulty = DifficultyGrade(DifficultySystem.values().first(), "0"),
tags = null,
location = null,
imagePaths = null,
isActive = false,
dateSet = null,
notes = null,
isDeleted = true,
createdAt = deletedAt,
updatedAt = deletedAt,
)
}
}
fun toProblem(): Problem {
@@ -147,6 +185,7 @@ data class BackupClimbSession(
val duration: Long? = null,
val status: SessionStatus,
val notes: String? = null,
val isDeleted: Boolean = false,
val createdAt: String,
val updatedAt: String,
) {
@@ -161,10 +200,27 @@ data class BackupClimbSession(
duration = session.duration,
status = session.status,
notes = session.notes,
isDeleted = false,
createdAt = session.createdAt,
updatedAt = session.updatedAt,
)
}
fun createTombstone(id: String, deletedAt: String): BackupClimbSession {
return BackupClimbSession(
id = id,
gymId = "00000000-0000-0000-0000-000000000000",
date = deletedAt,
startTime = null,
endTime = null,
duration = null,
status = SessionStatus.values().first(),
notes = null,
isDeleted = true,
createdAt = deletedAt,
updatedAt = deletedAt,
)
}
}
fun toClimbSession(): ClimbSession {
@@ -195,6 +251,7 @@ data class BackupAttempt(
val duration: Long? = null,
val restTime: Long? = null,
val timestamp: String,
val isDeleted: Boolean = false,
val createdAt: String,
val updatedAt: String? = null,
) {
@@ -210,10 +267,28 @@ data class BackupAttempt(
duration = attempt.duration,
restTime = attempt.restTime,
timestamp = attempt.timestamp,
isDeleted = false,
createdAt = attempt.createdAt,
updatedAt = attempt.updatedAt,
)
}
fun createTombstone(id: String, deletedAt: String): BackupAttempt {
return BackupAttempt(
id = id,
sessionId = "00000000-0000-0000-0000-000000000000",
problemId = "00000000-0000-0000-0000-000000000000",
result = AttemptResult.values().first(),
highestHold = null,
notes = null,
duration = null,
restTime = null,
timestamp = deletedAt,
isDeleted = true,
createdAt = deletedAt,
updatedAt = deletedAt,
)
}
}
fun toAttempt(): Attempt {

View File

@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.serialization.json.Json
import java.io.File
import java.time.Instant
class ClimbRepository(database: AscentlyDatabase, private val context: Context) {
private val gymDao = database.gymDao()
@@ -38,6 +39,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Gym operations
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms()
suspend fun getAllGymsSync(): List<Gym> = gymDao.getAllGyms().first()
suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id)
suspend fun insertGym(gym: Gym) {
gymDao.insertGym(gym)
@@ -60,6 +62,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Problem operations
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
suspend fun getAllProblemsSync(): List<Problem> = problemDao.getAllProblems().first()
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
suspend fun insertProblem(problem: Problem) {
@@ -80,6 +83,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Session operations
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
suspend fun getAllSessionsSync(): List<ClimbSession> = sessionDao.getAllSessions().first()
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
sessionDao.getSessionsByGym(gymId)
@@ -122,6 +126,8 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Attempt operations
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
suspend fun getAllAttemptsSync(): List<Attempt> = attemptDao.getAllAttempts().first()
suspend fun getAttemptById(id: String): Attempt? = attemptDao.getAttemptById(id)
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
attemptDao.getAttemptsBySession(sessionId)
@@ -273,10 +279,9 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
}
fun trackDeletion(itemId: String, itemType: String) {
val currentDeletions = getDeletedItems().toMutableList()
cleanupOldDeletions()
val newDeletion =
DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601())
currentDeletions.add(newDeletion)
val json = json.encodeToString(newDeletion)
deletionPreferences.edit { putString("deleted_$itemId", json) }
@@ -304,6 +309,27 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
deletionPreferences.edit { clear() }
}
private fun cleanupOldDeletions() {
val allPrefs = deletionPreferences.all
val cutoff = Instant.now().minusSeconds(90L * 24 * 60 * 60)
deletionPreferences.edit {
for ((key, value) in allPrefs) {
if (key.startsWith("deleted_") && value is String) {
try {
val deletion = json.decodeFromString<DeletedItem>(value)
val deletedAt = Instant.parse(deletion.deletedAt)
if (deletedAt.isBefore(cutoff)) {
remove(key)
}
} catch (_: Exception) {
// Ignore
}
}
}
}
}
private fun validateDataIntegrity(
gyms: List<Gym>,
problems: List<Problem>,

View File

@@ -4,7 +4,6 @@ import com.atridad.ascently.data.format.BackupAttempt
import com.atridad.ascently.data.format.BackupClimbSession
import com.atridad.ascently.data.format.BackupGym
import com.atridad.ascently.data.format.BackupProblem
import com.atridad.ascently.data.format.DeletedItem
import kotlinx.serialization.Serializable
/** Request structure for delta sync - sends only changes since last sync */
@@ -15,16 +14,15 @@ data class DeltaSyncRequest(
val problems: List<BackupProblem>,
val sessions: List<BackupClimbSession>,
val attempts: List<BackupAttempt>,
val deletedItems: List<DeletedItem>,
)
/** Response structure for delta sync - receives only changes from server */
@Serializable
data class DeltaSyncResponse(
val serverTime: String,
val requestFullSync: Boolean = false,
val gyms: List<BackupGym>,
val problems: List<BackupProblem>,
val sessions: List<BackupClimbSession>,
val attempts: List<BackupAttempt>,
val deletedItems: List<DeletedItem>,
)

View File

@@ -18,4 +18,6 @@ sealed class SyncException(message: String) : IOException(message), Serializable
SyncException("Invalid server response: $details")
data class NetworkError(val details: String) : SyncException("Network error: $details")
data class General(val details: String) : SyncException(details)
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.atridad.ascently.data.repository.ClimbRepository
import com.atridad.ascently.data.state.DataStateManager
import com.atridad.ascently.utils.AppLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -27,7 +28,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep
}
// Currently we only support one provider, but this allows for future expansion
private val provider: SyncProvider = AscentlySyncProvider(context, repository)
private val provider: SyncProvider = AscentlySyncProvider(context, repository, DataStateManager(context))
// State
private val _isSyncing = MutableStateFlow(false)

View File

@@ -1,11 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Clean white background -->
<path android:fillColor="#FFFFFF"
android:pathData="M0,0h108v108h-108z"/>
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="1024" fill="#1A1A1A" rx="180" ry="180"/>
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 411 B

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="1024" fill="#FFFFFF" rx="180" ry="180"/>
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 411 B

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="1024" fill="transparent" rx="180" ry="180"/>
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
<polygon points="8,75 35,14.25 62,75" fill="#000000" opacity="0.8"/>
<polygon points="31.25,75 65,0.75 98.75,75" fill="#000000" opacity="0.9"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 443 B

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="108" height="108" viewBox="0 0 108 108" xmlns="http://www.w3.org/2000/svg">
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
</svg>

Before

Width:  |  Height:  |  Size: 254 B

View File

@@ -7,52 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
D28C33372F0F87D60040FE49 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D28C33312F0F87D60040FE49 /* Assets.xcassets */; };
D28C33382F0F87D60040FE49 /* Balls.icon in Resources */ = {isa = PBXBuildFile; fileRef = D28C33322F0F87D60040FE49 /* Balls.icon */; };
D28C33392F0F87D60040FE49 /* Icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = D28C33342F0F87D60040FE49 /* Icon.icon */; };
D28C333B2F0F87D60040FE49 /* AscentlyShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */; };
D28C333C2F0F87D60040FE49 /* SessionIntentSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */; };
D28C333D2F0F87D60040FE49 /* ToggleSessionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */; };
D28C333E2F0F87D60040FE49 /* AsyncImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */; };
D28C333F2F0F87D60040FE49 /* CameraImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */; };
D28C33402F0F87D60040FE49 /* PhotoOptionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */; };
D28C33412F0F87D60040FE49 /* ActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */; };
D28C33422F0F87D60040FE49 /* BackupFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33032F0F87D60040FE49 /* BackupFormat.swift */; };
D28C33432F0F87D60040FE49 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33042F0F87D60040FE49 /* DataModels.swift */; };
D28C33442F0F87D60040FE49 /* DeltaSyncFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */; };
D28C33452F0F87D60040FE49 /* ServerSyncProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */; };
D28C33462F0F87D60040FE49 /* SyncMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33082F0F87D60040FE49 /* SyncMerger.swift */; };
D28C33472F0F87D60040FE49 /* SyncProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33092F0F87D60040FE49 /* SyncProvider.swift */; };
D28C33482F0F87D60040FE49 /* HealthKitService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330B2F0F87D60040FE49 /* HealthKitService.swift */; };
D28C33492F0F87D60040FE49 /* MusicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330C2F0F87D60040FE49 /* MusicService.swift */; };
D28C334A2F0F87D60040FE49 /* SyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330D2F0F87D60040FE49 /* SyncService.swift */; };
D28C334B2F0F87D60040FE49 /* AppIconHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33102F0F87D60040FE49 /* AppIconHelper.swift */; };
D28C334C2F0F87D60040FE49 /* AppLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33112F0F87D60040FE49 /* AppLogger.swift */; };
D28C334D2F0F87D60040FE49 /* DataStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33122F0F87D60040FE49 /* DataStateManager.swift */; };
D28C334E2F0F87D60040FE49 /* IconTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33132F0F87D60040FE49 /* IconTestView.swift */; };
D28C334F2F0F87D60040FE49 /* ImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33142F0F87D60040FE49 /* ImageManager.swift */; };
D28C33502F0F87D60040FE49 /* ImageNamingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */; };
D28C33512F0F87D60040FE49 /* OrientationAwareImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */; };
D28C33522F0F87D60040FE49 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33172F0F87D60040FE49 /* ThemeManager.swift */; };
D28C33532F0F87D60040FE49 /* ZipUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33182F0F87D60040FE49 /* ZipUtils.swift */; };
D28C33542F0F87D60040FE49 /* ClimbingDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */; };
D28C33552F0F87D60040FE49 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */; };
D28C33562F0F87D60040FE49 /* AddAttemptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */; };
D28C33572F0F87D60040FE49 /* AddEditGymView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */; };
D28C33582F0F87D60040FE49 /* AddEditProblemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */; };
D28C33592F0F87D60040FE49 /* AddEditSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */; };
D28C335A2F0F87D60040FE49 /* GymDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33232F0F87D60040FE49 /* GymDetailView.swift */; };
D28C335B2F0F87D60040FE49 /* ProblemDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */; };
D28C335C2F0F87D60040FE49 /* SessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33252F0F87D60040FE49 /* SessionDetailView.swift */; };
D28C335D2F0F87D60040FE49 /* AnalyticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33272F0F87D60040FE49 /* AnalyticsView.swift */; };
D28C335E2F0F87D60040FE49 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33282F0F87D60040FE49 /* CalendarView.swift */; };
D28C335F2F0F87D60040FE49 /* GymsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33292F0F87D60040FE49 /* GymsView.swift */; };
D28C33602F0F87D60040FE49 /* LiveActivityDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */; };
D28C33612F0F87D60040FE49 /* ProblemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332B2F0F87D60040FE49 /* ProblemsView.swift */; };
D28C33622F0F87D60040FE49 /* SessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332C2F0F87D60040FE49 /* SessionsView.swift */; };
D28C33632F0F87D60040FE49 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332D2F0F87D60040FE49 /* SettingsView.swift */; };
D28C33642F0F87D60040FE49 /* AscentlyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33302F0F87D60040FE49 /* AscentlyApp.swift */; };
D28C33652F0F87D60040FE49 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33332F0F87D60040FE49 /* ContentView.swift */; };
D2FE94822E78E95C008CDB25 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE94802E78E958008CDB25 /* ActivityKit.framework */; };
D2FE948D2E78FEE0008CDB25 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */; };
D2FE948F2E78FEE0008CDB25 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948E2E78FEE0008CDB25 /* SwiftUI.framework */; };
@@ -94,54 +48,6 @@
/* Begin PBXFileReference section */
D24C19682E75002A0045894C /* Ascently.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ascently.app; sourceTree = BUILT_PRODUCTS_DIR; };
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionStatusLiveExtension.entitlements; sourceTree = "<group>"; };
D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AscentlyShortcuts.swift; sourceTree = "<group>"; };
D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIntentSupport.swift; sourceTree = "<group>"; };
D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleSessionIntent.swift; sourceTree = "<group>"; };
D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncImageView.swift; sourceTree = "<group>"; };
D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraImagePicker.swift; sourceTree = "<group>"; };
D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoOptionSheet.swift; sourceTree = "<group>"; };
D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAttributes.swift; sourceTree = "<group>"; };
D28C33032F0F87D60040FE49 /* BackupFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupFormat.swift; sourceTree = "<group>"; };
D28C33042F0F87D60040FE49 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; };
D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeltaSyncFormat.swift; sourceTree = "<group>"; };
D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSyncProvider.swift; sourceTree = "<group>"; };
D28C33082F0F87D60040FE49 /* SyncMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMerger.swift; sourceTree = "<group>"; };
D28C33092F0F87D60040FE49 /* SyncProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncProvider.swift; sourceTree = "<group>"; };
D28C330B2F0F87D60040FE49 /* HealthKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitService.swift; sourceTree = "<group>"; };
D28C330C2F0F87D60040FE49 /* MusicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicService.swift; sourceTree = "<group>"; };
D28C330D2F0F87D60040FE49 /* SyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncService.swift; sourceTree = "<group>"; };
D28C33102F0F87D60040FE49 /* AppIconHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconHelper.swift; sourceTree = "<group>"; };
D28C33112F0F87D60040FE49 /* AppLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogger.swift; sourceTree = "<group>"; };
D28C33122F0F87D60040FE49 /* DataStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStateManager.swift; sourceTree = "<group>"; };
D28C33132F0F87D60040FE49 /* IconTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconTestView.swift; sourceTree = "<group>"; };
D28C33142F0F87D60040FE49 /* ImageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = "<group>"; };
D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNamingUtils.swift; sourceTree = "<group>"; };
D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationAwareImage.swift; sourceTree = "<group>"; };
D28C33172F0F87D60040FE49 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
D28C33182F0F87D60040FE49 /* ZipUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipUtils.swift; sourceTree = "<group>"; };
D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClimbingDataManager.swift; sourceTree = "<group>"; };
D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAttemptView.swift; sourceTree = "<group>"; };
D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditGymView.swift; sourceTree = "<group>"; };
D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditProblemView.swift; sourceTree = "<group>"; };
D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditSessionView.swift; sourceTree = "<group>"; };
D28C33232F0F87D60040FE49 /* GymDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailView.swift; sourceTree = "<group>"; };
D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemDetailView.swift; sourceTree = "<group>"; };
D28C33252F0F87D60040FE49 /* SessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailView.swift; sourceTree = "<group>"; };
D28C33272F0F87D60040FE49 /* AnalyticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsView.swift; sourceTree = "<group>"; };
D28C33282F0F87D60040FE49 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
D28C33292F0F87D60040FE49 /* GymsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymsView.swift; sourceTree = "<group>"; };
D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityDebugView.swift; sourceTree = "<group>"; };
D28C332B2F0F87D60040FE49 /* ProblemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemsView.swift; sourceTree = "<group>"; };
D28C332C2F0F87D60040FE49 /* SessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsView.swift; sourceTree = "<group>"; };
D28C332D2F0F87D60040FE49 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
D28C332F2F0F87D60040FE49 /* Ascently.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ascently.entitlements; sourceTree = "<group>"; };
D28C33302F0F87D60040FE49 /* AscentlyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AscentlyApp.swift; sourceTree = "<group>"; };
D28C33312F0F87D60040FE49 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D28C33322F0F87D60040FE49 /* Balls.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Balls.icon; sourceTree = "<group>"; };
D28C33332F0F87D60040FE49 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
D28C33342F0F87D60040FE49 /* Icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Icon.icon; sourceTree = "<group>"; };
D28C33352F0F87D60040FE49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D2F32FAD2E90B26500B1BC56 /* AscentlyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AscentlyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D2FE94802E78E958008CDB25 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; };
D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionStatusLiveExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -150,6 +56,13 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
D28C3C8B2E75111D00F7AEE9 /* Exceptions for "Ascently" folder in "Ascently" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = D24C19672E75002A0045894C /* Ascently */;
};
D2FE94A42E78FEE1008CDB25 /* Exceptions for "SessionStatusLive" folder in "SessionStatusLiveExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
@@ -160,6 +73,14 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
D24C196A2E75002A0045894C /* Ascently */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
D28C3C8B2E75111D00F7AEE9 /* Exceptions for "Ascently" folder in "Ascently" target */,
);
path = Ascently;
sourceTree = "<group>";
};
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AscentlyTests;
@@ -208,7 +129,7 @@
isa = PBXGroup;
children = (
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
D28C33362F0F87D60040FE49 /* Ascently */,
D24C196A2E75002A0045894C /* Ascently */,
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */,
D2FE947F2E78E958008CDB25 /* Frameworks */,
@@ -226,165 +147,6 @@
name = Products;
sourceTree = "<group>";
};
D28C32FC2F0F87D60040FE49 /* AppIntents */ = {
isa = PBXGroup;
children = (
D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */,
D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */,
D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */,
);
path = AppIntents;
sourceTree = "<group>";
};
D28C33002F0F87D60040FE49 /* Components */ = {
isa = PBXGroup;
children = (
D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */,
D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */,
D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */,
);
path = Components;
sourceTree = "<group>";
};
D28C33012F0F87D60040FE49 /* FocusFilter */ = {
isa = PBXGroup;
children = (
);
path = FocusFilter;
sourceTree = "<group>";
};
D28C33062F0F87D60040FE49 /* Models */ = {
isa = PBXGroup;
children = (
D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */,
D28C33032F0F87D60040FE49 /* BackupFormat.swift */,
D28C33042F0F87D60040FE49 /* DataModels.swift */,
D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */,
);
path = Models;
sourceTree = "<group>";
};
D28C330A2F0F87D60040FE49 /* Sync */ = {
isa = PBXGroup;
children = (
D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */,
D28C33082F0F87D60040FE49 /* SyncMerger.swift */,
D28C33092F0F87D60040FE49 /* SyncProvider.swift */,
);
path = Sync;
sourceTree = "<group>";
};
D28C330E2F0F87D60040FE49 /* Services */ = {
isa = PBXGroup;
children = (
D28C330A2F0F87D60040FE49 /* Sync */,
D28C330B2F0F87D60040FE49 /* HealthKitService.swift */,
D28C330C2F0F87D60040FE49 /* MusicService.swift */,
D28C330D2F0F87D60040FE49 /* SyncService.swift */,
);
path = Services;
sourceTree = "<group>";
};
D28C330F2F0F87D60040FE49 /* Spotlight */ = {
isa = PBXGroup;
children = (
);
path = Spotlight;
sourceTree = "<group>";
};
D28C33192F0F87D60040FE49 /* Utils */ = {
isa = PBXGroup;
children = (
D28C33102F0F87D60040FE49 /* AppIconHelper.swift */,
D28C33112F0F87D60040FE49 /* AppLogger.swift */,
D28C33122F0F87D60040FE49 /* DataStateManager.swift */,
D28C33132F0F87D60040FE49 /* IconTestView.swift */,
D28C33142F0F87D60040FE49 /* ImageManager.swift */,
D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */,
D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */,
D28C33172F0F87D60040FE49 /* ThemeManager.swift */,
D28C33182F0F87D60040FE49 /* ZipUtils.swift */,
);
path = Utils;
sourceTree = "<group>";
};
D28C331C2F0F87D60040FE49 /* ViewModels */ = {
isa = PBXGroup;
children = (
D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */,
D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
D28C33212F0F87D60040FE49 /* AddEdit */ = {
isa = PBXGroup;
children = (
D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */,
D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */,
D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */,
D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */,
);
path = AddEdit;
sourceTree = "<group>";
};
D28C33222F0F87D60040FE49 /* Debug */ = {
isa = PBXGroup;
children = (
);
path = Debug;
sourceTree = "<group>";
};
D28C33262F0F87D60040FE49 /* Detail */ = {
isa = PBXGroup;
children = (
D28C33232F0F87D60040FE49 /* GymDetailView.swift */,
D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */,
D28C33252F0F87D60040FE49 /* SessionDetailView.swift */,
);
path = Detail;
sourceTree = "<group>";
};
D28C332E2F0F87D60040FE49 /* Views */ = {
isa = PBXGroup;
children = (
D28C33212F0F87D60040FE49 /* AddEdit */,
D28C33222F0F87D60040FE49 /* Debug */,
D28C33262F0F87D60040FE49 /* Detail */,
D28C33272F0F87D60040FE49 /* AnalyticsView.swift */,
D28C33282F0F87D60040FE49 /* CalendarView.swift */,
D28C33292F0F87D60040FE49 /* GymsView.swift */,
D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */,
D28C332B2F0F87D60040FE49 /* ProblemsView.swift */,
D28C332C2F0F87D60040FE49 /* SessionsView.swift */,
D28C332D2F0F87D60040FE49 /* SettingsView.swift */,
);
path = Views;
sourceTree = "<group>";
};
D28C33362F0F87D60040FE49 /* Ascently */ = {
isa = PBXGroup;
children = (
D28C32FC2F0F87D60040FE49 /* AppIntents */,
D28C33002F0F87D60040FE49 /* Components */,
D28C33012F0F87D60040FE49 /* FocusFilter */,
D28C33062F0F87D60040FE49 /* Models */,
D28C330E2F0F87D60040FE49 /* Services */,
D28C330F2F0F87D60040FE49 /* Spotlight */,
D28C33192F0F87D60040FE49 /* Utils */,
D28C331C2F0F87D60040FE49 /* ViewModels */,
D28C332E2F0F87D60040FE49 /* Views */,
D28C332F2F0F87D60040FE49 /* Ascently.entitlements */,
D28C33302F0F87D60040FE49 /* AscentlyApp.swift */,
D28C33312F0F87D60040FE49 /* Assets.xcassets */,
D28C33322F0F87D60040FE49 /* Balls.icon */,
D28C33332F0F87D60040FE49 /* ContentView.swift */,
D28C33342F0F87D60040FE49 /* Icon.icon */,
D28C33352F0F87D60040FE49 /* Info.plist */,
);
path = Ascently;
sourceTree = "<group>";
};
D2FE947F2E78E958008CDB25 /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -412,6 +174,9 @@
dependencies = (
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
D24C196A2E75002A0045894C /* Ascently */,
);
name = Ascently;
packageProductDependencies = (
);
@@ -512,9 +277,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D28C33372F0F87D60040FE49 /* Assets.xcassets in Resources */,
D28C33382F0F87D60040FE49 /* Balls.icon in Resources */,
D28C33392F0F87D60040FE49 /* Icon.icon in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -539,49 +301,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D28C333B2F0F87D60040FE49 /* AscentlyShortcuts.swift in Sources */,
D28C333C2F0F87D60040FE49 /* SessionIntentSupport.swift in Sources */,
D28C333D2F0F87D60040FE49 /* ToggleSessionIntent.swift in Sources */,
D28C333E2F0F87D60040FE49 /* AsyncImageView.swift in Sources */,
D28C333F2F0F87D60040FE49 /* CameraImagePicker.swift in Sources */,
D28C33402F0F87D60040FE49 /* PhotoOptionSheet.swift in Sources */,
D28C33412F0F87D60040FE49 /* ActivityAttributes.swift in Sources */,
D28C33422F0F87D60040FE49 /* BackupFormat.swift in Sources */,
D28C33432F0F87D60040FE49 /* DataModels.swift in Sources */,
D28C33442F0F87D60040FE49 /* DeltaSyncFormat.swift in Sources */,
D28C33452F0F87D60040FE49 /* ServerSyncProvider.swift in Sources */,
D28C33462F0F87D60040FE49 /* SyncMerger.swift in Sources */,
D28C33472F0F87D60040FE49 /* SyncProvider.swift in Sources */,
D28C33482F0F87D60040FE49 /* HealthKitService.swift in Sources */,
D28C33492F0F87D60040FE49 /* MusicService.swift in Sources */,
D28C334A2F0F87D60040FE49 /* SyncService.swift in Sources */,
D28C334B2F0F87D60040FE49 /* AppIconHelper.swift in Sources */,
D28C334C2F0F87D60040FE49 /* AppLogger.swift in Sources */,
D28C334D2F0F87D60040FE49 /* DataStateManager.swift in Sources */,
D28C334E2F0F87D60040FE49 /* IconTestView.swift in Sources */,
D28C334F2F0F87D60040FE49 /* ImageManager.swift in Sources */,
D28C33502F0F87D60040FE49 /* ImageNamingUtils.swift in Sources */,
D28C33512F0F87D60040FE49 /* OrientationAwareImage.swift in Sources */,
D28C33522F0F87D60040FE49 /* ThemeManager.swift in Sources */,
D28C33532F0F87D60040FE49 /* ZipUtils.swift in Sources */,
D28C33542F0F87D60040FE49 /* ClimbingDataManager.swift in Sources */,
D28C33552F0F87D60040FE49 /* LiveActivityManager.swift in Sources */,
D28C33562F0F87D60040FE49 /* AddAttemptView.swift in Sources */,
D28C33572F0F87D60040FE49 /* AddEditGymView.swift in Sources */,
D28C33582F0F87D60040FE49 /* AddEditProblemView.swift in Sources */,
D28C33592F0F87D60040FE49 /* AddEditSessionView.swift in Sources */,
D28C335A2F0F87D60040FE49 /* GymDetailView.swift in Sources */,
D28C335B2F0F87D60040FE49 /* ProblemDetailView.swift in Sources */,
D28C335C2F0F87D60040FE49 /* SessionDetailView.swift in Sources */,
D28C335D2F0F87D60040FE49 /* AnalyticsView.swift in Sources */,
D28C335E2F0F87D60040FE49 /* CalendarView.swift in Sources */,
D28C335F2F0F87D60040FE49 /* GymsView.swift in Sources */,
D28C33602F0F87D60040FE49 /* LiveActivityDebugView.swift in Sources */,
D28C33612F0F87D60040FE49 /* ProblemsView.swift in Sources */,
D28C33622F0F87D60040FE49 /* SessionsView.swift in Sources */,
D28C33632F0F87D60040FE49 /* SettingsView.swift in Sources */,
D28C33642F0F87D60040FE49 /* AscentlyApp.swift in Sources */,
D28C33652F0F87D60040FE49 /* ContentView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -747,7 +466,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 44;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -756,20 +475,23 @@
INFOPLIST_KEY_CFBundleDisplayName = Ascently;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Ascently needs access to your photo library to save and display climbing problem images.";
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.";
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera to take photos of climbing problems.";
INFOPLIST_KEY_NSHealthShareUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs access to your photo library to add photos to climbing problems.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.6.0;
MARKETING_VERSION = 2.6.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -780,7 +502,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 18.6;
WATCHOS_DEPLOYMENT_TARGET = 11.6;
XROS_DEPLOYMENT_TARGET = 2.6;
@@ -796,7 +518,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 44;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -805,20 +527,23 @@
INFOPLIST_KEY_CFBundleDisplayName = Ascently;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Ascently needs access to your photo library to save and display climbing problem images.";
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.";
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera to take photos of climbing problems.";
INFOPLIST_KEY_NSHealthShareUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs access to your photo library to add photos to climbing problems.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.6.0;
MARKETING_VERSION = 2.6.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -829,7 +554,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 18.6;
WATCHOS_DEPLOYMENT_TARGET = 11.6;
XROS_DEPLOYMENT_TARGET = 2.6;
@@ -845,7 +570,7 @@
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.AscentlyTests;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.AscentlyTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -866,7 +591,7 @@
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.AscentlyTests;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.AscentlyTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -885,18 +610,19 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 44;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.0;
MARKETING_VERSION = 2.6.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -915,18 +641,19 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 44;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.0;
MARKETING_VERSION = 2.6.1;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;

View File

@@ -41,7 +41,7 @@ final class SessionIntentController {
func startSessionWithLastUsedGym() async throws -> SessionIntentSummary {
// Wait for data to load
if dataManager.gyms.isEmpty {
try? await Task.sleep(nanoseconds: 500_000_000)
try? await Task.sleep(for: .milliseconds(500))
}
guard let lastGym = dataManager.getLastUsedGym() else {
@@ -49,7 +49,7 @@ final class SessionIntentController {
throw SessionIntentError.noRecentGym
}
guard let startedSession = await dataManager.startSessionAsync(gymId: lastGym.id) else {
guard let startedSession = await dataManager.startSession(gymId: lastGym.id) else {
logFailure(.failedToStartSession, context: "Data manager failed to create new session")
throw SessionIntentError.failedToStartSession
}
@@ -68,7 +68,7 @@ final class SessionIntentController {
throw SessionIntentError.noActiveSession
}
guard let completedSession = await dataManager.endSessionAsync(activeSession.id) else {
guard let completedSession = await dataManager.endSession(activeSession.id) else {
logFailure(
.failedToEndSession, context: "Data manager failed to complete active session")
throw SessionIntentError.failedToEndSession
@@ -97,7 +97,7 @@ final class SessionIntentController {
func toggleSession() async throws -> (summary: SessionIntentSummary, wasStarted: Bool) {
// Wait for data to load
if dataManager.gyms.isEmpty {
try? await Task.sleep(nanoseconds: 500_000_000)
try? await Task.sleep(for: .milliseconds(500))
}
if dataManager.activeSession != nil {

View File

@@ -20,14 +20,14 @@ struct ToggleSessionIntent: AppIntent {
func perform() async throws -> some IntentResult & ProvidesDialog {
// Wait for app initialization
try? await Task.sleep(nanoseconds: 1_000_000_000)
try? await Task.sleep(for: .seconds(1))
let controller = await SessionIntentController()
let (summary, wasStarted) = try await controller.toggleSession()
if wasStarted {
// Wait for Live Activity
try? await Task.sleep(nanoseconds: 500_000_000)
try? await Task.sleep(for: .milliseconds(500))
return .result(dialog: IntentDialog("Session started at \(summary.gymName). Have an awesome climb!"))
} else {
return .result(dialog: IntentDialog("Session at \(summary.gymName) ended. Nice work!"))

View File

@@ -1,56 +0,0 @@
{
"images": [
{
"filename": "app_logo_256.png",
"idiom": "universal",
"scale": "1x"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"filename": "app_logo_256_dark.png",
"idiom": "universal",
"scale": "1x"
},
{
"filename": "app_logo_256.png",
"idiom": "universal",
"scale": "2x"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"filename": "app_logo_256_dark.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "app_logo_256.png",
"idiom": "universal",
"scale": "3x"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"filename": "app_logo_256_dark.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -49,7 +49,7 @@ struct ContentView: View {
if newPhase == .active {
// Add slight delay to ensure app is fully loaded
Task {
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
try? await Task.sleep(for: .milliseconds(200))
dataManager.onAppBecomeActive()
// Re-verify health integration when app becomes active
await dataManager.healthKitService.verifyAndRestoreIntegration()
@@ -96,7 +96,7 @@ struct ContentView: View {
AppLogger.info(
"App will enter foreground - preparing Live Activity check", tag: "Lifecycle")
// Small delay to ensure app is fully active
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
try? await Task.sleep(for: .milliseconds(800))
dataManager.onAppBecomeActive()
// Re-verify health integration when returning from background
await dataManager.healthKitService.verifyAndRestoreIntegration()
@@ -112,7 +112,7 @@ struct ContentView: View {
Task { @MainActor in
AppLogger.info(
"App did become active - checking Live Activity status", tag: "Lifecycle")
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
try? await Task.sleep(for: .milliseconds(300))
dataManager.onAppBecomeActive()
await dataManager.healthKitService.verifyAndRestoreIntegration()
}

View File

@@ -4,6 +4,8 @@
{
"layers" : [
{
"blend-mode" : "normal",
"glass" : true,
"image-name" : "AscetlyTriangle2.png",
"name" : "AscetlyTriangle2",
"position" : {

View File

@@ -4,17 +4,5 @@
<dict>
<key>UIFileSharingEnabled</key>
<true/>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to add photos to climbing problems.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to your camera to take photos of climbing problems.</string>
<key>NSHealthShareUsageDescription</key>
<string>This app needs access to save your climbing workouts to Apple Health.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>This app needs access to save your climbing workouts to Apple Health.</string>
<key>NSAppleMusicUsageDescription</key>
<string>This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.</string>
</dict>
</plist>

View File

@@ -20,7 +20,6 @@ struct ClimbDataBackup: Codable {
let problems: [BackupProblem]
let sessions: [BackupClimbSession]
let attempts: [BackupAttempt]
let deletedItems: [DeletedItem]
init(
exportedAt: String,
@@ -29,8 +28,7 @@ struct ClimbDataBackup: Codable {
gyms: [BackupGym],
problems: [BackupProblem],
sessions: [BackupClimbSession],
attempts: [BackupAttempt],
deletedItems: [DeletedItem] = []
attempts: [BackupAttempt]
) {
self.exportedAt = exportedAt
self.version = version
@@ -39,7 +37,6 @@ struct ClimbDataBackup: Codable {
self.problems = problems
self.sessions = sessions
self.attempts = attempts
self.deletedItems = deletedItems
}
}
@@ -52,6 +49,7 @@ struct BackupGym: Codable {
let difficultySystems: [DifficultySystem]
let customDifficultyGrades: [String]
let notes: String?
let isDeleted: Bool?
let createdAt: String
let updatedAt: String
@@ -64,6 +62,8 @@ struct BackupGym: Codable {
self.customDifficultyGrades = gym.customDifficultyGrades
self.notes = gym.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
self.createdAt = formatter.string(from: gym.createdAt)
@@ -78,6 +78,7 @@ struct BackupGym: Codable {
difficultySystems: [DifficultySystem],
customDifficultyGrades: [String] = [],
notes: String?,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String
) {
@@ -88,6 +89,7 @@ struct BackupGym: Codable {
self.difficultySystems = difficultySystems
self.customDifficultyGrades = customDifficultyGrades
self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -115,6 +117,25 @@ struct BackupGym: Codable {
updatedAt: updatedDate
)
}
static func createTombstone(id: String, deletedAt: Date) -> BackupGym {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupGym(
id: id,
name: "DELETED",
location: nil,
supportedClimbTypes: [],
difficultySystems: [],
customDifficultyGrades: [],
notes: nil,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// Platform-neutral problem representation for backup/restore
@@ -131,6 +152,7 @@ struct BackupProblem: Codable {
let isActive: Bool
let dateSet: String? // ISO 8601 format
let notes: String?
let isDeleted: Bool?
let createdAt: String
let updatedAt: String
@@ -146,6 +168,7 @@ struct BackupProblem: Codable {
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
self.isActive = problem.isActive
self.notes = problem.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -167,6 +190,7 @@ struct BackupProblem: Codable {
isActive: Bool,
dateSet: String?,
notes: String?,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String
) {
@@ -182,6 +206,7 @@ struct BackupProblem: Codable {
self.isActive = isActive
self.dateSet = dateSet
self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -232,10 +257,35 @@ struct BackupProblem: Codable {
isActive: self.isActive,
dateSet: self.dateSet,
notes: self.notes,
isDeleted: self.isDeleted ?? false,
createdAt: self.createdAt,
updatedAt: self.updatedAt
)
}
static func createTombstone(id: String, gymId: String = UUID().uuidString, deletedAt: Date) -> BackupProblem {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupProblem(
id: id,
gymId: gymId,
name: "DELETED",
description: nil,
climbType: ClimbType.allCases.first!,
difficulty: DifficultyGrade(system: DifficultySystem.allCases.first!, grade: "0"),
tags: [],
location: nil,
imagePaths: nil,
isActive: false,
dateSet: nil,
notes: nil,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// Platform-neutral climb session representation for backup/restore
@@ -248,6 +298,7 @@ struct BackupClimbSession: Codable {
let duration: Int64? // Duration in seconds
let status: SessionStatus
let notes: String?
let isDeleted: Bool?
let createdAt: String
let updatedAt: String
@@ -256,6 +307,7 @@ struct BackupClimbSession: Codable {
self.gymId = session.gymId.uuidString
self.status = session.status
self.notes = session.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -276,6 +328,7 @@ struct BackupClimbSession: Codable {
duration: Int64?,
status: SessionStatus,
notes: String?,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String
) {
@@ -287,6 +340,7 @@ struct BackupClimbSession: Codable {
self.duration = duration
self.status = status
self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -321,6 +375,26 @@ struct BackupClimbSession: Codable {
updatedAt: updatedDate
)
}
static func createTombstone(id: String, gymId: String = UUID().uuidString, deletedAt: Date) -> BackupClimbSession {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupClimbSession(
id: id,
gymId: gymId,
date: dateString,
startTime: nil,
endTime: nil,
duration: nil,
status: .completed,
notes: nil,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// Platform-neutral attempt representation for backup/restore
@@ -334,6 +408,7 @@ struct BackupAttempt: Codable {
let duration: Int64? // Duration in seconds
let restTime: Int64? // Rest time in seconds
let timestamp: String
let isDeleted: Bool?
let createdAt: String
let updatedAt: String?
@@ -346,6 +421,7 @@ struct BackupAttempt: Codable {
self.notes = attempt.notes
self.duration = attempt.duration.map { Int64($0) }
self.restTime = attempt.restTime.map { Int64($0) }
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -364,6 +440,7 @@ struct BackupAttempt: Codable {
duration: Int64?,
restTime: Int64?,
timestamp: String,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String?
) {
@@ -376,6 +453,7 @@ struct BackupAttempt: Codable {
self.duration = duration
self.restTime = restTime
self.timestamp = timestamp
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -412,6 +490,27 @@ struct BackupAttempt: Codable {
updatedAt: updatedDate
)
}
static func createTombstone(id: String, sessionId: String = UUID().uuidString, problemId: String = UUID().uuidString, deletedAt: Date) -> BackupAttempt {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupAttempt(
id: id,
sessionId: sessionId,
problemId: problemId,
result: AttemptResult.allCases.first!,
highestHold: nil,
notes: nil,
duration: nil,
restTime: nil,
timestamp: dateString,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// MARK: - Backup Format Errors

View File

@@ -13,14 +13,13 @@ struct DeltaSyncRequest: Codable {
let problems: [BackupProblem]
let sessions: [BackupClimbSession]
let attempts: [BackupAttempt]
let deletedItems: [DeletedItem]
}
struct DeltaSyncResponse: Codable {
let serverTime: String
let requestFullSync: Bool?
let gyms: [BackupGym]
let problems: [BackupProblem]
let sessions: [BackupClimbSession]
let attempts: [BackupAttempt]
let deletedItems: [DeletedItem]
}

View File

@@ -68,21 +68,16 @@ class MusicService: ObservableObject {
}
private func updatePlaybackStatus() {
Task { @MainActor [weak self] in
self?.isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
}
isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
}
private func checkQueueConsistency() {
guard hasStartedSessionPlayback else { return }
Task { @MainActor [weak self] in
guard let self = self else { return }
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
let item = currentEntry.item {
if !self.currentPlaylistTrackIds.isEmpty && !self.currentPlaylistTrackIds.contains(item.id) {
self.hasStartedSessionPlayback = false
}
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
let item = currentEntry.item {
if !currentPlaylistTrackIds.isEmpty && !currentPlaylistTrackIds.contains(item.id) {
hasStartedSessionPlayback = false
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,179 +0,0 @@
import Foundation
struct SyncMerger {
private static let logTag = "SyncMerger"
static func mergeDataSafely(
localBackup: ClimbDataBackup,
serverBackup: ClimbDataBackup,
dataManager: ClimbingDataManager,
imagePathMapping: [String: String]
) throws -> (gyms: [Gym], problems: [Problem], sessions: [ClimbSession], attempts: [Attempt], uniqueDeletions: [DeletedItem]) {
// Merge deletion lists first to prevent resurrection of deleted items
let localDeletions = dataManager.getDeletedItems()
let allDeletions = localDeletions + serverBackup.deletedItems
let uniqueDeletions = Array(Set(allDeletions))
AppLogger.info("Merging gyms...", tag: logTag)
let mergedGyms = mergeGyms(
local: dataManager.gyms,
server: serverBackup.gyms,
deletedItems: uniqueDeletions)
AppLogger.info("Merging problems...", tag: logTag)
let mergedProblems = try mergeProblems(
local: dataManager.problems,
server: serverBackup.problems,
imagePathMapping: imagePathMapping,
deletedItems: uniqueDeletions)
AppLogger.info("Merging sessions...", tag: logTag)
let mergedSessions = try mergeSessions(
local: dataManager.sessions,
server: serverBackup.sessions,
deletedItems: uniqueDeletions)
AppLogger.info("Merging attempts...", tag: logTag)
let mergedAttempts = try mergeAttempts(
local: dataManager.attempts,
server: serverBackup.attempts,
deletedItems: uniqueDeletions)
return (mergedGyms, mergedProblems, mergedSessions, mergedAttempts, uniqueDeletions)
}
private static func mergeGyms(local: [Gym], server: [BackupGym], deletedItems: [DeletedItem]) -> [Gym] {
var merged = local
let deletedGymIds = Set(deletedItems.filter { $0.type == "gym" }.map { $0.id })
let localGymIds = Set(local.map { $0.id.uuidString })
merged.removeAll { deletedGymIds.contains($0.id.uuidString) }
// Add new items from server (excluding deleted ones)
for serverGym in server {
if let serverGymConverted = try? serverGym.toGym() {
let localHasGym = localGymIds.contains(serverGym.id)
let isDeleted = deletedGymIds.contains(serverGym.id)
if !localHasGym && !isDeleted {
merged.append(serverGymConverted)
}
}
}
return merged
}
private static func mergeProblems(
local: [Problem],
server: [BackupProblem],
imagePathMapping: [String: String],
deletedItems: [DeletedItem]
) throws -> [Problem] {
var merged = local
let deletedProblemIds = Set(deletedItems.filter { $0.type == "problem" }.map { $0.id })
let localProblemIds = Set(local.map { $0.id.uuidString })
merged.removeAll { deletedProblemIds.contains($0.id.uuidString) }
for serverProblem in server {
let localHasProblem = localProblemIds.contains(serverProblem.id)
let isDeleted = deletedProblemIds.contains(serverProblem.id)
if !localHasProblem && !isDeleted {
var problemToAdd = serverProblem
if !imagePathMapping.isEmpty, let imagePaths = serverProblem.imagePaths, !imagePaths.isEmpty {
let updatedImagePaths = imagePaths.compactMap { oldPath in
imagePathMapping[oldPath] ?? oldPath
}
if updatedImagePaths != imagePaths {
problemToAdd = BackupProblem(
id: serverProblem.id,
gymId: serverProblem.gymId,
name: serverProblem.name,
description: serverProblem.description,
climbType: serverProblem.climbType,
difficulty: serverProblem.difficulty,
tags: serverProblem.tags,
location: serverProblem.location,
imagePaths: updatedImagePaths,
isActive: serverProblem.isActive,
dateSet: serverProblem.dateSet,
notes: serverProblem.notes,
createdAt: serverProblem.createdAt,
updatedAt: serverProblem.updatedAt
)
}
}
if let serverProblemConverted = try? problemToAdd.toProblem() {
merged.append(serverProblemConverted)
}
}
}
return merged
}
private static func mergeSessions(
local: [ClimbSession], server: [BackupClimbSession], deletedItems: [DeletedItem]
) throws -> [ClimbSession] {
var merged = local
let deletedSessionIds = Set(deletedItems.filter { $0.type == "session" }.map { $0.id })
let localSessionIds = Set(local.map { $0.id.uuidString })
merged.removeAll { session in
deletedSessionIds.contains(session.id.uuidString) && session.status != .active
}
for serverSession in server {
let localHasSession = localSessionIds.contains(serverSession.id)
let isDeleted = deletedSessionIds.contains(serverSession.id)
if !localHasSession && !isDeleted {
if let serverSessionConverted = try? serverSession.toClimbSession() {
merged.append(serverSessionConverted)
}
}
}
return merged
}
private static func mergeAttempts(
local: [Attempt], server: [BackupAttempt], deletedItems: [DeletedItem]
) throws -> [Attempt] {
var merged = local
let deletedAttemptIds = Set(deletedItems.filter { $0.type == "attempt" }.map { $0.id })
let localAttemptIds = Set(local.map { $0.id.uuidString })
// Get active session IDs to protect their attempts
let activeSessionIds = Set(
local.compactMap { attempt in
return attempt.sessionId
}.filter { _ in
return true
})
// Remove items that were deleted on other devices (but be conservative with attempts)
merged.removeAll { attempt in
deletedAttemptIds.contains(attempt.id.uuidString)
&& !activeSessionIds.contains(attempt.sessionId)
}
for serverAttempt in server {
let localHasAttempt = localAttemptIds.contains(serverAttempt.id)
let isDeleted = deletedAttemptIds.contains(serverAttempt.id)
if !localHasAttempt && !isDeleted {
if let serverAttemptConverted = try? serverAttempt.toAttempt() {
merged.append(serverAttemptConverted)
}
}
}
return merged
}
}

View File

@@ -23,8 +23,6 @@ class SyncService: ObservableObject {
private let userDefaults = UserDefaults.standard
private let logTag = "SyncService"
private var syncTask: Task<Void, Never>?
private var pendingChanges = false
private let syncDebounceDelay: TimeInterval = 2.0
private enum Keys {
static let serverURL = "sync_server_url"
@@ -162,34 +160,19 @@ class SyncService: ObservableObject {
return
}
if isSyncing {
pendingChanges = true
return
}
guard !isSyncing else { return }
syncTask?.cancel()
syncTask = Task {
try? await Task.sleep(nanoseconds: UInt64(syncDebounceDelay * 1_000_000_000))
try? await Task.sleep(for: .seconds(2))
guard !Task.isCancelled else { return }
repeat {
pendingChanges = false
do {
try await syncWithServer(dataManager: dataManager)
} catch {
await MainActor.run {
self.isSyncing = false
}
return
}
if pendingChanges {
try? await Task.sleep(nanoseconds: UInt64(syncDebounceDelay * 1_000_000_000))
}
} while pendingChanges && !Task.isCancelled
do {
try await syncWithServer(dataManager: dataManager)
} catch {
self.isSyncing = false
}
}
}
@@ -198,15 +181,12 @@ class SyncService: ObservableObject {
syncTask?.cancel()
syncTask = nil
pendingChanges = false
Task {
do {
try await syncWithServer(dataManager: dataManager)
} catch {
await MainActor.run {
self.isSyncing = false
}
self.isSyncing = false
}
}
}
@@ -216,7 +196,6 @@ class SyncService: ObservableObject {
syncTask?.cancel()
syncTask = nil
pendingChanges = false
isSyncing = false
isConnected = false
lastSyncTime = nil
@@ -239,7 +218,6 @@ class SyncService: ObservableObject {
userDefaults.removeObject(forKey: Keys.autoSyncEnabled)
syncTask?.cancel()
syncTask = nil
pendingChanges = false
activeProvider?.disconnect()
}

View File

@@ -1,115 +0,0 @@
import Combine
import SwiftUI
class AppIconHelper: ObservableObject {
static let shared = AppIconHelper()
@Published var isDarkMode: Bool = false
private init() {
}
func updateDarkModeStatus(for colorScheme: ColorScheme) {
isDarkMode = colorScheme == .dark
}
func isInDarkMode(for colorScheme: ColorScheme) -> Bool {
return colorScheme == .dark
}
var supportsModernIconFeatures: Bool {
if #available(iOS 17.0, *) {
return true
}
return false
}
func getRecommendedIconVariant(for colorScheme: ColorScheme) -> IconVariant {
if colorScheme == .dark {
return .dark
}
return .standard
}
var supportsAlternateIcons: Bool {
if #available(iOS 10.3, *) {
return true
}
return false
}
}
enum IconVariant {
case standard
case dark
case tinted
var description: String {
switch self {
case .standard:
return "Standard"
case .dark:
return "Dark Mode"
case .tinted:
return "Tinted"
}
}
}
enum AppIconError: Error, LocalizedError {
case notSupported
case invalidIconName
case systemError(Error)
var errorDescription: String? {
switch self {
case .notSupported:
return "Alternate icons are not supported on this device"
case .invalidIconName:
return "The specified icon name is invalid"
case .systemError(let error):
return "System error: \(error.localizedDescription)"
}
}
}
struct IconAppearanceModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@ObservedObject private var iconHelper = AppIconHelper.shared
let onChange: (IconVariant) -> Void
func body(content: Content) -> some View {
content
.onChange(of: colorScheme) {
iconHelper.updateDarkModeStatus(for: colorScheme)
onChange(iconHelper.getRecommendedIconVariant(for: colorScheme))
}
.onAppear {
iconHelper.updateDarkModeStatus(for: colorScheme)
onChange(iconHelper.getRecommendedIconVariant(for: colorScheme))
}
}
}
extension View {
func onIconAppearanceChange(_ onChange: @escaping (IconVariant) -> Void) -> some View {
modifier(IconAppearanceModifier(onChange: onChange))
}
}
#if DEBUG
extension AppIconHelper {
static var preview: AppIconHelper {
let helper = AppIconHelper()
helper.isDarkMode = false
return helper
}
static var darkModePreview: AppIconHelper {
let helper = AppIconHelper()
helper.isDarkMode = true
return helper
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More