Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
11850408be
|
|||
|
37beb4abb6
|
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 42 KiB |
@@ -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 {
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
6
android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 550 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 730 B |
|
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 388 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 514 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 628 B |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 854 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 970 B |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#000000</color>
|
|
||||||
</resources>
|
|
||||||
|
Before Width: | Height: | Size: 933 KiB |
|
Before Width: | Height: | Size: 565 KiB |
|
Before Width: | Height: | Size: 533 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 573 KiB |
BIN
branding/logos/logo-1024-dark.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
branding/logos/logo-1024-white.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
branding/logos/logo-1024.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
branding/logos/logo-128-dark.png
Normal file
|
After Width: | Height: | Size: 804 B |
BIN
branding/logos/logo-128-white.png
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
branding/logos/logo-128.png
Normal file
|
After Width: | Height: | Size: 795 B |
BIN
branding/logos/logo-2048-dark.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
branding/logos/logo-2048-white.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
branding/logos/logo-2048.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
branding/logos/logo-256-dark.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
branding/logos/logo-256-white.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
branding/logos/logo-256.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
branding/logos/logo-512-dark.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
branding/logos/logo-512-white.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
branding/logos/logo-512.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
branding/logos/logo-64-dark.png
Normal file
|
After Width: | Height: | Size: 411 B |
BIN
branding/logos/logo-64-white.png
Normal file
|
After Width: | Height: | Size: 413 B |
BIN
branding/logos/logo-64.png
Normal file
|
After Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
branding/source/Icon.icon/Assets/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
branding/source/Icon.icon/Assets/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
8
branding/source/icon-dark.svg
Normal 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 |
8
branding/source/icon-light.svg
Normal 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 |
8
branding/source/icon-tinted.svg
Normal 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
@@ -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 |
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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!"))
|
||||||
|
|||||||
56
ios/Ascently/Assets.xcassets/AppLogo.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Ascently/Assets.xcassets/AppLogo.imageset/app_logo_256.png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
ios/Ascently/Assets.xcassets/AppLogo.imageset/app_logo_256_dark.png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" : {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
179
ios/Ascently/Services/Sync/SyncMerger.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
115
ios/Ascently/Utils/AppIconHelper.swift
Normal 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
|
||||||