Compare commits

..

8 Commits

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

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

186
android/gradlew.bat vendored
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

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

Before

Width:  |  Height:  |  Size: 411 B

View File

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

Before

Width:  |  Height:  |  Size: 411 B

View File

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

Before

Width:  |  Height:  |  Size: 443 B

View File

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

Before

Width:  |  Height:  |  Size: 254 B

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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()
}

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