Compare commits
8 Commits
| 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.
|
||||
|
||||
## Project Structure
|
||||
|
||||
This is a standard Android Gradle project. The main code lives in `app/src/main/java/com/atridad/ascently/`.
|
||||
|
||||
- `data/`: Handles all the app's data.
|
||||
|
||||
@@ -18,8 +18,8 @@ android {
|
||||
applicationId = "com.atridad.ascently"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 50
|
||||
versionName = "2.5.0"
|
||||
versionCode = 51
|
||||
versionName = "2.5.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
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 sessions: List<BackupClimbSession>,
|
||||
val attempts: List<BackupAttempt>,
|
||||
val deletedItems: List<DeletedItem> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -34,6 +33,7 @@ data class BackupGym(
|
||||
@kotlinx.serialization.SerialName("customDifficultyGrades")
|
||||
val customDifficultyGrades: List<String>? = null,
|
||||
val notes: String? = null,
|
||||
val isDeleted: Boolean = false,
|
||||
val createdAt: String,
|
||||
val updatedAt: String,
|
||||
) {
|
||||
@@ -47,10 +47,26 @@ data class BackupGym(
|
||||
difficultySystems = gym.difficultySystems,
|
||||
customDifficultyGrades = gym.customDifficultyGrades.ifEmpty { null },
|
||||
notes = gym.notes,
|
||||
isDeleted = false,
|
||||
createdAt = gym.createdAt,
|
||||
updatedAt = gym.updatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
fun createTombstone(id: String, deletedAt: String): BackupGym {
|
||||
return BackupGym(
|
||||
id = id,
|
||||
name = "DELETED",
|
||||
location = null,
|
||||
supportedClimbTypes = emptyList(),
|
||||
difficultySystems = emptyList(),
|
||||
customDifficultyGrades = null,
|
||||
notes = null,
|
||||
isDeleted = true,
|
||||
createdAt = deletedAt,
|
||||
updatedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toGym(): Gym {
|
||||
@@ -83,6 +99,7 @@ data class BackupProblem(
|
||||
val isActive: Boolean = true,
|
||||
val dateSet: String? = null,
|
||||
val notes: String? = null,
|
||||
val isDeleted: Boolean = false,
|
||||
val createdAt: String,
|
||||
val updatedAt: String,
|
||||
) {
|
||||
@@ -106,10 +123,31 @@ data class BackupProblem(
|
||||
isActive = problem.isActive,
|
||||
dateSet = problem.dateSet,
|
||||
notes = problem.notes,
|
||||
isDeleted = false,
|
||||
createdAt = problem.createdAt,
|
||||
updatedAt = problem.updatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
fun createTombstone(id: String, deletedAt: String): BackupProblem {
|
||||
return BackupProblem(
|
||||
id = id,
|
||||
gymId = "00000000-0000-0000-0000-000000000000",
|
||||
name = "DELETED",
|
||||
description = null,
|
||||
climbType = ClimbType.values().first(),
|
||||
difficulty = DifficultyGrade(DifficultySystem.values().first(), "0"),
|
||||
tags = null,
|
||||
location = null,
|
||||
imagePaths = null,
|
||||
isActive = false,
|
||||
dateSet = null,
|
||||
notes = null,
|
||||
isDeleted = true,
|
||||
createdAt = deletedAt,
|
||||
updatedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toProblem(): Problem {
|
||||
@@ -147,6 +185,7 @@ data class BackupClimbSession(
|
||||
val duration: Long? = null,
|
||||
val status: SessionStatus,
|
||||
val notes: String? = null,
|
||||
val isDeleted: Boolean = false,
|
||||
val createdAt: String,
|
||||
val updatedAt: String,
|
||||
) {
|
||||
@@ -161,10 +200,27 @@ data class BackupClimbSession(
|
||||
duration = session.duration,
|
||||
status = session.status,
|
||||
notes = session.notes,
|
||||
isDeleted = false,
|
||||
createdAt = session.createdAt,
|
||||
updatedAt = session.updatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
fun createTombstone(id: String, deletedAt: String): BackupClimbSession {
|
||||
return BackupClimbSession(
|
||||
id = id,
|
||||
gymId = "00000000-0000-0000-0000-000000000000",
|
||||
date = deletedAt,
|
||||
startTime = null,
|
||||
endTime = null,
|
||||
duration = null,
|
||||
status = SessionStatus.values().first(),
|
||||
notes = null,
|
||||
isDeleted = true,
|
||||
createdAt = deletedAt,
|
||||
updatedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toClimbSession(): ClimbSession {
|
||||
@@ -195,6 +251,7 @@ data class BackupAttempt(
|
||||
val duration: Long? = null,
|
||||
val restTime: Long? = null,
|
||||
val timestamp: String,
|
||||
val isDeleted: Boolean = false,
|
||||
val createdAt: String,
|
||||
val updatedAt: String? = null,
|
||||
) {
|
||||
@@ -210,10 +267,28 @@ data class BackupAttempt(
|
||||
duration = attempt.duration,
|
||||
restTime = attempt.restTime,
|
||||
timestamp = attempt.timestamp,
|
||||
isDeleted = false,
|
||||
createdAt = attempt.createdAt,
|
||||
updatedAt = attempt.updatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
fun createTombstone(id: String, deletedAt: String): BackupAttempt {
|
||||
return BackupAttempt(
|
||||
id = id,
|
||||
sessionId = "00000000-0000-0000-0000-000000000000",
|
||||
problemId = "00000000-0000-0000-0000-000000000000",
|
||||
result = AttemptResult.values().first(),
|
||||
highestHold = null,
|
||||
notes = null,
|
||||
duration = null,
|
||||
restTime = null,
|
||||
timestamp = deletedAt,
|
||||
isDeleted = true,
|
||||
createdAt = deletedAt,
|
||||
updatedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toAttempt(): Attempt {
|
||||
|
||||
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
|
||||
class ClimbRepository(database: AscentlyDatabase, private val context: Context) {
|
||||
private val gymDao = database.gymDao()
|
||||
@@ -38,6 +39,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
||||
|
||||
// Gym operations
|
||||
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms()
|
||||
suspend fun getAllGymsSync(): List<Gym> = gymDao.getAllGyms().first()
|
||||
suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id)
|
||||
suspend fun insertGym(gym: Gym) {
|
||||
gymDao.insertGym(gym)
|
||||
@@ -60,6 +62,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
||||
|
||||
// Problem operations
|
||||
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
|
||||
suspend fun getAllProblemsSync(): List<Problem> = problemDao.getAllProblems().first()
|
||||
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
|
||||
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
|
||||
suspend fun insertProblem(problem: Problem) {
|
||||
@@ -80,6 +83,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
||||
|
||||
// Session operations
|
||||
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
||||
suspend fun getAllSessionsSync(): List<ClimbSession> = sessionDao.getAllSessions().first()
|
||||
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
||||
sessionDao.getSessionsByGym(gymId)
|
||||
@@ -122,6 +126,8 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
||||
|
||||
// Attempt operations
|
||||
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
||||
suspend fun getAllAttemptsSync(): List<Attempt> = attemptDao.getAllAttempts().first()
|
||||
suspend fun getAttemptById(id: String): Attempt? = attemptDao.getAttemptById(id)
|
||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
|
||||
attemptDao.getAttemptsBySession(sessionId)
|
||||
|
||||
@@ -273,10 +279,9 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
||||
}
|
||||
|
||||
fun trackDeletion(itemId: String, itemType: String) {
|
||||
val currentDeletions = getDeletedItems().toMutableList()
|
||||
cleanupOldDeletions()
|
||||
val newDeletion =
|
||||
DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601())
|
||||
currentDeletions.add(newDeletion)
|
||||
|
||||
val json = json.encodeToString(newDeletion)
|
||||
deletionPreferences.edit { putString("deleted_$itemId", json) }
|
||||
@@ -304,6 +309,27 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
||||
deletionPreferences.edit { clear() }
|
||||
}
|
||||
|
||||
private fun cleanupOldDeletions() {
|
||||
val allPrefs = deletionPreferences.all
|
||||
val cutoff = Instant.now().minusSeconds(90L * 24 * 60 * 60)
|
||||
|
||||
deletionPreferences.edit {
|
||||
for ((key, value) in allPrefs) {
|
||||
if (key.startsWith("deleted_") && value is String) {
|
||||
try {
|
||||
val deletion = json.decodeFromString<DeletedItem>(value)
|
||||
val deletedAt = Instant.parse(deletion.deletedAt)
|
||||
if (deletedAt.isBefore(cutoff)) {
|
||||
remove(key)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDataIntegrity(
|
||||
gyms: List<Gym>,
|
||||
problems: List<Problem>,
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.atridad.ascently.data.format.BackupAttempt
|
||||
import com.atridad.ascently.data.format.BackupClimbSession
|
||||
import com.atridad.ascently.data.format.BackupGym
|
||||
import com.atridad.ascently.data.format.BackupProblem
|
||||
import com.atridad.ascently.data.format.DeletedItem
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** Request structure for delta sync - sends only changes since last sync */
|
||||
@@ -15,16 +14,15 @@ data class DeltaSyncRequest(
|
||||
val problems: List<BackupProblem>,
|
||||
val sessions: List<BackupClimbSession>,
|
||||
val attempts: List<BackupAttempt>,
|
||||
val deletedItems: List<DeletedItem>,
|
||||
)
|
||||
|
||||
/** Response structure for delta sync - receives only changes from server */
|
||||
@Serializable
|
||||
data class DeltaSyncResponse(
|
||||
val serverTime: String,
|
||||
val requestFullSync: Boolean = false,
|
||||
val gyms: List<BackupGym>,
|
||||
val problems: List<BackupProblem>,
|
||||
val sessions: List<BackupClimbSession>,
|
||||
val attempts: List<BackupAttempt>,
|
||||
val deletedItems: List<DeletedItem>,
|
||||
)
|
||||
|
||||
@@ -18,4 +18,6 @@ sealed class SyncException(message: String) : IOException(message), Serializable
|
||||
SyncException("Invalid server response: $details")
|
||||
|
||||
data class NetworkError(val details: String) : SyncException("Network error: $details")
|
||||
|
||||
data class General(val details: String) : SyncException(details)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.atridad.ascently.data.repository.ClimbRepository
|
||||
import com.atridad.ascently.data.state.DataStateManager
|
||||
import com.atridad.ascently.utils.AppLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,7 +28,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
||||
}
|
||||
|
||||
// Currently we only support one provider, but this allows for future expansion
|
||||
private val provider: SyncProvider = AscentlySyncProvider(context, repository)
|
||||
private val provider: SyncProvider = AscentlySyncProvider(context, repository, DataStateManager(context))
|
||||
|
||||
// State
|
||||
private val _isSyncing = MutableStateFlow(false)
|
||||
|
||||
@@ -1,11 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- Clean white background -->
|
||||
<path android:fillColor="#FFFFFF"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
</vector>
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
|
||||
@@ -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 Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@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 obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@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 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
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.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 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 Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 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
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@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" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@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 obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@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 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
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.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 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 Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 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
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@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" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
: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 = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
D28C33372F0F87D60040FE49 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D28C33312F0F87D60040FE49 /* Assets.xcassets */; };
|
||||
D28C33382F0F87D60040FE49 /* Balls.icon in Resources */ = {isa = PBXBuildFile; fileRef = D28C33322F0F87D60040FE49 /* Balls.icon */; };
|
||||
D28C33392F0F87D60040FE49 /* Icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = D28C33342F0F87D60040FE49 /* Icon.icon */; };
|
||||
D28C333B2F0F87D60040FE49 /* AscentlyShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */; };
|
||||
D28C333C2F0F87D60040FE49 /* SessionIntentSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */; };
|
||||
D28C333D2F0F87D60040FE49 /* ToggleSessionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */; };
|
||||
D28C333E2F0F87D60040FE49 /* AsyncImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */; };
|
||||
D28C333F2F0F87D60040FE49 /* CameraImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */; };
|
||||
D28C33402F0F87D60040FE49 /* PhotoOptionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */; };
|
||||
D28C33412F0F87D60040FE49 /* ActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */; };
|
||||
D28C33422F0F87D60040FE49 /* BackupFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33032F0F87D60040FE49 /* BackupFormat.swift */; };
|
||||
D28C33432F0F87D60040FE49 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33042F0F87D60040FE49 /* DataModels.swift */; };
|
||||
D28C33442F0F87D60040FE49 /* DeltaSyncFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */; };
|
||||
D28C33452F0F87D60040FE49 /* ServerSyncProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */; };
|
||||
D28C33462F0F87D60040FE49 /* SyncMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33082F0F87D60040FE49 /* SyncMerger.swift */; };
|
||||
D28C33472F0F87D60040FE49 /* SyncProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33092F0F87D60040FE49 /* SyncProvider.swift */; };
|
||||
D28C33482F0F87D60040FE49 /* HealthKitService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330B2F0F87D60040FE49 /* HealthKitService.swift */; };
|
||||
D28C33492F0F87D60040FE49 /* MusicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330C2F0F87D60040FE49 /* MusicService.swift */; };
|
||||
D28C334A2F0F87D60040FE49 /* SyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330D2F0F87D60040FE49 /* SyncService.swift */; };
|
||||
D28C334B2F0F87D60040FE49 /* AppIconHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33102F0F87D60040FE49 /* AppIconHelper.swift */; };
|
||||
D28C334C2F0F87D60040FE49 /* AppLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33112F0F87D60040FE49 /* AppLogger.swift */; };
|
||||
D28C334D2F0F87D60040FE49 /* DataStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33122F0F87D60040FE49 /* DataStateManager.swift */; };
|
||||
D28C334E2F0F87D60040FE49 /* IconTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33132F0F87D60040FE49 /* IconTestView.swift */; };
|
||||
D28C334F2F0F87D60040FE49 /* ImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33142F0F87D60040FE49 /* ImageManager.swift */; };
|
||||
D28C33502F0F87D60040FE49 /* ImageNamingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */; };
|
||||
D28C33512F0F87D60040FE49 /* OrientationAwareImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */; };
|
||||
D28C33522F0F87D60040FE49 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33172F0F87D60040FE49 /* ThemeManager.swift */; };
|
||||
D28C33532F0F87D60040FE49 /* ZipUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33182F0F87D60040FE49 /* ZipUtils.swift */; };
|
||||
D28C33542F0F87D60040FE49 /* ClimbingDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */; };
|
||||
D28C33552F0F87D60040FE49 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */; };
|
||||
D28C33562F0F87D60040FE49 /* AddAttemptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */; };
|
||||
D28C33572F0F87D60040FE49 /* AddEditGymView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */; };
|
||||
D28C33582F0F87D60040FE49 /* AddEditProblemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */; };
|
||||
D28C33592F0F87D60040FE49 /* AddEditSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */; };
|
||||
D28C335A2F0F87D60040FE49 /* GymDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33232F0F87D60040FE49 /* GymDetailView.swift */; };
|
||||
D28C335B2F0F87D60040FE49 /* ProblemDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */; };
|
||||
D28C335C2F0F87D60040FE49 /* SessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33252F0F87D60040FE49 /* SessionDetailView.swift */; };
|
||||
D28C335D2F0F87D60040FE49 /* AnalyticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33272F0F87D60040FE49 /* AnalyticsView.swift */; };
|
||||
D28C335E2F0F87D60040FE49 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33282F0F87D60040FE49 /* CalendarView.swift */; };
|
||||
D28C335F2F0F87D60040FE49 /* GymsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33292F0F87D60040FE49 /* GymsView.swift */; };
|
||||
D28C33602F0F87D60040FE49 /* LiveActivityDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */; };
|
||||
D28C33612F0F87D60040FE49 /* ProblemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332B2F0F87D60040FE49 /* ProblemsView.swift */; };
|
||||
D28C33622F0F87D60040FE49 /* SessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332C2F0F87D60040FE49 /* SessionsView.swift */; };
|
||||
D28C33632F0F87D60040FE49 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332D2F0F87D60040FE49 /* SettingsView.swift */; };
|
||||
D28C33642F0F87D60040FE49 /* AscentlyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33302F0F87D60040FE49 /* AscentlyApp.swift */; };
|
||||
D28C33652F0F87D60040FE49 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33332F0F87D60040FE49 /* ContentView.swift */; };
|
||||
D2FE94822E78E95C008CDB25 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE94802E78E958008CDB25 /* ActivityKit.framework */; };
|
||||
D2FE948D2E78FEE0008CDB25 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */; };
|
||||
D2FE948F2E78FEE0008CDB25 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948E2E78FEE0008CDB25 /* SwiftUI.framework */; };
|
||||
@@ -94,54 +48,6 @@
|
||||
/* Begin PBXFileReference section */
|
||||
D24C19682E75002A0045894C /* Ascently.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ascently.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionStatusLiveExtension.entitlements; sourceTree = "<group>"; };
|
||||
D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AscentlyShortcuts.swift; sourceTree = "<group>"; };
|
||||
D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIntentSupport.swift; sourceTree = "<group>"; };
|
||||
D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleSessionIntent.swift; sourceTree = "<group>"; };
|
||||
D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncImageView.swift; sourceTree = "<group>"; };
|
||||
D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraImagePicker.swift; sourceTree = "<group>"; };
|
||||
D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoOptionSheet.swift; sourceTree = "<group>"; };
|
||||
D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAttributes.swift; sourceTree = "<group>"; };
|
||||
D28C33032F0F87D60040FE49 /* BackupFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupFormat.swift; sourceTree = "<group>"; };
|
||||
D28C33042F0F87D60040FE49 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; };
|
||||
D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeltaSyncFormat.swift; sourceTree = "<group>"; };
|
||||
D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSyncProvider.swift; sourceTree = "<group>"; };
|
||||
D28C33082F0F87D60040FE49 /* SyncMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMerger.swift; sourceTree = "<group>"; };
|
||||
D28C33092F0F87D60040FE49 /* SyncProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncProvider.swift; sourceTree = "<group>"; };
|
||||
D28C330B2F0F87D60040FE49 /* HealthKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitService.swift; sourceTree = "<group>"; };
|
||||
D28C330C2F0F87D60040FE49 /* MusicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicService.swift; sourceTree = "<group>"; };
|
||||
D28C330D2F0F87D60040FE49 /* SyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncService.swift; sourceTree = "<group>"; };
|
||||
D28C33102F0F87D60040FE49 /* AppIconHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconHelper.swift; sourceTree = "<group>"; };
|
||||
D28C33112F0F87D60040FE49 /* AppLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogger.swift; sourceTree = "<group>"; };
|
||||
D28C33122F0F87D60040FE49 /* DataStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStateManager.swift; sourceTree = "<group>"; };
|
||||
D28C33132F0F87D60040FE49 /* IconTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconTestView.swift; sourceTree = "<group>"; };
|
||||
D28C33142F0F87D60040FE49 /* ImageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = "<group>"; };
|
||||
D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNamingUtils.swift; sourceTree = "<group>"; };
|
||||
D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationAwareImage.swift; sourceTree = "<group>"; };
|
||||
D28C33172F0F87D60040FE49 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
D28C33182F0F87D60040FE49 /* ZipUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipUtils.swift; sourceTree = "<group>"; };
|
||||
D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClimbingDataManager.swift; sourceTree = "<group>"; };
|
||||
D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
|
||||
D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAttemptView.swift; sourceTree = "<group>"; };
|
||||
D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditGymView.swift; sourceTree = "<group>"; };
|
||||
D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditProblemView.swift; sourceTree = "<group>"; };
|
||||
D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditSessionView.swift; sourceTree = "<group>"; };
|
||||
D28C33232F0F87D60040FE49 /* GymDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailView.swift; sourceTree = "<group>"; };
|
||||
D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemDetailView.swift; sourceTree = "<group>"; };
|
||||
D28C33252F0F87D60040FE49 /* SessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailView.swift; sourceTree = "<group>"; };
|
||||
D28C33272F0F87D60040FE49 /* AnalyticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsView.swift; sourceTree = "<group>"; };
|
||||
D28C33282F0F87D60040FE49 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
||||
D28C33292F0F87D60040FE49 /* GymsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymsView.swift; sourceTree = "<group>"; };
|
||||
D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityDebugView.swift; sourceTree = "<group>"; };
|
||||
D28C332B2F0F87D60040FE49 /* ProblemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemsView.swift; sourceTree = "<group>"; };
|
||||
D28C332C2F0F87D60040FE49 /* SessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsView.swift; sourceTree = "<group>"; };
|
||||
D28C332D2F0F87D60040FE49 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
D28C332F2F0F87D60040FE49 /* Ascently.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ascently.entitlements; sourceTree = "<group>"; };
|
||||
D28C33302F0F87D60040FE49 /* AscentlyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AscentlyApp.swift; sourceTree = "<group>"; };
|
||||
D28C33312F0F87D60040FE49 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D28C33322F0F87D60040FE49 /* Balls.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Balls.icon; sourceTree = "<group>"; };
|
||||
D28C33332F0F87D60040FE49 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
D28C33342F0F87D60040FE49 /* Icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Icon.icon; sourceTree = "<group>"; };
|
||||
D28C33352F0F87D60040FE49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D2F32FAD2E90B26500B1BC56 /* AscentlyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AscentlyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D2FE94802E78E958008CDB25 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; };
|
||||
D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionStatusLiveExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -150,6 +56,13 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
D28C3C8B2E75111D00F7AEE9 /* Exceptions for "Ascently" folder in "Ascently" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = D24C19672E75002A0045894C /* Ascently */;
|
||||
};
|
||||
D2FE94A42E78FEE1008CDB25 /* Exceptions for "SessionStatusLive" folder in "SessionStatusLiveExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
@@ -160,6 +73,14 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
D24C196A2E75002A0045894C /* Ascently */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
D28C3C8B2E75111D00F7AEE9 /* Exceptions for "Ascently" folder in "Ascently" target */,
|
||||
);
|
||||
path = Ascently;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = AscentlyTests;
|
||||
@@ -208,7 +129,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
|
||||
D28C33362F0F87D60040FE49 /* Ascently */,
|
||||
D24C196A2E75002A0045894C /* Ascently */,
|
||||
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
|
||||
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */,
|
||||
D2FE947F2E78E958008CDB25 /* Frameworks */,
|
||||
@@ -226,165 +147,6 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C32FC2F0F87D60040FE49 /* AppIntents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */,
|
||||
D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */,
|
||||
D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */,
|
||||
);
|
||||
path = AppIntents;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33002F0F87D60040FE49 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */,
|
||||
D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */,
|
||||
D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33012F0F87D60040FE49 /* FocusFilter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = FocusFilter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33062F0F87D60040FE49 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */,
|
||||
D28C33032F0F87D60040FE49 /* BackupFormat.swift */,
|
||||
D28C33042F0F87D60040FE49 /* DataModels.swift */,
|
||||
D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C330A2F0F87D60040FE49 /* Sync */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */,
|
||||
D28C33082F0F87D60040FE49 /* SyncMerger.swift */,
|
||||
D28C33092F0F87D60040FE49 /* SyncProvider.swift */,
|
||||
);
|
||||
path = Sync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C330E2F0F87D60040FE49 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C330A2F0F87D60040FE49 /* Sync */,
|
||||
D28C330B2F0F87D60040FE49 /* HealthKitService.swift */,
|
||||
D28C330C2F0F87D60040FE49 /* MusicService.swift */,
|
||||
D28C330D2F0F87D60040FE49 /* SyncService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C330F2F0F87D60040FE49 /* Spotlight */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Spotlight;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33192F0F87D60040FE49 /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C33102F0F87D60040FE49 /* AppIconHelper.swift */,
|
||||
D28C33112F0F87D60040FE49 /* AppLogger.swift */,
|
||||
D28C33122F0F87D60040FE49 /* DataStateManager.swift */,
|
||||
D28C33132F0F87D60040FE49 /* IconTestView.swift */,
|
||||
D28C33142F0F87D60040FE49 /* ImageManager.swift */,
|
||||
D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */,
|
||||
D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */,
|
||||
D28C33172F0F87D60040FE49 /* ThemeManager.swift */,
|
||||
D28C33182F0F87D60040FE49 /* ZipUtils.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C331C2F0F87D60040FE49 /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */,
|
||||
D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33212F0F87D60040FE49 /* AddEdit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */,
|
||||
D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */,
|
||||
D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */,
|
||||
D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */,
|
||||
);
|
||||
path = AddEdit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33222F0F87D60040FE49 /* Debug */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Debug;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33262F0F87D60040FE49 /* Detail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C33232F0F87D60040FE49 /* GymDetailView.swift */,
|
||||
D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */,
|
||||
D28C33252F0F87D60040FE49 /* SessionDetailView.swift */,
|
||||
);
|
||||
path = Detail;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C332E2F0F87D60040FE49 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C33212F0F87D60040FE49 /* AddEdit */,
|
||||
D28C33222F0F87D60040FE49 /* Debug */,
|
||||
D28C33262F0F87D60040FE49 /* Detail */,
|
||||
D28C33272F0F87D60040FE49 /* AnalyticsView.swift */,
|
||||
D28C33282F0F87D60040FE49 /* CalendarView.swift */,
|
||||
D28C33292F0F87D60040FE49 /* GymsView.swift */,
|
||||
D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */,
|
||||
D28C332B2F0F87D60040FE49 /* ProblemsView.swift */,
|
||||
D28C332C2F0F87D60040FE49 /* SessionsView.swift */,
|
||||
D28C332D2F0F87D60040FE49 /* SettingsView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D28C33362F0F87D60040FE49 /* Ascently */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D28C32FC2F0F87D60040FE49 /* AppIntents */,
|
||||
D28C33002F0F87D60040FE49 /* Components */,
|
||||
D28C33012F0F87D60040FE49 /* FocusFilter */,
|
||||
D28C33062F0F87D60040FE49 /* Models */,
|
||||
D28C330E2F0F87D60040FE49 /* Services */,
|
||||
D28C330F2F0F87D60040FE49 /* Spotlight */,
|
||||
D28C33192F0F87D60040FE49 /* Utils */,
|
||||
D28C331C2F0F87D60040FE49 /* ViewModels */,
|
||||
D28C332E2F0F87D60040FE49 /* Views */,
|
||||
D28C332F2F0F87D60040FE49 /* Ascently.entitlements */,
|
||||
D28C33302F0F87D60040FE49 /* AscentlyApp.swift */,
|
||||
D28C33312F0F87D60040FE49 /* Assets.xcassets */,
|
||||
D28C33322F0F87D60040FE49 /* Balls.icon */,
|
||||
D28C33332F0F87D60040FE49 /* ContentView.swift */,
|
||||
D28C33342F0F87D60040FE49 /* Icon.icon */,
|
||||
D28C33352F0F87D60040FE49 /* Info.plist */,
|
||||
);
|
||||
path = Ascently;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D2FE947F2E78E958008CDB25 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -412,6 +174,9 @@
|
||||
dependencies = (
|
||||
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
D24C196A2E75002A0045894C /* Ascently */,
|
||||
);
|
||||
name = Ascently;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
@@ -512,9 +277,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D28C33372F0F87D60040FE49 /* Assets.xcassets in Resources */,
|
||||
D28C33382F0F87D60040FE49 /* Balls.icon in Resources */,
|
||||
D28C33392F0F87D60040FE49 /* Icon.icon in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -539,49 +301,6 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D28C333B2F0F87D60040FE49 /* AscentlyShortcuts.swift in Sources */,
|
||||
D28C333C2F0F87D60040FE49 /* SessionIntentSupport.swift in Sources */,
|
||||
D28C333D2F0F87D60040FE49 /* ToggleSessionIntent.swift in Sources */,
|
||||
D28C333E2F0F87D60040FE49 /* AsyncImageView.swift in Sources */,
|
||||
D28C333F2F0F87D60040FE49 /* CameraImagePicker.swift in Sources */,
|
||||
D28C33402F0F87D60040FE49 /* PhotoOptionSheet.swift in Sources */,
|
||||
D28C33412F0F87D60040FE49 /* ActivityAttributes.swift in Sources */,
|
||||
D28C33422F0F87D60040FE49 /* BackupFormat.swift in Sources */,
|
||||
D28C33432F0F87D60040FE49 /* DataModels.swift in Sources */,
|
||||
D28C33442F0F87D60040FE49 /* DeltaSyncFormat.swift in Sources */,
|
||||
D28C33452F0F87D60040FE49 /* ServerSyncProvider.swift in Sources */,
|
||||
D28C33462F0F87D60040FE49 /* SyncMerger.swift in Sources */,
|
||||
D28C33472F0F87D60040FE49 /* SyncProvider.swift in Sources */,
|
||||
D28C33482F0F87D60040FE49 /* HealthKitService.swift in Sources */,
|
||||
D28C33492F0F87D60040FE49 /* MusicService.swift in Sources */,
|
||||
D28C334A2F0F87D60040FE49 /* SyncService.swift in Sources */,
|
||||
D28C334B2F0F87D60040FE49 /* AppIconHelper.swift in Sources */,
|
||||
D28C334C2F0F87D60040FE49 /* AppLogger.swift in Sources */,
|
||||
D28C334D2F0F87D60040FE49 /* DataStateManager.swift in Sources */,
|
||||
D28C334E2F0F87D60040FE49 /* IconTestView.swift in Sources */,
|
||||
D28C334F2F0F87D60040FE49 /* ImageManager.swift in Sources */,
|
||||
D28C33502F0F87D60040FE49 /* ImageNamingUtils.swift in Sources */,
|
||||
D28C33512F0F87D60040FE49 /* OrientationAwareImage.swift in Sources */,
|
||||
D28C33522F0F87D60040FE49 /* ThemeManager.swift in Sources */,
|
||||
D28C33532F0F87D60040FE49 /* ZipUtils.swift in Sources */,
|
||||
D28C33542F0F87D60040FE49 /* ClimbingDataManager.swift in Sources */,
|
||||
D28C33552F0F87D60040FE49 /* LiveActivityManager.swift in Sources */,
|
||||
D28C33562F0F87D60040FE49 /* AddAttemptView.swift in Sources */,
|
||||
D28C33572F0F87D60040FE49 /* AddEditGymView.swift in Sources */,
|
||||
D28C33582F0F87D60040FE49 /* AddEditProblemView.swift in Sources */,
|
||||
D28C33592F0F87D60040FE49 /* AddEditSessionView.swift in Sources */,
|
||||
D28C335A2F0F87D60040FE49 /* GymDetailView.swift in Sources */,
|
||||
D28C335B2F0F87D60040FE49 /* ProblemDetailView.swift in Sources */,
|
||||
D28C335C2F0F87D60040FE49 /* SessionDetailView.swift in Sources */,
|
||||
D28C335D2F0F87D60040FE49 /* AnalyticsView.swift in Sources */,
|
||||
D28C335E2F0F87D60040FE49 /* CalendarView.swift in Sources */,
|
||||
D28C335F2F0F87D60040FE49 /* GymsView.swift in Sources */,
|
||||
D28C33602F0F87D60040FE49 /* LiveActivityDebugView.swift in Sources */,
|
||||
D28C33612F0F87D60040FE49 /* ProblemsView.swift in Sources */,
|
||||
D28C33622F0F87D60040FE49 /* SessionsView.swift in Sources */,
|
||||
D28C33632F0F87D60040FE49 /* SettingsView.swift in Sources */,
|
||||
D28C33642F0F87D60040FE49 /* AscentlyApp.swift in Sources */,
|
||||
D28C33652F0F87D60040FE49 /* ContentView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -747,7 +466,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
CURRENT_PROJECT_VERSION = 44;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -756,20 +475,23 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ascently;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Ascently needs access to your photo library to save and display climbing problem images.";
|
||||
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera to take photos of climbing problems.";
|
||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs access to your photo library to add photos to climbing problems.";
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
||||
MARKETING_VERSION = 2.6.0;
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -780,7 +502,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TVOS_DEPLOYMENT_TARGET = 18.6;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
XROS_DEPLOYMENT_TARGET = 2.6;
|
||||
@@ -796,7 +518,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
CURRENT_PROJECT_VERSION = 44;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -805,20 +527,23 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ascently;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Ascently needs camera access to take photos of climbing problems.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Ascently needs access to your photo library to save and display climbing problem images.";
|
||||
INFOPLIST_KEY_NSAppleMusicUsageDescription = "This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs access to your camera to take photos of climbing problems.";
|
||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "This app needs access to save your climbing workouts to Apple Health.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs access to your photo library to add photos to climbing problems.";
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
||||
MARKETING_VERSION = 2.6.0;
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -829,7 +554,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TVOS_DEPLOYMENT_TARGET = 18.6;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
XROS_DEPLOYMENT_TARGET = 2.6;
|
||||
@@ -845,7 +570,7 @@
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.AscentlyTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.AscentlyTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
@@ -866,7 +591,7 @@
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.AscentlyTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.AscentlyTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
@@ -885,18 +610,19 @@
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
CURRENT_PROJECT_VERSION = 44;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.0;
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -915,18 +641,19 @@
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
CURRENT_PROJECT_VERSION = 44;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SessionStatusLive;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.0;
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
||||
@@ -41,7 +41,7 @@ final class SessionIntentController {
|
||||
func startSessionWithLastUsedGym() async throws -> SessionIntentSummary {
|
||||
// Wait for data to load
|
||||
if dataManager.gyms.isEmpty {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
}
|
||||
|
||||
guard let lastGym = dataManager.getLastUsedGym() else {
|
||||
@@ -49,7 +49,7 @@ final class SessionIntentController {
|
||||
throw SessionIntentError.noRecentGym
|
||||
}
|
||||
|
||||
guard let startedSession = await dataManager.startSessionAsync(gymId: lastGym.id) else {
|
||||
guard let startedSession = await dataManager.startSession(gymId: lastGym.id) else {
|
||||
logFailure(.failedToStartSession, context: "Data manager failed to create new session")
|
||||
throw SessionIntentError.failedToStartSession
|
||||
}
|
||||
@@ -68,7 +68,7 @@ final class SessionIntentController {
|
||||
throw SessionIntentError.noActiveSession
|
||||
}
|
||||
|
||||
guard let completedSession = await dataManager.endSessionAsync(activeSession.id) else {
|
||||
guard let completedSession = await dataManager.endSession(activeSession.id) else {
|
||||
logFailure(
|
||||
.failedToEndSession, context: "Data manager failed to complete active session")
|
||||
throw SessionIntentError.failedToEndSession
|
||||
@@ -97,7 +97,7 @@ final class SessionIntentController {
|
||||
func toggleSession() async throws -> (summary: SessionIntentSummary, wasStarted: Bool) {
|
||||
// Wait for data to load
|
||||
if dataManager.gyms.isEmpty {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
}
|
||||
|
||||
if dataManager.activeSession != nil {
|
||||
|
||||
@@ -20,14 +20,14 @@ struct ToggleSessionIntent: AppIntent {
|
||||
|
||||
func perform() async throws -> some IntentResult & ProvidesDialog {
|
||||
// Wait for app initialization
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
|
||||
let controller = await SessionIntentController()
|
||||
let (summary, wasStarted) = try await controller.toggleSession()
|
||||
|
||||
if wasStarted {
|
||||
// Wait for Live Activity
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
return .result(dialog: IntentDialog("Session started at \(summary.gymName). Have an awesome climb!"))
|
||||
} else {
|
||||
return .result(dialog: IntentDialog("Session at \(summary.gymName) ended. Nice work!"))
|
||||
|
||||
@@ -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 {
|
||||
// Add slight delay to ensure app is fully loaded
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
|
||||
try? await Task.sleep(for: .milliseconds(200))
|
||||
dataManager.onAppBecomeActive()
|
||||
// Re-verify health integration when app becomes active
|
||||
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||
@@ -96,7 +96,7 @@ struct ContentView: View {
|
||||
AppLogger.info(
|
||||
"App will enter foreground - preparing Live Activity check", tag: "Lifecycle")
|
||||
// Small delay to ensure app is fully active
|
||||
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
|
||||
try? await Task.sleep(for: .milliseconds(800))
|
||||
dataManager.onAppBecomeActive()
|
||||
// Re-verify health integration when returning from background
|
||||
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||
@@ -112,7 +112,7 @@ struct ContentView: View {
|
||||
Task { @MainActor in
|
||||
AppLogger.info(
|
||||
"App did become active - checking Live Activity status", tag: "Lifecycle")
|
||||
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
|
||||
try? await Task.sleep(for: .milliseconds(300))
|
||||
dataManager.onAppBecomeActive()
|
||||
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"glass" : true,
|
||||
"image-name" : "AscetlyTriangle2.png",
|
||||
"name" : "AscetlyTriangle2",
|
||||
"position" : {
|
||||
|
||||
@@ -4,17 +4,5 @@
|
||||
<dict>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs access to your photo library to add photos to climbing problems.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs access to your camera to take photos of climbing problems.</string>
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>This app needs access to save your climbing workouts to Apple Health.</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
<string>This app needs access to save your climbing workouts to Apple Health.</string>
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>This app (optionally) needs access to your music library to play your selected playlist during climbing sessions.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -20,7 +20,6 @@ struct ClimbDataBackup: Codable {
|
||||
let problems: [BackupProblem]
|
||||
let sessions: [BackupClimbSession]
|
||||
let attempts: [BackupAttempt]
|
||||
let deletedItems: [DeletedItem]
|
||||
|
||||
init(
|
||||
exportedAt: String,
|
||||
@@ -29,8 +28,7 @@ struct ClimbDataBackup: Codable {
|
||||
gyms: [BackupGym],
|
||||
problems: [BackupProblem],
|
||||
sessions: [BackupClimbSession],
|
||||
attempts: [BackupAttempt],
|
||||
deletedItems: [DeletedItem] = []
|
||||
attempts: [BackupAttempt]
|
||||
) {
|
||||
self.exportedAt = exportedAt
|
||||
self.version = version
|
||||
@@ -39,7 +37,6 @@ struct ClimbDataBackup: Codable {
|
||||
self.problems = problems
|
||||
self.sessions = sessions
|
||||
self.attempts = attempts
|
||||
self.deletedItems = deletedItems
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +49,7 @@ struct BackupGym: Codable {
|
||||
let difficultySystems: [DifficultySystem]
|
||||
let customDifficultyGrades: [String]
|
||||
let notes: String?
|
||||
let isDeleted: Bool?
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
@@ -64,6 +62,8 @@ struct BackupGym: Codable {
|
||||
self.customDifficultyGrades = gym.customDifficultyGrades
|
||||
self.notes = gym.notes
|
||||
|
||||
self.isDeleted = false // Default to false until model is updated
|
||||
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
self.createdAt = formatter.string(from: gym.createdAt)
|
||||
@@ -78,6 +78,7 @@ struct BackupGym: Codable {
|
||||
difficultySystems: [DifficultySystem],
|
||||
customDifficultyGrades: [String] = [],
|
||||
notes: String?,
|
||||
isDeleted: Bool = false,
|
||||
createdAt: String,
|
||||
updatedAt: String
|
||||
) {
|
||||
@@ -88,6 +89,7 @@ struct BackupGym: Codable {
|
||||
self.difficultySystems = difficultySystems
|
||||
self.customDifficultyGrades = customDifficultyGrades
|
||||
self.notes = notes
|
||||
self.isDeleted = isDeleted
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
@@ -115,6 +117,25 @@ struct BackupGym: Codable {
|
||||
updatedAt: updatedDate
|
||||
)
|
||||
}
|
||||
|
||||
static func createTombstone(id: String, deletedAt: Date) -> BackupGym {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let dateString = formatter.string(from: deletedAt)
|
||||
|
||||
return BackupGym(
|
||||
id: id,
|
||||
name: "DELETED",
|
||||
location: nil,
|
||||
supportedClimbTypes: [],
|
||||
difficultySystems: [],
|
||||
customDifficultyGrades: [],
|
||||
notes: nil,
|
||||
isDeleted: true,
|
||||
createdAt: dateString,
|
||||
updatedAt: dateString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-neutral problem representation for backup/restore
|
||||
@@ -131,6 +152,7 @@ struct BackupProblem: Codable {
|
||||
let isActive: Bool
|
||||
let dateSet: String? // ISO 8601 format
|
||||
let notes: String?
|
||||
let isDeleted: Bool?
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
@@ -146,6 +168,7 @@ struct BackupProblem: Codable {
|
||||
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
|
||||
self.isActive = problem.isActive
|
||||
self.notes = problem.notes
|
||||
self.isDeleted = false // Default to false until model is updated
|
||||
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
@@ -167,6 +190,7 @@ struct BackupProblem: Codable {
|
||||
isActive: Bool,
|
||||
dateSet: String?,
|
||||
notes: String?,
|
||||
isDeleted: Bool = false,
|
||||
createdAt: String,
|
||||
updatedAt: String
|
||||
) {
|
||||
@@ -182,6 +206,7 @@ struct BackupProblem: Codable {
|
||||
self.isActive = isActive
|
||||
self.dateSet = dateSet
|
||||
self.notes = notes
|
||||
self.isDeleted = isDeleted
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
@@ -232,10 +257,35 @@ struct BackupProblem: Codable {
|
||||
isActive: self.isActive,
|
||||
dateSet: self.dateSet,
|
||||
notes: self.notes,
|
||||
isDeleted: self.isDeleted ?? false,
|
||||
createdAt: self.createdAt,
|
||||
updatedAt: self.updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
static func createTombstone(id: String, gymId: String = UUID().uuidString, deletedAt: Date) -> BackupProblem {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let dateString = formatter.string(from: deletedAt)
|
||||
|
||||
return BackupProblem(
|
||||
id: id,
|
||||
gymId: gymId,
|
||||
name: "DELETED",
|
||||
description: nil,
|
||||
climbType: ClimbType.allCases.first!,
|
||||
difficulty: DifficultyGrade(system: DifficultySystem.allCases.first!, grade: "0"),
|
||||
tags: [],
|
||||
location: nil,
|
||||
imagePaths: nil,
|
||||
isActive: false,
|
||||
dateSet: nil,
|
||||
notes: nil,
|
||||
isDeleted: true,
|
||||
createdAt: dateString,
|
||||
updatedAt: dateString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-neutral climb session representation for backup/restore
|
||||
@@ -248,6 +298,7 @@ struct BackupClimbSession: Codable {
|
||||
let duration: Int64? // Duration in seconds
|
||||
let status: SessionStatus
|
||||
let notes: String?
|
||||
let isDeleted: Bool?
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
@@ -256,6 +307,7 @@ struct BackupClimbSession: Codable {
|
||||
self.gymId = session.gymId.uuidString
|
||||
self.status = session.status
|
||||
self.notes = session.notes
|
||||
self.isDeleted = false // Default to false until model is updated
|
||||
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
@@ -276,6 +328,7 @@ struct BackupClimbSession: Codable {
|
||||
duration: Int64?,
|
||||
status: SessionStatus,
|
||||
notes: String?,
|
||||
isDeleted: Bool = false,
|
||||
createdAt: String,
|
||||
updatedAt: String
|
||||
) {
|
||||
@@ -287,6 +340,7 @@ struct BackupClimbSession: Codable {
|
||||
self.duration = duration
|
||||
self.status = status
|
||||
self.notes = notes
|
||||
self.isDeleted = isDeleted
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
@@ -321,6 +375,26 @@ struct BackupClimbSession: Codable {
|
||||
updatedAt: updatedDate
|
||||
)
|
||||
}
|
||||
|
||||
static func createTombstone(id: String, gymId: String = UUID().uuidString, deletedAt: Date) -> BackupClimbSession {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let dateString = formatter.string(from: deletedAt)
|
||||
|
||||
return BackupClimbSession(
|
||||
id: id,
|
||||
gymId: gymId,
|
||||
date: dateString,
|
||||
startTime: nil,
|
||||
endTime: nil,
|
||||
duration: nil,
|
||||
status: .completed,
|
||||
notes: nil,
|
||||
isDeleted: true,
|
||||
createdAt: dateString,
|
||||
updatedAt: dateString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-neutral attempt representation for backup/restore
|
||||
@@ -334,6 +408,7 @@ struct BackupAttempt: Codable {
|
||||
let duration: Int64? // Duration in seconds
|
||||
let restTime: Int64? // Rest time in seconds
|
||||
let timestamp: String
|
||||
let isDeleted: Bool?
|
||||
let createdAt: String
|
||||
let updatedAt: String?
|
||||
|
||||
@@ -346,6 +421,7 @@ struct BackupAttempt: Codable {
|
||||
self.notes = attempt.notes
|
||||
self.duration = attempt.duration.map { Int64($0) }
|
||||
self.restTime = attempt.restTime.map { Int64($0) }
|
||||
self.isDeleted = false // Default to false until model is updated
|
||||
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
@@ -364,6 +440,7 @@ struct BackupAttempt: Codable {
|
||||
duration: Int64?,
|
||||
restTime: Int64?,
|
||||
timestamp: String,
|
||||
isDeleted: Bool = false,
|
||||
createdAt: String,
|
||||
updatedAt: String?
|
||||
) {
|
||||
@@ -376,6 +453,7 @@ struct BackupAttempt: Codable {
|
||||
self.duration = duration
|
||||
self.restTime = restTime
|
||||
self.timestamp = timestamp
|
||||
self.isDeleted = isDeleted
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
@@ -412,6 +490,27 @@ struct BackupAttempt: Codable {
|
||||
updatedAt: updatedDate
|
||||
)
|
||||
}
|
||||
|
||||
static func createTombstone(id: String, sessionId: String = UUID().uuidString, problemId: String = UUID().uuidString, deletedAt: Date) -> BackupAttempt {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let dateString = formatter.string(from: deletedAt)
|
||||
|
||||
return BackupAttempt(
|
||||
id: id,
|
||||
sessionId: sessionId,
|
||||
problemId: problemId,
|
||||
result: AttemptResult.allCases.first!,
|
||||
highestHold: nil,
|
||||
notes: nil,
|
||||
duration: nil,
|
||||
restTime: nil,
|
||||
timestamp: dateString,
|
||||
isDeleted: true,
|
||||
createdAt: dateString,
|
||||
updatedAt: dateString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Backup Format Errors
|
||||
|
||||
@@ -13,14 +13,13 @@ struct DeltaSyncRequest: Codable {
|
||||
let problems: [BackupProblem]
|
||||
let sessions: [BackupClimbSession]
|
||||
let attempts: [BackupAttempt]
|
||||
let deletedItems: [DeletedItem]
|
||||
}
|
||||
|
||||
struct DeltaSyncResponse: Codable {
|
||||
let serverTime: String
|
||||
let requestFullSync: Bool?
|
||||
let gyms: [BackupGym]
|
||||
let problems: [BackupProblem]
|
||||
let sessions: [BackupClimbSession]
|
||||
let attempts: [BackupAttempt]
|
||||
let deletedItems: [DeletedItem]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import SwiftUI
|
||||
@MainActor
|
||||
class MusicService: ObservableObject {
|
||||
static let shared = MusicService()
|
||||
|
||||
|
||||
@Published var isAuthorized = false
|
||||
@Published var playlists: MusicItemCollection<Playlist> = []
|
||||
@Published var selectedPlaylistId: String? {
|
||||
@@ -33,60 +33,55 @@ class MusicService: ObservableObject {
|
||||
}
|
||||
}
|
||||
@Published var isPlaying = false
|
||||
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var hasStartedSessionPlayback = false
|
||||
private var currentPlaylistTrackIds: Set<MusicItemID> = []
|
||||
|
||||
|
||||
private init() {
|
||||
self.selectedPlaylistId = UserDefaults.standard.string(forKey: "ascently_selected_playlist_id")
|
||||
self.isMusicEnabled = UserDefaults.standard.bool(forKey: "ascently_music_enabled")
|
||||
self.isAutoPlayEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autoplay_enabled")
|
||||
self.isAutoStopEnabled = UserDefaults.standard.bool(forKey: "ascently_music_autostop_enabled")
|
||||
|
||||
|
||||
if isMusicEnabled {
|
||||
Task {
|
||||
await checkAuthorizationStatus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
|
||||
private func setupObservers() {
|
||||
SystemMusicPlayer.shared.state.objectWillChange
|
||||
.sink { [weak self] _ in
|
||||
self?.updatePlaybackStatus()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
SystemMusicPlayer.shared.queue.objectWillChange
|
||||
.sink { [weak self] _ in
|
||||
self?.checkQueueConsistency()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
private func updatePlaybackStatus() {
|
||||
Task { @MainActor [weak self] in
|
||||
self?.isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
|
||||
}
|
||||
isPlaying = SystemMusicPlayer.shared.state.playbackStatus == .playing
|
||||
}
|
||||
|
||||
|
||||
private func checkQueueConsistency() {
|
||||
guard hasStartedSessionPlayback else { return }
|
||||
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self = self else { return }
|
||||
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
|
||||
let item = currentEntry.item {
|
||||
if !self.currentPlaylistTrackIds.isEmpty && !self.currentPlaylistTrackIds.contains(item.id) {
|
||||
self.hasStartedSessionPlayback = false
|
||||
}
|
||||
|
||||
if let currentEntry = SystemMusicPlayer.shared.queue.currentEntry,
|
||||
let item = currentEntry.item {
|
||||
if !currentPlaylistTrackIds.isEmpty && !currentPlaylistTrackIds.contains(item.id) {
|
||||
hasStartedSessionPlayback = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func toggleMusicEnabled(_ enabled: Bool) {
|
||||
isMusicEnabled = enabled
|
||||
if enabled {
|
||||
@@ -95,7 +90,7 @@ class MusicService: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func checkAuthorizationStatus() async {
|
||||
let status = await MusicAuthorization.request()
|
||||
self.isAuthorized = status == .authorized
|
||||
@@ -103,7 +98,7 @@ class MusicService: ObservableObject {
|
||||
await fetchPlaylists()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fetchPlaylists() async {
|
||||
guard isAuthorized else { return }
|
||||
do {
|
||||
@@ -115,20 +110,20 @@ class MusicService: ObservableObject {
|
||||
print("Error fetching playlists: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func playSelectedPlaylistIfHeadphonesConnected() {
|
||||
guard isMusicEnabled, isAutoPlayEnabled, let playlistId = selectedPlaylistId else { return }
|
||||
|
||||
|
||||
if isHeadphonesConnected() {
|
||||
playPlaylist(id: playlistId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func resetSessionPlaybackState() {
|
||||
hasStartedSessionPlayback = false
|
||||
currentPlaylistTrackIds.removeAll()
|
||||
}
|
||||
|
||||
|
||||
func playPlaylist(id: String) {
|
||||
print("Attempting to play playlist \(id)")
|
||||
Task {
|
||||
@@ -136,9 +131,9 @@ class MusicService: ObservableObject {
|
||||
if playlists.isEmpty {
|
||||
await fetchPlaylists()
|
||||
}
|
||||
|
||||
|
||||
var targetPlaylist: Playlist?
|
||||
|
||||
|
||||
if let playlist = playlists.first(where: { $0.id.rawValue == id }) {
|
||||
targetPlaylist = playlist
|
||||
} else {
|
||||
@@ -147,13 +142,13 @@ class MusicService: ObservableObject {
|
||||
let response = try await request.response()
|
||||
targetPlaylist = response.items.first
|
||||
}
|
||||
|
||||
|
||||
if let playlist = targetPlaylist {
|
||||
let detailedPlaylist = try await playlist.with([.tracks])
|
||||
if let tracks = detailedPlaylist.tracks {
|
||||
self.currentPlaylistTrackIds = Set(tracks.map { $0.id })
|
||||
}
|
||||
|
||||
|
||||
SystemMusicPlayer.shared.queue = [playlist]
|
||||
try await SystemMusicPlayer.shared.play()
|
||||
hasStartedSessionPlayback = true
|
||||
@@ -163,12 +158,12 @@ class MusicService: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func stopPlaybackIfEnabled() {
|
||||
guard isMusicEnabled, isAutoStopEnabled else { return }
|
||||
SystemMusicPlayer.shared.stop()
|
||||
}
|
||||
|
||||
|
||||
func togglePlayback() {
|
||||
Task {
|
||||
if isPlaying {
|
||||
@@ -182,7 +177,7 @@ class MusicService: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func isHeadphonesConnected() -> Bool {
|
||||
let route = AVAudioSession.sharedInstance().currentRoute
|
||||
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 isTesting = false
|
||||
@Published var isOfflineMode = false
|
||||
|
||||
|
||||
@Published var providerType: SyncProviderType = .server {
|
||||
didSet {
|
||||
updateActiveProvider()
|
||||
@@ -23,8 +23,6 @@ class SyncService: ObservableObject {
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let logTag = "SyncService"
|
||||
private var syncTask: Task<Void, Never>?
|
||||
private var pendingChanges = false
|
||||
private let syncDebounceDelay: TimeInterval = 2.0
|
||||
|
||||
private enum Keys {
|
||||
static let serverURL = "sync_server_url"
|
||||
@@ -39,7 +37,7 @@ class SyncService: ObservableObject {
|
||||
// Legacy properties for compatibility with SettingsView
|
||||
var serverURL: String {
|
||||
get { userDefaults.string(forKey: Keys.serverURL) ?? "" }
|
||||
set {
|
||||
set {
|
||||
userDefaults.set(newValue, forKey: Keys.serverURL)
|
||||
// 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)
|
||||
isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true
|
||||
isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode)
|
||||
|
||||
|
||||
if let savedType = userDefaults.string(forKey: Keys.providerType),
|
||||
let type = SyncProviderType(rawValue: savedType) {
|
||||
self.providerType = type
|
||||
} else {
|
||||
self.providerType = .server // Default
|
||||
}
|
||||
|
||||
|
||||
updateActiveProvider()
|
||||
}
|
||||
|
||||
|
||||
private func updateActiveProvider() {
|
||||
switch providerType {
|
||||
case .server:
|
||||
activeProvider = ServerSyncProvider()
|
||||
case .iCloud:
|
||||
// Placeholder for iCloud provider
|
||||
activeProvider = nil
|
||||
activeProvider = nil
|
||||
case .none:
|
||||
activeProvider = nil
|
||||
}
|
||||
|
||||
|
||||
// Update status based on new provider
|
||||
if let provider = activeProvider {
|
||||
isConnected = provider.isConnected
|
||||
@@ -101,7 +99,7 @@ class SyncService: ObservableObject {
|
||||
AppLogger.info("Sync skipped: Offline mode is enabled.", tag: logTag)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let provider = activeProvider else {
|
||||
if providerType == .none {
|
||||
return
|
||||
@@ -127,7 +125,7 @@ class SyncService: ObservableObject {
|
||||
|
||||
do {
|
||||
try await provider.sync(dataManager: dataManager)
|
||||
|
||||
|
||||
// Update last sync time
|
||||
// Provider might have updated it in UserDefaults, reload it
|
||||
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)
|
||||
throw SyncError.notConfigured
|
||||
}
|
||||
|
||||
|
||||
isTesting = true
|
||||
defer { isTesting = false }
|
||||
|
||||
|
||||
try await provider.testConnection()
|
||||
|
||||
|
||||
isConnected = provider.isConnected
|
||||
userDefaults.set(isConnected, forKey: Keys.isConnected)
|
||||
}
|
||||
@@ -162,34 +160,19 @@ class SyncService: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if isSyncing {
|
||||
pendingChanges = true
|
||||
return
|
||||
}
|
||||
guard !isSyncing else { return }
|
||||
|
||||
syncTask?.cancel()
|
||||
|
||||
syncTask = Task {
|
||||
try? await Task.sleep(nanoseconds: UInt64(syncDebounceDelay * 1_000_000_000))
|
||||
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
repeat {
|
||||
pendingChanges = false
|
||||
|
||||
do {
|
||||
try await syncWithServer(dataManager: dataManager)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.isSyncing = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if pendingChanges {
|
||||
try? await Task.sleep(nanoseconds: UInt64(syncDebounceDelay * 1_000_000_000))
|
||||
}
|
||||
} while pendingChanges && !Task.isCancelled
|
||||
do {
|
||||
try await syncWithServer(dataManager: dataManager)
|
||||
} catch {
|
||||
self.isSyncing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,30 +181,26 @@ class SyncService: ObservableObject {
|
||||
|
||||
syncTask?.cancel()
|
||||
syncTask = nil
|
||||
pendingChanges = false
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await syncWithServer(dataManager: dataManager)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.isSyncing = false
|
||||
}
|
||||
self.isSyncing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
activeProvider?.disconnect()
|
||||
|
||||
|
||||
syncTask?.cancel()
|
||||
syncTask = nil
|
||||
pendingChanges = false
|
||||
isSyncing = false
|
||||
isConnected = false
|
||||
lastSyncTime = nil
|
||||
syncError = nil
|
||||
|
||||
|
||||
// These are shared keys, so clearing them affects all providers if they use them
|
||||
// But disconnect() is usually user initiated action
|
||||
userDefaults.set(false, forKey: Keys.isConnected)
|
||||
@@ -239,8 +218,7 @@ class SyncService: ObservableObject {
|
||||
userDefaults.removeObject(forKey: Keys.autoSyncEnabled)
|
||||
syncTask?.cancel()
|
||||
syncTask = nil
|
||||
pendingChanges = false
|
||||
|
||||
|
||||
activeProvider?.disconnect()
|
||||
}
|
||||
|
||||
|
||||