Compare commits
3 Commits
ANDROID_2.
...
balls
| Author | SHA1 | Date | |
|---|---|---|---|
|
11850408be
|
|||
|
37beb4abb6
|
|||
|
d263c6c87e
|
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId = "com.atridad.ascently"
|
applicationId = "com.atridad.ascently"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 49
|
versionCode = 50
|
||||||
versionName = "2.4.1"
|
versionName = "2.5.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.health.connect.client.permission.HealthPermission
|
|||||||
import androidx.health.connect.client.records.ExerciseSessionRecord
|
import androidx.health.connect.client.records.ExerciseSessionRecord
|
||||||
import androidx.health.connect.client.records.HeartRateRecord
|
import androidx.health.connect.client.records.HeartRateRecord
|
||||||
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
|
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
|
||||||
|
|
||||||
import androidx.health.connect.client.units.Energy
|
import androidx.health.connect.client.units.Energy
|
||||||
import com.atridad.ascently.data.model.ClimbSession
|
import com.atridad.ascently.data.model.ClimbSession
|
||||||
import com.atridad.ascently.data.model.SessionStatus
|
import com.atridad.ascently.data.model.SessionStatus
|
||||||
@@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.flow
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Health Connect manager for Ascently that syncs climbing sessions to Samsung Health, Google Fit,
|
* Health Connect manager for Ascently that syncs climbing sessions to Samsung Health, Google Fit,
|
||||||
@@ -197,6 +199,7 @@ class HealthConnectManager(private val context: Context) {
|
|||||||
exerciseType =
|
exerciseType =
|
||||||
ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING,
|
ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING,
|
||||||
title = "Rock Climbing at $gymName",
|
title = "Rock Climbing at $gymName",
|
||||||
|
metadata = androidx.health.connect.client.records.metadata.Metadata.manualEntry(),
|
||||||
)
|
)
|
||||||
records.add(exerciseSession)
|
records.add(exerciseSession)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -217,6 +220,7 @@ class HealthConnectManager(private val context: Context) {
|
|||||||
endZoneOffset =
|
endZoneOffset =
|
||||||
ZoneOffset.systemDefault().rules.getOffset(endTime),
|
ZoneOffset.systemDefault().rules.getOffset(endTime),
|
||||||
energy = Energy.calories(estimatedCalories),
|
energy = Energy.calories(estimatedCalories),
|
||||||
|
metadata = androidx.health.connect.client.records.metadata.Metadata.manualEntry(),
|
||||||
)
|
)
|
||||||
records.add(caloriesRecord)
|
records.add(caloriesRecord)
|
||||||
}
|
}
|
||||||
@@ -239,9 +243,9 @@ class HealthConnectManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences
|
preferences
|
||||||
.edit()
|
.edit {
|
||||||
.putString("last_sync_success", DateFormatUtils.nowISO8601())
|
putString("last_sync_success", DateFormatUtils.nowISO8601())
|
||||||
.apply()
|
}
|
||||||
} else {
|
} else {
|
||||||
val reason =
|
val reason =
|
||||||
when {
|
when {
|
||||||
@@ -326,6 +330,7 @@ class HealthConnectManager(private val context: Context) {
|
|||||||
endTime = endTime,
|
endTime = endTime,
|
||||||
endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(endTime),
|
endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(endTime),
|
||||||
samples = samples,
|
samples = samples,
|
||||||
|
metadata = androidx.health.connect.client.records.metadata.Metadata.manualEntry(),
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
AppLogger.e(TAG, e) { "Error creating heart rate record" }
|
AppLogger.e(TAG, e) { "Error creating heart rate record" }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.atridad.ascently.data.model
|
package com.atridad.ascently.data.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
@@ -34,6 +35,7 @@ enum class AttemptResult {
|
|||||||
],
|
],
|
||||||
indices = [Index(value = ["sessionId"]), Index(value = ["problemId"])],
|
indices = [Index(value = ["sessionId"]), Index(value = ["problemId"])],
|
||||||
)
|
)
|
||||||
|
@Immutable
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Attempt(
|
data class Attempt(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.atridad.ascently.data.model
|
package com.atridad.ascently.data.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
@@ -27,6 +28,7 @@ enum class SessionStatus {
|
|||||||
],
|
],
|
||||||
indices = [Index(value = ["gymId"])],
|
indices = [Index(value = ["gymId"])],
|
||||||
)
|
)
|
||||||
|
@Immutable
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ClimbSession(
|
data class ClimbSession(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.atridad.ascently.data.model
|
package com.atridad.ascently.data.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.atridad.ascently.utils.DateFormatUtils
|
import com.atridad.ascently.utils.DateFormatUtils
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
@Entity(tableName = "gyms")
|
@Entity(tableName = "gyms")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Gym(
|
data class Gym(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.atridad.ascently.data.model
|
package com.atridad.ascently.data.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
@@ -7,6 +8,7 @@ import androidx.room.PrimaryKey
|
|||||||
import com.atridad.ascently.utils.DateFormatUtils
|
import com.atridad.ascently.utils.DateFormatUtils
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "problems",
|
tableName = "problems",
|
||||||
foreignKeys =
|
foreignKeys =
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.atridad.ascently.R
|
|||||||
import com.atridad.ascently.data.database.AscentlyDatabase
|
import com.atridad.ascently.data.database.AscentlyDatabase
|
||||||
import com.atridad.ascently.data.repository.ClimbRepository
|
import com.atridad.ascently.data.repository.ClimbRepository
|
||||||
import com.atridad.ascently.utils.AppLogger
|
import com.atridad.ascently.utils.AppLogger
|
||||||
|
import com.atridad.ascently.utils.DateFormatUtils
|
||||||
import com.atridad.ascently.widget.ClimbStatsWidgetProvider
|
import com.atridad.ascently.widget.ClimbStatsWidgetProvider
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
@@ -237,9 +238,8 @@ class SessionTrackingService : Service() {
|
|||||||
val startTimeMillis =
|
val startTimeMillis =
|
||||||
session.startTime?.let { startTime ->
|
session.startTime?.let { startTime ->
|
||||||
try {
|
try {
|
||||||
val start = LocalDateTime.parse(startTime)
|
DateFormatUtils.parseISO8601(startTime)?.toEpochMilli()
|
||||||
val zoneId = ZoneId.systemDefault()
|
?: System.currentTimeMillis()
|
||||||
start.atZone(zoneId).toInstant().toEpochMilli()
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
@@ -263,9 +263,9 @@ class SessionTrackingService : Service() {
|
|||||||
val duration =
|
val duration =
|
||||||
session.startTime?.let { startTime ->
|
session.startTime?.let { startTime ->
|
||||||
try {
|
try {
|
||||||
val start = LocalDateTime.parse(startTime)
|
val start = DateFormatUtils.parseISO8601(startTime)
|
||||||
val now = LocalDateTime.now()
|
val now = java.time.Instant.now()
|
||||||
val totalSeconds = ChronoUnit.SECONDS.between(start, now)
|
val totalSeconds = if (start != null) ChronoUnit.SECONDS.between(start, now) else 0L
|
||||||
val hours = totalSeconds / 3600
|
val hours = totalSeconds / 3600
|
||||||
val minutes = (totalSeconds % 3600) / 60
|
val minutes = (totalSeconds % 3600) / 60
|
||||||
val seconds = totalSeconds % 60
|
val seconds = totalSeconds % 60
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.atridad.ascently.data.model.ClimbSession
|
import com.atridad.ascently.data.model.ClimbSession
|
||||||
import com.atridad.ascently.data.model.Gym
|
import com.atridad.ascently.data.model.Gym
|
||||||
import com.atridad.ascently.ui.theme.CustomIcons
|
import com.atridad.ascently.ui.theme.CustomIcons
|
||||||
|
import com.atridad.ascently.utils.DateFormatUtils
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -23,101 +24,112 @@ fun ActiveSessionBanner(
|
|||||||
gym: Gym?,
|
gym: Gym?,
|
||||||
onSessionClick: () -> Unit,
|
onSessionClick: () -> Unit,
|
||||||
onEndSession: () -> Unit,
|
onEndSession: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (activeSession != null) {
|
if (activeSession == null) return
|
||||||
// Add a timer that updates every second for real-time duration counting
|
|
||||||
var currentTime by remember { mutableStateOf(LocalDateTime.now()) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
val sessionId = activeSession.id
|
||||||
while (true) {
|
val startTimeString = activeSession.startTime
|
||||||
delay(1000) // Update every second
|
val gymName = gym?.name ?: "Unknown Gym"
|
||||||
currentTime = LocalDateTime.now()
|
|
||||||
}
|
var elapsedSeconds by remember(sessionId) { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
|
LaunchedEffect(sessionId, startTimeString) {
|
||||||
|
if (startTimeString == null) return@LaunchedEffect
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
elapsedSeconds = calculateElapsedSeconds(startTimeString)
|
||||||
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Card(
|
val durationText = remember(elapsedSeconds) {
|
||||||
|
formatDuration(elapsedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onSessionClick() },
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onSessionClick() },
|
.padding(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
modifier = Modifier
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
.fillMaxWidth()
|
Icon(
|
||||||
.padding(16.dp),
|
Icons.Default.PlayArrow,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
contentDescription = null,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
) {
|
modifier = Modifier.size(16.dp),
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
)
|
||||||
Row(
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.PlayArrow,
|
|
||||||
contentDescription = "Active session",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "Active Session",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = gym?.name ?: "Unknown Gym",
|
text = "Active Session",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
activeSession.startTime?.let { startTime ->
|
|
||||||
val duration = calculateDuration(startTime, currentTime)
|
|
||||||
Text(
|
|
||||||
text = duration,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
onClick = onEndSession,
|
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
Text(
|
||||||
containerColor = MaterialTheme.colorScheme.error,
|
text = gymName,
|
||||||
contentColor = MaterialTheme.colorScheme.onError,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
),
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
) {
|
)
|
||||||
Icon(
|
|
||||||
imageVector = CustomIcons.Stop(MaterialTheme.colorScheme.onError),
|
if (startTimeString != null) {
|
||||||
contentDescription = "End session",
|
Text(
|
||||||
|
text = durationText,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilledIconButton(
|
||||||
|
onClick = onEndSession,
|
||||||
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onError,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CustomIcons.Stop(MaterialTheme.colorScheme.onError),
|
||||||
|
contentDescription = "End session",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateDuration(startTimeString: String, currentTime: LocalDateTime): String {
|
private fun calculateElapsedSeconds(startTimeString: String): Long {
|
||||||
return try {
|
return try {
|
||||||
val startTime = LocalDateTime.parse(startTimeString)
|
val startTime = DateFormatUtils.parseISO8601(startTimeString) ?: return 0L
|
||||||
val totalSeconds = ChronoUnit.SECONDS.between(startTime, currentTime)
|
val now = Instant.now()
|
||||||
val hours = totalSeconds / 3600
|
ChronoUnit.SECONDS.between(startTime, now).coerceAtLeast(0)
|
||||||
val minutes = (totalSeconds % 3600) / 60
|
|
||||||
val seconds = totalSeconds % 60
|
|
||||||
|
|
||||||
when {
|
|
||||||
hours > 0 -> "${hours}h ${minutes}m ${seconds}s"
|
|
||||||
minutes > 0 -> "${minutes}m ${seconds}s"
|
|
||||||
else -> "${totalSeconds}s"
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
"Active"
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDuration(totalSeconds: Long): String {
|
||||||
|
val hours = totalSeconds / 3600
|
||||||
|
val minutes = (totalSeconds % 3600) / 60
|
||||||
|
val seconds = totalSeconds % 60
|
||||||
|
|
||||||
|
return when {
|
||||||
|
hours > 0 -> "${hours}h ${minutes}m ${seconds}s"
|
||||||
|
minutes > 0 -> "${minutes}m ${seconds}s"
|
||||||
|
else -> "${seconds}s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ fun AnalyticsScreen(viewModel: ClimbViewModel) {
|
|||||||
val attempts by viewModel.attempts.collectAsState()
|
val attempts by viewModel.attempts.collectAsState()
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
val gyms by viewModel.gyms.collectAsState()
|
||||||
|
|
||||||
|
val gradeDistributionData = remember(sessions, problems, attempts) {
|
||||||
|
calculateGradeDistribution(sessions, problems, attempts)
|
||||||
|
}
|
||||||
|
|
||||||
|
val favoriteGym = remember(sessions, gyms) {
|
||||||
|
sessions.groupBy { it.gymId }.maxByOrNull { it.value.size }?.let { (gymId, sessionList) ->
|
||||||
|
gyms.find { it.id == gymId }?.name to sessionList.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
@@ -65,18 +75,11 @@ fun AnalyticsScreen(viewModel: ClimbViewModel) {
|
|||||||
|
|
||||||
// Grade Distribution Chart
|
// Grade Distribution Chart
|
||||||
item {
|
item {
|
||||||
val gradeDistributionData = calculateGradeDistribution(sessions, problems, attempts)
|
|
||||||
GradeDistributionChartCard(gradeDistributionData = gradeDistributionData)
|
GradeDistributionChartCard(gradeDistributionData = gradeDistributionData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Favorite Gym
|
// Favorite Gym
|
||||||
item {
|
item {
|
||||||
val favoriteGym =
|
|
||||||
sessions.groupBy { it.gymId }.maxByOrNull { it.value.size }?.let {
|
|
||||||
(gymId, sessions) ->
|
|
||||||
gyms.find { it.id == gymId }?.name to sessions.size
|
|
||||||
}
|
|
||||||
|
|
||||||
FavoriteGymCard(
|
FavoriteGymCard(
|
||||||
gymName = favoriteGym?.first ?: "No sessions yet",
|
gymName = favoriteGym?.first ?: "No sessions yet",
|
||||||
sessionCount = favoriteGym?.second ?: 0,
|
sessionCount = favoriteGym?.second ?: 0,
|
||||||
|
|||||||
@@ -225,26 +225,24 @@ fun SessionDetailScreen(
|
|||||||
var showEditAttemptDialog by remember { mutableStateOf<Attempt?>(null) }
|
var showEditAttemptDialog by remember { mutableStateOf<Attempt?>(null) }
|
||||||
|
|
||||||
// Get session details
|
// Get session details
|
||||||
val session = sessions.find { it.id == sessionId }
|
val session = remember(sessions, sessionId) { sessions.find { it.id == sessionId } }
|
||||||
val gym = session?.let { s -> gyms.find { it.id == s.gymId } }
|
val gym = remember(session, gyms) { session?.let { s -> gyms.find { it.id == s.gymId } } }
|
||||||
|
|
||||||
// Calculate stats
|
// Calculate stats
|
||||||
val successfulAttempts =
|
val successfulAttempts = remember(attempts) {
|
||||||
attempts.filter { it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH) }
|
attempts.filter { it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH) }
|
||||||
val uniqueProblems = attempts.map { it.problemId }.distinct()
|
}
|
||||||
|
val uniqueProblems = remember(attempts) { attempts.map { it.problemId }.distinct() }
|
||||||
|
val completedProblems = remember(successfulAttempts) { successfulAttempts.map { it.problemId }.distinct() }
|
||||||
|
|
||||||
val completedProblems = successfulAttempts.map { it.problemId }.distinct()
|
val attemptsWithProblems = remember(attempts, problems) {
|
||||||
|
|
||||||
val attemptsWithProblems =
|
|
||||||
attempts
|
attempts
|
||||||
.mapNotNull { attempt ->
|
.mapNotNull { attempt ->
|
||||||
val problem = problems.find { it.id == attempt.problemId }
|
val problem = problems.find { it.id == attempt.problemId }
|
||||||
if (problem != null) attempt to problem else null
|
if (problem != null) attempt to problem else null
|
||||||
}
|
}
|
||||||
.sortedBy { attempt ->
|
.sortedBy { it.first.timestamp }
|
||||||
// Sort by timestamp (when attempt was logged)
|
}
|
||||||
attempt.first.timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -267,7 +265,7 @@ fun SessionDetailScreen(
|
|||||||
if (session?.status == SessionStatus.ACTIVE) {
|
if (session?.status == SessionStatus.ACTIVE) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
session.let { s ->
|
session?.let { s ->
|
||||||
viewModel.endSession(context, s.id)
|
viewModel.endSession(context, s.id)
|
||||||
onNavigateBack()
|
onNavigateBack()
|
||||||
}
|
}
|
||||||
@@ -313,8 +311,11 @@ fun SessionDetailScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
val formattedDate = remember(session?.date) {
|
||||||
|
DateFormatUtils.formatDateForDisplay(session?.date ?: "")
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = formatDate(session?.date ?: ""),
|
text = formattedDate,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
@@ -503,11 +504,13 @@ fun SessionDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAddAttemptDialog && session != null && gym != null) {
|
val currentSession = session
|
||||||
|
val currentGym = gym
|
||||||
|
if (showAddAttemptDialog && currentSession != null && currentGym != null) {
|
||||||
EnhancedAddAttemptDialog(
|
EnhancedAddAttemptDialog(
|
||||||
session = session,
|
session = currentSession,
|
||||||
gym = gym,
|
gym = currentGym,
|
||||||
problems = problems.filter { it.gymId == gym.id && it.isActive },
|
problems = problems.filter { it.gymId == currentGym.id && it.isActive },
|
||||||
onDismiss = { showAddAttemptDialog = false },
|
onDismiss = { showAddAttemptDialog = false },
|
||||||
onAttemptAdded = { attempt ->
|
onAttemptAdded = { attempt ->
|
||||||
viewModel.addAttempt(attempt)
|
viewModel.addAttempt(attempt)
|
||||||
@@ -728,9 +731,12 @@ fun ProblemDetailScreen(
|
|||||||
}
|
}
|
||||||
firstSuccess?.let { attempt ->
|
firstSuccess?.let { attempt ->
|
||||||
val session = sessions.find { it.id == attempt.sessionId }
|
val session = sessions.find { it.id == attempt.sessionId }
|
||||||
|
val firstSuccessDate = remember(session?.date) {
|
||||||
|
DateFormatUtils.formatDateForDisplay(session?.date ?: "")
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
"First success: ${formatDate(session?.date ?: "")} (${attempt.result.name.lowercase().replaceFirstChar { it.uppercase() }})",
|
"First success: $firstSuccessDate (${attempt.result.name.lowercase().replaceFirstChar { it.uppercase() }})",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
@@ -1309,6 +1315,10 @@ fun StatItem(label: String, value: String) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AttemptHistoryCard(attempt: Attempt, session: ClimbSession, gym: Gym?) {
|
fun AttemptHistoryCard(attempt: Attempt, session: ClimbSession, gym: Gym?) {
|
||||||
|
val formattedDate = remember(session.date) {
|
||||||
|
DateFormatUtils.formatDateForDisplay(session.date)
|
||||||
|
}
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Row(
|
Row(
|
||||||
@@ -1318,7 +1328,7 @@ fun AttemptHistoryCard(attempt: Attempt, session: ClimbSession, gym: Gym?) {
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = formatDate(session.date),
|
text = formattedDate,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
@@ -1478,9 +1488,7 @@ fun SessionAttemptCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatDate(dateString: String): String {
|
|
||||||
return DateFormatUtils.formatDateForDisplay(dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fun GymsScreen(viewModel: ClimbViewModel, onNavigateToGymDetail: (String) -> Uni
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(gyms) { gym ->
|
items(gyms, key = { it.id }) { gym ->
|
||||||
GymCard(gym = gym, onClick = { onNavigateToGymDetail(gym.id) })
|
GymCard(gym = gym, onClick = { onNavigateToGymDetail(gym.id) })
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,18 @@ fun ProblemsScreen(viewModel: ClimbViewModel, onNavigateToProblemDetail: (String
|
|||||||
var selectedGym by remember { mutableStateOf<Gym?>(null) }
|
var selectedGym by remember { mutableStateOf<Gym?>(null) }
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
val filteredProblems =
|
val filteredProblems = remember(problems, selectedClimbType, selectedGym) {
|
||||||
problems.filter { problem ->
|
problems.filter { problem ->
|
||||||
val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } != false
|
val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } != false
|
||||||
val gymMatch = selectedGym?.let { it.id == problem.gymId } != false
|
val gymMatch = selectedGym?.let { it.id == problem.gymId } != false
|
||||||
climbTypeMatch && gymMatch
|
climbTypeMatch && gymMatch
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Separate active and inactive problems
|
// Separate active and inactive problems
|
||||||
val activeProblems = filteredProblems.filter { it.isActive }
|
val activeProblems = remember(filteredProblems) { filteredProblems.filter { it.isActive } }
|
||||||
val inactiveProblems = filteredProblems.filter { !it.isActive }
|
val inactiveProblems = remember(filteredProblems) { filteredProblems.filter { !it.isActive } }
|
||||||
val sortedProblems = activeProblems + inactiveProblems
|
val sortedProblems = remember(activeProblems, inactiveProblems) { activeProblems + inactiveProblems }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||||
Row(
|
Row(
|
||||||
@@ -175,7 +176,7 @@ fun ProblemsScreen(viewModel: ClimbViewModel, onNavigateToProblemDetail: (String
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(sortedProblems) { problem ->
|
items(sortedProblems, key = { it.id }) { problem ->
|
||||||
ProblemCard(
|
ProblemCard(
|
||||||
problem = problem,
|
problem = problem,
|
||||||
gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym",
|
gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym",
|
||||||
|
|||||||
@@ -61,8 +61,12 @@ fun SessionsScreen(viewModel: ClimbViewModel, onNavigateToSessionDetail: (String
|
|||||||
var selectedMonth by remember { mutableStateOf(YearMonth.now()) }
|
var selectedMonth by remember { mutableStateOf(YearMonth.now()) }
|
||||||
var selectedDate by remember { mutableStateOf<LocalDate?>(LocalDate.now()) }
|
var selectedDate by remember { mutableStateOf<LocalDate?>(LocalDate.now()) }
|
||||||
|
|
||||||
val completedSessions = sessions.filter { it.status == SessionStatus.COMPLETED }
|
val completedSessions = remember(sessions) {
|
||||||
val activeSessionGym = activeSession?.let { session -> gyms.find { it.id == session.gymId } }
|
sessions.filter { it.status == SessionStatus.COMPLETED }
|
||||||
|
}
|
||||||
|
val activeSessionGym = remember(activeSession, gyms) {
|
||||||
|
activeSession?.let { session -> gyms.find { it.id == session.gymId } }
|
||||||
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||||
Row(
|
Row(
|
||||||
@@ -136,7 +140,7 @@ fun SessionsScreen(viewModel: ClimbViewModel, onNavigateToSessionDetail: (String
|
|||||||
when (viewMode) {
|
when (viewMode) {
|
||||||
ViewMode.LIST -> {
|
ViewMode.LIST -> {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(completedSessions) { session ->
|
items(completedSessions, key = { it.id }) { session ->
|
||||||
SessionCard(
|
SessionCard(
|
||||||
session = session,
|
session = session,
|
||||||
gymName = gyms.find { it.id == session.gymId }?.name
|
gymName = gyms.find { it.id == session.gymId }?.name
|
||||||
@@ -232,6 +236,10 @@ fun SessionsScreen(viewModel: ClimbViewModel, onNavigateToSessionDetail: (String
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SessionCard(session: ClimbSession, gymName: String, onClick: () -> Unit) {
|
fun SessionCard(session: ClimbSession, gymName: String, onClick: () -> Unit) {
|
||||||
|
val formattedDate = remember(session.date) {
|
||||||
|
DateFormatUtils.formatDateForDisplay(session.date)
|
||||||
|
}
|
||||||
|
|
||||||
Card(onClick = onClick, modifier = Modifier.fillMaxWidth()) {
|
Card(onClick = onClick, modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
|
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
|
||||||
Row(
|
Row(
|
||||||
@@ -244,7 +252,7 @@ fun SessionCard(session: ClimbSession, gymName: String, onClick: () -> Unit) {
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = formatDate(session.date),
|
text = formattedDate,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -539,7 +547,3 @@ fun CalendarDay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatDate(dateString: String): String {
|
|
||||||
return DateFormatUtils.formatDateForDisplay(dateString)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.12.3"
|
agp = "8.12.3"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.3.0"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
@@ -10,8 +10,8 @@ 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.0"
|
activityCompose = "1.12.2"
|
||||||
composeBom = "2025.11.01"
|
composeBom = "2025.12.01"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
navigation = "2.9.6"
|
navigation = "2.9.6"
|
||||||
viewmodel = "2.10.0"
|
viewmodel = "2.10.0"
|
||||||
@@ -19,10 +19,10 @@ 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.1"
|
exifinterface = "1.4.2"
|
||||||
healthConnect = "1.1.0-alpha07"
|
healthConnect = "1.1.0"
|
||||||
detekt = "1.23.7"
|
detekt = "1.23.8"
|
||||||
spotless = "6.25.0"
|
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" }
|
||||||
|
|||||||
3
branding/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
*.tmp
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
@@ -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/source/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
branding/source/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
branding/source/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
67
branding/source/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/source/Icon.icon/Assets/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
branding/source/Icon.icon/Assets/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
45
branding/source/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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,52 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
D28C33372F0F87D60040FE49 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D28C33312F0F87D60040FE49 /* Assets.xcassets */; };
|
||||||
|
D28C33382F0F87D60040FE49 /* Balls.icon in Resources */ = {isa = PBXBuildFile; fileRef = D28C33322F0F87D60040FE49 /* Balls.icon */; };
|
||||||
|
D28C33392F0F87D60040FE49 /* Icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = D28C33342F0F87D60040FE49 /* Icon.icon */; };
|
||||||
|
D28C333B2F0F87D60040FE49 /* AscentlyShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */; };
|
||||||
|
D28C333C2F0F87D60040FE49 /* SessionIntentSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */; };
|
||||||
|
D28C333D2F0F87D60040FE49 /* ToggleSessionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */; };
|
||||||
|
D28C333E2F0F87D60040FE49 /* AsyncImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */; };
|
||||||
|
D28C333F2F0F87D60040FE49 /* CameraImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */; };
|
||||||
|
D28C33402F0F87D60040FE49 /* PhotoOptionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */; };
|
||||||
|
D28C33412F0F87D60040FE49 /* ActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */; };
|
||||||
|
D28C33422F0F87D60040FE49 /* BackupFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33032F0F87D60040FE49 /* BackupFormat.swift */; };
|
||||||
|
D28C33432F0F87D60040FE49 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33042F0F87D60040FE49 /* DataModels.swift */; };
|
||||||
|
D28C33442F0F87D60040FE49 /* DeltaSyncFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */; };
|
||||||
|
D28C33452F0F87D60040FE49 /* ServerSyncProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */; };
|
||||||
|
D28C33462F0F87D60040FE49 /* SyncMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33082F0F87D60040FE49 /* SyncMerger.swift */; };
|
||||||
|
D28C33472F0F87D60040FE49 /* SyncProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33092F0F87D60040FE49 /* SyncProvider.swift */; };
|
||||||
|
D28C33482F0F87D60040FE49 /* HealthKitService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330B2F0F87D60040FE49 /* HealthKitService.swift */; };
|
||||||
|
D28C33492F0F87D60040FE49 /* MusicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330C2F0F87D60040FE49 /* MusicService.swift */; };
|
||||||
|
D28C334A2F0F87D60040FE49 /* SyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C330D2F0F87D60040FE49 /* SyncService.swift */; };
|
||||||
|
D28C334B2F0F87D60040FE49 /* AppIconHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33102F0F87D60040FE49 /* AppIconHelper.swift */; };
|
||||||
|
D28C334C2F0F87D60040FE49 /* AppLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33112F0F87D60040FE49 /* AppLogger.swift */; };
|
||||||
|
D28C334D2F0F87D60040FE49 /* DataStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33122F0F87D60040FE49 /* DataStateManager.swift */; };
|
||||||
|
D28C334E2F0F87D60040FE49 /* IconTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33132F0F87D60040FE49 /* IconTestView.swift */; };
|
||||||
|
D28C334F2F0F87D60040FE49 /* ImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33142F0F87D60040FE49 /* ImageManager.swift */; };
|
||||||
|
D28C33502F0F87D60040FE49 /* ImageNamingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */; };
|
||||||
|
D28C33512F0F87D60040FE49 /* OrientationAwareImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */; };
|
||||||
|
D28C33522F0F87D60040FE49 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33172F0F87D60040FE49 /* ThemeManager.swift */; };
|
||||||
|
D28C33532F0F87D60040FE49 /* ZipUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33182F0F87D60040FE49 /* ZipUtils.swift */; };
|
||||||
|
D28C33542F0F87D60040FE49 /* ClimbingDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */; };
|
||||||
|
D28C33552F0F87D60040FE49 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */; };
|
||||||
|
D28C33562F0F87D60040FE49 /* AddAttemptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */; };
|
||||||
|
D28C33572F0F87D60040FE49 /* AddEditGymView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */; };
|
||||||
|
D28C33582F0F87D60040FE49 /* AddEditProblemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */; };
|
||||||
|
D28C33592F0F87D60040FE49 /* AddEditSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */; };
|
||||||
|
D28C335A2F0F87D60040FE49 /* GymDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33232F0F87D60040FE49 /* GymDetailView.swift */; };
|
||||||
|
D28C335B2F0F87D60040FE49 /* ProblemDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */; };
|
||||||
|
D28C335C2F0F87D60040FE49 /* SessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33252F0F87D60040FE49 /* SessionDetailView.swift */; };
|
||||||
|
D28C335D2F0F87D60040FE49 /* AnalyticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33272F0F87D60040FE49 /* AnalyticsView.swift */; };
|
||||||
|
D28C335E2F0F87D60040FE49 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33282F0F87D60040FE49 /* CalendarView.swift */; };
|
||||||
|
D28C335F2F0F87D60040FE49 /* GymsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33292F0F87D60040FE49 /* GymsView.swift */; };
|
||||||
|
D28C33602F0F87D60040FE49 /* LiveActivityDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */; };
|
||||||
|
D28C33612F0F87D60040FE49 /* ProblemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332B2F0F87D60040FE49 /* ProblemsView.swift */; };
|
||||||
|
D28C33622F0F87D60040FE49 /* SessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332C2F0F87D60040FE49 /* SessionsView.swift */; };
|
||||||
|
D28C33632F0F87D60040FE49 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C332D2F0F87D60040FE49 /* SettingsView.swift */; };
|
||||||
|
D28C33642F0F87D60040FE49 /* AscentlyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33302F0F87D60040FE49 /* AscentlyApp.swift */; };
|
||||||
|
D28C33652F0F87D60040FE49 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28C33332F0F87D60040FE49 /* ContentView.swift */; };
|
||||||
D2FE94822E78E95C008CDB25 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE94802E78E958008CDB25 /* ActivityKit.framework */; };
|
D2FE94822E78E95C008CDB25 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE94802E78E958008CDB25 /* ActivityKit.framework */; };
|
||||||
D2FE948D2E78FEE0008CDB25 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */; };
|
D2FE948D2E78FEE0008CDB25 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */; };
|
||||||
D2FE948F2E78FEE0008CDB25 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948E2E78FEE0008CDB25 /* SwiftUI.framework */; };
|
D2FE948F2E78FEE0008CDB25 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FE948E2E78FEE0008CDB25 /* SwiftUI.framework */; };
|
||||||
@@ -48,6 +94,54 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
D24C19682E75002A0045894C /* Ascently.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ascently.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D24C19682E75002A0045894C /* Ascently.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ascently.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionStatusLiveExtension.entitlements; sourceTree = "<group>"; };
|
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionStatusLiveExtension.entitlements; sourceTree = "<group>"; };
|
||||||
|
D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AscentlyShortcuts.swift; sourceTree = "<group>"; };
|
||||||
|
D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIntentSupport.swift; sourceTree = "<group>"; };
|
||||||
|
D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleSessionIntent.swift; sourceTree = "<group>"; };
|
||||||
|
D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncImageView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraImagePicker.swift; sourceTree = "<group>"; };
|
||||||
|
D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoOptionSheet.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAttributes.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33032F0F87D60040FE49 /* BackupFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupFormat.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33042F0F87D60040FE49 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeltaSyncFormat.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSyncProvider.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33082F0F87D60040FE49 /* SyncMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMerger.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33092F0F87D60040FE49 /* SyncProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncProvider.swift; sourceTree = "<group>"; };
|
||||||
|
D28C330B2F0F87D60040FE49 /* HealthKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitService.swift; sourceTree = "<group>"; };
|
||||||
|
D28C330C2F0F87D60040FE49 /* MusicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicService.swift; sourceTree = "<group>"; };
|
||||||
|
D28C330D2F0F87D60040FE49 /* SyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncService.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33102F0F87D60040FE49 /* AppIconHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconHelper.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33112F0F87D60040FE49 /* AppLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogger.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33122F0F87D60040FE49 /* DataStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStateManager.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33132F0F87D60040FE49 /* IconTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconTestView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33142F0F87D60040FE49 /* ImageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNamingUtils.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationAwareImage.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33172F0F87D60040FE49 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33182F0F87D60040FE49 /* ZipUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipUtils.swift; sourceTree = "<group>"; };
|
||||||
|
D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClimbingDataManager.swift; sourceTree = "<group>"; };
|
||||||
|
D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
|
||||||
|
D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAttemptView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditGymView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditProblemView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditSessionView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33232F0F87D60040FE49 /* GymDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33252F0F87D60040FE49 /* SessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33272F0F87D60040FE49 /* AnalyticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33282F0F87D60040FE49 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33292F0F87D60040FE49 /* GymsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymsView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityDebugView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C332B2F0F87D60040FE49 /* ProblemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemsView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C332C2F0F87D60040FE49 /* SessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C332D2F0F87D60040FE49 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C332F2F0F87D60040FE49 /* Ascently.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ascently.entitlements; sourceTree = "<group>"; };
|
||||||
|
D28C33302F0F87D60040FE49 /* AscentlyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AscentlyApp.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33312F0F87D60040FE49 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
D28C33322F0F87D60040FE49 /* Balls.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Balls.icon; sourceTree = "<group>"; };
|
||||||
|
D28C33332F0F87D60040FE49 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
D28C33342F0F87D60040FE49 /* Icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Icon.icon; sourceTree = "<group>"; };
|
||||||
|
D28C33352F0F87D60040FE49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D2F32FAD2E90B26500B1BC56 /* AscentlyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AscentlyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D2F32FAD2E90B26500B1BC56 /* AscentlyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AscentlyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D2FE94802E78E958008CDB25 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; };
|
D2FE94802E78E958008CDB25 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; };
|
||||||
D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionStatusLiveExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionStatusLiveExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -56,13 +150,6 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
D28C3C8B2E75111D00F7AEE9 /* Exceptions for "Ascently" folder in "Ascently" target */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
||||||
membershipExceptions = (
|
|
||||||
Info.plist,
|
|
||||||
);
|
|
||||||
target = D24C19672E75002A0045894C /* Ascently */;
|
|
||||||
};
|
|
||||||
D2FE94A42E78FEE1008CDB25 /* Exceptions for "SessionStatusLive" folder in "SessionStatusLiveExtension" target */ = {
|
D2FE94A42E78FEE1008CDB25 /* Exceptions for "SessionStatusLive" folder in "SessionStatusLiveExtension" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
@@ -73,14 +160,6 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
D24C196A2E75002A0045894C /* Ascently */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
D28C3C8B2E75111D00F7AEE9 /* Exceptions for "Ascently" folder in "Ascently" target */,
|
|
||||||
);
|
|
||||||
path = Ascently;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */ = {
|
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = AscentlyTests;
|
path = AscentlyTests;
|
||||||
@@ -129,7 +208,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
|
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
|
||||||
D24C196A2E75002A0045894C /* Ascently */,
|
D28C33362F0F87D60040FE49 /* Ascently */,
|
||||||
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
|
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
|
||||||
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */,
|
D2F32FAE2E90B26500B1BC56 /* AscentlyTests */,
|
||||||
D2FE947F2E78E958008CDB25 /* Frameworks */,
|
D2FE947F2E78E958008CDB25 /* Frameworks */,
|
||||||
@@ -147,6 +226,165 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D28C32FC2F0F87D60040FE49 /* AppIntents */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C32F92F0F87D60040FE49 /* AscentlyShortcuts.swift */,
|
||||||
|
D28C32FA2F0F87D60040FE49 /* SessionIntentSupport.swift */,
|
||||||
|
D28C32FB2F0F87D60040FE49 /* ToggleSessionIntent.swift */,
|
||||||
|
);
|
||||||
|
path = AppIntents;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33002F0F87D60040FE49 /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C32FD2F0F87D60040FE49 /* AsyncImageView.swift */,
|
||||||
|
D28C32FE2F0F87D60040FE49 /* CameraImagePicker.swift */,
|
||||||
|
D28C32FF2F0F87D60040FE49 /* PhotoOptionSheet.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33012F0F87D60040FE49 /* FocusFilter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
path = FocusFilter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33062F0F87D60040FE49 /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C33022F0F87D60040FE49 /* ActivityAttributes.swift */,
|
||||||
|
D28C33032F0F87D60040FE49 /* BackupFormat.swift */,
|
||||||
|
D28C33042F0F87D60040FE49 /* DataModels.swift */,
|
||||||
|
D28C33052F0F87D60040FE49 /* DeltaSyncFormat.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C330A2F0F87D60040FE49 /* Sync */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C33072F0F87D60040FE49 /* ServerSyncProvider.swift */,
|
||||||
|
D28C33082F0F87D60040FE49 /* SyncMerger.swift */,
|
||||||
|
D28C33092F0F87D60040FE49 /* SyncProvider.swift */,
|
||||||
|
);
|
||||||
|
path = Sync;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C330E2F0F87D60040FE49 /* Services */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C330A2F0F87D60040FE49 /* Sync */,
|
||||||
|
D28C330B2F0F87D60040FE49 /* HealthKitService.swift */,
|
||||||
|
D28C330C2F0F87D60040FE49 /* MusicService.swift */,
|
||||||
|
D28C330D2F0F87D60040FE49 /* SyncService.swift */,
|
||||||
|
);
|
||||||
|
path = Services;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C330F2F0F87D60040FE49 /* Spotlight */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
path = Spotlight;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33192F0F87D60040FE49 /* Utils */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C33102F0F87D60040FE49 /* AppIconHelper.swift */,
|
||||||
|
D28C33112F0F87D60040FE49 /* AppLogger.swift */,
|
||||||
|
D28C33122F0F87D60040FE49 /* DataStateManager.swift */,
|
||||||
|
D28C33132F0F87D60040FE49 /* IconTestView.swift */,
|
||||||
|
D28C33142F0F87D60040FE49 /* ImageManager.swift */,
|
||||||
|
D28C33152F0F87D60040FE49 /* ImageNamingUtils.swift */,
|
||||||
|
D28C33162F0F87D60040FE49 /* OrientationAwareImage.swift */,
|
||||||
|
D28C33172F0F87D60040FE49 /* ThemeManager.swift */,
|
||||||
|
D28C33182F0F87D60040FE49 /* ZipUtils.swift */,
|
||||||
|
);
|
||||||
|
path = Utils;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C331C2F0F87D60040FE49 /* ViewModels */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C331A2F0F87D60040FE49 /* ClimbingDataManager.swift */,
|
||||||
|
D28C331B2F0F87D60040FE49 /* LiveActivityManager.swift */,
|
||||||
|
);
|
||||||
|
path = ViewModels;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33212F0F87D60040FE49 /* AddEdit */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C331D2F0F87D60040FE49 /* AddAttemptView.swift */,
|
||||||
|
D28C331E2F0F87D60040FE49 /* AddEditGymView.swift */,
|
||||||
|
D28C331F2F0F87D60040FE49 /* AddEditProblemView.swift */,
|
||||||
|
D28C33202F0F87D60040FE49 /* AddEditSessionView.swift */,
|
||||||
|
);
|
||||||
|
path = AddEdit;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33222F0F87D60040FE49 /* Debug */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
path = Debug;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33262F0F87D60040FE49 /* Detail */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C33232F0F87D60040FE49 /* GymDetailView.swift */,
|
||||||
|
D28C33242F0F87D60040FE49 /* ProblemDetailView.swift */,
|
||||||
|
D28C33252F0F87D60040FE49 /* SessionDetailView.swift */,
|
||||||
|
);
|
||||||
|
path = Detail;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C332E2F0F87D60040FE49 /* Views */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C33212F0F87D60040FE49 /* AddEdit */,
|
||||||
|
D28C33222F0F87D60040FE49 /* Debug */,
|
||||||
|
D28C33262F0F87D60040FE49 /* Detail */,
|
||||||
|
D28C33272F0F87D60040FE49 /* AnalyticsView.swift */,
|
||||||
|
D28C33282F0F87D60040FE49 /* CalendarView.swift */,
|
||||||
|
D28C33292F0F87D60040FE49 /* GymsView.swift */,
|
||||||
|
D28C332A2F0F87D60040FE49 /* LiveActivityDebugView.swift */,
|
||||||
|
D28C332B2F0F87D60040FE49 /* ProblemsView.swift */,
|
||||||
|
D28C332C2F0F87D60040FE49 /* SessionsView.swift */,
|
||||||
|
D28C332D2F0F87D60040FE49 /* SettingsView.swift */,
|
||||||
|
);
|
||||||
|
path = Views;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D28C33362F0F87D60040FE49 /* Ascently */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D28C32FC2F0F87D60040FE49 /* AppIntents */,
|
||||||
|
D28C33002F0F87D60040FE49 /* Components */,
|
||||||
|
D28C33012F0F87D60040FE49 /* FocusFilter */,
|
||||||
|
D28C33062F0F87D60040FE49 /* Models */,
|
||||||
|
D28C330E2F0F87D60040FE49 /* Services */,
|
||||||
|
D28C330F2F0F87D60040FE49 /* Spotlight */,
|
||||||
|
D28C33192F0F87D60040FE49 /* Utils */,
|
||||||
|
D28C331C2F0F87D60040FE49 /* ViewModels */,
|
||||||
|
D28C332E2F0F87D60040FE49 /* Views */,
|
||||||
|
D28C332F2F0F87D60040FE49 /* Ascently.entitlements */,
|
||||||
|
D28C33302F0F87D60040FE49 /* AscentlyApp.swift */,
|
||||||
|
D28C33312F0F87D60040FE49 /* Assets.xcassets */,
|
||||||
|
D28C33322F0F87D60040FE49 /* Balls.icon */,
|
||||||
|
D28C33332F0F87D60040FE49 /* ContentView.swift */,
|
||||||
|
D28C33342F0F87D60040FE49 /* Icon.icon */,
|
||||||
|
D28C33352F0F87D60040FE49 /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = Ascently;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D2FE947F2E78E958008CDB25 /* Frameworks */ = {
|
D2FE947F2E78E958008CDB25 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -174,9 +412,6 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */,
|
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
D24C196A2E75002A0045894C /* Ascently */,
|
|
||||||
);
|
|
||||||
name = Ascently;
|
name = Ascently;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
@@ -277,6 +512,9 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D28C33372F0F87D60040FE49 /* Assets.xcassets in Resources */,
|
||||||
|
D28C33382F0F87D60040FE49 /* Balls.icon in Resources */,
|
||||||
|
D28C33392F0F87D60040FE49 /* Icon.icon in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -301,6 +539,49 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D28C333B2F0F87D60040FE49 /* AscentlyShortcuts.swift in Sources */,
|
||||||
|
D28C333C2F0F87D60040FE49 /* SessionIntentSupport.swift in Sources */,
|
||||||
|
D28C333D2F0F87D60040FE49 /* ToggleSessionIntent.swift in Sources */,
|
||||||
|
D28C333E2F0F87D60040FE49 /* AsyncImageView.swift in Sources */,
|
||||||
|
D28C333F2F0F87D60040FE49 /* CameraImagePicker.swift in Sources */,
|
||||||
|
D28C33402F0F87D60040FE49 /* PhotoOptionSheet.swift in Sources */,
|
||||||
|
D28C33412F0F87D60040FE49 /* ActivityAttributes.swift in Sources */,
|
||||||
|
D28C33422F0F87D60040FE49 /* BackupFormat.swift in Sources */,
|
||||||
|
D28C33432F0F87D60040FE49 /* DataModels.swift in Sources */,
|
||||||
|
D28C33442F0F87D60040FE49 /* DeltaSyncFormat.swift in Sources */,
|
||||||
|
D28C33452F0F87D60040FE49 /* ServerSyncProvider.swift in Sources */,
|
||||||
|
D28C33462F0F87D60040FE49 /* SyncMerger.swift in Sources */,
|
||||||
|
D28C33472F0F87D60040FE49 /* SyncProvider.swift in Sources */,
|
||||||
|
D28C33482F0F87D60040FE49 /* HealthKitService.swift in Sources */,
|
||||||
|
D28C33492F0F87D60040FE49 /* MusicService.swift in Sources */,
|
||||||
|
D28C334A2F0F87D60040FE49 /* SyncService.swift in Sources */,
|
||||||
|
D28C334B2F0F87D60040FE49 /* AppIconHelper.swift in Sources */,
|
||||||
|
D28C334C2F0F87D60040FE49 /* AppLogger.swift in Sources */,
|
||||||
|
D28C334D2F0F87D60040FE49 /* DataStateManager.swift in Sources */,
|
||||||
|
D28C334E2F0F87D60040FE49 /* IconTestView.swift in Sources */,
|
||||||
|
D28C334F2F0F87D60040FE49 /* ImageManager.swift in Sources */,
|
||||||
|
D28C33502F0F87D60040FE49 /* ImageNamingUtils.swift in Sources */,
|
||||||
|
D28C33512F0F87D60040FE49 /* OrientationAwareImage.swift in Sources */,
|
||||||
|
D28C33522F0F87D60040FE49 /* ThemeManager.swift in Sources */,
|
||||||
|
D28C33532F0F87D60040FE49 /* ZipUtils.swift in Sources */,
|
||||||
|
D28C33542F0F87D60040FE49 /* ClimbingDataManager.swift in Sources */,
|
||||||
|
D28C33552F0F87D60040FE49 /* LiveActivityManager.swift in Sources */,
|
||||||
|
D28C33562F0F87D60040FE49 /* AddAttemptView.swift in Sources */,
|
||||||
|
D28C33572F0F87D60040FE49 /* AddEditGymView.swift in Sources */,
|
||||||
|
D28C33582F0F87D60040FE49 /* AddEditProblemView.swift in Sources */,
|
||||||
|
D28C33592F0F87D60040FE49 /* AddEditSessionView.swift in Sources */,
|
||||||
|
D28C335A2F0F87D60040FE49 /* GymDetailView.swift in Sources */,
|
||||||
|
D28C335B2F0F87D60040FE49 /* ProblemDetailView.swift in Sources */,
|
||||||
|
D28C335C2F0F87D60040FE49 /* SessionDetailView.swift in Sources */,
|
||||||
|
D28C335D2F0F87D60040FE49 /* AnalyticsView.swift in Sources */,
|
||||||
|
D28C335E2F0F87D60040FE49 /* CalendarView.swift in Sources */,
|
||||||
|
D28C335F2F0F87D60040FE49 /* GymsView.swift in Sources */,
|
||||||
|
D28C33602F0F87D60040FE49 /* LiveActivityDebugView.swift in Sources */,
|
||||||
|
D28C33612F0F87D60040FE49 /* ProblemsView.swift in Sources */,
|
||||||
|
D28C33622F0F87D60040FE49 /* SessionsView.swift in Sources */,
|
||||||
|
D28C33632F0F87D60040FE49 /* SettingsView.swift in Sources */,
|
||||||
|
D28C33642F0F87D60040FE49 /* AscentlyApp.swift in Sources */,
|
||||||
|
D28C33652F0F87D60040FE49 /* ContentView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -460,12 +741,13 @@
|
|||||||
D24C19742E75002A0045894C /* Debug */ = {
|
D24C19742E75002A0045894C /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = Balls;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 39;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -487,7 +769,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
||||||
MARKETING_VERSION = 2.5.2;
|
MARKETING_VERSION = 2.6.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -508,12 +790,13 @@
|
|||||||
D24C19752E75002A0045894C /* Release */ = {
|
D24C19752E75002A0045894C /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = Balls;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 39;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -535,7 +818,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
||||||
MARKETING_VERSION = 2.5.2;
|
MARKETING_VERSION = 2.6.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -602,7 +885,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 39;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -613,7 +896,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.5.2;
|
MARKETING_VERSION = 2.6.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -632,7 +915,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 39;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -643,7 +926,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.5.2;
|
MARKETING_VERSION = 2.6.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"filename": "app_icon_1024.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"platform": "ios",
|
|
||||||
"size": "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"filename": "app_icon_1024_dark.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"platform": "ios",
|
|
||||||
"size": "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "tinted"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"filename": "app_icon_1024_tinted.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"platform": "ios",
|
|
||||||
"size": "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"author": "xcode",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#1A1A1A" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#FFFFFF" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="transparent" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#000000" opacity="0.8"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#000000" opacity="0.9"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 443 B |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
67
ios/Ascently/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
ios/Ascently/Icon.icon/Assets/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
ios/Ascently/Icon.icon/Assets/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
45
ios/Ascently/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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>NSSupportsLiveActivities</key>
|
<key>NSSupportsLiveActivities</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>This app needs access to your photo library to add photos to climbing problems.</string>
|
<string>This app needs access to your photo library to add photos to climbing problems.</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ import SwiftUI
|
|||||||
private func validateAssetConfiguration() {
|
private func validateAssetConfiguration() {
|
||||||
// Check if main bundle contains the expected icon assets
|
// Check if main bundle contains the expected icon assets
|
||||||
let expectedAssets = [
|
let expectedAssets = [
|
||||||
"AppIcon",
|
"Icon",
|
||||||
"AppLogo",
|
"AppLogo",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ struct ProgressChartSection: View {
|
|||||||
.font(.title)
|
.font(.title)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Text("No data available for \(selectedSystem.displayName) system")
|
Text("No data available.")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,34 +57,29 @@ struct GymDetailView: View {
|
|||||||
.navigationTitle(gym?.name ?? "Gym Details")
|
.navigationTitle(gym?.name ?? "Gym Details")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
if gym != nil {
|
if gym != nil {
|
||||||
Menu {
|
Button {
|
||||||
Button {
|
showingDeleteAlert = true
|
||||||
// Navigate to edit view
|
|
||||||
} label: {
|
|
||||||
Label("Edit Gym", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(role: .destructive) {
|
|
||||||
showingDeleteAlert = true
|
|
||||||
} label: {
|
|
||||||
Label("Delete Gym", systemImage: "trash")
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis.circle")
|
Image(systemName: "trash")
|
||||||
}
|
}
|
||||||
|
.tint(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Delete Gym", isPresented: $showingDeleteAlert) {
|
.confirmationDialog(
|
||||||
Button("Cancel", role: .cancel) {}
|
"Delete Gym",
|
||||||
|
isPresented: $showingDeleteAlert,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let gym = gym {
|
if let gym = gym {
|
||||||
dataManager.deleteGym(gym)
|
dataManager.deleteGym(gym)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this gym? This will also delete all problems and sessions associated with this gym."
|
"Are you sure you want to delete this gym? This will also delete all problems and sessions associated with this gym."
|
||||||
@@ -131,7 +126,6 @@ struct GymHeaderCard: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supported Climb Types
|
|
||||||
if !gym.supportedClimbTypes.isEmpty {
|
if !gym.supportedClimbTypes.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Climb Types")
|
Text("Climb Types")
|
||||||
@@ -157,7 +151,6 @@ struct GymHeaderCard: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Difficulty Systems
|
|
||||||
if !gym.difficultySystems.isEmpty {
|
if !gym.difficultySystems.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Difficulty Systems")
|
Text("Difficulty Systems")
|
||||||
@@ -330,6 +323,12 @@ struct SessionRowCard: View {
|
|||||||
let session: ClimbSession
|
let session: ClimbSession
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
|
|
||||||
|
private static let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
private var sessionAttempts: [Attempt] {
|
private var sessionAttempts: [Attempt] {
|
||||||
dataManager.attempts(forSession: session.id)
|
dataManager.attempts(forSession: session.id)
|
||||||
}
|
}
|
||||||
@@ -357,7 +356,7 @@ struct SessionRowCard: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("\(formatDate(session.date)) • \(sessionAttempts.count) attempts")
|
Text("\(Self.dateFormatter.string(from: session.date)) • \(sessionAttempts.count) attempts")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@@ -377,12 +376,6 @@ struct SessionRowCard: View {
|
|||||||
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatDate(_ date: Date) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EmptyGymStateView: View {
|
struct EmptyGymStateView: View {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ struct ProblemDetailView: View {
|
|||||||
.navigationTitle("Problem Details")
|
.navigationTitle("Problem Details")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
if problem != nil {
|
if problem != nil {
|
||||||
Menu {
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
@@ -81,14 +81,18 @@ struct ProblemDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Delete Problem", isPresented: $showingDeleteAlert) {
|
.confirmationDialog(
|
||||||
Button("Cancel", role: .cancel) {}
|
"Delete Problem",
|
||||||
|
isPresented: $showingDeleteAlert,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let problem = problem {
|
if let problem = problem {
|
||||||
dataManager.deleteProblem(problem)
|
dataManager.deleteProblem(problem)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this problem? This will also delete all attempts associated with this problem."
|
"Are you sure you want to delete this problem? This will also delete all attempts associated with this problem."
|
||||||
@@ -227,6 +231,12 @@ struct ProgressSummaryCard: View {
|
|||||||
let firstSuccess: (date: Date, result: AttemptResult)?
|
let firstSuccess: (date: Date, result: AttemptResult)?
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
|
|
||||||
|
private static let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
Text("Progress Summary")
|
Text("Progress Summary")
|
||||||
@@ -251,7 +261,7 @@ struct ProgressSummaryCard: View {
|
|||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"\(formatDate(firstSuccess.date)) (\(firstSuccess.result.displayName))"
|
"\(Self.dateFormatter.string(from: firstSuccess.date)) (\(firstSuccess.result.displayName))"
|
||||||
)
|
)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(themeManager.accentColor)
|
.foregroundColor(themeManager.accentColor)
|
||||||
@@ -266,12 +276,6 @@ struct ProgressSummaryCard: View {
|
|||||||
.fill(.regularMaterial)
|
.fill(.regularMaterial)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatDate(_ date: Date) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PhotosSection: View {
|
struct PhotosSection: View {
|
||||||
@@ -360,6 +364,12 @@ struct AttemptHistoryCard: View {
|
|||||||
let session: ClimbSession
|
let session: ClimbSession
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
|
|
||||||
|
private static let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
private var gym: Gym? {
|
private var gym: Gym? {
|
||||||
dataManager.gym(withId: session.gymId)
|
dataManager.gym(withId: session.gymId)
|
||||||
}
|
}
|
||||||
@@ -368,7 +378,7 @@ struct AttemptHistoryCard: View {
|
|||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(formatDate(session.date))
|
Text(Self.dateFormatter.string(from: session.date))
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
@@ -403,12 +413,6 @@ struct AttemptHistoryCard: View {
|
|||||||
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatDate(_ date: Date) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageViewerView: View {
|
struct ImageViewerView: View {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ struct SessionDetailView: View {
|
|||||||
.navigationTitle("Session Details")
|
.navigationTitle("Session Details")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
if let session = session {
|
if let session = session {
|
||||||
if session.status == .active {
|
if session.status == .active {
|
||||||
Button("End Session") {
|
Button("End Session") {
|
||||||
@@ -143,15 +143,12 @@ struct SessionDetailView: View {
|
|||||||
}
|
}
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
} else {
|
} else {
|
||||||
Menu {
|
Button {
|
||||||
Button(role: .destructive) {
|
showingDeleteAlert = true
|
||||||
showingDeleteAlert = true
|
|
||||||
} label: {
|
|
||||||
Label("Delete Session", systemImage: "trash")
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis.circle")
|
Image(systemName: "trash")
|
||||||
}
|
}
|
||||||
|
.tint(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +185,7 @@ struct SessionDetailView: View {
|
|||||||
Button(action: { showingAddAttempt = true }) {
|
Button(action: { showingAddAttempt = true }) {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.white) // Keep white for contrast on colored button
|
.foregroundColor(.white)
|
||||||
.frame(width: 56, height: 56)
|
.frame(width: 56, height: 56)
|
||||||
.background(Circle().fill(themeManager.accentColor))
|
.background(Circle().fill(themeManager.accentColor))
|
||||||
.shadow(radius: 4)
|
.shadow(radius: 4)
|
||||||
@@ -196,14 +193,18 @@ struct SessionDetailView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Delete Session", isPresented: $showingDeleteAlert) {
|
.confirmationDialog(
|
||||||
Button("Cancel", role: .cancel) {}
|
"Delete Session",
|
||||||
|
isPresented: $showingDeleteAlert,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let session = session {
|
if let session = session {
|
||||||
dataManager.deleteSession(session)
|
dataManager.deleteSession(session)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this session? This will also delete all attempts associated with this session."
|
"Are you sure you want to delete this session? This will also delete all attempts associated with this session."
|
||||||
@@ -239,6 +240,12 @@ struct SessionHeaderCard: View {
|
|||||||
let stats: SessionStats
|
let stats: SessionStats
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
|
|
||||||
|
private static let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .full
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@@ -246,7 +253,7 @@ struct SessionHeaderCard: View {
|
|||||||
.font(.title)
|
.font(.title)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text(formatDate(session.date))
|
Text(Self.dateFormatter.string(from: session.date))
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(themeManager.accentColor)
|
.foregroundColor(themeManager.accentColor)
|
||||||
|
|
||||||
@@ -273,7 +280,6 @@ struct SessionHeaderCard: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status indicator
|
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: session.status == .active ? "play.fill" : "checkmark.circle.fill")
|
Image(systemName: session.status == .active ? "play.fill" : "checkmark.circle.fill")
|
||||||
.foregroundColor(session.status == .active ? .green : themeManager.accentColor)
|
.foregroundColor(session.status == .active ? .green : themeManager.accentColor)
|
||||||
@@ -298,12 +304,6 @@ struct SessionHeaderCard: View {
|
|||||||
.fill(.regularMaterial)
|
.fill(.regularMaterial)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatDate(_ date: Date) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .full
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SessionStatsCard: View {
|
struct SessionStatsCard: View {
|
||||||
@@ -356,8 +356,6 @@ struct StatItem: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttemptsSection removed as it is now integrated into the main List
|
|
||||||
|
|
||||||
struct AttemptCard: View {
|
struct AttemptCard: View {
|
||||||
let attempt: Attempt
|
let attempt: Attempt
|
||||||
let problem: Problem
|
let problem: Problem
|
||||||
@@ -404,7 +402,7 @@ struct AttemptCard: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.fill(Color(uiColor: .secondarySystemGroupedBackground)) // Better contrast in light mode
|
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
||||||
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,16 +77,23 @@ struct GymsList: View {
|
|||||||
.tint(.indigo)
|
.tint(.indigo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Delete Gym", isPresented: .constant(gymToDelete != nil)) {
|
.confirmationDialog(
|
||||||
Button("Cancel", role: .cancel) {
|
"Delete Gym",
|
||||||
gymToDelete = nil
|
isPresented: .init(
|
||||||
}
|
get: { gymToDelete != nil },
|
||||||
|
set: { if !$0 { gymToDelete = nil } }
|
||||||
|
),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let gym = gymToDelete {
|
if let gym = gymToDelete {
|
||||||
dataManager.deleteGym(gym)
|
dataManager.deleteGym(gym)
|
||||||
gymToDelete = nil
|
gymToDelete = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
gymToDelete = nil
|
||||||
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this gym? This will also delete all associated problems and sessions."
|
"Are you sure you want to delete this gym? This will also delete all associated problems and sessions."
|
||||||
|
|||||||
@@ -209,16 +209,23 @@ struct ProblemsView: View {
|
|||||||
.sheet(item: $problemToEdit) { problem in
|
.sheet(item: $problemToEdit) { problem in
|
||||||
AddEditProblemView(problemId: problem.id)
|
AddEditProblemView(problemId: problem.id)
|
||||||
}
|
}
|
||||||
.alert("Delete Problem", isPresented: .constant(problemToDelete != nil)) {
|
.confirmationDialog(
|
||||||
Button("Cancel", role: .cancel) {
|
"Delete Problem",
|
||||||
problemToDelete = nil
|
isPresented: .init(
|
||||||
}
|
get: { problemToDelete != nil },
|
||||||
|
set: { if !$0 { problemToDelete = nil } }
|
||||||
|
),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let problem = problemToDelete {
|
if let problem = problemToDelete {
|
||||||
dataManager.deleteProblem(problem)
|
dataManager.deleteProblem(problem)
|
||||||
problemToDelete = nil
|
problemToDelete = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
problemToDelete = nil
|
||||||
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this problem? This will also delete all associated attempts."
|
"Are you sure you want to delete this problem? This will also delete all associated attempts."
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ struct SessionsView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// View mode toggle
|
|
||||||
if !dataManager.sessions.isEmpty || dataManager.activeSession != nil {
|
if !dataManager.sessions.isEmpty || dataManager.activeSession != nil {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
@@ -116,7 +115,6 @@ struct SessionsList: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
// Active session banner section
|
|
||||||
if let activeSession = dataManager.activeSession,
|
if let activeSession = dataManager.activeSession,
|
||||||
let gym = dataManager.gym(withId: activeSession.gymId)
|
let gym = dataManager.gym(withId: activeSession.gymId)
|
||||||
{
|
{
|
||||||
@@ -130,7 +128,6 @@ struct SessionsList: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completed sessions section
|
|
||||||
if !completedSessions.isEmpty {
|
if !completedSessions.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
ForEach(completedSessions) { session in
|
ForEach(completedSessions) { session in
|
||||||
@@ -156,16 +153,23 @@ struct SessionsList: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.alert("Delete Session", isPresented: .constant(sessionToDelete != nil)) {
|
.confirmationDialog(
|
||||||
Button("Cancel", role: .cancel) {
|
"Delete Session",
|
||||||
sessionToDelete = nil
|
isPresented: .init(
|
||||||
}
|
get: { sessionToDelete != nil },
|
||||||
|
set: { if !$0 { sessionToDelete = nil } }
|
||||||
|
),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let session = sessionToDelete {
|
if let session = sessionToDelete {
|
||||||
dataManager.deleteSession(session)
|
dataManager.deleteSession(session)
|
||||||
sessionToDelete = nil
|
sessionToDelete = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
sessionToDelete = nil
|
||||||
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this session? This will also delete all attempts associated with this session."
|
"Are you sure you want to delete this session? This will also delete all attempts associated with this session."
|
||||||
@@ -178,18 +182,8 @@ struct ActiveSessionBanner: View {
|
|||||||
let session: ClimbSession
|
let session: ClimbSession
|
||||||
let gym: Gym
|
let gym: Gym
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
@State private var navigateToDetail = false
|
|
||||||
|
|
||||||
// Access MusicService via DataManager if possible, or EnvironmentObject if injected
|
|
||||||
// Since DataManager holds MusicService, we can access it through there if we expose it or inject it.
|
|
||||||
// In SettingsView we saw .environmentObject(dataManager.musicService).
|
|
||||||
// We should probably inject it here too or access via dataManager if it's public.
|
|
||||||
// Let's check ClimbingDataManager again. It has `let musicService = MusicService.shared`.
|
|
||||||
// But it's not @Published so it won't trigger updates unless we observe the service itself.
|
|
||||||
// The best way is to use @EnvironmentObject var musicService: MusicService
|
|
||||||
// and ensure it's injected in the parent view.
|
|
||||||
|
|
||||||
@EnvironmentObject var musicService: MusicService
|
@EnvironmentObject var musicService: MusicService
|
||||||
|
@State private var navigateToDetail = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -251,7 +245,6 @@ struct ActiveSessionBanner: View {
|
|||||||
.fill(.green.opacity(0.1))
|
.fill(.green.opacity(0.1))
|
||||||
.stroke(.green.opacity(0.3), lineWidth: 1)
|
.stroke(.green.opacity(0.3), lineWidth: 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
.navigationDestination(isPresented: $navigateToDetail) {
|
.navigationDestination(isPresented: $navigateToDetail) {
|
||||||
SessionDetailView(sessionId: session.id)
|
SessionDetailView(sessionId: session.id)
|
||||||
}
|
}
|
||||||
@@ -262,6 +255,12 @@ struct SessionRow: View {
|
|||||||
let session: ClimbSession
|
let session: ClimbSession
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
|
|
||||||
|
private static let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
private var gym: Gym? {
|
private var gym: Gym? {
|
||||||
dataManager.gym(withId: session.gymId)
|
dataManager.gym(withId: session.gymId)
|
||||||
}
|
}
|
||||||
@@ -275,7 +274,7 @@ struct SessionRow: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(formatDate(session.date))
|
Text(Self.dateFormatter.string(from: session.date))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@@ -295,12 +294,6 @@ struct SessionRow: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatDate(_ date: Date) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EmptySessionsView: View {
|
struct EmptySessionsView: View {
|
||||||
|
|||||||