Compare commits

..

2 Commits

Author SHA1 Message Date
11850408be Testing 2026-01-07 23:37:22 -07:00
37beb4abb6 Re-worked assets 2026-01-07 13:52:01 -07:00
115 changed files with 3600 additions and 1487 deletions

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -13,6 +13,7 @@ data class ClimbDataBackup(
val problems: List<BackupProblem>, val problems: List<BackupProblem>,
val sessions: List<BackupClimbSession>, val sessions: List<BackupClimbSession>,
val attempts: List<BackupAttempt>, val attempts: List<BackupAttempt>,
val deletedItems: List<DeletedItem> = emptyList(),
) )
@Serializable @Serializable
@@ -33,7 +34,6 @@ data class BackupGym(
@kotlinx.serialization.SerialName("customDifficultyGrades") @kotlinx.serialization.SerialName("customDifficultyGrades")
val customDifficultyGrades: List<String>? = null, val customDifficultyGrades: List<String>? = null,
val notes: String? = null, val notes: String? = null,
val isDeleted: Boolean = false,
val createdAt: String, val createdAt: String,
val updatedAt: String, val updatedAt: String,
) { ) {
@@ -47,26 +47,10 @@ data class BackupGym(
difficultySystems = gym.difficultySystems, difficultySystems = gym.difficultySystems,
customDifficultyGrades = gym.customDifficultyGrades.ifEmpty { null }, customDifficultyGrades = gym.customDifficultyGrades.ifEmpty { null },
notes = gym.notes, notes = gym.notes,
isDeleted = false,
createdAt = gym.createdAt, createdAt = gym.createdAt,
updatedAt = gym.updatedAt, 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 { fun toGym(): Gym {
@@ -99,7 +83,6 @@ data class BackupProblem(
val isActive: Boolean = true, val isActive: Boolean = true,
val dateSet: String? = null, val dateSet: String? = null,
val notes: String? = null, val notes: String? = null,
val isDeleted: Boolean = false,
val createdAt: String, val createdAt: String,
val updatedAt: String, val updatedAt: String,
) { ) {
@@ -123,31 +106,10 @@ data class BackupProblem(
isActive = problem.isActive, isActive = problem.isActive,
dateSet = problem.dateSet, dateSet = problem.dateSet,
notes = problem.notes, notes = problem.notes,
isDeleted = false,
createdAt = problem.createdAt, createdAt = problem.createdAt,
updatedAt = problem.updatedAt, 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 { fun toProblem(): Problem {
@@ -185,7 +147,6 @@ data class BackupClimbSession(
val duration: Long? = null, val duration: Long? = null,
val status: SessionStatus, val status: SessionStatus,
val notes: String? = null, val notes: String? = null,
val isDeleted: Boolean = false,
val createdAt: String, val createdAt: String,
val updatedAt: String, val updatedAt: String,
) { ) {
@@ -200,27 +161,10 @@ data class BackupClimbSession(
duration = session.duration, duration = session.duration,
status = session.status, status = session.status,
notes = session.notes, notes = session.notes,
isDeleted = false,
createdAt = session.createdAt, createdAt = session.createdAt,
updatedAt = session.updatedAt, 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 { fun toClimbSession(): ClimbSession {
@@ -251,7 +195,6 @@ data class BackupAttempt(
val duration: Long? = null, val duration: Long? = null,
val restTime: Long? = null, val restTime: Long? = null,
val timestamp: String, val timestamp: String,
val isDeleted: Boolean = false,
val createdAt: String, val createdAt: String,
val updatedAt: String? = null, val updatedAt: String? = null,
) { ) {
@@ -267,28 +210,10 @@ data class BackupAttempt(
duration = attempt.duration, duration = attempt.duration,
restTime = attempt.restTime, restTime = attempt.restTime,
timestamp = attempt.timestamp, timestamp = attempt.timestamp,
isDeleted = false,
createdAt = attempt.createdAt, createdAt = attempt.createdAt,
updatedAt = attempt.updatedAt, 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 { fun toAttempt(): Attempt {

View File

@@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.Instant
class ClimbRepository(database: AscentlyDatabase, private val context: Context) { class ClimbRepository(database: AscentlyDatabase, private val context: Context) {
private val gymDao = database.gymDao() private val gymDao = database.gymDao()
@@ -39,7 +38,6 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Gym operations // Gym operations
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms() 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 getGymById(id: String): Gym? = gymDao.getGymById(id)
suspend fun insertGym(gym: Gym) { suspend fun insertGym(gym: Gym) {
gymDao.insertGym(gym) gymDao.insertGym(gym)
@@ -62,7 +60,6 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Problem operations // Problem operations
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems() fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
suspend fun getAllProblemsSync(): List<Problem> = problemDao.getAllProblems().first()
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id) suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId) fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
suspend fun insertProblem(problem: Problem) { suspend fun insertProblem(problem: Problem) {
@@ -83,7 +80,6 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Session operations // Session operations
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions() fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
suspend fun getAllSessionsSync(): List<ClimbSession> = sessionDao.getAllSessions().first()
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id) suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> = fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
sessionDao.getSessionsByGym(gymId) sessionDao.getSessionsByGym(gymId)
@@ -126,8 +122,6 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
// Attempt operations // Attempt operations
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts() 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>> = fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
attemptDao.getAttemptsBySession(sessionId) attemptDao.getAttemptsBySession(sessionId)
@@ -279,9 +273,10 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
} }
fun trackDeletion(itemId: String, itemType: String) { fun trackDeletion(itemId: String, itemType: String) {
cleanupOldDeletions() val currentDeletions = getDeletedItems().toMutableList()
val newDeletion = val newDeletion =
DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601()) DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601())
currentDeletions.add(newDeletion)
val json = json.encodeToString(newDeletion) val json = json.encodeToString(newDeletion)
deletionPreferences.edit { putString("deleted_$itemId", json) } deletionPreferences.edit { putString("deleted_$itemId", json) }
@@ -309,27 +304,6 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
deletionPreferences.edit { clear() } 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( private fun validateDataIntegrity(
gyms: List<Gym>, gyms: List<Gym>,
problems: List<Problem>, problems: List<Problem>,

View File

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

View File

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

View File

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

View File

@@ -1,74 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp" android:width="108dp"
android:viewportHeight="108" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="108">
<path android:fillColor="#3DDC84"
<!-- Clean white background -->
<path android:fillColor="#FFFFFF"
android:pathData="M0,0h108v108h-108z"/> 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> </vector>

View File

@@ -1,5 +0,0 @@
<?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,5 +0,0 @@
<?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,6 @@
<?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

@@ -0,0 +1,6 @@
<?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: 1.2 KiB

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

BIN
branding/logos/logo-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
branding/logos/logo-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
branding/logos/logo-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

BIN
branding/logos/logo-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

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.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,8 @@
<?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>

After

Width:  |  Height:  |  Size: 411 B

View File

@@ -0,0 +1,8 @@
<?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>

After

Width:  |  Height:  |  Size: 411 B

View File

@@ -0,0 +1,8 @@
<?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>

After

Width:  |  Height:  |  Size: 443 B

5
branding/source/logo.svg Normal file
View File

@@ -0,0 +1,5 @@
<?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>

After

Width:  |  Height:  |  Size: 254 B

View File

@@ -7,6 +7,52 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; D2FE94822E78E95C008CDB25 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE94802E78E958008CDB25 /* ActivityKit.framework */; };
D2FE948D2E78FEE0008CDB25 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */; }; D2FE948D2E78FEE0008CDB25 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */; };
D2FE948F2E78FEE0008CDB25 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948E2E78FEE0008CDB25 /* SwiftUI.framework */; }; D2FE948F2E78FEE0008CDB25 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948E2E78FEE0008CDB25 /* SwiftUI.framework */; };
@@ -48,6 +94,54 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
D24C19682E75002A0045894C /* Ascently.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ascently.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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>"; }; 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; }; 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; }; 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; }; D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionStatusLiveExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -56,13 +150,6 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet 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 */ = { D2FE94A42E78FEE1008CDB25 /* Exceptions for "SessionStatusLive" folder in "SessionStatusLiveExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
@@ -73,14 +160,6 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
D24C196A2E75002A0045894C /* Ascently */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
D28C3C8B2E75111D00F7AEE9 /* Exceptions for "Ascently" folder in "Ascently" target */,
);
path = Ascently;
sourceTree = "<group>";
};
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */ = { D2F32FAE2E90B26500B1BC56 /* AscentlyTests */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
path = AscentlyTests; path = AscentlyTests;
@@ -129,7 +208,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */, D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
D24C196A2E75002A0045894C /* Ascently */, D28C33362F0F87D60040FE49 /* Ascently */,
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */, D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */, D2F32FAE2E90B26500B1BC56 /* AscentlyTests */,
D2FE947F2E78E958008CDB25 /* Frameworks */, D2FE947F2E78E958008CDB25 /* Frameworks */,
@@ -147,6 +226,165 @@
name = Products; name = Products;
sourceTree = "<group>"; 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 */ = { D2FE947F2E78E958008CDB25 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -174,9 +412,6 @@
dependencies = ( dependencies = (
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */, D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = (
D24C196A2E75002A0045894C /* Ascently */,
);
name = Ascently; name = Ascently;
packageProductDependencies = ( packageProductDependencies = (
); );
@@ -277,6 +512,9 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D28C33372F0F87D60040FE49 /* Assets.xcassets in Resources */,
D28C33382F0F87D60040FE49 /* Balls.icon in Resources */,
D28C33392F0F87D60040FE49 /* Icon.icon in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -301,6 +539,49 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( 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; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -466,7 +747,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements; CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 44; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6; DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -475,23 +756,20 @@
INFOPLIST_KEY_CFBundleDisplayName = Ascently; INFOPLIST_KEY_CFBundleDisplayName = Ascently;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions."; INFOPLIST_KEY_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera 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_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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 18.6; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.6.1; MARKETING_VERSION = 2.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -502,7 +780,7 @@
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 6.0; SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
TVOS_DEPLOYMENT_TARGET = 18.6; TVOS_DEPLOYMENT_TARGET = 18.6;
WATCHOS_DEPLOYMENT_TARGET = 11.6; WATCHOS_DEPLOYMENT_TARGET = 11.6;
XROS_DEPLOYMENT_TARGET = 2.6; XROS_DEPLOYMENT_TARGET = 2.6;
@@ -518,7 +796,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements; CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 44; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6; DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -527,23 +805,20 @@
INFOPLIST_KEY_CFBundleDisplayName = Ascently; INFOPLIST_KEY_CFBundleDisplayName = Ascently;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions."; INFOPLIST_KEY_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera 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_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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 18.6; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.6.1; MARKETING_VERSION = 2.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -554,7 +829,7 @@
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 6.0; SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
TVOS_DEPLOYMENT_TARGET = 18.6; TVOS_DEPLOYMENT_TARGET = 18.6;
WATCHOS_DEPLOYMENT_TARGET = 11.6; WATCHOS_DEPLOYMENT_TARGET = 11.6;
XROS_DEPLOYMENT_TARGET = 2.6; XROS_DEPLOYMENT_TARGET = 2.6;
@@ -570,7 +845,7 @@
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.AscentlyTests; PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.AscentlyTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -591,7 +866,7 @@
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.AscentlyTests; PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.AscentlyTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -610,19 +885,18 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 44; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist; INFOPLIST_FILE = SessionStatusLive/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive; INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.6.1; MARKETING_VERSION = 2.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -641,19 +915,18 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 44; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist; INFOPLIST_FILE = SessionStatusLive/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive; INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.6.1; MARKETING_VERSION = 2.6.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
{
"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.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

View File

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

View File

@@ -4,5 +4,17 @@
<dict> <dict>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <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> </dict>
</plist> </plist>

View File

@@ -20,6 +20,7 @@ struct ClimbDataBackup: Codable {
let problems: [BackupProblem] let problems: [BackupProblem]
let sessions: [BackupClimbSession] let sessions: [BackupClimbSession]
let attempts: [BackupAttempt] let attempts: [BackupAttempt]
let deletedItems: [DeletedItem]
init( init(
exportedAt: String, exportedAt: String,
@@ -28,7 +29,8 @@ struct ClimbDataBackup: Codable {
gyms: [BackupGym], gyms: [BackupGym],
problems: [BackupProblem], problems: [BackupProblem],
sessions: [BackupClimbSession], sessions: [BackupClimbSession],
attempts: [BackupAttempt] attempts: [BackupAttempt],
deletedItems: [DeletedItem] = []
) { ) {
self.exportedAt = exportedAt self.exportedAt = exportedAt
self.version = version self.version = version
@@ -37,6 +39,7 @@ struct ClimbDataBackup: Codable {
self.problems = problems self.problems = problems
self.sessions = sessions self.sessions = sessions
self.attempts = attempts self.attempts = attempts
self.deletedItems = deletedItems
} }
} }
@@ -49,7 +52,6 @@ struct BackupGym: Codable {
let difficultySystems: [DifficultySystem] let difficultySystems: [DifficultySystem]
let customDifficultyGrades: [String] let customDifficultyGrades: [String]
let notes: String? let notes: String?
let isDeleted: Bool?
let createdAt: String let createdAt: String
let updatedAt: String let updatedAt: String
@@ -62,8 +64,6 @@ struct BackupGym: Codable {
self.customDifficultyGrades = gym.customDifficultyGrades self.customDifficultyGrades = gym.customDifficultyGrades
self.notes = gym.notes self.notes = gym.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
self.createdAt = formatter.string(from: gym.createdAt) self.createdAt = formatter.string(from: gym.createdAt)
@@ -78,7 +78,6 @@ struct BackupGym: Codable {
difficultySystems: [DifficultySystem], difficultySystems: [DifficultySystem],
customDifficultyGrades: [String] = [], customDifficultyGrades: [String] = [],
notes: String?, notes: String?,
isDeleted: Bool = false,
createdAt: String, createdAt: String,
updatedAt: String updatedAt: String
) { ) {
@@ -89,7 +88,6 @@ struct BackupGym: Codable {
self.difficultySystems = difficultySystems self.difficultySystems = difficultySystems
self.customDifficultyGrades = customDifficultyGrades self.customDifficultyGrades = customDifficultyGrades
self.notes = notes self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
@@ -117,25 +115,6 @@ struct BackupGym: Codable {
updatedAt: updatedDate 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 // Platform-neutral problem representation for backup/restore
@@ -152,7 +131,6 @@ struct BackupProblem: Codable {
let isActive: Bool let isActive: Bool
let dateSet: String? // ISO 8601 format let dateSet: String? // ISO 8601 format
let notes: String? let notes: String?
let isDeleted: Bool?
let createdAt: String let createdAt: String
let updatedAt: String let updatedAt: String
@@ -168,7 +146,6 @@ struct BackupProblem: Codable {
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
self.isActive = problem.isActive self.isActive = problem.isActive
self.notes = problem.notes self.notes = problem.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -190,7 +167,6 @@ struct BackupProblem: Codable {
isActive: Bool, isActive: Bool,
dateSet: String?, dateSet: String?,
notes: String?, notes: String?,
isDeleted: Bool = false,
createdAt: String, createdAt: String,
updatedAt: String updatedAt: String
) { ) {
@@ -206,7 +182,6 @@ struct BackupProblem: Codable {
self.isActive = isActive self.isActive = isActive
self.dateSet = dateSet self.dateSet = dateSet
self.notes = notes self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
@@ -257,35 +232,10 @@ struct BackupProblem: Codable {
isActive: self.isActive, isActive: self.isActive,
dateSet: self.dateSet, dateSet: self.dateSet,
notes: self.notes, notes: self.notes,
isDeleted: self.isDeleted ?? false,
createdAt: self.createdAt, createdAt: self.createdAt,
updatedAt: self.updatedAt 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 // Platform-neutral climb session representation for backup/restore
@@ -298,7 +248,6 @@ struct BackupClimbSession: Codable {
let duration: Int64? // Duration in seconds let duration: Int64? // Duration in seconds
let status: SessionStatus let status: SessionStatus
let notes: String? let notes: String?
let isDeleted: Bool?
let createdAt: String let createdAt: String
let updatedAt: String let updatedAt: String
@@ -307,7 +256,6 @@ struct BackupClimbSession: Codable {
self.gymId = session.gymId.uuidString self.gymId = session.gymId.uuidString
self.status = session.status self.status = session.status
self.notes = session.notes self.notes = session.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -328,7 +276,6 @@ struct BackupClimbSession: Codable {
duration: Int64?, duration: Int64?,
status: SessionStatus, status: SessionStatus,
notes: String?, notes: String?,
isDeleted: Bool = false,
createdAt: String, createdAt: String,
updatedAt: String updatedAt: String
) { ) {
@@ -340,7 +287,6 @@ struct BackupClimbSession: Codable {
self.duration = duration self.duration = duration
self.status = status self.status = status
self.notes = notes self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
@@ -375,26 +321,6 @@ struct BackupClimbSession: Codable {
updatedAt: updatedDate 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 // Platform-neutral attempt representation for backup/restore
@@ -408,7 +334,6 @@ struct BackupAttempt: Codable {
let duration: Int64? // Duration in seconds let duration: Int64? // Duration in seconds
let restTime: Int64? // Rest time in seconds let restTime: Int64? // Rest time in seconds
let timestamp: String let timestamp: String
let isDeleted: Bool?
let createdAt: String let createdAt: String
let updatedAt: String? let updatedAt: String?
@@ -421,7 +346,6 @@ struct BackupAttempt: Codable {
self.notes = attempt.notes self.notes = attempt.notes
self.duration = attempt.duration.map { Int64($0) } self.duration = attempt.duration.map { Int64($0) }
self.restTime = attempt.restTime.map { Int64($0) } self.restTime = attempt.restTime.map { Int64($0) }
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -440,7 +364,6 @@ struct BackupAttempt: Codable {
duration: Int64?, duration: Int64?,
restTime: Int64?, restTime: Int64?,
timestamp: String, timestamp: String,
isDeleted: Bool = false,
createdAt: String, createdAt: String,
updatedAt: String? updatedAt: String?
) { ) {
@@ -453,7 +376,6 @@ struct BackupAttempt: Codable {
self.duration = duration self.duration = duration
self.restTime = restTime self.restTime = restTime
self.timestamp = timestamp self.timestamp = timestamp
self.isDeleted = isDeleted
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
@@ -490,27 +412,6 @@ struct BackupAttempt: Codable {
updatedAt: updatedDate 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 // MARK: - Backup Format Errors

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,115 @@
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