Compare commits
8 Commits
balls
...
ANDROID_2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
f4f4968431
|
|||
|
d002c703d5
|
|||
|
afb0456692
|
|||
|
74db155d93
|
|||
|
ec63d7c58f
|
|||
|
1c47dd93b0
|
|||
|
ef05727cde
|
|||
|
452fd96372
|
@@ -3,7 +3,6 @@
|
|||||||
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 = 50
|
versionCode = 51
|
||||||
versionName = "2.5.0"
|
versionName = "2.5.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
@@ -13,7 +13,6 @@ 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
|
||||||
@@ -34,6 +33,7 @@ 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,10 +47,26 @@ 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 {
|
||||||
@@ -83,6 +99,7 @@ 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,
|
||||||
) {
|
) {
|
||||||
@@ -106,10 +123,31 @@ 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 {
|
||||||
@@ -147,6 +185,7 @@ 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,
|
||||||
) {
|
) {
|
||||||
@@ -161,10 +200,27 @@ 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 {
|
||||||
@@ -195,6 +251,7 @@ 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,
|
||||||
) {
|
) {
|
||||||
@@ -210,10 +267,28 @@ 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,6 +19,7 @@ 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()
|
||||||
@@ -38,6 +39,7 @@ 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)
|
||||||
@@ -60,6 +62,7 @@ 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) {
|
||||||
@@ -80,6 +83,7 @@ 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)
|
||||||
@@ -122,6 +126,8 @@ 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)
|
||||||
|
|
||||||
@@ -273,10 +279,9 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun trackDeletion(itemId: String, itemType: String) {
|
fun trackDeletion(itemId: String, itemType: String) {
|
||||||
val currentDeletions = getDeletedItems().toMutableList()
|
cleanupOldDeletions()
|
||||||
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) }
|
||||||
@@ -304,6 +309,27 @@ 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,7 +4,6 @@ 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 */
|
||||||
@@ -15,16 +14,15 @@ 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,4 +18,6 @@ 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,6 +4,7 @@ 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
|
||||||
@@ -27,7 +28,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Currently we only support one provider, but this allows for future expansion
|
// Currently we only support one provider, but this allows for future expansion
|
||||||
private val provider: SyncProvider = AscentlySyncProvider(context, repository)
|
private val provider: SyncProvider = AscentlySyncProvider(context, repository, DataStateManager(context))
|
||||||
|
|
||||||
// State
|
// State
|
||||||
private val _isSyncing = MutableStateFlow(false)
|
private val _isSyncing = MutableStateFlow(false)
|
||||||
|
|||||||
@@ -1,11 +1,74 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#3DDC84"
|
||||||
<!-- Clean white background -->
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
<path android:fillColor="#FFFFFF"
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
android:pathData="M0,0h108v108h-108z"/>
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
</vector>
|
<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>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 550 B After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 730 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 868 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 970 B After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#000000</color>
|
||||||
|
</resources>
|
||||||
186
android/gradlew.bat
vendored
@@ -1,93 +1,93 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@rem This is normally unused
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
exit /b %EXIT_CODE%
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
:omega
|
:omega
|
||||||
|
|||||||
BIN
branding/Android/Icon-Android-Default-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 933 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: 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/Photomator Files/AscentlyBlueBall.pxd
Normal file
BIN
branding/Photomator Files/AscentlyGreenBall.pxd
Normal file
BIN
branding/Photomator Files/AscentlyRedBall.pxd
Normal file
BIN
branding/Photomator Files/AscentlyYellowBall.pxd
Normal file
BIN
branding/Photomator Files/Ascently_Phone_1.pxd
Normal file
BIN
branding/Photomator Files/Ascently_Phone_2.pxd
Normal file
BIN
branding/Photomator Files/Ascently_Phone_3.pxd
Normal file
BIN
branding/Photomator Files/AscetlyTriangle1.pxd
Normal file
BIN
branding/Photomator Files/AscetlyTriangle2.pxd
Normal file
BIN
branding/Photomator Files/PeaksAndroid.pxd
Normal file
BIN
branding/iOS/Icon-iOS-ClearDark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 565 KiB |
BIN
branding/iOS/Icon-iOS-ClearLight-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 533 KiB |
BIN
branding/iOS/Icon-iOS-Dark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
branding/iOS/Icon-iOS-Default-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
branding/iOS/Icon-iOS-TintedDark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
BIN
branding/iOS/Icon-iOS-TintedLight-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 573 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 804 B |
|
Before Width: | Height: | Size: 798 B |
|
Before Width: | Height: | Size: 795 B |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 411 B |
|
Before Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 66 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#1A1A1A" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#FFFFFF" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="transparent" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#000000" opacity="0.8"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#000000" opacity="0.9"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 443 B |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="108" height="108" viewBox="0 0 108 108" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 254 B |
@@ -7,52 +7,6 @@
|
|||||||
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 */; };
|
||||||
@@ -94,54 +48,6 @@
|
|||||||
/* 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; };
|
||||||
@@ -150,6 +56,13 @@
|
|||||||
/* 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 = (
|
||||||
@@ -160,6 +73,14 @@
|
|||||||
/* 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;
|
||||||
@@ -208,7 +129,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
|
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
|
||||||
D28C33362F0F87D60040FE49 /* Ascently */,
|
D24C196A2E75002A0045894C /* Ascently */,
|
||||||
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
|
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
|
||||||
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */,
|
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */,
|
||||||
D2FE947F2E78E958008CDB25 /* Frameworks */,
|
D2FE947F2E78E958008CDB25 /* Frameworks */,
|
||||||
@@ -226,165 +147,6 @@
|
|||||||
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 = (
|
||||||
@@ -412,6 +174,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */,
|
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
D24C196A2E75002A0045894C /* Ascently */,
|
||||||
|
);
|
||||||
name = Ascently;
|
name = Ascently;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
@@ -512,9 +277,6 @@
|
|||||||
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;
|
||||||
};
|
};
|
||||||
@@ -539,49 +301,6 @@
|
|||||||
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;
|
||||||
};
|
};
|
||||||
@@ -747,7 +466,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 = 40;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -756,20 +475,23 @@
|
|||||||
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_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
|
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.";
|
||||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Ascently needs access to your photo library to save and display climbing problem images.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera to take photos of climbing problems.";
|
||||||
|
INFOPLIST_KEY_NSHealthShareUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||||
|
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs access to your photo library to add photos to climbing problems.";
|
||||||
|
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
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.0;
|
MARKETING_VERSION = 2.6.1;
|
||||||
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 = "";
|
||||||
@@ -780,7 +502,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;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
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;
|
||||||
@@ -796,7 +518,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 = 40;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -805,20 +527,23 @@
|
|||||||
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_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
|
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.";
|
||||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Ascently needs access to your photo library to save and display climbing problem images.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera to take photos of climbing problems.";
|
||||||
|
INFOPLIST_KEY_NSHealthShareUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||||
|
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs access to your photo library to add photos to climbing problems.";
|
||||||
|
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
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.0;
|
MARKETING_VERSION = 2.6.1;
|
||||||
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 = "";
|
||||||
@@ -829,7 +554,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;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
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;
|
||||||
@@ -845,7 +570,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.atri.dad.OpenClimb.Watch.AscentlyTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.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;
|
||||||
@@ -866,7 +591,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.atri.dad.OpenClimb.Watch.AscentlyTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.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;
|
||||||
@@ -885,18 +610,19 @@
|
|||||||
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 = 40;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
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.0;
|
MARKETING_VERSION = 2.6.1;
|
||||||
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;
|
||||||
@@ -915,18 +641,19 @@
|
|||||||
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 = 40;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
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.0;
|
MARKETING_VERSION = 2.6.1;
|
||||||
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(nanoseconds: 500_000_000)
|
try? await Task.sleep(for: .milliseconds(500))
|
||||||
}
|
}
|
||||||
|
|
||||||
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.startSessionAsync(gymId: lastGym.id) else {
|
guard let startedSession = await dataManager.startSession(gymId: lastGym.id) else {
|
||||||
logFailure(.failedToStartSession, context: "Data manager failed to create new session")
|
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.endSessionAsync(activeSession.id) else {
|
guard let completedSession = await dataManager.endSession(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(nanoseconds: 500_000_000)
|
try? await Task.sleep(for: .milliseconds(500))
|
||||||
}
|
}
|
||||||
|
|
||||||
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(nanoseconds: 1_000_000_000)
|
try? await Task.sleep(for: .seconds(1))
|
||||||
|
|
||||||
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(nanoseconds: 500_000_000)
|
try? await Task.sleep(for: .milliseconds(500))
|
||||||
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!"))
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"filename": "app_logo_256.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"filename": "app_logo_256_dark.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename": "app_logo_256.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"filename": "app_logo_256_dark.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename": "app_logo_256.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"filename": "app_logo_256_dark.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"author": "xcode",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before 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(nanoseconds: 200_000_000) // 0.2 seconds
|
try? await Task.sleep(for: .milliseconds(200))
|
||||||
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(nanoseconds: 800_000_000) // 0.8 seconds
|
try? await Task.sleep(for: .milliseconds(800))
|
||||||
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(nanoseconds: 300_000_000) // 0.3 seconds
|
try? await Task.sleep(for: .milliseconds(300))
|
||||||
dataManager.onAppBecomeActive()
|
dataManager.onAppBecomeActive()
|
||||||
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
{
|
{
|
||||||
"layers" : [
|
"layers" : [
|
||||||
{
|
{
|
||||||
|
"blend-mode" : "normal",
|
||||||
|
"glass" : true,
|
||||||
"image-name" : "AscetlyTriangle2.png",
|
"image-name" : "AscetlyTriangle2.png",
|
||||||
"name" : "AscetlyTriangle2",
|
"name" : "AscetlyTriangle2",
|
||||||
"position" : {
|
"position" : {
|
||||||
|
|||||||
@@ -4,17 +4,5 @@
|
|||||||
<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,7 +20,6 @@ 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,
|
||||||
@@ -29,8 +28,7 @@ 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
|
||||||
@@ -39,7 +37,6 @@ struct ClimbDataBackup: Codable {
|
|||||||
self.problems = problems
|
self.problems = problems
|
||||||
self.sessions = sessions
|
self.sessions = sessions
|
||||||
self.attempts = attempts
|
self.attempts = attempts
|
||||||
self.deletedItems = deletedItems
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +49,7 @@ 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
|
||||||
|
|
||||||
@@ -64,6 +62,8 @@ 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,6 +78,7 @@ 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
|
||||||
) {
|
) {
|
||||||
@@ -88,6 +89,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -115,6 +117,25 @@ 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
|
||||||
@@ -131,6 +152,7 @@ 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
|
||||||
|
|
||||||
@@ -146,6 +168,7 @@ 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]
|
||||||
@@ -167,6 +190,7 @@ 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
|
||||||
) {
|
) {
|
||||||
@@ -182,6 +206,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -232,10 +257,35 @@ 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
|
||||||
@@ -248,6 +298,7 @@ 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
|
||||||
|
|
||||||
@@ -256,6 +307,7 @@ 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]
|
||||||
@@ -276,6 +328,7 @@ 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
|
||||||
) {
|
) {
|
||||||
@@ -287,6 +340,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -321,6 +375,26 @@ 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
|
||||||
@@ -334,6 +408,7 @@ 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?
|
||||||
|
|
||||||
@@ -346,6 +421,7 @@ 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]
|
||||||
@@ -364,6 +440,7 @@ 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?
|
||||||
) {
|
) {
|
||||||
@@ -376,6 +453,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -412,6 +490,27 @@ 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,14 +13,13 @@ 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]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import SwiftUI
|
|||||||
@MainActor
|
@MainActor
|
||||||
class MusicService: ObservableObject {
|
class MusicService: ObservableObject {
|
||||||
static let shared = MusicService()
|
static let shared = MusicService()
|
||||||
|
|
||||||
@Published var isAuthorized = false
|
@Published var isAuthorized = false
|
||||||
@Published var playlists: MusicItemCollection<Playlist> = []
|
@Published var playlists: MusicItemCollection<Playlist> = []
|
||||||
@Published var selectedPlaylistId: String? {
|
@Published var selectedPlaylistId: String? {
|
||||||
@@ -33,60 +33,55 @@ class MusicService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published var isPlaying = false
|
@Published var isPlaying = false
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var hasStartedSessionPlayback = false
|
private var hasStartedSessionPlayback = false
|
||||||
private var currentPlaylistTrackIds: Set<MusicItemID> = []
|
private var currentPlaylistTrackIds: Set<MusicItemID> = []
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
self.selectedPlaylistId = UserDefaults.standard.string(forKey: "ascently_selected_playlist_id")
|
self.selectedPlaylistId = UserDefaults.standard.string(forKey: "ascently_selected_playlist_id")
|
||||||
self.isMusicEnabled = UserDefaults.standard.bool(forKey: "ascently_music_enabled")
|
self.isMusicEnabled = UserDefaults.standard.bool(forKey: "ascently_music_enabled")
|
||||||
self.isAutoPlayEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autoplay_enabled")
|
self.isAutoPlayEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autoplay_enabled")
|
||||||
self.isAutoStopEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autostop_enabled")
|
self.isAutoStopEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autostop_enabled")
|
||||||
|
|
||||||
if isMusicEnabled {
|
if isMusicEnabled {
|
||||||
Task {
|
Task {
|
||||||
await checkAuthorizationStatus()
|
await checkAuthorizationStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupObservers()
|
setupObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupObservers() {
|
private func setupObservers() {
|
||||||
SystemMusicPlayer.shared.state.objectWillChange
|
SystemMusicPlayer.shared.state.objectWillChange
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.updatePlaybackStatus()
|
self?.updatePlaybackStatus()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
SystemMusicPlayer.shared.queue.objectWillChange
|
SystemMusicPlayer.shared.queue.objectWillChange
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.checkQueueConsistency()
|
self?.checkQueueConsistency()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatePlaybackStatus() {
|
private func updatePlaybackStatus() {
|
||||||
Task { @MainActor [weak self] in
|
isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
|
||||||
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
|
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
|
||||||
guard let self = self else { return }
|
let item = currentEntry.item {
|
||||||
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
|
if !currentPlaylistTrackIds.isEmpty && !currentPlaylistTrackIds.contains(item.id) {
|
||||||
let item = currentEntry.item {
|
hasStartedSessionPlayback = false
|
||||||
if !self.currentPlaylistTrackIds.isEmpty && !self.currentPlaylistTrackIds.contains(item.id) {
|
|
||||||
self.hasStartedSessionPlayback = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleMusicEnabled(_ enabled: Bool) {
|
func toggleMusicEnabled(_ enabled: Bool) {
|
||||||
isMusicEnabled = enabled
|
isMusicEnabled = enabled
|
||||||
if enabled {
|
if enabled {
|
||||||
@@ -95,7 +90,7 @@ class MusicService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAuthorizationStatus() async {
|
func checkAuthorizationStatus() async {
|
||||||
let status = await MusicAuthorization.request()
|
let status = await MusicAuthorization.request()
|
||||||
self.isAuthorized = status == .authorized
|
self.isAuthorized = status == .authorized
|
||||||
@@ -103,7 +98,7 @@ class MusicService: ObservableObject {
|
|||||||
await fetchPlaylists()
|
await fetchPlaylists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPlaylists() async {
|
func fetchPlaylists() async {
|
||||||
guard isAuthorized else { return }
|
guard isAuthorized else { return }
|
||||||
do {
|
do {
|
||||||
@@ -115,20 +110,20 @@ class MusicService: ObservableObject {
|
|||||||
print("Error fetching playlists: \(error)")
|
print("Error fetching playlists: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func playSelectedPlaylistIfHeadphonesConnected() {
|
func playSelectedPlaylistIfHeadphonesConnected() {
|
||||||
guard isMusicEnabled, isAutoPlayEnabled, let playlistId = selectedPlaylistId else { return }
|
guard isMusicEnabled, isAutoPlayEnabled, let playlistId = selectedPlaylistId else { return }
|
||||||
|
|
||||||
if isHeadphonesConnected() {
|
if isHeadphonesConnected() {
|
||||||
playPlaylist(id: playlistId)
|
playPlaylist(id: playlistId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetSessionPlaybackState() {
|
func resetSessionPlaybackState() {
|
||||||
hasStartedSessionPlayback = false
|
hasStartedSessionPlayback = false
|
||||||
currentPlaylistTrackIds.removeAll()
|
currentPlaylistTrackIds.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func playPlaylist(id: String) {
|
func playPlaylist(id: String) {
|
||||||
print("Attempting to play playlist \(id)")
|
print("Attempting to play playlist \(id)")
|
||||||
Task {
|
Task {
|
||||||
@@ -136,9 +131,9 @@ class MusicService: ObservableObject {
|
|||||||
if playlists.isEmpty {
|
if playlists.isEmpty {
|
||||||
await fetchPlaylists()
|
await fetchPlaylists()
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetPlaylist: Playlist?
|
var targetPlaylist: Playlist?
|
||||||
|
|
||||||
if let playlist = playlists.first(where: { $0.id.rawValue == id }) {
|
if let playlist = playlists.first(where: { $0.id.rawValue == id }) {
|
||||||
targetPlaylist = playlist
|
targetPlaylist = playlist
|
||||||
} else {
|
} else {
|
||||||
@@ -147,13 +142,13 @@ class MusicService: ObservableObject {
|
|||||||
let response = try await request.response()
|
let response = try await request.response()
|
||||||
targetPlaylist = response.items.first
|
targetPlaylist = response.items.first
|
||||||
}
|
}
|
||||||
|
|
||||||
if let playlist = targetPlaylist {
|
if let playlist = targetPlaylist {
|
||||||
let detailedPlaylist = try await playlist.with([.tracks])
|
let detailedPlaylist = try await playlist.with([.tracks])
|
||||||
if let tracks = detailedPlaylist.tracks {
|
if let tracks = detailedPlaylist.tracks {
|
||||||
self.currentPlaylistTrackIds = Set(tracks.map { $0.id })
|
self.currentPlaylistTrackIds = Set(tracks.map { $0.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemMusicPlayer.shared.queue = [playlist]
|
SystemMusicPlayer.shared.queue = [playlist]
|
||||||
try await SystemMusicPlayer.shared.play()
|
try await SystemMusicPlayer.shared.play()
|
||||||
hasStartedSessionPlayback = true
|
hasStartedSessionPlayback = true
|
||||||
@@ -163,12 +158,12 @@ class MusicService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopPlaybackIfEnabled() {
|
func stopPlaybackIfEnabled() {
|
||||||
guard isMusicEnabled, isAutoStopEnabled else { return }
|
guard isMusicEnabled, isAutoStopEnabled else { return }
|
||||||
SystemMusicPlayer.shared.stop()
|
SystemMusicPlayer.shared.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func togglePlayback() {
|
func togglePlayback() {
|
||||||
Task {
|
Task {
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
@@ -182,7 +177,7 @@ class MusicService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isHeadphonesConnected() -> Bool {
|
private func isHeadphonesConnected() -> Bool {
|
||||||
let route = AVAudioSession.sharedInstance().currentRoute
|
let route = AVAudioSession.sharedInstance().currentRoute
|
||||||
return route.outputs.contains { port in
|
return route.outputs.contains { port in
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
struct SyncMerger {
|
|
||||||
private static let logTag = "SyncMerger"
|
|
||||||
|
|
||||||
static func mergeDataSafely(
|
|
||||||
localBackup: ClimbDataBackup,
|
|
||||||
serverBackup: ClimbDataBackup,
|
|
||||||
dataManager: ClimbingDataManager,
|
|
||||||
imagePathMapping: [String: String]
|
|
||||||
) throws -> (gyms: [Gym], problems: [Problem], sessions: [ClimbSession], attempts: [Attempt], uniqueDeletions: [DeletedItem]) {
|
|
||||||
|
|
||||||
// Merge deletion lists first to prevent resurrection of deleted items
|
|
||||||
let localDeletions = dataManager.getDeletedItems()
|
|
||||||
let allDeletions = localDeletions + serverBackup.deletedItems
|
|
||||||
let uniqueDeletions = Array(Set(allDeletions))
|
|
||||||
|
|
||||||
AppLogger.info("Merging gyms...", tag: logTag)
|
|
||||||
let mergedGyms = mergeGyms(
|
|
||||||
local: dataManager.gyms,
|
|
||||||
server: serverBackup.gyms,
|
|
||||||
deletedItems: uniqueDeletions)
|
|
||||||
|
|
||||||
AppLogger.info("Merging problems...", tag: logTag)
|
|
||||||
let mergedProblems = try mergeProblems(
|
|
||||||
local: dataManager.problems,
|
|
||||||
server: serverBackup.problems,
|
|
||||||
imagePathMapping: imagePathMapping,
|
|
||||||
deletedItems: uniqueDeletions)
|
|
||||||
|
|
||||||
AppLogger.info("Merging sessions...", tag: logTag)
|
|
||||||
let mergedSessions = try mergeSessions(
|
|
||||||
local: dataManager.sessions,
|
|
||||||
server: serverBackup.sessions,
|
|
||||||
deletedItems: uniqueDeletions)
|
|
||||||
|
|
||||||
AppLogger.info("Merging attempts...", tag: logTag)
|
|
||||||
let mergedAttempts = try mergeAttempts(
|
|
||||||
local: dataManager.attempts,
|
|
||||||
server: serverBackup.attempts,
|
|
||||||
deletedItems: uniqueDeletions)
|
|
||||||
|
|
||||||
return (mergedGyms, mergedProblems, mergedSessions, mergedAttempts, uniqueDeletions)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func mergeGyms(local: [Gym], server: [BackupGym], deletedItems: [DeletedItem]) -> [Gym] {
|
|
||||||
var merged = local
|
|
||||||
let deletedGymIds = Set(deletedItems.filter { $0.type == "gym" }.map { $0.id })
|
|
||||||
let localGymIds = Set(local.map { $0.id.uuidString })
|
|
||||||
|
|
||||||
merged.removeAll { deletedGymIds.contains($0.id.uuidString) }
|
|
||||||
|
|
||||||
// Add new items from server (excluding deleted ones)
|
|
||||||
for serverGym in server {
|
|
||||||
if let serverGymConverted = try? serverGym.toGym() {
|
|
||||||
let localHasGym = localGymIds.contains(serverGym.id)
|
|
||||||
let isDeleted = deletedGymIds.contains(serverGym.id)
|
|
||||||
|
|
||||||
if !localHasGym && !isDeleted {
|
|
||||||
merged.append(serverGymConverted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func mergeProblems(
|
|
||||||
local: [Problem],
|
|
||||||
server: [BackupProblem],
|
|
||||||
imagePathMapping: [String: String],
|
|
||||||
deletedItems: [DeletedItem]
|
|
||||||
) throws -> [Problem] {
|
|
||||||
var merged = local
|
|
||||||
let deletedProblemIds = Set(deletedItems.filter { $0.type == "problem" }.map { $0.id })
|
|
||||||
let localProblemIds = Set(local.map { $0.id.uuidString })
|
|
||||||
|
|
||||||
merged.removeAll { deletedProblemIds.contains($0.id.uuidString) }
|
|
||||||
|
|
||||||
for serverProblem in server {
|
|
||||||
let localHasProblem = localProblemIds.contains(serverProblem.id)
|
|
||||||
let isDeleted = deletedProblemIds.contains(serverProblem.id)
|
|
||||||
|
|
||||||
if !localHasProblem && !isDeleted {
|
|
||||||
var problemToAdd = serverProblem
|
|
||||||
|
|
||||||
if !imagePathMapping.isEmpty, let imagePaths = serverProblem.imagePaths, !imagePaths.isEmpty {
|
|
||||||
let updatedImagePaths = imagePaths.compactMap { oldPath in
|
|
||||||
imagePathMapping[oldPath] ?? oldPath
|
|
||||||
}
|
|
||||||
if updatedImagePaths != imagePaths {
|
|
||||||
problemToAdd = BackupProblem(
|
|
||||||
id: serverProblem.id,
|
|
||||||
gymId: serverProblem.gymId,
|
|
||||||
name: serverProblem.name,
|
|
||||||
description: serverProblem.description,
|
|
||||||
climbType: serverProblem.climbType,
|
|
||||||
difficulty: serverProblem.difficulty,
|
|
||||||
tags: serverProblem.tags,
|
|
||||||
location: serverProblem.location,
|
|
||||||
imagePaths: updatedImagePaths,
|
|
||||||
isActive: serverProblem.isActive,
|
|
||||||
dateSet: serverProblem.dateSet,
|
|
||||||
notes: serverProblem.notes,
|
|
||||||
createdAt: serverProblem.createdAt,
|
|
||||||
updatedAt: serverProblem.updatedAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let serverProblemConverted = try? problemToAdd.toProblem() {
|
|
||||||
merged.append(serverProblemConverted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func mergeSessions(
|
|
||||||
local: [ClimbSession], server: [BackupClimbSession], deletedItems: [DeletedItem]
|
|
||||||
) throws -> [ClimbSession] {
|
|
||||||
var merged = local
|
|
||||||
let deletedSessionIds = Set(deletedItems.filter { $0.type == "session" }.map { $0.id })
|
|
||||||
let localSessionIds = Set(local.map { $0.id.uuidString })
|
|
||||||
|
|
||||||
merged.removeAll { session in
|
|
||||||
deletedSessionIds.contains(session.id.uuidString) && session.status != .active
|
|
||||||
}
|
|
||||||
|
|
||||||
for serverSession in server {
|
|
||||||
let localHasSession = localSessionIds.contains(serverSession.id)
|
|
||||||
let isDeleted = deletedSessionIds.contains(serverSession.id)
|
|
||||||
|
|
||||||
if !localHasSession && !isDeleted {
|
|
||||||
if let serverSessionConverted = try? serverSession.toClimbSession() {
|
|
||||||
merged.append(serverSessionConverted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func mergeAttempts(
|
|
||||||
local: [Attempt], server: [BackupAttempt], deletedItems: [DeletedItem]
|
|
||||||
) throws -> [Attempt] {
|
|
||||||
var merged = local
|
|
||||||
let deletedAttemptIds = Set(deletedItems.filter { $0.type == "attempt" }.map { $0.id })
|
|
||||||
let localAttemptIds = Set(local.map { $0.id.uuidString })
|
|
||||||
|
|
||||||
// Get active session IDs to protect their attempts
|
|
||||||
let activeSessionIds = Set(
|
|
||||||
local.compactMap { attempt in
|
|
||||||
return attempt.sessionId
|
|
||||||
}.filter { _ in
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Remove items that were deleted on other devices (but be conservative with attempts)
|
|
||||||
merged.removeAll { attempt in
|
|
||||||
deletedAttemptIds.contains(attempt.id.uuidString)
|
|
||||||
&& !activeSessionIds.contains(attempt.sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
for serverAttempt in server {
|
|
||||||
let localHasAttempt = localAttemptIds.contains(serverAttempt.id)
|
|
||||||
let isDeleted = deletedAttemptIds.contains(serverAttempt.id)
|
|
||||||
|
|
||||||
if !localHasAttempt && !isDeleted {
|
|
||||||
if let serverAttemptConverted = try? serverAttempt.toAttempt() {
|
|
||||||
merged.append(serverAttemptConverted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ class SyncService: ObservableObject {
|
|||||||
@Published var isConnected = false
|
@Published var isConnected = false
|
||||||
@Published var isTesting = false
|
@Published var isTesting = false
|
||||||
@Published var isOfflineMode = false
|
@Published var isOfflineMode = false
|
||||||
|
|
||||||
@Published var providerType: SyncProviderType = .server {
|
@Published var providerType: SyncProviderType = .server {
|
||||||
didSet {
|
didSet {
|
||||||
updateActiveProvider()
|
updateActiveProvider()
|
||||||
@@ -23,8 +23,6 @@ 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"
|
||||||
@@ -39,7 +37,7 @@ class SyncService: ObservableObject {
|
|||||||
// Legacy properties for compatibility with SettingsView
|
// Legacy properties for compatibility with SettingsView
|
||||||
var serverURL: String {
|
var serverURL: String {
|
||||||
get { userDefaults.string(forKey: Keys.serverURL) ?? "" }
|
get { userDefaults.string(forKey: Keys.serverURL) ?? "" }
|
||||||
set {
|
set {
|
||||||
userDefaults.set(newValue, forKey: Keys.serverURL)
|
userDefaults.set(newValue, forKey: Keys.serverURL)
|
||||||
// If active provider is server, it will pick up the change from UserDefaults
|
// If active provider is server, it will pick up the change from UserDefaults
|
||||||
}
|
}
|
||||||
@@ -66,28 +64,28 @@ class SyncService: ObservableObject {
|
|||||||
isConnected = userDefaults.bool(forKey: Keys.isConnected)
|
isConnected = userDefaults.bool(forKey: Keys.isConnected)
|
||||||
isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true
|
isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true
|
||||||
isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode)
|
isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode)
|
||||||
|
|
||||||
if let savedType = userDefaults.string(forKey: Keys.providerType),
|
if let savedType = userDefaults.string(forKey: Keys.providerType),
|
||||||
let type = SyncProviderType(rawValue: savedType) {
|
let type = SyncProviderType(rawValue: savedType) {
|
||||||
self.providerType = type
|
self.providerType = type
|
||||||
} else {
|
} else {
|
||||||
self.providerType = .server // Default
|
self.providerType = .server // Default
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActiveProvider()
|
updateActiveProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateActiveProvider() {
|
private func updateActiveProvider() {
|
||||||
switch providerType {
|
switch providerType {
|
||||||
case .server:
|
case .server:
|
||||||
activeProvider = ServerSyncProvider()
|
activeProvider = ServerSyncProvider()
|
||||||
case .iCloud:
|
case .iCloud:
|
||||||
// Placeholder for iCloud provider
|
// Placeholder for iCloud provider
|
||||||
activeProvider = nil
|
activeProvider = nil
|
||||||
case .none:
|
case .none:
|
||||||
activeProvider = nil
|
activeProvider = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status based on new provider
|
// Update status based on new provider
|
||||||
if let provider = activeProvider {
|
if let provider = activeProvider {
|
||||||
isConnected = provider.isConnected
|
isConnected = provider.isConnected
|
||||||
@@ -101,7 +99,7 @@ class SyncService: ObservableObject {
|
|||||||
AppLogger.info("Sync skipped: Offline mode is enabled.", tag: logTag)
|
AppLogger.info("Sync skipped: Offline mode is enabled.", tag: logTag)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let provider = activeProvider else {
|
guard let provider = activeProvider else {
|
||||||
if providerType == .none {
|
if providerType == .none {
|
||||||
return
|
return
|
||||||
@@ -127,7 +125,7 @@ class SyncService: ObservableObject {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
try await provider.sync(dataManager: dataManager)
|
try await provider.sync(dataManager: dataManager)
|
||||||
|
|
||||||
// Update last sync time
|
// Update last sync time
|
||||||
// Provider might have updated it in UserDefaults, reload it
|
// Provider might have updated it in UserDefaults, reload it
|
||||||
if let lastSync = userDefaults.object(forKey: Keys.lastSyncTime) as? Date {
|
if let lastSync = userDefaults.object(forKey: Keys.lastSyncTime) as? Date {
|
||||||
@@ -144,12 +142,12 @@ class SyncService: ObservableObject {
|
|||||||
AppLogger.error("Test connection failed: No active provider", tag: logTag)
|
AppLogger.error("Test connection failed: No active provider", tag: logTag)
|
||||||
throw SyncError.notConfigured
|
throw SyncError.notConfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
isTesting = true
|
isTesting = true
|
||||||
defer { isTesting = false }
|
defer { isTesting = false }
|
||||||
|
|
||||||
try await provider.testConnection()
|
try await provider.testConnection()
|
||||||
|
|
||||||
isConnected = provider.isConnected
|
isConnected = provider.isConnected
|
||||||
userDefaults.set(isConnected, forKey: Keys.isConnected)
|
userDefaults.set(isConnected, forKey: Keys.isConnected)
|
||||||
}
|
}
|
||||||
@@ -162,34 +160,19 @@ class SyncService: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSyncing {
|
guard !isSyncing else { return }
|
||||||
pendingChanges = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
syncTask?.cancel()
|
syncTask?.cancel()
|
||||||
|
|
||||||
syncTask = Task {
|
syncTask = Task {
|
||||||
try? await Task.sleep(nanoseconds: UInt64(syncDebounceDelay * 1_000_000_000))
|
try? await Task.sleep(for: .seconds(2))
|
||||||
|
|
||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
repeat {
|
do {
|
||||||
pendingChanges = false
|
try await syncWithServer(dataManager: dataManager)
|
||||||
|
} catch {
|
||||||
do {
|
self.isSyncing = false
|
||||||
try await syncWithServer(dataManager: dataManager)
|
}
|
||||||
} catch {
|
|
||||||
await MainActor.run {
|
|
||||||
self.isSyncing = false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if pendingChanges {
|
|
||||||
try? await Task.sleep(nanoseconds: UInt64(syncDebounceDelay * 1_000_000_000))
|
|
||||||
}
|
|
||||||
} while pendingChanges && !Task.isCancelled
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,30 +181,26 @@ 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
|
||||||
syncError = nil
|
syncError = nil
|
||||||
|
|
||||||
// These are shared keys, so clearing them affects all providers if they use them
|
// These are shared keys, so clearing them affects all providers if they use them
|
||||||
// But disconnect() is usually user initiated action
|
// But disconnect() is usually user initiated action
|
||||||
userDefaults.set(false, forKey: Keys.isConnected)
|
userDefaults.set(false, forKey: Keys.isConnected)
|
||||||
@@ -239,8 +218,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||