Compare commits

..

2 Commits

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

2
.gitattributes vendored
View File

@@ -75,5 +75,3 @@ pnpm-lock.yaml text -diff
# Documentation # Documentation
LICENSE text eol=lf LICENSE text eol=lf
README.md text eol=lf README.md text eol=lf
*.pxd linguist-vendored

View File

@@ -1,7 +1,5 @@
# Ascently # Ascently
<img src="https://git.atri.dad/atridad/Ascently/raw/branch/main/docs/src/assets/logo.png" alt="Ascently Logo" width="250" height="250">
_Formerly OpenClimb_ _Formerly OpenClimb_
Ascently is an **offline-first FOSS** app designed to help climbers track their sessions, routes/problems, and overall progress. There is an optional self-hosted sync server and integrations with Apple Health and Health Connect. There are no analytics or tracking baked into any part of this project. I am committed to maintaining a transparent and open-source solution for climbers, ensuring that you have full control over your data and privacy. Ascently is an **offline-first FOSS** app designed to help climbers track their sessions, routes/problems, and overall progress. There is an optional self-hosted sync server and integrations with Apple Health and Health Connect. There are no analytics or tracking baked into any part of this project. I am committed to maintaining a transparent and open-source solution for climbers, ensuring that you have full control over your data and privacy.

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,74 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp" android:width="108dp"
android:viewportHeight="108" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="108">
<path android:fillColor="#3DDC84"
<!-- Clean white background -->
<path android:fillColor="#FFFFFF"
android:pathData="M0,0h108v108h-108z"/> android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector> </vector>

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

@@ -10,19 +10,19 @@ androidxTestExt = "1.3.0"
androidxTestRunner = "1.7.0" androidxTestRunner = "1.7.0"
androidxTestRules = "1.7.0" androidxTestRules = "1.7.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.3" activityCompose = "1.12.2"
composeBom = "2026.01.01" composeBom = "2025.12.01"
room = "2.8.4" room = "2.8.4"
navigation = "2.9.7" navigation = "2.9.6"
viewmodel = "2.10.0" viewmodel = "2.10.0"
kotlinxSerialization = "1.10.0" kotlinxSerialization = "1.9.0"
kotlinxCoroutines = "1.10.2" kotlinxCoroutines = "1.10.2"
coil = "2.7.0" coil = "2.7.0"
ksp = "2.2.20-2.0.3" ksp = "2.2.20-2.0.3"
exifinterface = "1.4.2" exifinterface = "1.4.2"
healthConnect = "1.1.0" healthConnect = "1.1.0"
detekt = "1.23.8" detekt = "1.23.8"
spotless = "8.2.1" spotless = "8.1.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

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

View File

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

After

Width:  |  Height:  |  Size: 411 B

View File

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

After

Width:  |  Height:  |  Size: 411 B

View File

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

After

Width:  |  Height:  |  Size: 443 B

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

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

After

Width:  |  Height:  |  Size: 254 B

View File

@@ -14,8 +14,8 @@ export default defineConfig({
description: description:
"An offline-first FOSS climb tracking app with an optional sync server.", "An offline-first FOSS climb tracking app with an optional sync server.",
logo: { logo: {
light: "./src/assets/logo.png", light: "./src/assets/logo.svg",
dark: "./src/assets/logo.png", dark: "./src/assets/logo-dark.svg",
}, },
favicon: "/favicon.png", favicon: "/favicon.png",
social: [ social: [
@@ -55,14 +55,4 @@ export default defineConfig({
adapter: node({ adapter: node({
mode: "standalone", mode: "standalone",
}), }),
output: "server",
build: {
inlineStylesheets: "always",
},
experimental: {
svgo: true,
},
}); });

View File

@@ -25,9 +25,9 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^9.5.2", "@astrojs/node": "^9.5.1",
"@astrojs/starlight": "^0.37.5", "@astrojs/starlight": "^0.37.1",
"astro": "^5.17.1", "astro": "^5.16.5",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"sharp": "^0.34.5" "sharp": "^0.34.5"
}, },

789
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

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