Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
77f7092287
|
|||
|
ed25cf7ecd
|
|||
|
255f85c2df
|
|||
|
a3d47d29c5
|
|||
|
b94b823986
|
|||
|
58d84af29b
|
|||
|
12f9463e8c
|
|||
|
aa3ddfc7cb
|
|||
|
25688b0615
|
|||
|
3874703fcb
|
|||
| aa08892e75 | |||
|
4da10912fc
|
|||
|
94d2f9d951
|
|||
|
6e679236c8
|
|||
|
06fe659478
|
|||
|
390b4bf499
|
|||
|
394789d609
|
|||
|
94566eabf6
|
|||
|
c020287d1f
|
|||
|
98589645e6
|
|||
|
33610a5959
|
|||
|
20058e9ac0
|
|||
|
e4d6e6fb7e
|
|||
|
d97a5f36ea
|
|||
| 1a85dab6ae | |||
|
2d5382ba28
|
|||
|
05c0430b40
|
|||
|
f4f4968431
|
|||
|
d002c703d5
|
|||
|
afb0456692
|
|||
|
74db155d93
|
|||
|
ec63d7c58f
|
|||
|
1c47dd93b0
|
|||
|
ef05727cde
|
|||
|
452fd96372
|
2
.gitattributes
vendored
@@ -75,3 +75,5 @@ 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
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# 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.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
This is the native Android app for Ascently, built with Kotlin and Jetpack Compose.
|
This is the native Android app for Ascently, built with Kotlin and Jetpack Compose.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
This is a standard Android Gradle project. The main code lives in `app/src/main/java/com/atridad/ascently/`.
|
This is a standard Android Gradle project. The main code lives in `app/src/main/java/com/atridad/ascently/`.
|
||||||
|
|
||||||
- `data/`: Handles all the app's data.
|
- `data/`: Handles all the app's data.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId = "com.atridad.ascently"
|
applicationId = "com.atridad.ascently"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 50
|
versionCode = 52
|
||||||
versionName = "2.5.0"
|
versionName = "2.5.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
@@ -13,7 +13,6 @@ data class ClimbDataBackup(
|
|||||||
val problems: List<BackupProblem>,
|
val problems: List<BackupProblem>,
|
||||||
val sessions: List<BackupClimbSession>,
|
val sessions: List<BackupClimbSession>,
|
||||||
val attempts: List<BackupAttempt>,
|
val attempts: List<BackupAttempt>,
|
||||||
val deletedItems: List<DeletedItem> = emptyList(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -34,6 +33,7 @@ data class BackupGym(
|
|||||||
@kotlinx.serialization.SerialName("customDifficultyGrades")
|
@kotlinx.serialization.SerialName("customDifficultyGrades")
|
||||||
val customDifficultyGrades: List<String>? = null,
|
val customDifficultyGrades: List<String>? = null,
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
|
val isDeleted: Boolean = false,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String,
|
val updatedAt: String,
|
||||||
) {
|
) {
|
||||||
@@ -47,10 +47,26 @@ data class BackupGym(
|
|||||||
difficultySystems = gym.difficultySystems,
|
difficultySystems = gym.difficultySystems,
|
||||||
customDifficultyGrades = gym.customDifficultyGrades.ifEmpty { null },
|
customDifficultyGrades = gym.customDifficultyGrades.ifEmpty { null },
|
||||||
notes = gym.notes,
|
notes = gym.notes,
|
||||||
|
isDeleted = false,
|
||||||
createdAt = gym.createdAt,
|
createdAt = gym.createdAt,
|
||||||
updatedAt = gym.updatedAt,
|
updatedAt = gym.updatedAt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createTombstone(id: String, deletedAt: String): BackupGym {
|
||||||
|
return BackupGym(
|
||||||
|
id = id,
|
||||||
|
name = "DELETED",
|
||||||
|
location = null,
|
||||||
|
supportedClimbTypes = emptyList(),
|
||||||
|
difficultySystems = emptyList(),
|
||||||
|
customDifficultyGrades = null,
|
||||||
|
notes = null,
|
||||||
|
isDeleted = true,
|
||||||
|
createdAt = deletedAt,
|
||||||
|
updatedAt = deletedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toGym(): Gym {
|
fun toGym(): Gym {
|
||||||
@@ -83,6 +99,7 @@ data class BackupProblem(
|
|||||||
val isActive: Boolean = true,
|
val isActive: Boolean = true,
|
||||||
val dateSet: String? = null,
|
val dateSet: String? = null,
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
|
val isDeleted: Boolean = false,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String,
|
val updatedAt: String,
|
||||||
) {
|
) {
|
||||||
@@ -106,10 +123,31 @@ data class BackupProblem(
|
|||||||
isActive = problem.isActive,
|
isActive = problem.isActive,
|
||||||
dateSet = problem.dateSet,
|
dateSet = problem.dateSet,
|
||||||
notes = problem.notes,
|
notes = problem.notes,
|
||||||
|
isDeleted = false,
|
||||||
createdAt = problem.createdAt,
|
createdAt = problem.createdAt,
|
||||||
updatedAt = problem.updatedAt,
|
updatedAt = problem.updatedAt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createTombstone(id: String, deletedAt: String): BackupProblem {
|
||||||
|
return BackupProblem(
|
||||||
|
id = id,
|
||||||
|
gymId = "00000000-0000-0000-0000-000000000000",
|
||||||
|
name = "DELETED",
|
||||||
|
description = null,
|
||||||
|
climbType = ClimbType.values().first(),
|
||||||
|
difficulty = DifficultyGrade(DifficultySystem.values().first(), "0"),
|
||||||
|
tags = null,
|
||||||
|
location = null,
|
||||||
|
imagePaths = null,
|
||||||
|
isActive = false,
|
||||||
|
dateSet = null,
|
||||||
|
notes = null,
|
||||||
|
isDeleted = true,
|
||||||
|
createdAt = deletedAt,
|
||||||
|
updatedAt = deletedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toProblem(): Problem {
|
fun toProblem(): Problem {
|
||||||
@@ -147,6 +185,7 @@ data class BackupClimbSession(
|
|||||||
val duration: Long? = null,
|
val duration: Long? = null,
|
||||||
val status: SessionStatus,
|
val status: SessionStatus,
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
|
val isDeleted: Boolean = false,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String,
|
val updatedAt: String,
|
||||||
) {
|
) {
|
||||||
@@ -161,10 +200,27 @@ data class BackupClimbSession(
|
|||||||
duration = session.duration,
|
duration = session.duration,
|
||||||
status = session.status,
|
status = session.status,
|
||||||
notes = session.notes,
|
notes = session.notes,
|
||||||
|
isDeleted = false,
|
||||||
createdAt = session.createdAt,
|
createdAt = session.createdAt,
|
||||||
updatedAt = session.updatedAt,
|
updatedAt = session.updatedAt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createTombstone(id: String, deletedAt: String): BackupClimbSession {
|
||||||
|
return BackupClimbSession(
|
||||||
|
id = id,
|
||||||
|
gymId = "00000000-0000-0000-0000-000000000000",
|
||||||
|
date = deletedAt,
|
||||||
|
startTime = null,
|
||||||
|
endTime = null,
|
||||||
|
duration = null,
|
||||||
|
status = SessionStatus.values().first(),
|
||||||
|
notes = null,
|
||||||
|
isDeleted = true,
|
||||||
|
createdAt = deletedAt,
|
||||||
|
updatedAt = deletedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toClimbSession(): ClimbSession {
|
fun toClimbSession(): ClimbSession {
|
||||||
@@ -195,6 +251,7 @@ data class BackupAttempt(
|
|||||||
val duration: Long? = null,
|
val duration: Long? = null,
|
||||||
val restTime: Long? = null,
|
val restTime: Long? = null,
|
||||||
val timestamp: String,
|
val timestamp: String,
|
||||||
|
val isDeleted: Boolean = false,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String? = null,
|
val updatedAt: String? = null,
|
||||||
) {
|
) {
|
||||||
@@ -210,10 +267,28 @@ data class BackupAttempt(
|
|||||||
duration = attempt.duration,
|
duration = attempt.duration,
|
||||||
restTime = attempt.restTime,
|
restTime = attempt.restTime,
|
||||||
timestamp = attempt.timestamp,
|
timestamp = attempt.timestamp,
|
||||||
|
isDeleted = false,
|
||||||
createdAt = attempt.createdAt,
|
createdAt = attempt.createdAt,
|
||||||
updatedAt = attempt.updatedAt,
|
updatedAt = attempt.updatedAt,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createTombstone(id: String, deletedAt: String): BackupAttempt {
|
||||||
|
return BackupAttempt(
|
||||||
|
id = id,
|
||||||
|
sessionId = "00000000-0000-0000-0000-000000000000",
|
||||||
|
problemId = "00000000-0000-0000-0000-000000000000",
|
||||||
|
result = AttemptResult.values().first(),
|
||||||
|
highestHold = null,
|
||||||
|
notes = null,
|
||||||
|
duration = null,
|
||||||
|
restTime = null,
|
||||||
|
timestamp = deletedAt,
|
||||||
|
isDeleted = true,
|
||||||
|
createdAt = deletedAt,
|
||||||
|
updatedAt = deletedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toAttempt(): Attempt {
|
fun toAttempt(): Attempt {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
class ClimbRepository(database: AscentlyDatabase, private val context: Context) {
|
class ClimbRepository(database: AscentlyDatabase, private val context: Context) {
|
||||||
private val gymDao = database.gymDao()
|
private val gymDao = database.gymDao()
|
||||||
@@ -38,6 +39,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
|||||||
|
|
||||||
// Gym operations
|
// Gym operations
|
||||||
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms()
|
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms()
|
||||||
|
suspend fun getAllGymsSync(): List<Gym> = gymDao.getAllGyms().first()
|
||||||
suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id)
|
suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id)
|
||||||
suspend fun insertGym(gym: Gym) {
|
suspend fun insertGym(gym: Gym) {
|
||||||
gymDao.insertGym(gym)
|
gymDao.insertGym(gym)
|
||||||
@@ -60,6 +62,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
|||||||
|
|
||||||
// Problem operations
|
// Problem operations
|
||||||
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
|
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
|
||||||
|
suspend fun getAllProblemsSync(): List<Problem> = problemDao.getAllProblems().first()
|
||||||
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
|
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
|
||||||
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
|
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
|
||||||
suspend fun insertProblem(problem: Problem) {
|
suspend fun insertProblem(problem: Problem) {
|
||||||
@@ -80,6 +83,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
|||||||
|
|
||||||
// Session operations
|
// Session operations
|
||||||
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
||||||
|
suspend fun getAllSessionsSync(): List<ClimbSession> = sessionDao.getAllSessions().first()
|
||||||
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
||||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
||||||
sessionDao.getSessionsByGym(gymId)
|
sessionDao.getSessionsByGym(gymId)
|
||||||
@@ -122,6 +126,8 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
|||||||
|
|
||||||
// Attempt operations
|
// Attempt operations
|
||||||
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
||||||
|
suspend fun getAllAttemptsSync(): List<Attempt> = attemptDao.getAllAttempts().first()
|
||||||
|
suspend fun getAttemptById(id: String): Attempt? = attemptDao.getAttemptById(id)
|
||||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
|
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
|
||||||
attemptDao.getAttemptsBySession(sessionId)
|
attemptDao.getAttemptsBySession(sessionId)
|
||||||
|
|
||||||
@@ -273,10 +279,9 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun trackDeletion(itemId: String, itemType: String) {
|
fun trackDeletion(itemId: String, itemType: String) {
|
||||||
val currentDeletions = getDeletedItems().toMutableList()
|
cleanupOldDeletions()
|
||||||
val newDeletion =
|
val newDeletion =
|
||||||
DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601())
|
DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601())
|
||||||
currentDeletions.add(newDeletion)
|
|
||||||
|
|
||||||
val json = json.encodeToString(newDeletion)
|
val json = json.encodeToString(newDeletion)
|
||||||
deletionPreferences.edit { putString("deleted_$itemId", json) }
|
deletionPreferences.edit { putString("deleted_$itemId", json) }
|
||||||
@@ -304,6 +309,27 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context)
|
|||||||
deletionPreferences.edit { clear() }
|
deletionPreferences.edit { clear() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanupOldDeletions() {
|
||||||
|
val allPrefs = deletionPreferences.all
|
||||||
|
val cutoff = Instant.now().minusSeconds(90L * 24 * 60 * 60)
|
||||||
|
|
||||||
|
deletionPreferences.edit {
|
||||||
|
for ((key, value) in allPrefs) {
|
||||||
|
if (key.startsWith("deleted_") && value is String) {
|
||||||
|
try {
|
||||||
|
val deletion = json.decodeFromString<DeletedItem>(value)
|
||||||
|
val deletedAt = Instant.parse(deletion.deletedAt)
|
||||||
|
if (deletedAt.isBefore(cutoff)) {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun validateDataIntegrity(
|
private fun validateDataIntegrity(
|
||||||
gyms: List<Gym>,
|
gyms: List<Gym>,
|
||||||
problems: List<Problem>,
|
problems: List<Problem>,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.atridad.ascently.data.format.BackupAttempt
|
|||||||
import com.atridad.ascently.data.format.BackupClimbSession
|
import com.atridad.ascently.data.format.BackupClimbSession
|
||||||
import com.atridad.ascently.data.format.BackupGym
|
import com.atridad.ascently.data.format.BackupGym
|
||||||
import com.atridad.ascently.data.format.BackupProblem
|
import com.atridad.ascently.data.format.BackupProblem
|
||||||
import com.atridad.ascently.data.format.DeletedItem
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/** Request structure for delta sync - sends only changes since last sync */
|
/** Request structure for delta sync - sends only changes since last sync */
|
||||||
@@ -15,16 +14,15 @@ data class DeltaSyncRequest(
|
|||||||
val problems: List<BackupProblem>,
|
val problems: List<BackupProblem>,
|
||||||
val sessions: List<BackupClimbSession>,
|
val sessions: List<BackupClimbSession>,
|
||||||
val attempts: List<BackupAttempt>,
|
val attempts: List<BackupAttempt>,
|
||||||
val deletedItems: List<DeletedItem>,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Response structure for delta sync - receives only changes from server */
|
/** Response structure for delta sync - receives only changes from server */
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DeltaSyncResponse(
|
data class DeltaSyncResponse(
|
||||||
val serverTime: String,
|
val serverTime: String,
|
||||||
|
val requestFullSync: Boolean = false,
|
||||||
val gyms: List<BackupGym>,
|
val gyms: List<BackupGym>,
|
||||||
val problems: List<BackupProblem>,
|
val problems: List<BackupProblem>,
|
||||||
val sessions: List<BackupClimbSession>,
|
val sessions: List<BackupClimbSession>,
|
||||||
val attempts: List<BackupAttempt>,
|
val attempts: List<BackupAttempt>,
|
||||||
val deletedItems: List<DeletedItem>,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ sealed class SyncException(message: String) : IOException(message), Serializable
|
|||||||
SyncException("Invalid server response: $details")
|
SyncException("Invalid server response: $details")
|
||||||
|
|
||||||
data class NetworkError(val details: String) : SyncException("Network error: $details")
|
data class NetworkError(val details: String) : SyncException("Network error: $details")
|
||||||
|
|
||||||
|
data class General(val details: String) : SyncException(details)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.atridad.ascently.data.repository.ClimbRepository
|
import com.atridad.ascently.data.repository.ClimbRepository
|
||||||
|
import com.atridad.ascently.data.state.DataStateManager
|
||||||
import com.atridad.ascently.utils.AppLogger
|
import com.atridad.ascently.utils.AppLogger
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,7 +28,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Currently we only support one provider, but this allows for future expansion
|
// Currently we only support one provider, but this allows for future expansion
|
||||||
private val provider: SyncProvider = AscentlySyncProvider(context, repository)
|
private val provider: SyncProvider = AscentlySyncProvider(context, repository, DataStateManager(context))
|
||||||
|
|
||||||
// State
|
// State
|
||||||
private val _isSyncing = MutableStateFlow(false)
|
private val _isSyncing = MutableStateFlow(false)
|
||||||
|
|||||||
@@ -1,11 +1,74 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#3DDC84"
|
||||||
<!-- Clean white background -->
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
<path android:fillColor="#FFFFFF"
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
android:pathData="M0,0h108v108h-108z"/>
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
</vector>
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 550 B After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 730 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 868 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 970 B After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#000000</color>
|
||||||
|
</resources>
|
||||||
@@ -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.2"
|
activityCompose = "1.12.3"
|
||||||
composeBom = "2025.12.01"
|
composeBom = "2026.01.01"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
navigation = "2.9.6"
|
navigation = "2.9.7"
|
||||||
viewmodel = "2.10.0"
|
viewmodel = "2.10.0"
|
||||||
kotlinxSerialization = "1.9.0"
|
kotlinxSerialization = "1.10.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.1.0"
|
spotless = "8.2.1"
|
||||||
|
|
||||||
[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" }
|
||||||
|
|||||||
186
android/gradlew.bat
vendored
@@ -1,93 +1,93 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@rem This is normally unused
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo. 1>&2
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
exit /b %EXIT_CODE%
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
:omega
|
:omega
|
||||||
|
|||||||
3
branding/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
*.tmp
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
BIN
branding/Android/Icon-Android-Default-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 933 KiB |
BIN
branding/Balls.icon/Assets/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/Balls.icon/Assets/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/Balls.icon/Assets/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/Balls.icon/Assets/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
67
branding/Balls.icon/icon.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"fill" : "automatic",
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyRedBall.png",
|
||||||
|
"name" : "AscentlyRedBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.4,
|
||||||
|
"translation-in-points" : [
|
||||||
|
90.60312499999992,
|
||||||
|
127.86484375000009
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyYellowBall.png",
|
||||||
|
"name" : "AscentlyYellowBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.3,
|
||||||
|
"translation-in-points" : [
|
||||||
|
90.50312500000001,
|
||||||
|
-177.66484375
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyBlueBall.png",
|
||||||
|
"name" : "AscentlyBlueBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.3,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-138.20312500000006,
|
||||||
|
177.3648437500001
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyGreenBall.png",
|
||||||
|
"name" : "AscentlyGreenBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.2,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-138.30312499999997,
|
||||||
|
-43.08515625000001
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
branding/Icon.icon/Assets/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
branding/Icon.icon/Assets/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
45
branding/Icon.icon/icon.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"fill" : "automatic",
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"image-name" : "AscetlyTriangle2.png",
|
||||||
|
"name" : "AscetlyTriangle2",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.75,
|
||||||
|
"translation-in-points" : [
|
||||||
|
108,
|
||||||
|
-53.8125
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscetlyTriangle1.png",
|
||||||
|
"name" : "AscetlyTriangle1",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.5,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-215,
|
||||||
|
39.9375
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
branding/Photomator Files/AscentlyBlueBall.pxd
vendored
Normal file
BIN
branding/Photomator Files/AscentlyGreenBall.pxd
vendored
Normal file
BIN
branding/Photomator Files/AscentlyRedBall.pxd
vendored
Normal file
BIN
branding/Photomator Files/AscentlyYellowBall.pxd
vendored
Normal file
BIN
branding/Photomator Files/Ascently_Phone_1.pxd
vendored
Normal file
BIN
branding/Photomator Files/Ascently_Phone_2.pxd
vendored
Normal file
BIN
branding/Photomator Files/Ascently_Phone_3.pxd
vendored
Normal file
BIN
branding/Photomator Files/Ascently_iPad_1.pxd
vendored
Normal file
BIN
branding/Photomator Files/Ascently_iPad_2.pxd
vendored
Normal file
BIN
branding/Photomator Files/Ascently_iPad_3.pxd
vendored
Normal file
BIN
branding/Photomator Files/AscetlyTriangle1.pxd
vendored
Normal file
BIN
branding/Photomator Files/AscetlyTriangle2.pxd
vendored
Normal file
BIN
branding/Photomator Files/Peaks.pxd
vendored
Normal file
@@ -1,394 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable, TypedDict
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
|
|
||||||
class Polygon(TypedDict):
|
|
||||||
coords: list[tuple[float, float]]
|
|
||||||
fill: str
|
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).parent
|
|
||||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
||||||
SOURCE_DIR = SCRIPT_DIR / "source"
|
|
||||||
LOGOS_DIR = SCRIPT_DIR / "logos"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_svg_polygons(svg_path: Path) -> list[Polygon]:
|
|
||||||
tree = ET.parse(svg_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
ns = {"svg": "http://www.w3.org/2000/svg"}
|
|
||||||
polygons = root.findall(".//svg:polygon", ns)
|
|
||||||
if not polygons:
|
|
||||||
polygons = root.findall(".//polygon")
|
|
||||||
|
|
||||||
result: list[Polygon] = []
|
|
||||||
for poly in polygons:
|
|
||||||
points_str = poly.get("points", "").strip()
|
|
||||||
fill = poly.get("fill", "#000000")
|
|
||||||
|
|
||||||
coords: list[tuple[float, float]] = []
|
|
||||||
for pair in points_str.split():
|
|
||||||
x, y = pair.split(",")
|
|
||||||
coords.append((float(x), float(y)))
|
|
||||||
|
|
||||||
result.append({"coords": coords, "fill": fill})
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_bbox(polygons: list[Polygon]) -> dict[str, float]:
|
|
||||||
all_coords: list[tuple[float, float]] = []
|
|
||||||
for poly in polygons:
|
|
||||||
all_coords.extend(poly["coords"])
|
|
||||||
|
|
||||||
xs = [c[0] for c in all_coords]
|
|
||||||
ys = [c[1] for c in all_coords]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"min_x": min(xs),
|
|
||||||
"max_x": max(xs),
|
|
||||||
"min_y": min(ys),
|
|
||||||
"max_y": max(ys),
|
|
||||||
"width": max(xs) - min(xs),
|
|
||||||
"height": max(ys) - min(ys),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def scale_and_center(
|
|
||||||
polygons: list[Polygon], viewbox_size: float, target_width: float
|
|
||||||
) -> list[Polygon]:
|
|
||||||
bbox = get_bbox(polygons)
|
|
||||||
|
|
||||||
scale = target_width / bbox["width"]
|
|
||||||
center = viewbox_size / 2
|
|
||||||
|
|
||||||
scaled_polys: list[Polygon] = []
|
|
||||||
for poly in polygons:
|
|
||||||
scaled_coords = [(x * scale, y * scale) for x, y in poly["coords"]]
|
|
||||||
scaled_polys.append({"coords": scaled_coords, "fill": poly["fill"]})
|
|
||||||
|
|
||||||
scaled_bbox = get_bbox(scaled_polys)
|
|
||||||
current_center_x = (scaled_bbox["min_x"] + scaled_bbox["max_x"]) / 2
|
|
||||||
current_center_y = (scaled_bbox["min_y"] + scaled_bbox["max_y"]) / 2
|
|
||||||
|
|
||||||
offset_x = center - current_center_x
|
|
||||||
offset_y = center - current_center_y
|
|
||||||
|
|
||||||
final_polys: list[Polygon] = []
|
|
||||||
for poly in scaled_polys:
|
|
||||||
final_coords = [(x + offset_x, y + offset_y) for x, y in poly["coords"]]
|
|
||||||
final_polys.append({"coords": final_coords, "fill": poly["fill"]})
|
|
||||||
|
|
||||||
return final_polys
|
|
||||||
|
|
||||||
|
|
||||||
def format_svg_points(coords: list[tuple[float, float]]) -> str:
|
|
||||||
return " ".join(f"{x:.3f},{y:.3f}" for x, y in coords)
|
|
||||||
|
|
||||||
|
|
||||||
def format_android_path(coords: list[tuple[float, float]]) -> str:
|
|
||||||
points = " ".join(f"{x:.3f},{y:.3f}" for x, y in coords)
|
|
||||||
pairs = points.split()
|
|
||||||
return f"M{pairs[0]} L{pairs[1]} L{pairs[2]} Z"
|
|
||||||
|
|
||||||
|
|
||||||
def generate_svg(polygons: list[Polygon], width: int, height: int) -> str:
|
|
||||||
lines = [
|
|
||||||
f'<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">'
|
|
||||||
]
|
|
||||||
for poly in polygons:
|
|
||||||
points = format_svg_points(poly["coords"])
|
|
||||||
lines.append(f' <polygon points="{points}" fill="{poly["fill"]}"/>')
|
|
||||||
lines.append("</svg>")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_android_vector(
|
|
||||||
polygons: list[Polygon], width: int, height: int, viewbox: int
|
|
||||||
) -> str:
|
|
||||||
lines = [
|
|
||||||
'<?xml version="1.0" encoding="utf-8"?>',
|
|
||||||
'<vector xmlns:android="http://schemas.android.com/apk/res/android"',
|
|
||||||
f' android:width="{width}dp"',
|
|
||||||
f' android:height="{height}dp"',
|
|
||||||
f' android:viewportWidth="{viewbox}"',
|
|
||||||
f' android:viewportHeight="{viewbox}">',
|
|
||||||
]
|
|
||||||
for poly in polygons:
|
|
||||||
path = format_android_path(poly["coords"])
|
|
||||||
lines.append(
|
|
||||||
f' <path android:fillColor="{poly["fill"]}" android:pathData="{path}" />'
|
|
||||||
)
|
|
||||||
lines.append("</vector>")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def rasterize_svg(
|
|
||||||
svg_path: Path,
|
|
||||||
output_path: Path,
|
|
||||||
size: int,
|
|
||||||
bg_color: tuple[int, int, int, int] | None = None,
|
|
||||||
circular: bool = False,
|
|
||||||
) -> None:
|
|
||||||
from xml.dom import minidom
|
|
||||||
|
|
||||||
doc = minidom.parse(str(svg_path))
|
|
||||||
|
|
||||||
img = Image.new(
|
|
||||||
"RGBA", (size, size), (255, 255, 255, 0) if bg_color is None else bg_color
|
|
||||||
)
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
svg_elem = doc.getElementsByTagName("svg")[0]
|
|
||||||
viewbox = svg_elem.getAttribute("viewBox").split()
|
|
||||||
if viewbox:
|
|
||||||
vb_width = float(viewbox[2])
|
|
||||||
vb_height = float(viewbox[3])
|
|
||||||
scale_x = size / vb_width
|
|
||||||
scale_y = size / vb_height
|
|
||||||
else:
|
|
||||||
scale_x = scale_y = 1
|
|
||||||
|
|
||||||
def parse_transform(
|
|
||||||
transform_str: str,
|
|
||||||
) -> Callable[[float, float], tuple[float, float]]:
|
|
||||||
import re
|
|
||||||
|
|
||||||
if not transform_str:
|
|
||||||
return lambda x, y: (x, y)
|
|
||||||
|
|
||||||
transforms: list[tuple[str, list[float]]] = []
|
|
||||||
for match in re.finditer(r"(\w+)\(([^)]+)\)", transform_str):
|
|
||||||
func, args_str = match.groups()
|
|
||||||
args = [float(x) for x in args_str.replace(",", " ").split()]
|
|
||||||
transforms.append((func, args))
|
|
||||||
|
|
||||||
def apply_transforms(x: float, y: float) -> tuple[float, float]:
|
|
||||||
for func, args in transforms:
|
|
||||||
if func == "translate":
|
|
||||||
x += args[0]
|
|
||||||
y += args[1] if len(args) > 1 else args[0]
|
|
||||||
elif func == "scale":
|
|
||||||
x *= args[0]
|
|
||||||
y *= args[1] if len(args) > 1 else args[0]
|
|
||||||
return x, y
|
|
||||||
|
|
||||||
return apply_transforms
|
|
||||||
|
|
||||||
for g in doc.getElementsByTagName("g"):
|
|
||||||
transform = parse_transform(g.getAttribute("transform"))
|
|
||||||
|
|
||||||
for poly in g.getElementsByTagName("polygon"):
|
|
||||||
points_str = poly.getAttribute("points").strip()
|
|
||||||
fill = poly.getAttribute("fill")
|
|
||||||
if not fill:
|
|
||||||
fill = "#000000"
|
|
||||||
|
|
||||||
coords: list[tuple[float, float]] = []
|
|
||||||
for pair in points_str.split():
|
|
||||||
x, y = pair.split(",")
|
|
||||||
x, y = float(x), float(y)
|
|
||||||
x, y = transform(x, y)
|
|
||||||
coords.append((x * scale_x, y * scale_y))
|
|
||||||
|
|
||||||
draw.polygon(coords, fill=fill)
|
|
||||||
|
|
||||||
for poly in doc.getElementsByTagName("polygon"):
|
|
||||||
if poly.parentNode and getattr(poly.parentNode, "tagName", None) == "g":
|
|
||||||
continue
|
|
||||||
|
|
||||||
points_str = poly.getAttribute("points").strip()
|
|
||||||
fill = poly.getAttribute("fill")
|
|
||||||
if not fill:
|
|
||||||
fill = "#000000"
|
|
||||||
|
|
||||||
coords = []
|
|
||||||
for pair in points_str.split():
|
|
||||||
x, y = pair.split(",")
|
|
||||||
coords.append((float(x) * scale_x, float(y) * scale_y))
|
|
||||||
|
|
||||||
draw.polygon(coords, fill=fill)
|
|
||||||
|
|
||||||
if circular:
|
|
||||||
mask = Image.new("L", (size, size), 0)
|
|
||||||
mask_draw = ImageDraw.Draw(mask)
|
|
||||||
mask_draw.ellipse((0, 0, size, size), fill=255)
|
|
||||||
img.putalpha(mask)
|
|
||||||
|
|
||||||
img.save(output_path)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
print("Generating branding assets...")
|
|
||||||
|
|
||||||
logo_svg = SOURCE_DIR / "logo.svg"
|
|
||||||
icon_light = SOURCE_DIR / "icon-light.svg"
|
|
||||||
icon_dark = SOURCE_DIR / "icon-dark.svg"
|
|
||||||
icon_tinted = SOURCE_DIR / "icon-tinted.svg"
|
|
||||||
|
|
||||||
polygons = parse_svg_polygons(logo_svg)
|
|
||||||
|
|
||||||
print(" iOS...")
|
|
||||||
ios_assets = PROJECT_ROOT / "ios/Ascently/Assets.xcassets/AppIcon.appiconset"
|
|
||||||
|
|
||||||
for src, dst in [
|
|
||||||
(icon_light, ios_assets / "app_icon_light_template.svg"),
|
|
||||||
(icon_dark, ios_assets / "app_icon_dark_template.svg"),
|
|
||||||
(icon_tinted, ios_assets / "app_icon_tinted_template.svg"),
|
|
||||||
]:
|
|
||||||
with open(src) as f:
|
|
||||||
content = f.read()
|
|
||||||
with open(dst, "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
img_light = Image.new("RGB", (1024, 1024), (255, 255, 255))
|
|
||||||
draw_light = ImageDraw.Draw(img_light)
|
|
||||||
scaled = scale_and_center(polygons, 1024, int(1024 * 0.7))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_light.polygon(coords, fill=poly["fill"])
|
|
||||||
img_light.save(ios_assets / "app_icon_1024.png")
|
|
||||||
|
|
||||||
img_dark = Image.new("RGB", (1024, 1024), (26, 26, 26))
|
|
||||||
draw_dark = ImageDraw.Draw(img_dark)
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_dark.polygon(coords, fill=poly["fill"])
|
|
||||||
img_dark.save(ios_assets / "app_icon_1024_dark.png")
|
|
||||||
|
|
||||||
img_tinted = Image.new("RGB", (1024, 1024), (0, 0, 0))
|
|
||||||
draw_tinted = ImageDraw.Draw(img_tinted)
|
|
||||||
for i, poly in enumerate(scaled):
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_tinted.polygon(coords, fill=(0, 0, 0))
|
|
||||||
img_tinted.save(ios_assets / "app_icon_1024_tinted.png")
|
|
||||||
|
|
||||||
print(" Android...")
|
|
||||||
|
|
||||||
polys_108 = scale_and_center(polygons, 108, 60)
|
|
||||||
android_xml = generate_android_vector(polys_108, 108, 108, 108)
|
|
||||||
(
|
|
||||||
PROJECT_ROOT / "android/app/src/main/res/drawable/ic_launcher_foreground.xml"
|
|
||||||
).write_text(android_xml)
|
|
||||||
|
|
||||||
polys_24 = scale_and_center(polygons, 24, 20)
|
|
||||||
mountains_xml = generate_android_vector(polys_24, 24, 24, 24)
|
|
||||||
(PROJECT_ROOT / "android/app/src/main/res/drawable/ic_mountains.xml").write_text(
|
|
||||||
mountains_xml
|
|
||||||
)
|
|
||||||
|
|
||||||
for density, size in [
|
|
||||||
("mdpi", 48),
|
|
||||||
("hdpi", 72),
|
|
||||||
("xhdpi", 96),
|
|
||||||
("xxhdpi", 144),
|
|
||||||
("xxxhdpi", 192),
|
|
||||||
]:
|
|
||||||
mipmap_dir = PROJECT_ROOT / f"android/app/src/main/res/mipmap-{density}"
|
|
||||||
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.6))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(mipmap_dir / "ic_launcher.webp")
|
|
||||||
|
|
||||||
img_round = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
|
||||||
draw_round = ImageDraw.Draw(img_round)
|
|
||||||
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_round.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
mask = Image.new("L", (size, size), 0)
|
|
||||||
mask_draw = ImageDraw.Draw(mask)
|
|
||||||
mask_draw.ellipse((0, 0, size, size), fill=255)
|
|
||||||
img_round.putalpha(mask)
|
|
||||||
|
|
||||||
img_round.save(mipmap_dir / "ic_launcher_round.webp")
|
|
||||||
|
|
||||||
print(" Docs...")
|
|
||||||
|
|
||||||
polys_32 = scale_and_center(polygons, 32, 26)
|
|
||||||
logo_svg_32 = generate_svg(polys_32, 32, 32)
|
|
||||||
(PROJECT_ROOT / "docs/src/assets/logo.svg").write_text(logo_svg_32)
|
|
||||||
(PROJECT_ROOT / "docs/src/assets/logo-dark.svg").write_text(logo_svg_32)
|
|
||||||
|
|
||||||
polys_256 = scale_and_center(polygons, 256, 208)
|
|
||||||
logo_svg_256 = generate_svg(polys_256, 256, 256)
|
|
||||||
(PROJECT_ROOT / "docs/src/assets/logo-highres.svg").write_text(logo_svg_256)
|
|
||||||
|
|
||||||
logo_32_path = PROJECT_ROOT / "docs/src/assets/logo.svg"
|
|
||||||
rasterize_svg(logo_32_path, PROJECT_ROOT / "docs/public/favicon.png", 32)
|
|
||||||
|
|
||||||
sizes = [16, 32, 48]
|
|
||||||
imgs = []
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 0))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
imgs.append(img)
|
|
||||||
|
|
||||||
imgs[0].save(
|
|
||||||
PROJECT_ROOT / "docs/public/favicon.ico",
|
|
||||||
format="ICO",
|
|
||||||
sizes=[(s, s) for s in sizes],
|
|
||||||
append_images=imgs[1:],
|
|
||||||
)
|
|
||||||
|
|
||||||
print(" Logos...")
|
|
||||||
|
|
||||||
LOGOS_DIR.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
sizes = [64, 128, 256, 512, 1024, 2048]
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 0))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(LOGOS_DIR / f"logo-{size}.png")
|
|
||||||
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(LOGOS_DIR / f"logo-{size}-white.png")
|
|
||||||
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (26, 26, 26, 255))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(LOGOS_DIR / f"logo-{size}-dark.png")
|
|
||||||
|
|
||||||
print("Done.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "Error: Python 3 required"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 "$SCRIPT_DIR/generate.py"
|
|
||||||
BIN
branding/iOS/Balls-iOS-ClearDark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 698 KiB |
BIN
branding/iOS/Balls-iOS-ClearLight-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 657 KiB |
BIN
branding/iOS/Balls-iOS-Dark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
branding/iOS/Balls-iOS-Default-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
branding/iOS/Balls-iOS-TintedDark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 684 KiB |
BIN
branding/iOS/Balls-iOS-TintedLight-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 707 KiB |
BIN
branding/iOS/Balls-watchOS-Default-1088x1088@1x.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
branding/iOS/Icon-iOS-ClearDark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 565 KiB |
BIN
branding/iOS/Icon-iOS-ClearLight-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 533 KiB |
BIN
branding/iOS/Icon-iOS-Dark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
branding/iOS/Icon-iOS-Default-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
branding/iOS/Icon-iOS-TintedDark-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
BIN
branding/iOS/Icon-iOS-TintedLight-1024x1024@1x.png
Normal file
|
After Width: | Height: | Size: 573 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 804 B |
|
Before Width: | Height: | Size: 798 B |
|
Before Width: | Height: | Size: 795 B |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 411 B |
|
Before Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 413 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#1A1A1A" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#FFFFFF" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="transparent" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#000000" opacity="0.8"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#000000" opacity="0.9"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 443 B |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="108" height="108" viewBox="0 0 108 108" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 254 B |
@@ -6,53 +6,63 @@ import node from "@astrojs/node";
|
|||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://docs.ascently.app",
|
site: "https://docs.ascently.app",
|
||||||
|
|
||||||
integrations: [
|
integrations: [
|
||||||
starlight({
|
starlight({
|
||||||
title: "Ascently",
|
title: "Ascently",
|
||||||
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.svg",
|
light: "./src/assets/logo.png",
|
||||||
dark: "./src/assets/logo-dark.svg",
|
dark: "./src/assets/logo.png",
|
||||||
},
|
},
|
||||||
favicon: "/favicon.png",
|
favicon: "/favicon.png",
|
||||||
social: [
|
social: [
|
||||||
{
|
{
|
||||||
icon: "seti:git",
|
icon: "seti:git",
|
||||||
label: "Gitea",
|
label: "Gitea",
|
||||||
href: "https://git.atri.dad/atridad/Ascently",
|
href: "https://git.atri.dad/atridad/Ascently",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "email",
|
icon: "email",
|
||||||
label: "Contact",
|
label: "Contact",
|
||||||
href: "mailto:me@atri.dad",
|
href: "mailto:me@atri.dad",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sidebar: [
|
sidebar: [
|
||||||
{
|
{
|
||||||
label: "Download",
|
label: "Download",
|
||||||
link: "/download/",
|
link: "/download/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Self-Hosted Sync",
|
label: "Self-Hosted Sync",
|
||||||
items: [
|
items: [
|
||||||
{ label: "Overview", slug: "sync/overview" },
|
{ label: "Overview", slug: "sync/overview" },
|
||||||
{ label: "Quick Start", slug: "sync/quick-start" },
|
{ label: "Quick Start", slug: "sync/quick-start" },
|
||||||
{ label: "API Reference", slug: "sync/api-reference" },
|
{ label: "API Reference", slug: "sync/api-reference" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Privacy",
|
label: "Privacy",
|
||||||
link: "/privacy/",
|
link: "/privacy/",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
customCss: ["./src/styles/custom.css"],
|
customCss: ["./src/styles/custom.css"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
adapter: node({
|
||||||
|
mode: "standalone",
|
||||||
}),
|
}),
|
||||||
],
|
|
||||||
|
|
||||||
adapter: node({
|
output: "server",
|
||||||
mode: "standalone",
|
|
||||||
}),
|
build: {
|
||||||
|
inlineStylesheets: "always",
|
||||||
|
},
|
||||||
|
|
||||||
|
experimental: {
|
||||||
|
svgo: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,9 +25,9 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.5.1",
|
"@astrojs/node": "^9.5.2",
|
||||||
"@astrojs/starlight": "^0.37.1",
|
"@astrojs/starlight": "^0.37.5",
|
||||||
"astro": "^5.16.5",
|
"astro": "^5.17.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
|
|||||||
789
docs/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 166 B |
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 42 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<polygon points="3.000,26.636 10.736,9.231 18.471,26.636" fill="#FFC107"/>
|
|
||||||
<polygon points="9.661,26.636 19.331,5.364 29.000,26.636" fill="#F44336"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 244 B |