Compare commits

...

3 Commits

Author SHA1 Message Date
11850408be Testing 2026-01-07 23:37:22 -07:00
37beb4abb6 Re-worked assets 2026-01-07 13:52:01 -07:00
d263c6c87e iOS and Android dependency updates and optimizations 2026-01-06 12:27:28 -07:00
58 changed files with 839 additions and 761 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,20 +24,31 @@ 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()) } val sessionId = activeSession.id
val startTimeString = activeSession.startTime
val gymName = gym?.name ?: "Unknown Gym"
var elapsedSeconds by remember(sessionId) { mutableLongStateOf(0L) }
LaunchedEffect(sessionId, startTimeString) {
if (startTimeString == null) return@LaunchedEffect
LaunchedEffect(Unit) {
while (true) { while (true) {
delay(1000) // Update every second elapsedSeconds = calculateElapsedSeconds(startTimeString)
currentTime = LocalDateTime.now() delay(1000)
} }
} }
val durationText = remember(elapsedSeconds) {
formatDuration(elapsedSeconds)
}
Card( Card(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onSessionClick() }, .clickable { onSessionClick() },
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
@@ -51,12 +63,10 @@ fun ActiveSessionBanner(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Row( Row(verticalAlignment = Alignment.CenterVertically) {
verticalAlignment = Alignment.CenterVertically,
) {
Icon( Icon(
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
contentDescription = "Active session", contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
) )
@@ -72,24 +82,23 @@ fun ActiveSessionBanner(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = gym?.name ?: "Unknown Gym", text = gymName,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer, color = MaterialTheme.colorScheme.onPrimaryContainer,
) )
activeSession.startTime?.let { startTime -> if (startTimeString != null) {
val duration = calculateDuration(startTime, currentTime)
Text( Text(
text = duration, text = durationText,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f), color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
) )
} }
} }
IconButton( FilledIconButton(
onClick = onEndSession, onClick = onEndSession,
colors = IconButtonDefaults.iconButtonColors( colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.error, containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError, contentColor = MaterialTheme.colorScheme.onError,
), ),
@@ -102,22 +111,25 @@ fun ActiveSessionBanner(
} }
} }
} }
private fun calculateElapsedSeconds(startTimeString: String): Long {
return try {
val startTime = DateFormatUtils.parseISO8601(startTimeString) ?: return 0L
val now = Instant.now()
ChronoUnit.SECONDS.between(startTime, now).coerceAtLeast(0)
} catch (_: Exception) {
0L
}
} }
private fun calculateDuration(startTimeString: String, currentTime: LocalDateTime): String { private fun formatDuration(totalSeconds: Long): String {
return try {
val startTime = LocalDateTime.parse(startTimeString)
val totalSeconds = ChronoUnit.SECONDS.between(startTime, currentTime)
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
when { return when {
hours > 0 -> "${hours}h ${minutes}m ${seconds}s" hours > 0 -> "${hours}h ${minutes}m ${seconds}s"
minutes > 0 -> "${minutes}m ${seconds}s" minutes > 0 -> "${minutes}m ${seconds}s"
else -> "${totalSeconds}s" else -> "${seconds}s"
}
} catch (_: Exception) {
"Active"
} }
} }

View File

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

View File

@@ -225,25 +225,23 @@ 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(
@@ -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

View File

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

View File

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

View File

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

View File

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

@@ -1,3 +0,0 @@
*.tmp
.DS_Store
*.log

View File

@@ -1,394 +0,0 @@
#!/usr/bin/env python3
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Callable, TypedDict
from PIL import Image, ImageDraw
class Polygon(TypedDict):
coords: list[tuple[float, float]]
fill: str
SCRIPT_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPT_DIR.parent
SOURCE_DIR = SCRIPT_DIR / "source"
LOGOS_DIR = SCRIPT_DIR / "logos"
def parse_svg_polygons(svg_path: Path) -> list[Polygon]:
tree = ET.parse(svg_path)
root = tree.getroot()
ns = {"svg": "http://www.w3.org/2000/svg"}
polygons = root.findall(".//svg:polygon", ns)
if not polygons:
polygons = root.findall(".//polygon")
result: list[Polygon] = []
for poly in polygons:
points_str = poly.get("points", "").strip()
fill = poly.get("fill", "#000000")
coords: list[tuple[float, float]] = []
for pair in points_str.split():
x, y = pair.split(",")
coords.append((float(x), float(y)))
result.append({"coords": coords, "fill": fill})
return result
def get_bbox(polygons: list[Polygon]) -> dict[str, float]:
all_coords: list[tuple[float, float]] = []
for poly in polygons:
all_coords.extend(poly["coords"])
xs = [c[0] for c in all_coords]
ys = [c[1] for c in all_coords]
return {
"min_x": min(xs),
"max_x": max(xs),
"min_y": min(ys),
"max_y": max(ys),
"width": max(xs) - min(xs),
"height": max(ys) - min(ys),
}
def scale_and_center(
polygons: list[Polygon], viewbox_size: float, target_width: float
) -> list[Polygon]:
bbox = get_bbox(polygons)
scale = target_width / bbox["width"]
center = viewbox_size / 2
scaled_polys: list[Polygon] = []
for poly in polygons:
scaled_coords = [(x * scale, y * scale) for x, y in poly["coords"]]
scaled_polys.append({"coords": scaled_coords, "fill": poly["fill"]})
scaled_bbox = get_bbox(scaled_polys)
current_center_x = (scaled_bbox["min_x"] + scaled_bbox["max_x"]) / 2
current_center_y = (scaled_bbox["min_y"] + scaled_bbox["max_y"]) / 2
offset_x = center - current_center_x
offset_y = center - current_center_y
final_polys: list[Polygon] = []
for poly in scaled_polys:
final_coords = [(x + offset_x, y + offset_y) for x, y in poly["coords"]]
final_polys.append({"coords": final_coords, "fill": poly["fill"]})
return final_polys
def format_svg_points(coords: list[tuple[float, float]]) -> str:
return " ".join(f"{x:.3f},{y:.3f}" for x, y in coords)
def format_android_path(coords: list[tuple[float, float]]) -> str:
points = " ".join(f"{x:.3f},{y:.3f}" for x, y in coords)
pairs = points.split()
return f"M{pairs[0]} L{pairs[1]} L{pairs[2]} Z"
def generate_svg(polygons: list[Polygon], width: int, height: int) -> str:
lines = [
f'<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">'
]
for poly in polygons:
points = format_svg_points(poly["coords"])
lines.append(f' <polygon points="{points}" fill="{poly["fill"]}"/>')
lines.append("</svg>")
return "\n".join(lines)
def generate_android_vector(
polygons: list[Polygon], width: int, height: int, viewbox: int
) -> str:
lines = [
'<?xml version="1.0" encoding="utf-8"?>',
'<vector xmlns:android="http://schemas.android.com/apk/res/android"',
f' android:width="{width}dp"',
f' android:height="{height}dp"',
f' android:viewportWidth="{viewbox}"',
f' android:viewportHeight="{viewbox}">',
]
for poly in polygons:
path = format_android_path(poly["coords"])
lines.append(
f' <path android:fillColor="{poly["fill"]}" android:pathData="{path}" />'
)
lines.append("</vector>")
return "\n".join(lines)
def rasterize_svg(
svg_path: Path,
output_path: Path,
size: int,
bg_color: tuple[int, int, int, int] | None = None,
circular: bool = False,
) -> None:
from xml.dom import minidom
doc = minidom.parse(str(svg_path))
img = Image.new(
"RGBA", (size, size), (255, 255, 255, 0) if bg_color is None else bg_color
)
draw = ImageDraw.Draw(img)
svg_elem = doc.getElementsByTagName("svg")[0]
viewbox = svg_elem.getAttribute("viewBox").split()
if viewbox:
vb_width = float(viewbox[2])
vb_height = float(viewbox[3])
scale_x = size / vb_width
scale_y = size / vb_height
else:
scale_x = scale_y = 1
def parse_transform(
transform_str: str,
) -> Callable[[float, float], tuple[float, float]]:
import re
if not transform_str:
return lambda x, y: (x, y)
transforms: list[tuple[str, list[float]]] = []
for match in re.finditer(r"(\w+)\(([^)]+)\)", transform_str):
func, args_str = match.groups()
args = [float(x) for x in args_str.replace(",", " ").split()]
transforms.append((func, args))
def apply_transforms(x: float, y: float) -> tuple[float, float]:
for func, args in transforms:
if func == "translate":
x += args[0]
y += args[1] if len(args) > 1 else args[0]
elif func == "scale":
x *= args[0]
y *= args[1] if len(args) > 1 else args[0]
return x, y
return apply_transforms
for g in doc.getElementsByTagName("g"):
transform = parse_transform(g.getAttribute("transform"))
for poly in g.getElementsByTagName("polygon"):
points_str = poly.getAttribute("points").strip()
fill = poly.getAttribute("fill")
if not fill:
fill = "#000000"
coords: list[tuple[float, float]] = []
for pair in points_str.split():
x, y = pair.split(",")
x, y = float(x), float(y)
x, y = transform(x, y)
coords.append((x * scale_x, y * scale_y))
draw.polygon(coords, fill=fill)
for poly in doc.getElementsByTagName("polygon"):
if poly.parentNode and getattr(poly.parentNode, "tagName", None) == "g":
continue
points_str = poly.getAttribute("points").strip()
fill = poly.getAttribute("fill")
if not fill:
fill = "#000000"
coords = []
for pair in points_str.split():
x, y = pair.split(",")
coords.append((float(x) * scale_x, float(y) * scale_y))
draw.polygon(coords, fill=fill)
if circular:
mask = Image.new("L", (size, size), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.ellipse((0, 0, size, size), fill=255)
img.putalpha(mask)
img.save(output_path)
def main() -> None:
print("Generating branding assets...")
logo_svg = SOURCE_DIR / "logo.svg"
icon_light = SOURCE_DIR / "icon-light.svg"
icon_dark = SOURCE_DIR / "icon-dark.svg"
icon_tinted = SOURCE_DIR / "icon-tinted.svg"
polygons = parse_svg_polygons(logo_svg)
print(" iOS...")
ios_assets = PROJECT_ROOT / "ios/Ascently/Assets.xcassets/AppIcon.appiconset"
for src, dst in [
(icon_light, ios_assets / "app_icon_light_template.svg"),
(icon_dark, ios_assets / "app_icon_dark_template.svg"),
(icon_tinted, ios_assets / "app_icon_tinted_template.svg"),
]:
with open(src) as f:
content = f.read()
with open(dst, "w") as f:
f.write(content)
img_light = Image.new("RGB", (1024, 1024), (255, 255, 255))
draw_light = ImageDraw.Draw(img_light)
scaled = scale_and_center(polygons, 1024, int(1024 * 0.7))
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw_light.polygon(coords, fill=poly["fill"])
img_light.save(ios_assets / "app_icon_1024.png")
img_dark = Image.new("RGB", (1024, 1024), (26, 26, 26))
draw_dark = ImageDraw.Draw(img_dark)
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw_dark.polygon(coords, fill=poly["fill"])
img_dark.save(ios_assets / "app_icon_1024_dark.png")
img_tinted = Image.new("RGB", (1024, 1024), (0, 0, 0))
draw_tinted = ImageDraw.Draw(img_tinted)
for i, poly in enumerate(scaled):
coords = [(x, y) for x, y in poly["coords"]]
draw_tinted.polygon(coords, fill=(0, 0, 0))
img_tinted.save(ios_assets / "app_icon_1024_tinted.png")
print(" Android...")
polys_108 = scale_and_center(polygons, 108, 60)
android_xml = generate_android_vector(polys_108, 108, 108, 108)
(
PROJECT_ROOT / "android/app/src/main/res/drawable/ic_launcher_foreground.xml"
).write_text(android_xml)
polys_24 = scale_and_center(polygons, 24, 20)
mountains_xml = generate_android_vector(polys_24, 24, 24, 24)
(PROJECT_ROOT / "android/app/src/main/res/drawable/ic_mountains.xml").write_text(
mountains_xml
)
for density, size in [
("mdpi", 48),
("hdpi", 72),
("xhdpi", 96),
("xxhdpi", 144),
("xxxhdpi", 192),
]:
mipmap_dir = PROJECT_ROOT / f"android/app/src/main/res/mipmap-{density}"
img = Image.new("RGBA", (size, size), (255, 255, 255, 255))
draw = ImageDraw.Draw(img)
scaled = scale_and_center(polygons, size, int(size * 0.6))
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw.polygon(coords, fill=poly["fill"])
img.save(mipmap_dir / "ic_launcher.webp")
img_round = Image.new("RGBA", (size, size), (255, 255, 255, 255))
draw_round = ImageDraw.Draw(img_round)
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw_round.polygon(coords, fill=poly["fill"])
mask = Image.new("L", (size, size), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.ellipse((0, 0, size, size), fill=255)
img_round.putalpha(mask)
img_round.save(mipmap_dir / "ic_launcher_round.webp")
print(" Docs...")
polys_32 = scale_and_center(polygons, 32, 26)
logo_svg_32 = generate_svg(polys_32, 32, 32)
(PROJECT_ROOT / "docs/src/assets/logo.svg").write_text(logo_svg_32)
(PROJECT_ROOT / "docs/src/assets/logo-dark.svg").write_text(logo_svg_32)
polys_256 = scale_and_center(polygons, 256, 208)
logo_svg_256 = generate_svg(polys_256, 256, 256)
(PROJECT_ROOT / "docs/src/assets/logo-highres.svg").write_text(logo_svg_256)
logo_32_path = PROJECT_ROOT / "docs/src/assets/logo.svg"
rasterize_svg(logo_32_path, PROJECT_ROOT / "docs/public/favicon.png", 32)
sizes = [16, 32, 48]
imgs = []
for size in sizes:
img = Image.new("RGBA", (size, size), (255, 255, 255, 0))
draw = ImageDraw.Draw(img)
scaled = scale_and_center(polygons, size, int(size * 0.8))
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw.polygon(coords, fill=poly["fill"])
imgs.append(img)
imgs[0].save(
PROJECT_ROOT / "docs/public/favicon.ico",
format="ICO",
sizes=[(s, s) for s in sizes],
append_images=imgs[1:],
)
print(" Logos...")
LOGOS_DIR.mkdir(exist_ok=True)
sizes = [64, 128, 256, 512, 1024, 2048]
for size in sizes:
img = Image.new("RGBA", (size, size), (255, 255, 255, 0))
draw = ImageDraw.Draw(img)
scaled = scale_and_center(polygons, size, int(size * 0.8))
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw.polygon(coords, fill=poly["fill"])
img.save(LOGOS_DIR / f"logo-{size}.png")
for size in sizes:
img = Image.new("RGBA", (size, size), (255, 255, 255, 255))
draw = ImageDraw.Draw(img)
scaled = scale_and_center(polygons, size, int(size * 0.8))
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw.polygon(coords, fill=poly["fill"])
img.save(LOGOS_DIR / f"logo-{size}-white.png")
for size in sizes:
img = Image.new("RGBA", (size, size), (26, 26, 26, 255))
draw = ImageDraw.Draw(img)
scaled = scale_and_center(polygons, size, int(size * 0.8))
for poly in scaled:
coords = [(x, y) for x, y in poly["coords"]]
draw.polygon(coords, fill=poly["fill"])
img.save(LOGOS_DIR / f"logo-{size}-dark.png")
print("Done.")
if __name__ == "__main__":
main()

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View 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"
}
}

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 411 B

View File

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

Before

Width:  |  Height:  |  Size: 411 B

View File

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

Before

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View 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"
}
}

View File

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

View File

@@ -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",
] ]

View File

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

View File

@@ -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 {
// Navigate to edit view
} label: {
Label("Edit Gym", systemImage: "pencil")
}
Button(role: .destructive) {
showingDeleteAlert = true showingDeleteAlert = true
} label: { } label: {
Label("Delete Gym", systemImage: "trash") Image(systemName: "trash")
} }
} label: { .tint(.red)
Image(systemName: "ellipsis.circle")
} }
} }
} }
} .confirmationDialog(
.alert("Delete Gym", isPresented: $showingDeleteAlert) { "Delete Gym",
Button("Cancel", role: .cancel) {} 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 {

View File

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

View File

@@ -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: {
Label("Delete Session", systemImage: "trash") Image(systemName: "trash")
}
} label: {
Image(systemName: "ellipsis.circle")
} }
.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)
) )
} }

View File

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

View File

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

View File

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