From 346f1a438ed4dbcc2b22e59205b42da96cf0d858 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Sat, 27 Sep 2025 02:13:51 -0600 Subject: [PATCH] 1.03 for iOS and 1.5.0 for Android --- ...kotlin-compiler-8439154287983817179.salive | 0 android/app/build.gradle.kts | 4 +- .../openclimb/ui/screens/DetailScreens.kt | 2273 +++++++++-------- .../openclimb/ui/viewmodel/ClimbViewModel.kt | 9 +- ios/OpenClimb.xcodeproj/project.pbxproj | 16 +- .../UserInterfaceState.xcuserstate | Bin 106217 -> 114205 bytes .../xcschemes/xcschememanagement.plist | 2 +- ios/OpenClimb/ContentView.swift | 57 +- ios/OpenClimb/Utils/ImageManager.swift | 3 +- .../ViewModels/ClimbingDataManager.swift | 223 +- .../ViewModels/LiveActivityManager.swift | 152 +- ios/OpenClimb/Views/AnalyticsView.swift | 19 +- .../Views/Detail/SessionDetailView.swift | 70 +- ios/OpenClimb/Views/ProblemsView.swift | 22 +- ios/OpenClimb/Views/SessionsView.swift | 20 +- ios/OpenClimb/Views/SettingsView.swift | 109 +- 16 files changed, 1708 insertions(+), 1271 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-8439154287983817179.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-8439154287983817179.salive b/android/.kotlin/sessions/kotlin-compiler-8439154287983817179.salive deleted file mode 100644 index e69de29..0000000 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index be6b465..a777f93 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.atridad.openclimb" minSdk = 31 targetSdk = 36 - versionCode = 24 - versionName = "1.5.0" + versionCode = 25 + versionName = "1.5.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt b/android/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt index f693631..2e40d07 100644 --- a/android/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt +++ b/android/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt @@ -3,13 +3,13 @@ package com.atridad.openclimb.ui.screens import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add @@ -43,10 +43,10 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditAttemptDialog( - attempt: Attempt, - problems: List, - onDismiss: () -> Unit, - onAttemptUpdated: (Attempt) -> Unit + attempt: Attempt, + problems: List, + onDismiss: () -> Unit, + onAttemptUpdated: (Attempt) -> Unit ) { var selectedProblem by remember { mutableStateOf(problems.find { it.id == attempt.problemId }) } var selectedResult by remember { mutableStateOf(attempt.result) } @@ -56,61 +56,59 @@ fun EditAttemptDialog( Dialog(onDismissRequest = onDismiss) { Card(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.spacedBy(20.dp) + modifier = Modifier.fillMaxSize().padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) ) { Text( - text = "Edit Attempt", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold + text = "Edit Attempt", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold ) // Problem Selection Text( - text = "Problem", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium + text = "Problem", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium ) if (problems.isEmpty()) { Text( - text = "No problems available.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error + text = "No problems available.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error ) } else { var expanded by remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxWidth()) { OutlinedTextField( - value = selectedProblem?.name ?: "Unknown Problem", - onValueChange = {}, - readOnly = true, - label = { Text("Problem") }, - trailingIcon = { - Icon( - if (expanded) Icons.Default.KeyboardArrowUp - else Icons.Default.KeyboardArrowDown, - contentDescription = "Toggle dropdown" - ) - }, - modifier = Modifier.fillMaxWidth().clickable { expanded = true } + value = selectedProblem?.name ?: "Unknown Problem", + onValueChange = {}, + readOnly = true, + label = { Text("Problem") }, + trailingIcon = { + Icon( + if (expanded) Icons.Default.KeyboardArrowUp + else Icons.Default.KeyboardArrowDown, + contentDescription = "Toggle dropdown" + ) + }, + modifier = Modifier.fillMaxWidth().clickable { expanded = true } ) DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - modifier = Modifier.fillMaxWidth(0.9f) + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth(0.9f) ) { problems.forEach { problem -> DropdownMenuItem( - text = { Text(problem.name ?: "Unknown Problem") }, - onClick = { - selectedProblem = problem - expanded = false - } + text = { Text(problem.name ?: "Unknown Problem") }, + onClick = { + selectedProblem = problem + expanded = false + } ) } } @@ -119,38 +117,39 @@ fun EditAttemptDialog( // Result Selection Text( - text = "Result", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium + text = "Result", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium ) Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { AttemptResult.entries.forEach { result -> Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = selectedResult == result, - onClick = { selectedResult = result }, - role = Role.RadioButton - ) - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier.fillMaxWidth() + .selectable( + selected = selectedResult == result, + onClick = { selectedResult = result }, + role = Role.RadioButton + ) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically ) { - RadioButton( - selected = selectedResult == result, - onClick = null - ) + RadioButton(selected = selectedResult == result, onClick = null) Spacer(modifier = Modifier.width(8.dp)) Text( - text = when (result) { - AttemptResult.NO_PROGRESS -> "No Progress" - else -> result.name.lowercase().replaceFirstChar { it.uppercase() } - }, - style = MaterialTheme.typography.bodyMedium + text = + when (result) { + AttemptResult.NO_PROGRESS -> "No Progress" + else -> + result.name.lowercase().replaceFirstChar { + it.uppercase() + } + }, + style = MaterialTheme.typography.bodyMedium ) } } @@ -158,51 +157,49 @@ fun EditAttemptDialog( // Highest Hold OutlinedTextField( - value = highestHold, - onValueChange = { highestHold = it }, - label = { Text("Highest Hold (Optional)") }, - placeholder = { Text("e.g., 'jug on the left'") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true + value = highestHold, + onValueChange = { highestHold = it }, + label = { Text("Highest Hold (Optional)") }, + placeholder = { Text("e.g., 'jug on the left'") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true ) // Notes OutlinedTextField( - value = notes, - onValueChange = { notes = it }, - label = { Text("Notes (Optional)") }, - modifier = Modifier.fillMaxWidth(), - minLines = 2, - placeholder = { Text("e.g., 'need to work on heel hooks', 'pumped out'") } + value = notes, + onValueChange = { notes = it }, + label = { Text("Notes (Optional)") }, + modifier = Modifier.fillMaxWidth(), + minLines = 2, + placeholder = { Text("e.g., 'need to work on heel hooks', 'pumped out'") } ) // Buttons Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { TextButton(onClick = onDismiss, modifier = Modifier.weight(1f)) { Text("Cancel") } Button( - onClick = { - selectedProblem?.let { problem -> - val updatedAttempt = - attempt.copy( - problemId = problem.id, - result = selectedResult, - highestHold = highestHold.ifBlank { null }, - notes = notes.ifBlank { null } - ) - onAttemptUpdated(updatedAttempt) - } - }, - enabled = selectedProblem != null, - modifier = Modifier.weight(1f) - ) { - Text("Update") - } + onClick = { + selectedProblem?.let { problem -> + val updatedAttempt = + attempt.copy( + problemId = problem.id, + result = selectedResult, + highestHold = highestHold.ifBlank { null }, + notes = notes.ifBlank { null } + ) + onAttemptUpdated(updatedAttempt) + } + }, + enabled = selectedProblem != null, + modifier = Modifier.weight(1f) + ) { Text("Update") } } } } @@ -212,10 +209,10 @@ fun EditAttemptDialog( @OptIn(ExperimentalMaterial3Api::class) @Composable fun SessionDetailScreen( - sessionId: String, - viewModel: ClimbViewModel, - onNavigateBack: () -> Unit, - onNavigateToProblemDetail: (String) -> Unit = {} + sessionId: String, + viewModel: ClimbViewModel, + onNavigateBack: () -> Unit, + onNavigateToProblemDetail: (String) -> Unit = {} ) { val context = LocalContext.current val attempts by viewModel.getAttemptsBySession(sessionId).collectAsState(initial = emptyList()) @@ -234,111 +231,123 @@ fun SessionDetailScreen( // Calculate stats val successfulAttempts = - 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 attemptedProblems = problems.filter { it.id in uniqueProblems } val completedProblems = successfulAttempts.map { it.problemId }.distinct() val attemptsWithProblems = - attempts - .mapNotNull { attempt -> - val problem = problems.find { it.id == attempt.problemId } - if (problem != null) attempt to problem else null - } - .sortedBy { attempt -> - // Sort by timestamp (when attempt was logged) - attempt.first.timestamp - } + attempts + .mapNotNull { attempt -> + val problem = problems.find { it.id == attempt.problemId } + if (problem != null) attempt to problem else null + } + .sortedBy { attempt -> + // Sort by timestamp (when attempt was logged) + attempt.first.timestamp + } Scaffold( - topBar = { - TopAppBar( - title = { Text("Session Details") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - actions = { - // Share button - if (session?.duration != null) { // Only show for completed sessions - IconButton( - onClick = { - isGeneratingShare = true - viewModel.viewModelScope.launch { - val shareFile = - viewModel.generateSessionShareCard(context, sessionId) - isGeneratingShare = false - shareFile?.let { file -> - viewModel.shareSessionCard(context, file) + topBar = { + TopAppBar( + title = { Text("Session Details") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + // Share button + if (session?.duration != null) { // Only show for completed sessions + IconButton( + onClick = { + isGeneratingShare = true + viewModel.viewModelScope.launch { + val shareFile = + viewModel.generateSessionShareCard( + context, + sessionId + ) + isGeneratingShare = false + shareFile?.let { file -> + viewModel.shareSessionCard(context, file) + } + } + }, + enabled = !isGeneratingShare + ) { + if (isGeneratingShare) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp + ) + } else { + Icon( + imageVector = Icons.Default.Share, + contentDescription = "Share Session" + ) } } - }, - enabled = !isGeneratingShare - ) { - if (isGeneratingShare) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp - ) - } else { - Icon( - imageVector = Icons.Default.Share, - contentDescription = "Share Session" - ) } - } - } - // Show stop icon for active sessions, delete icon for completed sessions - if (session?.status == SessionStatus.ACTIVE) { - IconButton(onClick = { - session.let { s -> - viewModel.endSession(context, s.id) - onNavigateBack() + // Show stop icon for active sessions, delete icon for completed + // sessions + if (session?.status == SessionStatus.ACTIVE) { + IconButton( + onClick = { + session.let { s -> + viewModel.endSession(context, s.id) + onNavigateBack() + } + } + ) { + Icon( + imageVector = + CustomIcons.Stop( + MaterialTheme.colorScheme.onSurface + ), + contentDescription = "Stop Session" + ) + } + } else { + IconButton(onClick = { showDeleteDialog = true }) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } } - }) { - Icon( - imageVector = CustomIcons.Stop(MaterialTheme.colorScheme.onSurface), - contentDescription = "Stop Session" - ) - } - } else { - IconButton(onClick = { showDeleteDialog = true }) { - Icon(Icons.Default.Delete, contentDescription = "Delete") } + ) + }, + floatingActionButton = { + if (session?.status == SessionStatus.ACTIVE) { + FloatingActionButton(onClick = { showAddAttemptDialog = true }) { + Icon(Icons.Default.Add, contentDescription = "Add Attempt") } } - ) - }, - floatingActionButton = { - if (session?.status == SessionStatus.ACTIVE) { - FloatingActionButton(onClick = { showAddAttemptDialog = true }) { - Icon(Icons.Default.Add, contentDescription = "Add Attempt") - } } - } ) { paddingValues -> LazyColumn( - modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Session Header item { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = gym?.name ?: "Unknown Gym", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold + text = gym?.name ?: "Unknown Gym", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = formatDate(session?.date ?: ""), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary + text = formatDate(session?.date ?: ""), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary ) session?.let { s -> @@ -348,9 +357,9 @@ fun SessionDetailScreen( val timeText = "Duration: ${s.duration} minutes" Text( - text = timeText, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = timeText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -364,22 +373,24 @@ fun SessionDetailScreen( Spacer(modifier = Modifier.height(12.dp)) Surface( - color = - if (session?.duration != null) - MaterialTheme.colorScheme.primaryContainer - else MaterialTheme.colorScheme.secondaryContainer, - shape = RoundedCornerShape(12.dp) + color = + if (session?.duration != null) + MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.secondaryContainer, + shape = RoundedCornerShape(12.dp) ) { Text( - text = - if (session?.duration != null) "Completed" else "In Progress", - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - style = MaterialTheme.typography.labelMedium, - color = - if (session?.duration != null) - MaterialTheme.colorScheme.onPrimaryContainer - else MaterialTheme.colorScheme.onSecondaryContainer, - fontWeight = FontWeight.Medium + text = + if (session?.duration != null) "Completed" + else "In Progress", + modifier = + Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + style = MaterialTheme.typography.labelMedium, + color = + if (session?.duration != null) + MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSecondaryContainer, + fontWeight = FontWeight.Medium ) } } @@ -391,92 +402,43 @@ fun SessionDetailScreen( Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = "Session Stats", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + text = "Session Stats", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(16.dp)) if (attempts.isEmpty()) { Text( - text = "No attempts recorded yet", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "No attempts recorded yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } else { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly ) { StatItem(label = "Total Attempts", value = attempts.size.toString()) StatItem(label = "Problems", value = uniqueProblems.size.toString()) StatItem( - label = "Successful", - value = successfulAttempts.size.toString() + label = "Successful", + value = successfulAttempts.size.toString() ) } Spacer(modifier = Modifier.height(16.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly ) { StatItem( - label = "Completed", - value = completedProblems.size.toString() + label = "Completed", + value = completedProblems.size.toString() ) } - - // Show grade range(s) with better layout - val grades = attemptedProblems.map { it.difficulty } - if (grades.isNotEmpty()) { - val boulderProblems = attemptedProblems.filter { it.climbType == ClimbType.BOULDER } - val ropeProblems = attemptedProblems.filter { it.climbType == ClimbType.ROPE } - - val boulderRange = if (boulderProblems.isNotEmpty()) { - val boulderGrades = boulderProblems.map { it.difficulty } - val sorted = boulderGrades.sortedWith { a, b -> a.compareTo(b) } - "${sorted.first().grade} - ${sorted.last().grade}" - } else null - - val ropeRange = if (ropeProblems.isNotEmpty()) { - val ropeGrades = ropeProblems.map { it.difficulty } - val sorted = ropeGrades.sortedWith { a, b -> a.compareTo(b) } - "${sorted.first().grade} - ${sorted.last().grade}" - } else null - - if (boulderRange != null && ropeRange != null) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - StatItem(label = "Boulder Range", value = boulderRange) - StatItem(label = "Rope Range", value = ropeRange) - } - } else { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - StatItem( - label = "Grade Range", - value = boulderRange ?: ropeRange ?: "N/A" - ) - } - } - } else { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - StatItem( - label = "Grade Range", - value = "N/A" - ) - } - } } } } @@ -485,9 +447,9 @@ fun SessionDetailScreen( // Attempts List item { Text( - text = "Attempts (${attempts.size})", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + text = "Attempts (${attempts.size})", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold ) } @@ -495,19 +457,19 @@ fun SessionDetailScreen( item { Card(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.fillMaxWidth().padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.fillMaxWidth().padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "No attempts yet", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "No attempts yet", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Start attempting problems to see your progress!", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Start attempting problems to see your progress!", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -516,13 +478,15 @@ fun SessionDetailScreen( items(attemptsWithProblems.size) { index -> val (attempt, problem) = attemptsWithProblems[index] SessionAttemptCard( - attempt = attempt, - problem = problem, - onEditAttempt = { attemptToEdit -> showEditAttemptDialog = attemptToEdit }, - onDeleteAttempt = { attemptToDelete -> - viewModel.deleteAttempt(attemptToDelete) - }, - onAttemptClick = { onNavigateToProblemDetail(problem.id) } + attempt = attempt, + problem = problem, + onEditAttempt = { attemptToEdit -> + showEditAttemptDialog = attemptToEdit + }, + onDeleteAttempt = { attemptToDelete -> + viewModel.deleteAttempt(attemptToDelete) + }, + onAttemptClick = { onNavigateToProblemDetail(problem.id) } ) } } @@ -532,62 +496,61 @@ fun SessionDetailScreen( // Delete confirmation dialog if (showDeleteDialog) { AlertDialog( - onDismissRequest = { showDeleteDialog = false }, - title = { Text("Delete Session") }, - text = { - Column { - Text("Are you sure you want to delete this session?") - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "This will also delete all attempts associated with this session.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - }, - confirmButton = { - TextButton( - onClick = { - session?.let { s -> - viewModel.deleteSession(s) - onNavigateBack() - } - showDeleteDialog = false + onDismissRequest = { showDeleteDialog = false }, + title = { Text("Delete Session") }, + text = { + Column { + Text("Are you sure you want to delete this session?") + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = + "This will also delete all attempts associated with this session.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) } - ) { - Text("Delete", color = MaterialTheme.colorScheme.error) + }, + confirmButton = { + TextButton( + onClick = { + session?.let { s -> + viewModel.deleteSession(s) + onNavigateBack() + } + showDeleteDialog = false + } + ) { Text("Delete", color = MaterialTheme.colorScheme.error) } + }, + dismissButton = { + TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } } - }, - dismissButton = { - TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } - } ) } if (showAddAttemptDialog && session != null && gym != null) { EnhancedAddAttemptDialog( - session = session, - gym = gym, - problems = problems.filter { it.gymId == gym.id && it.isActive }, - onDismiss = { showAddAttemptDialog = false }, - onAttemptAdded = { attempt -> - viewModel.addAttempt(attempt) - showAddAttemptDialog = false - }, - onProblemCreated = { problem -> viewModel.addProblem(problem) } + session = session, + gym = gym, + problems = problems.filter { it.gymId == gym.id && it.isActive }, + onDismiss = { showAddAttemptDialog = false }, + onAttemptAdded = { attempt -> + viewModel.addAttempt(attempt) + showAddAttemptDialog = false + }, + onProblemCreated = { problem -> viewModel.addProblem(problem) } ) } // Edit attempt dialog showEditAttemptDialog?.let { attempt -> EditAttemptDialog( - attempt = attempt, - problems = problems.filter { it.isActive }, - onDismiss = { showEditAttemptDialog = null }, - onAttemptUpdated = { updatedAttempt -> - viewModel.updateAttempt(updatedAttempt) - showEditAttemptDialog = null - } + attempt = attempt, + problems = problems.filter { it.isActive }, + onDismiss = { showEditAttemptDialog = null }, + onAttemptUpdated = { updatedAttempt -> + viewModel.updateAttempt(updatedAttempt) + showEditAttemptDialog = null + } ) } } @@ -595,10 +558,10 @@ fun SessionDetailScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ProblemDetailScreen( - problemId: String, - viewModel: ClimbViewModel, - onNavigateBack: () -> Unit, - onNavigateToEdit: (String) -> Unit + problemId: String, + viewModel: ClimbViewModel, + onNavigateBack: () -> Unit, + onNavigateToEdit: (String) -> Unit ) { val context = LocalContext.current var showDeleteDialog by remember { mutableStateOf(false) } @@ -617,73 +580,76 @@ fun ProblemDetailScreen( // Calculate stats val successfulAttempts = - attempts.filter { it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH) } + attempts.filter { it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH) } val attemptsWithSessions = - attempts - .mapNotNull { attempt -> - val session = sessions.find { it.id == attempt.sessionId } - if (session != null) attempt to session else null - } - .sortedByDescending { it.second.date } + attempts + .mapNotNull { attempt -> + val session = sessions.find { it.id == attempt.sessionId } + if (session != null) attempt to session else null + } + .sortedByDescending { it.second.date } Scaffold( - topBar = { - TopAppBar( - title = { Text("Problem Details") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - actions = { - IconButton(onClick = { showDeleteDialog = true }) { - Icon(Icons.Default.Delete, contentDescription = "Delete") - } - IconButton(onClick = { onNavigateToEdit(problemId) }) { - Icon(Icons.Default.Edit, contentDescription = "Edit") - } - } - ) - } + topBar = { + TopAppBar( + title = { Text("Problem Details") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + IconButton(onClick = { showDeleteDialog = true }) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + IconButton(onClick = { onNavigateToEdit(problemId) }) { + Icon(Icons.Default.Edit, contentDescription = "Edit") + } + } + ) + } ) { paddingValues -> LazyColumn( - modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Problem Header item { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = problem?.name ?: "Unknown Problem", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold + text = problem?.name ?: "Unknown Problem", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { Column { problem?.let { p -> Text( - text = - "${p.difficulty.system.getDisplayName()}: ${p.difficulty.grade}", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold + text = + "${p.difficulty.system.getDisplayName()}: ${p.difficulty.grade}", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold ) } problem?.let { p -> Text( - text = p.climbType.getDisplayName(), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = p.climbType.getDisplayName(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -691,16 +657,16 @@ fun ProblemDetailScreen( gym?.let { g -> Column(horizontalAlignment = Alignment.End) { Text( - text = g.name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium + text = g.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium ) problem?.location?.let { location -> Text( - text = location, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = location, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -717,12 +683,12 @@ fun ProblemDetailScreen( if (p.imagePaths.isNotEmpty()) { Spacer(modifier = Modifier.height(16.dp)) ImageDisplaySection( - imagePaths = p.imagePaths, - title = "Photos", - onImageClick = { index -> - selectedImageIndex = index - showImageViewer = true - } + imagePaths = p.imagePaths, + title = "Photos", + onImageClick = { index -> + selectedImageIndex = index + showImageViewer = true + } ) } } @@ -730,9 +696,9 @@ fun ProblemDetailScreen( problem?.setter?.let { setter -> Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Set by: $setter", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Set by: $setter", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -748,9 +714,9 @@ fun ProblemDetailScreen( problem?.notes?.let { notes -> Spacer(modifier = Modifier.height(12.dp)) Text( - text = notes, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = notes, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -762,28 +728,28 @@ fun ProblemDetailScreen( Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = "Progress Summary", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + text = "Progress Summary", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(16.dp)) if (attempts.isEmpty()) { Text( - text = "No attempts recorded yet", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "No attempts recorded yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } else { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly ) { StatItem(label = "Total Attempts", value = attempts.size.toString()) StatItem( - label = "Successful", - value = successfulAttempts.size.toString() + label = "Successful", + value = successfulAttempts.size.toString() ) } @@ -791,16 +757,16 @@ fun ProblemDetailScreen( if (successfulAttempts.isNotEmpty()) { val firstSuccess = - successfulAttempts.minByOrNull { attempt -> - sessions.find { it.id == attempt.sessionId }?.date ?: "" - } + successfulAttempts.minByOrNull { attempt -> + sessions.find { it.id == attempt.sessionId }?.date ?: "" + } firstSuccess?.let { attempt -> val session = sessions.find { it.id == attempt.sessionId } Text( - text = - "First success: ${formatDate(session?.date ?: "")} (${attempt.result.name.lowercase().replaceFirstChar { it.uppercase() }})", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary + text = + "First success: ${formatDate(session?.date ?: "")} (${attempt.result.name.lowercase().replaceFirstChar { it.uppercase() }})", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary ) } } @@ -812,9 +778,9 @@ fun ProblemDetailScreen( // Attempt History item { Text( - text = "Attempt History (${attempts.size})", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + text = "Attempt History (${attempts.size})", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold ) } @@ -822,19 +788,20 @@ fun ProblemDetailScreen( item { Card(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.fillMaxWidth().padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.fillMaxWidth().padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "No attempts yet", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "No attempts yet", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Start a session and track your attempts on this problem!", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = + "Start a session and track your attempts on this problem!", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -851,35 +818,34 @@ fun ProblemDetailScreen( // Delete confirmation dialog if (showDeleteDialog) { AlertDialog( - onDismissRequest = { showDeleteDialog = false }, - title = { Text("Delete Problem") }, - text = { - Column { - Text("Are you sure you want to delete this problem?") - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "This will also delete all attempts associated with this problem.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - }, - confirmButton = { - TextButton( - onClick = { - problem?.let { p -> - viewModel.deleteProblem(p, context) - onNavigateBack() - } - showDeleteDialog = false + onDismissRequest = { showDeleteDialog = false }, + title = { Text("Delete Problem") }, + text = { + Column { + Text("Are you sure you want to delete this problem?") + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = + "This will also delete all attempts associated with this problem.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) } - ) { - Text("Delete", color = MaterialTheme.colorScheme.error) + }, + confirmButton = { + TextButton( + onClick = { + problem?.let { p -> + viewModel.deleteProblem(p, context) + onNavigateBack() + } + showDeleteDialog = false + } + ) { Text("Delete", color = MaterialTheme.colorScheme.error) } + }, + dismissButton = { + TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } } - }, - dismissButton = { - TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } - } ) } @@ -887,9 +853,9 @@ fun ProblemDetailScreen( problem?.let { p -> if (showImageViewer && p.imagePaths.isNotEmpty()) { FullscreenImageViewer( - imagePaths = p.imagePaths, - initialIndex = selectedImageIndex, - onDismiss = { showImageViewer = false } + imagePaths = p.imagePaths, + initialIndex = selectedImageIndex, + onDismiss = { showImageViewer = false } ) } } @@ -898,12 +864,12 @@ fun ProblemDetailScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun GymDetailScreen( - gymId: String, - viewModel: ClimbViewModel, - onNavigateBack: () -> Unit, - onNavigateToEdit: (String) -> Unit, - onNavigateToSessionDetail: (String) -> Unit = {}, - onNavigateToProblemDetail: (String) -> Unit = {} + gymId: String, + viewModel: ClimbViewModel, + onNavigateBack: () -> Unit, + onNavigateToEdit: (String) -> Unit, + onNavigateToSessionDetail: (String) -> Unit = {}, + onNavigateToProblemDetail: (String) -> Unit = {} ) { val gyms by viewModel.gyms.collectAsState() val gym = gyms.find { it.id == gymId } @@ -913,9 +879,9 @@ fun GymDetailScreen( // Calculate statistics val gymAttempts = - allAttempts.filter { attempt -> - problems.any { problem -> problem.id == attempt.problemId } - } + allAttempts.filter { attempt -> + problems.any { problem -> problem.id == attempt.problemId } + } val uniqueProblemsClimbed = gymAttempts.map { it.problemId }.toSet().size val totalSessions = sessions.size @@ -924,53 +890,54 @@ fun GymDetailScreen( var showDeleteDialog by remember { mutableStateOf(false) } Scaffold( - topBar = { - TopAppBar( - title = { Text(gym?.name ?: "Gym Details") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - actions = { - IconButton(onClick = { showDeleteDialog = true }) { - Icon(Icons.Default.Delete, contentDescription = "Delete") - } - IconButton(onClick = { onNavigateToEdit(gymId) }) { - Icon(Icons.Default.Edit, contentDescription = "Edit") - } - } - ) - } + topBar = { + TopAppBar( + title = { Text(gym?.name ?: "Gym Details") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + IconButton(onClick = { showDeleteDialog = true }) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + IconButton(onClick = { onNavigateToEdit(gymId) }) { + Icon(Icons.Default.Edit, contentDescription = "Edit") + } + } + ) + } ) { paddingValues -> if (gym == null) { Box( - modifier = Modifier.fillMaxSize().padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Text("Gym not found") - } + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = Alignment.Center + ) { Text("Gym not found") } } else { LazyColumn( - modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Gym Information Card item { Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp)) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = gym.name, - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold + text = gym.name, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold ) if (gym.location?.isNotBlank() == true) { Spacer(modifier = Modifier.height(4.dp)) Text( - text = gym.location, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = gym.location, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -987,91 +954,90 @@ fun GymDetailScreen( Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp)) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = "Statistics", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + text = "Statistics", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(16.dp)) // Statistics Grid Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = problems.size.toString(), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = problems.size.toString(), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary ) Text( - text = "Problems", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Problems", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = totalSessions.toString(), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = totalSessions.toString(), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary ) Text( - text = "Sessions", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Sessions", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - } Spacer(modifier = Modifier.height(12.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = gymAttempts.size.toString(), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = gymAttempts.size.toString(), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary ) Text( - text = "Total Attempts", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Total Attempts", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = uniqueProblemsClimbed.toString(), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = uniqueProblemsClimbed.toString(), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary ) Text( - text = "Problems Climbed", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Problems Climbed", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } if (activeSessions > 0) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = activeSessions.toString(), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = activeSessions.toString(), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary ) Text( - text = "Active Sessions", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Active Sessions", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -1084,78 +1050,87 @@ fun GymDetailScreen( if (problems.isNotEmpty()) { item { Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp) ) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = "Problems (${problems.size})", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + text = "Problems (${problems.size})", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(12.dp)) // Show recent problems (limit to 5) - problems - .sortedByDescending { it.createdAt } - .take(5) - .forEach { problem -> - val problemAttempts = + problems.sortedByDescending { it.createdAt }.take(5).forEach { + problem -> + val problemAttempts = gymAttempts.filter { it.problemId == problem.id } - val problemSuccessful = + val problemSuccessful = problemAttempts.any { it.result in - listOf( - AttemptResult.SUCCESS, - AttemptResult.FLASH - ) + listOf( + AttemptResult.SUCCESS, + AttemptResult.FLASH + ) } - Card( + Card( modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .clickable { onNavigateToProblemDetail(problem.id) }, + Modifier.fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { + onNavigateToProblemDetail( + problem.id + ) + }, colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), + CardDefaults.cardColors( + containerColor = + MaterialTheme.colorScheme + .surface + ), shape = RoundedCornerShape(12.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - ListItem( + elevation = + CardDefaults.cardElevation( + defaultElevation = 4.dp + ) + ) { + ListItem( headlineContent = { Text( - text = problem.name ?: "Unnamed Problem", - fontWeight = FontWeight.Medium + text = problem.name + ?: "Unnamed Problem", + fontWeight = FontWeight.Medium ) }, supportingContent = { Text( - "${problem.difficulty.grade} โ€ข ${problem.climbType} โ€ข ${problemAttempts.size} attempts" + "${problem.difficulty.grade} โ€ข ${problem.climbType} โ€ข ${problemAttempts.size} attempts" ) }, trailingContent = { if (problemSuccessful) { Icon( - Icons.Default.Check, - contentDescription = "Completed", - tint = MaterialTheme.colorScheme.primary + Icons.Default.Check, + contentDescription = "Completed", + tint = + MaterialTheme.colorScheme + .primary ) } } - ) - } + ) } + } if (problems.size > 5) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "... and ${problems.size - 5} more problems", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "... and ${problems.size - 5} more problems", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -1167,70 +1142,76 @@ fun GymDetailScreen( if (sessions.isNotEmpty()) { item { Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp) ) { Column(modifier = Modifier.padding(20.dp)) { Text( - text = "Recent Sessions (${sessions.size})", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + text = "Recent Sessions (${sessions.size})", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(12.dp)) // Show recent sessions (limit to 3) - sessions - .sortedByDescending { it.date } - .take(3) - .forEach { session -> - val sessionAttempts = + sessions.sortedByDescending { it.date }.take(3).forEach { session -> + val sessionAttempts = gymAttempts.filter { it.sessionId == session.id } - Card( + Card( modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .clickable { onNavigateToSessionDetail(session.id) }, + Modifier.fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { + onNavigateToSessionDetail( + session.id + ) + }, colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), + CardDefaults.cardColors( + containerColor = + MaterialTheme.colorScheme + .surface + ), shape = RoundedCornerShape(12.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - ListItem( + elevation = + CardDefaults.cardElevation( + defaultElevation = 4.dp + ) + ) { + ListItem( headlineContent = { Row( - horizontalArrangement = - Arrangement.spacedBy(8.dp), - verticalAlignment = - Alignment.CenterVertically + horizontalArrangement = + Arrangement.spacedBy(8.dp), + verticalAlignment = + Alignment.CenterVertically ) { Text( - text = - if ( - session.status == - SessionStatus.ACTIVE - ) - "Active Session" - else "Session", - fontWeight = FontWeight.Medium + text = + if (session.status == + SessionStatus + .ACTIVE + ) + "Active Session" + else "Session", + fontWeight = FontWeight.Medium ) - if ( - session.status == SessionStatus.ACTIVE + if (session.status == SessionStatus.ACTIVE ) { Badge( - containerColor = - MaterialTheme.colorScheme - .primary + containerColor = + MaterialTheme + .colorScheme + .primary ) { Text( - "ACTIVE", - style = - MaterialTheme.typography - .labelSmall + "ACTIVE", + style = + MaterialTheme + .typography + .labelSmall ) } } @@ -1238,44 +1219,46 @@ fun GymDetailScreen( }, supportingContent = { val dateTime = - try { - LocalDateTime.parse(session.date) - } catch (_: Exception) { - null - } + try { + LocalDateTime.parse(session.date) + } catch (_: Exception) { + null + } val formattedDate = - dateTime?.format( - DateTimeFormatter.ofPattern( - "MMM dd, yyyy" + dateTime?.format( + DateTimeFormatter.ofPattern( + "MMM dd, yyyy" + ) ) - ) ?: session.date + ?: session.date Text( - "$formattedDate โ€ข ${sessionAttempts.size} attempts" + "$formattedDate โ€ข ${sessionAttempts.size} attempts" ) }, trailingContent = { session.duration?.let { duration -> Text( - text = "${duration}min", - style = - MaterialTheme.typography.bodySmall, - color = - MaterialTheme.colorScheme - .onSurfaceVariant + text = "${duration}min", + style = + MaterialTheme.typography + .bodySmall, + color = + MaterialTheme.colorScheme + .onSurfaceVariant ) } } - ) - } + ) } + } if (sessions.size > 3) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "... and ${sessions.size - 3} more sessions", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "... and ${sessions.size - 3} more sessions", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -1287,23 +1270,23 @@ fun GymDetailScreen( if (problems.isEmpty() && sessions.isEmpty()) { item { Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp) ) { Column( - modifier = Modifier.fillMaxWidth().padding(40.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.fillMaxWidth().padding(40.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "No activity yet", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium + text = "No activity yet", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Start a session or add problems to see them here", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Start a session or add problems to see them here", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -1316,36 +1299,34 @@ fun GymDetailScreen( // Delete confirmation dialog if (showDeleteDialog) { AlertDialog( - onDismissRequest = { showDeleteDialog = false }, - title = { Text("Delete Gym") }, - text = { - Column { - Text("Are you sure you want to delete this gym?") - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = - "This will also delete all problems and sessions associated with this gym.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - }, - confirmButton = { - TextButton( - onClick = { - gym?.let { g -> - viewModel.deleteGym(g) - onNavigateBack() - } - showDeleteDialog = false + onDismissRequest = { showDeleteDialog = false }, + title = { Text("Delete Gym") }, + text = { + Column { + Text("Are you sure you want to delete this gym?") + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = + "This will also delete all problems and sessions associated with this gym.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) } - ) { - Text("Delete", color = MaterialTheme.colorScheme.error) + }, + confirmButton = { + TextButton( + onClick = { + gym?.let { g -> + viewModel.deleteGym(g) + onNavigateBack() + } + showDeleteDialog = false + } + ) { Text("Delete", color = MaterialTheme.colorScheme.error) } + }, + dismissButton = { + TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } } - }, - dismissButton = { - TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } - } ) } } @@ -1354,15 +1335,15 @@ fun GymDetailScreen( fun StatItem(label: String, value: String) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = value, - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = value, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary ) Text( - text = label, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = label, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -1372,21 +1353,21 @@ fun AttemptHistoryCard(attempt: Attempt, session: ClimbSession, gym: Gym?) { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { Column { Text( - text = formatDate(session.date), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium + text = formatDate(session.date), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium ) gym?.let { g -> Text( - text = g.name, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = g.name, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -1405,107 +1386,106 @@ fun AttemptHistoryCard(attempt: Attempt, session: ClimbSession, gym: Gym?) { @Composable fun AttemptResultBadge(result: AttemptResult) { val backgroundColor = - when (result) { - AttemptResult.SUCCESS, - AttemptResult.FLASH -> MaterialTheme.colorScheme.primaryContainer - AttemptResult.FALL -> MaterialTheme.colorScheme.secondaryContainer - else -> MaterialTheme.colorScheme.surfaceVariant - } + when (result) { + AttemptResult.SUCCESS, AttemptResult.FLASH -> + MaterialTheme.colorScheme.primaryContainer + AttemptResult.FALL -> MaterialTheme.colorScheme.secondaryContainer + else -> MaterialTheme.colorScheme.surfaceVariant + } val textColor = - when (result) { - AttemptResult.SUCCESS, - AttemptResult.FLASH -> MaterialTheme.colorScheme.onPrimaryContainer - AttemptResult.FALL -> MaterialTheme.colorScheme.onSecondaryContainer - else -> MaterialTheme.colorScheme.onSurfaceVariant - } + when (result) { + AttemptResult.SUCCESS, AttemptResult.FLASH -> + MaterialTheme.colorScheme.onPrimaryContainer + AttemptResult.FALL -> MaterialTheme.colorScheme.onSecondaryContainer + else -> MaterialTheme.colorScheme.onSurfaceVariant + } Surface(color = backgroundColor, shape = RoundedCornerShape(12.dp)) { Text( - text = when (result) { - AttemptResult.NO_PROGRESS -> "No Progress" - else -> result.name.lowercase().replaceFirstChar { it.uppercase() } - }, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - style = MaterialTheme.typography.labelMedium, - color = textColor, - fontWeight = FontWeight.Medium + text = + when (result) { + AttemptResult.NO_PROGRESS -> "No Progress" + else -> result.name.lowercase().replaceFirstChar { it.uppercase() } + }, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + style = MaterialTheme.typography.labelMedium, + color = textColor, + fontWeight = FontWeight.Medium ) } } @Composable fun SessionAttemptCard( - attempt: Attempt, - problem: Problem, - onEditAttempt: (Attempt) -> Unit = {}, - onDeleteAttempt: (Attempt) -> Unit = {}, - onAttemptClick: () -> Unit = {} + attempt: Attempt, + problem: Problem, + onEditAttempt: (Attempt) -> Unit = {}, + onDeleteAttempt: (Attempt) -> Unit = {}, + onAttemptClick: () -> Unit = {} ) { var showDeleteDialog by remember { mutableStateOf(false) } Card( - modifier = Modifier - .fillMaxWidth() - .clickable { onAttemptClick() }, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + modifier = Modifier.fillMaxWidth().clickable { onAttemptClick() }, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { Text( - text = problem.name ?: "Unknown Problem", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium + text = problem.name ?: "Unknown Problem", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium ) Text( - text = - "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary + text = + "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary ) problem.location?.let { location -> Text( - text = location, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = location, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically ) { AttemptResultBadge(result = attempt.result) // Edit button IconButton( - onClick = { onEditAttempt(attempt) }, - modifier = Modifier.size(32.dp) + onClick = { onEditAttempt(attempt) }, + modifier = Modifier.size(32.dp) ) { Icon( - Icons.Default.Edit, - contentDescription = "Edit attempt", - modifier = Modifier.size(16.dp) + Icons.Default.Edit, + contentDescription = "Edit attempt", + modifier = Modifier.size(16.dp) ) } // Delete button IconButton( - onClick = { showDeleteDialog = true }, - modifier = Modifier.size(32.dp) + onClick = { showDeleteDialog = true }, + modifier = Modifier.size(32.dp) ) { Icon( - Icons.Default.Delete, - contentDescription = "Delete attempt", - modifier = Modifier.size(16.dp) + Icons.Default.Delete, + contentDescription = "Delete attempt", + modifier = Modifier.size(16.dp) ) } } @@ -1521,22 +1501,20 @@ fun SessionAttemptCard( // Delete confirmation dialog if (showDeleteDialog) { AlertDialog( - onDismissRequest = { showDeleteDialog = false }, - title = { Text("Delete Attempt") }, - text = { Text("Are you sure you want to delete this attempt?") }, - confirmButton = { - TextButton( - onClick = { - onDeleteAttempt(attempt) - showDeleteDialog = false - } - ) { - Text("Delete", color = MaterialTheme.colorScheme.error) + onDismissRequest = { showDeleteDialog = false }, + title = { Text("Delete Attempt") }, + text = { Text("Are you sure you want to delete this attempt?") }, + confirmButton = { + TextButton( + onClick = { + onDeleteAttempt(attempt) + showDeleteDialog = false + } + ) { Text("Delete", color = MaterialTheme.colorScheme.error) } + }, + dismissButton = { + TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } } - }, - dismissButton = { - TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") } - } ) } } @@ -1555,12 +1533,12 @@ private fun formatDate(dateString: String): String { @OptIn(ExperimentalMaterial3Api::class) @Composable fun EnhancedAddAttemptDialog( - session: ClimbSession, - gym: Gym, - problems: List, - onDismiss: () -> Unit, - onAttemptAdded: (Attempt) -> Unit, - onProblemCreated: (Problem) -> Unit + session: ClimbSession, + gym: Gym, + problems: List, + onDismiss: () -> Unit, + onAttemptAdded: (Attempt) -> Unit, + onProblemCreated: (Problem) -> Unit ) { var selectedProblem by remember { mutableStateOf(null) } var selectedResult by remember { mutableStateOf(AttemptResult.FALL) } @@ -1578,9 +1556,8 @@ fun EnhancedAddAttemptDialog( // Auto-select climb type if there's only one available LaunchedEffect(gym.supportedClimbTypes) { - if ( - gym.supportedClimbTypes.size == 1 && - selectedClimbType != gym.supportedClimbTypes.first() + if (gym.supportedClimbTypes.size == 1 && + selectedClimbType != gym.supportedClimbTypes.first() ) { selectedClimbType = gym.supportedClimbTypes.first() } @@ -1589,17 +1566,16 @@ fun EnhancedAddAttemptDialog( // Auto-select difficulty system if there's only one available for the selected climb type LaunchedEffect(selectedClimbType, gym.difficultySystems) { val availableSystems = - DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system -> - gym.difficultySystems.contains(system) - } + DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system -> + gym.difficultySystems.contains(system) + } when { // If current system is not compatible, select the first available one selectedDifficultySystem !in availableSystems -> { selectedDifficultySystem = - availableSystems.firstOrNull() - ?: gym.difficultySystems.firstOrNull() - ?: DifficultySystem.CUSTOM + availableSystems.firstOrNull() + ?: gym.difficultySystems.firstOrNull() ?: DifficultySystem.CUSTOM } // If there's only one available system, auto-select it availableSystems.size == 1 && selectedDifficultySystem != availableSystems.first() -> { @@ -1617,58 +1593,54 @@ fun EnhancedAddAttemptDialog( } Dialog(onDismissRequest = onDismiss) { - Card( - modifier = Modifier - .fillMaxWidth(0.95f) - .fillMaxHeight(0.9f) - .padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth(0.95f).fillMaxHeight(0.9f).padding(16.dp)) { Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.spacedBy(20.dp) + modifier = Modifier.fillMaxSize().padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) ) { Text( - text = "Add Attempt", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 20.dp) + text = "Add Attempt", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 20.dp) ) LazyColumn( - modifier = Modifier.weight(1f).fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(20.dp) + modifier = Modifier.weight(1f).fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp) ) { item { if (!showCreateProblem) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - text = "Select Problem", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + text = "Select Problem", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface ) if (problems.isEmpty()) { Card( - colors = - CardDefaults.cardColors( - containerColor = - MaterialTheme.colorScheme.surfaceVariant.copy( - alpha = 0.5f - ) - ), - modifier = Modifier.fillMaxWidth() + colors = + CardDefaults.cardColors( + containerColor = + MaterialTheme.colorScheme + .surfaceVariant.copy( + alpha = 0.5f + ) + ), + modifier = Modifier.fillMaxWidth() ) { Column( - modifier = Modifier.padding(16.dp).fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "No active problems in this gym", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "No active problems in this gym", + style = MaterialTheme.typography.bodyMedium, + color = + MaterialTheme.colorScheme + .onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) @@ -1680,58 +1652,77 @@ fun EnhancedAddAttemptDialog( } } else { LazyColumn( - modifier = Modifier.height(140.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.height(140.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(problems) { problem -> val isSelected = selectedProblem?.id == problem.id Card( - onClick = { selectedProblem = problem }, - colors = - CardDefaults.cardColors( - containerColor = + onClick = { selectedProblem = problem }, + colors = + CardDefaults.cardColors( + containerColor = + if (isSelected) + MaterialTheme + .colorScheme + .primaryContainer + else + MaterialTheme + .colorScheme + .surfaceVariant, + ), + border = if (isSelected) - MaterialTheme.colorScheme - .primaryContainer - else - MaterialTheme.colorScheme - .surfaceVariant, - ), - border = - if (isSelected) - BorderStroke( - 2.dp, - MaterialTheme.colorScheme.primary - ) - else null, - modifier = Modifier.fillMaxWidth() + BorderStroke( + 2.dp, + MaterialTheme + .colorScheme + .primary + ) + else null, + modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = problem.name ?: "Unnamed Problem", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold, - color = - if (isSelected) - MaterialTheme.colorScheme.onSurface - else - MaterialTheme.colorScheme - .onSurfaceVariant + text = problem.name + ?: "Unnamed Problem", + style = + MaterialTheme.typography + .bodyLarge, + fontWeight = FontWeight.SemiBold, + color = + if (isSelected) + MaterialTheme + .colorScheme + .onSurface + else + MaterialTheme + .colorScheme + .onSurfaceVariant ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = - "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}", - style = MaterialTheme.typography.bodyMedium, - color = - if (isSelected) - MaterialTheme.colorScheme.onSurface - .copy(alpha = 0.8f) - else - MaterialTheme.colorScheme - .onSurfaceVariant - .copy(alpha = 0.7f), - fontWeight = FontWeight.Medium + text = + "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}", + style = + MaterialTheme.typography + .bodyMedium, + color = + if (isSelected) + MaterialTheme + .colorScheme + .onSurface.copy( + alpha = 0.8f + ) + else + MaterialTheme + .colorScheme + .onSurfaceVariant + .copy( + alpha = + 0.7f + ), + fontWeight = FontWeight.Medium ) } } @@ -1740,78 +1731,80 @@ fun EnhancedAddAttemptDialog( // Option to create new problem OutlinedButton( - onClick = { showCreateProblem = true }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Create New Problem") - } + onClick = { showCreateProblem = true }, + modifier = Modifier.fillMaxWidth() + ) { Text("Create New Problem") } } } } else { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() ) { Text( - text = "Create New Problem", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + text = "Create New Problem", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface ) IconButton(onClick = { showCreateProblem = false }) { Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - tint = MaterialTheme.colorScheme.onSurfaceVariant + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurfaceVariant ) } } OutlinedTextField( - value = newProblemName, - onValueChange = { newProblemName = it }, - label = { Text("Problem Name") }, - placeholder = { Text("e.g., 'The Red Overhang'") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline - ) + value = newProblemName, + onValueChange = { newProblemName = it }, + label = { Text("Problem Name") }, + placeholder = { Text("e.g., 'The Red Overhang'") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = + MaterialTheme.colorScheme.primary, + unfocusedBorderColor = + MaterialTheme.colorScheme.outline + ) ) // Climb Type Selection Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( - text = "Climb Type", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + text = "Climb Type", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface ) LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { items(gym.supportedClimbTypes) { climbType -> FilterChip( - onClick = { selectedClimbType = climbType }, - label = { - Text( - climbType.getDisplayName(), - fontWeight = FontWeight.Medium - ) - }, - selected = selectedClimbType == climbType, - colors = - FilterChipDefaults.filterChipColors( - selectedContainerColor = - MaterialTheme.colorScheme - .primaryContainer, - selectedLabelColor = - MaterialTheme.colorScheme - .onPrimaryContainer - ) + onClick = { selectedClimbType = climbType }, + label = { + Text( + climbType.getDisplayName(), + fontWeight = FontWeight.Medium + ) + }, + selected = selectedClimbType == climbType, + colors = + FilterChipDefaults.filterChipColors( + selectedContainerColor = + MaterialTheme + .colorScheme + .primaryContainer, + selectedLabelColor = + MaterialTheme + .colorScheme + .onPrimaryContainer + ) ) } } @@ -1820,38 +1813,40 @@ fun EnhancedAddAttemptDialog( // Difficulty System Selection Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( - text = "Difficulty System", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + text = "Difficulty System", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface ) LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { val availableSystems = - DifficultySystem.getSystemsForClimbType( - selectedClimbType - ) - .filter { system -> - gym.difficultySystems.contains(system) - } + DifficultySystem.getSystemsForClimbType( + selectedClimbType + ) + .filter { system -> + gym.difficultySystems.contains(system) + } items(availableSystems) { system -> FilterChip( - onClick = { selectedDifficultySystem = system }, - label = { - Text( - system.getDisplayName(), - fontWeight = FontWeight.Medium - ) - }, - selected = selectedDifficultySystem == system, - colors = - FilterChipDefaults.filterChipColors( - selectedContainerColor = - MaterialTheme.colorScheme - .primaryContainer, - selectedLabelColor = - MaterialTheme.colorScheme - .onPrimaryContainer - ) + onClick = { selectedDifficultySystem = system }, + label = { + Text( + system.getDisplayName(), + fontWeight = FontWeight.Medium + ) + }, + selected = selectedDifficultySystem == system, + colors = + FilterChipDefaults.filterChipColors( + selectedContainerColor = + MaterialTheme + .colorScheme + .primaryContainer, + selectedLabelColor = + MaterialTheme + .colorScheme + .onPrimaryContainer + ) ) } } @@ -1859,89 +1854,114 @@ fun EnhancedAddAttemptDialog( if (selectedDifficultySystem == DifficultySystem.CUSTOM) { OutlinedTextField( - value = newProblemGrade, - onValueChange = { newValue -> - // Only allow integers for custom scales - if (newValue.isEmpty() || newValue.all { it.isDigit() }) { - newProblemGrade = newValue - } - }, - label = { Text("Grade *") }, - placeholder = { Text("Enter numeric grade (e.g. 5, 10, 15)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = - MaterialTheme.colorScheme.primary, - unfocusedBorderColor = - MaterialTheme.colorScheme.outline - ), - isError = newProblemGrade.isBlank(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - supportingText = - if (newProblemGrade.isBlank()) { - { - Text( - "Grade is required", - color = MaterialTheme.colorScheme.error - ) + value = newProblemGrade, + onValueChange = { newValue -> + // Only allow integers for custom scales + if (newValue.isEmpty() || + newValue.all { it.isDigit() } + ) { + newProblemGrade = newValue } - } else { - { - Text( - "Custom grades must be whole numbers", - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + }, + label = { Text("Grade *") }, + placeholder = { + Text("Enter numeric grade (e.g. 5, 10, 15)") + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = + MaterialTheme.colorScheme + .primary, + unfocusedBorderColor = + MaterialTheme.colorScheme + .outline + ), + isError = newProblemGrade.isBlank(), + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Number + ), + supportingText = + if (newProblemGrade.isBlank()) { + { + Text( + "Grade is required", + color = + MaterialTheme + .colorScheme + .error + ) + } + } else { + { + Text( + "Custom grades must be whole numbers", + color = + MaterialTheme + .colorScheme + .onSurfaceVariant + ) + } + } ) } else { var expanded by remember { mutableStateOf(false) } val availableGrades = - selectedDifficultySystem.getAvailableGrades() + selectedDifficultySystem.getAvailableGrades() ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - modifier = Modifier.fillMaxWidth() + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth() ) { OutlinedTextField( - value = newProblemGrade, - onValueChange = {}, - readOnly = true, - label = { Text("Grade *") }, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon( - expanded = expanded - ) - }, - colors = - ExposedDropdownMenuDefaults - .outlinedTextFieldColors(), - modifier = Modifier.menuAnchor(androidx.compose.material3.MenuAnchorType.PrimaryNotEditable, enabled = true).fillMaxWidth(), - isError = newProblemGrade.isBlank(), - supportingText = - if (newProblemGrade.isBlank()) { - { - Text( - "Grade is required", - color = MaterialTheme.colorScheme.error - ) - } - } else null + value = newProblemGrade, + onValueChange = {}, + readOnly = true, + label = { Text("Grade *") }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + }, + colors = + ExposedDropdownMenuDefaults + .outlinedTextFieldColors(), + modifier = + Modifier.menuAnchor( + androidx.compose.material3 + .MenuAnchorType + .PrimaryNotEditable, + enabled = true + ) + .fillMaxWidth(), + isError = newProblemGrade.isBlank(), + supportingText = + if (newProblemGrade.isBlank()) { + { + Text( + "Grade is required", + color = + MaterialTheme + .colorScheme + .error + ) + } + } else null ) ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } + expanded = expanded, + onDismissRequest = { expanded = false } ) { availableGrades.forEach { grade -> DropdownMenuItem( - text = { Text(grade) }, - onClick = { - newProblemGrade = grade - expanded = false - } + text = { Text(grade) }, + onClick = { + newProblemGrade = grade + expanded = false + } ) } } @@ -1955,54 +1975,61 @@ fun EnhancedAddAttemptDialog( item { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - text = "Attempt Result", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + text = "Attempt Result", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface ) Card( - colors = - CardDefaults.cardColors( - containerColor = - MaterialTheme.colorScheme.surfaceVariant.copy( - alpha = 0.3f + colors = + CardDefaults.cardColors( + containerColor = + MaterialTheme.colorScheme.surfaceVariant + .copy(alpha = 0.3f) ) - ) ) { Column(modifier = Modifier.padding(12.dp).selectableGroup()) { AttemptResult.entries.forEach { result -> Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.fillMaxWidth() - .selectable( - selected = selectedResult == result, - onClick = { selectedResult = result }, - role = Role.RadioButton - ) - .padding(vertical = 4.dp) + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth() + .selectable( + selected = + selectedResult == + result, + onClick = { + selectedResult = result + }, + role = Role.RadioButton + ) + .padding(vertical = 4.dp) ) { RadioButton( - selected = selectedResult == result, - onClick = null, - colors = - RadioButtonDefaults.colors( - selectedColor = - MaterialTheme.colorScheme.primary - ) + selected = selectedResult == result, + onClick = null, + colors = + RadioButtonDefaults.colors( + selectedColor = + MaterialTheme + .colorScheme + .primary + ) ) Spacer(modifier = Modifier.width(12.dp)) Text( - text = - result.name.lowercase().replaceFirstChar { - it.uppercase() - }, - style = MaterialTheme.typography.bodyMedium, - fontWeight = - if (selectedResult == result) FontWeight.Medium - else FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface + text = + result.name.lowercase() + .replaceFirstChar { + it.uppercase() + }, + style = MaterialTheme.typography.bodyMedium, + fontWeight = + if (selectedResult == result) + FontWeight.Medium + else FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface ) } } @@ -2011,131 +2038,149 @@ fun EnhancedAddAttemptDialog( Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - text = "Additional Details", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + text = "Additional Details", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface ) OutlinedTextField( - value = highestHold, - onValueChange = { highestHold = it }, - label = { Text("Highest Hold") }, - placeholder = { - Text("e.g., 'jugs near the top', 'crux move'") - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline - ) + value = highestHold, + onValueChange = { highestHold = it }, + label = { Text("Highest Hold") }, + placeholder = { + Text("e.g., 'jugs near the top', 'crux move'") + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = + MaterialTheme.colorScheme.primary, + unfocusedBorderColor = + MaterialTheme.colorScheme.outline + ) ) OutlinedTextField( - value = notes, - onValueChange = { notes = it }, - label = { Text("Notes") }, - placeholder = { - Text("e.g., 'need to work on heel hooks', 'pumped out'") - }, - modifier = Modifier.fillMaxWidth(), - minLines = 3, - maxLines = 4, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline - ) + value = notes, + onValueChange = { notes = it }, + label = { Text("Notes") }, + placeholder = { + Text("e.g., 'need to work on heel hooks', 'pumped out'") + }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + maxLines = 4, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = + MaterialTheme.colorScheme.primary, + unfocusedBorderColor = + MaterialTheme.colorScheme.outline + ) ) } Row( - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { OutlinedButton( - onClick = onDismiss, - modifier = Modifier.weight(1f), - colors = - ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface - ) - ) { - Text("Cancel", fontWeight = FontWeight.Medium) - } + onClick = onDismiss, + modifier = Modifier.weight(1f), + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = + MaterialTheme.colorScheme.onSurface + ) + ) { Text("Cancel", fontWeight = FontWeight.Medium) } Button( - onClick = { - if (showCreateProblem) { - // Create new problem first - if (newProblemGrade.isNotBlank()) { - val difficulty = - DifficultyGrade( - system = selectedDifficultySystem, - grade = newProblemGrade, - numericValue = - when (selectedDifficultySystem) { - DifficultySystem.V_SCALE -> - newProblemGrade - .removePrefix("V") - .toIntOrNull() ?: 0 - else -> - newProblemGrade.hashCode() % 100 - } - ) + onClick = { + if (showCreateProblem) { + // Create new problem first + if (newProblemGrade.isNotBlank()) { + val difficulty = + DifficultyGrade( + system = + selectedDifficultySystem, + grade = newProblemGrade, + numericValue = + when (selectedDifficultySystem + ) { + DifficultySystem + .V_SCALE -> + newProblemGrade + .removePrefix( + "V" + ) + .toIntOrNull() + ?: 0 + else -> + newProblemGrade + .hashCode() % + 100 + } + ) - val newProblem = - Problem.create( - gymId = gym.id, - name = newProblemName.ifBlank { null }, - climbType = selectedClimbType, - difficulty = difficulty - ) + val newProblem = + Problem.create( + gymId = gym.id, + name = + newProblemName.ifBlank { + null + }, + climbType = selectedClimbType, + difficulty = difficulty + ) - onProblemCreated(newProblem) + onProblemCreated(newProblem) - // Create attempt for the new problem - val attempt = - Attempt.create( - sessionId = session.id, - problemId = newProblem.id, - result = selectedResult, - highestHold = highestHold.ifBlank { null }, - notes = notes.ifBlank { null } - ) - onAttemptAdded(attempt) + // Create attempt for the new problem + val attempt = + Attempt.create( + sessionId = session.id, + problemId = newProblem.id, + result = selectedResult, + highestHold = + highestHold.ifBlank { + null + }, + notes = notes.ifBlank { null } + ) + onAttemptAdded(attempt) + } + } else { + // Create attempt for selected problem + selectedProblem?.let { problem -> + val attempt = + Attempt.create( + sessionId = session.id, + problemId = problem.id, + result = selectedResult, + highestHold = + highestHold.ifBlank { + null + }, + notes = notes.ifBlank { null } + ) + onAttemptAdded(attempt) + } } - } else { - // Create attempt for selected problem - selectedProblem?.let { problem -> - val attempt = - Attempt.create( - sessionId = session.id, - problemId = problem.id, - result = selectedResult, - highestHold = highestHold.ifBlank { null }, - notes = notes.ifBlank { null } - ) - onAttemptAdded(attempt) - } - } - }, - enabled = - if (showCreateProblem) newProblemGrade.isNotBlank() - else selectedProblem != null, - modifier = Modifier.weight(1f), - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - disabledContainerColor = - MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.12f + }, + enabled = + if (showCreateProblem) newProblemGrade.isNotBlank() + else selectedProblem != null, + modifier = Modifier.weight(1f), + colors = + ButtonDefaults.buttonColors( + containerColor = + MaterialTheme.colorScheme.primary, + disabledContainerColor = + MaterialTheme.colorScheme.onSurface + .copy(alpha = 0.12f) ) - ) - ) { - Text("Add", fontWeight = FontWeight.Medium) - } + ) { Text("Add", fontWeight = FontWeight.Medium) } } } } diff --git a/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt b/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt index 375935d..024f789 100644 --- a/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt +++ b/android/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt @@ -324,12 +324,17 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() { fun exportDataToZipUri(context: Context, uri: android.net.Uri) { viewModelScope.launch { try { - _uiState.value = _uiState.value.copy(isLoading = true) + _uiState.value = + _uiState.value.copy( + isLoading = true, + message = "Creating ZIP file with images..." + ) repository.exportAllDataToZipUri(context, uri) _uiState.value = _uiState.value.copy( isLoading = false, - message = "Data with images exported successfully" + message = + "Export complete! Your climbing data and images have been saved." ) } catch (e: Exception) { _uiState.value = diff --git a/ios/OpenClimb.xcodeproj/project.pbxproj b/ios/OpenClimb.xcodeproj/project.pbxproj index d03d003..d01e414 100644 --- a/ios/OpenClimb.xcodeproj/project.pbxproj +++ b/ios/OpenClimb.xcodeproj/project.pbxproj @@ -394,7 +394,7 @@ CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -414,7 +414,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -437,7 +437,7 @@ CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -457,7 +457,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -479,7 +479,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -490,7 +490,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -509,7 +509,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -520,7 +520,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/OpenClimb.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index e8446e6e803ce8c40458dd3da56f38c7945e807b..10fa4a89987b53863ed7fb4282d4b59246ae2be6 100644 GIT binary patch literal 114205 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

>=5b-uO1DqGLAo^QRty25&y^&0Cs>-E-K zt(&b|tXr*jTJNo?YKtv^|Rw*Fx~ zVEwbiSYjzDD(P9$x8#@-cZsLOTM{e@l?*P4mXw!FDM^;hE}2s@x1_FQK}o9Q%#yQ8 z&MrBp5iju2Kt|_^;yT*2_ZL@8QZL94z z+wHbHZ1>n6v^`{d%J#JF8QZhAZMNrZ&)Z(M?X_I>vK_CM?g?0=SOO0}iB(!5fAsiD+ZYAP)%?OEEZw0CKr(!QnrN?oPy zQh({d(h;R2OGlNCE*(=kwsd@H#f07sE%o&~86A_y=otfJW(p^bspvm1xgdH{c06B7sQI5exd_ z38%}IaJwT_hWzqad{Sd-Ub?BeCK+q2X_=pFXwFi$GQF6x8<`Tu#@LxsrW@0p>B01r zd0CJ}S&~(DVs(8>GMz{@&L2}TKACQAOjXaSPiD}T zSaoxCm7#BY2bI;cBGsu$)#-_K={jgMsjhi$MP0+3`Xt<`G8|7EX6Ff4qKysBsmA*H zWU9*VjYkuSpw|%$guD)~GwODP!Y-d9;&Vk@o?zJTiMpx`-JpYTO)~&PN9e{-+%cBM z#`;;+sq*YtstiRP_cG&bza>=}?9T2!Uw2o~>Fn-u2E5&!@K^cda)-LBvYQcNVocd4 zW*~DMGl&_?9M244hB9F$!bD}GY?94#zFZ&|$`-juE|#sEU?URDFlIPY&WvD2!sd)- z#=vHj$d}41g$^8lIclxwas&tX5|g(CIHDAQytCPpX}5uUR#%K;|fflSeINBOU`PUGY2-IeE{v< zR~gKslk*!>ONOPYo91R4&VUhgbdz11Y0PZ8Hq)6I%uJ?=IgzPmW-&EPEt8aOvRy8f zyUE?<9&%5)m)u+Kvx%9*%w_7BlbCr-Ju{zaU>cbwxv%V%eez&=lsrMMk{jj4@`ZF^ zj+tE3Sexumm#48MRg-LEoC)Rq+h^E6I=8xEPBIPaQ<1EPbx78>->Wk8Z0}%XU2~_0 zDuJK-kA}We4FD?+OEtDMDNh3k30)oA{#bcKbF#j^ZcefRsL!qL+y{(`Ua2zlYHvNJ zvAM1$naz!rgX$uQJ$<&?K`vRR&M{?Y|gvw6PY%GMr8W z5L?}$YffQKXUaA(r!uE8GDGBk@=@~9@-Z8jGng}(vzRmFV`YcjAAZ7?E$Xz;e>|ShizP>^pUKcP@daQmZdIaE@BGTu9eGnFqbkb zn9G>Urz-lk0Tk6_<=13-`k0DX9cYEdhU(OkR%o?y@|cRT8N^r-`ke7pG=k(n!z_&} znbl0;I_4^Qz&d7)?4)X&DvHBCvjZ%_b|yZeL}XRaMzMEIQHUk4>!XpaG)b_IG^jM6z;8Q!*WBBUd%CqRQjyXqQ>J z3ec7fbINOB`Riuaff`e$aTBu%wAYx5(Jjq%z#Sa2p1Fzg$Ul&^EQ{RA+)l+}GqZ)+ z%G@UV<$xTNLyB13)g~4L{};vLA?6XvvS-SUbt&bv8Rix0?L5nDW1eH4XI@}lWL{!kmXDW*$V26@9Fe1POpf2k zY-e_W`FNGt1-9mO<_$R^4+GONTrLMYGeRCoHB^swMVO;#GfKXHo3cEDx>edJ8oAG3 zbyLxtYN=^%Nr9FNwYU4{dbfXu&=Jj(GK}(c8vL&#va8aF8nP;buYC+hw3AA6YED3C zqK!@9LsS`D?a%$i*xT$}cFccchW^^j{U=81pBmUMISXB}@L3-Hg87CiThDySe8qe% zkCw;CW7jj^GT$+KK9U z*~{#k2BAt*V_ie@!s>b&won(buDV_xFIO1Q#HG%%?#@#beR+WS6XKhUv){@bU{;Q6 zn=ZmgJe*U2B<4({MjCk{I0cjC>9QyLKprw=Eh;j~ljO3oM%-|)HT=($y^^Xj)!5i9 zqnk8ZOG%I3{f;eDVzphmJiWnaGUpc*wuNQW8=I04!!)L-6@mDtxp7{yAyJpCuT|WG z%xuAFNhNE*dMMsV#{O0rN|O!LY}7_tnwuNp8S3nG*qF2|85oH7I9b?Hq${>|iU`-r z><*i~wA#DQ^V+pPaO{!;%WoE`31~^^a|F%_?p}3=)O1g3# zJsf&3v#OciAsKIEtPt!R4Jr2kgg&Dn)m{NnK@B+hE#P6U221)fXz-n&y33UJsG$1Ir|fR znPtTSWC2!0h4Pej5MoXJC(a$RgAtwfKVd|FZC?L(MpVvBq1Vt0n=-%rXGRotM?KpG zu$Mfe-9JG6U}`hR8qtn}q|s>l?|joQ_Ws)ZVyl?9LsF@Al}**nbEiNwlUzI;6i=&0 zJ7sKBvLRYuHy<#7dq=m_H`g`QC%e})&Yxf1P@C@FlxnPPsY!P4H(){<6y<>G=2Ts6 zb?tyrb+c07s1JxG7bfeeBoD}#DfpZM1c3U_)dSjlc2p-9%}Uj_EIzrqu6EY^|K1yrnRl+!q)tP!#L)aB5=&t=5B08 zMtVbO${>IRDxv&jIz-F!Kt2{D5AggzDEJr;1s@a96f_ICz5y-DJm10b#meXGPb6nK z{utz7%39H}s7yXlu5LyB!7R;Ej8gG}mSk$lxN15(&}Qil_+`fIjD8ze-3-?ovR!#m z5bPWb$BzPXja(}y*Ml?;M90aqc>GzaWAszJ3ViDt`9 z@&Y*}r!&zDI;kysY5w1fUQh}(Q=OcaTh^f#dEr0O$tQ#2S~S+k%{xeOEdZzJzX__p z)I=Y+ev6?@)SC*5O6=Ww^zM7q(Z@QRZm&Od-0?#J2OTkb?D)!wlc!FfSq0$ioRjJs zsglb(NXc!`XtcTxH8+6<%jt=+WOH?0y#eh&24mh~701352i=+T)@wBRQ|Ut#F{w|( z1BDhvwd_k30NYBcVk@-K8Zg|lwcoPCjnceUjRvlEZ@aqx<6Gcr&t7oVz{2%D?XCO5 z^?nwmbsr0SIL5M$K3SH1@|~SW1#h&u~+dfuoO5Gn0UM44awT4(^pJW7@kfH4Yk$r0Y6r0!1?^tufgF|9lnP z90#;c0D*7sLs9#4xLgZc6M?I<+aJ9QuFj?FS2wYJ|L0`~yJsrgoe$(dT>`Y(?#i{K z=~_cSP3?1Dvn~Z~)081vTNbx2Tyz*~wB+Q!wMJ(^!Q+Kc=6E%LJGU@*Fn2+D<2F#8 zuR~$urx5h~1jUQ{m_MO(Q9ybqU9=z@lr4hcK?9H%iWVcFwnsq0;t62CszB@1L7C!0 zbQ(GnoeM>ZtI!%KPP_%(hVF+l#K+N#ARcd{575Wx0F)W>SPg}SHrxXrg9l&_6c-+k zhe2uKSX_-~<3=baT!fe5v+#0!A(RrX!E2$E@FplAydB?zAH`4MXQ6QLHT*VyAAbtv zffkbecYvx58=V#sU zwwS{Lw=!->Ws15J5PoF5YcQIYSY%i``bcdw+*5$mOm~nU7~4?4WPD3Q1K6iXePhkM zG)>{OrCi1X^bP3@O1P~h4e22SXwJm@ncfUIJGaZxRn#`Dgk8A;T`3d!bomVV%nfKY zS_3n>Mm|eE7iM!FHTBe@Wd;lp1-Na=+O|Nc7hI3Fq*5@-QPnLCHFMi|bbN9)kOjCr zgA2Q$)$r9W*hAHlU5` z(Dm}!@;M#qS23qIp-q4cXRUd%c|E!b-OQ|X`p`cUgW;5EdR$i_= zcpJL?;5ZDp^$uobHhhD{SV}`@NAQ%g?n^x(cUkw4*L}(<*;Tz8-A8?}d(ge|Me@b1 z=zj3QF8MoP;P3&zFA|P~BGH5+8g=_1;7@oQ5y&7qyrF~Jjk0ntpcmK5tCW?y1HIfz7fQLaJ+r3m>Lxq7UFZYI6QS48>*x*iCVC5` z{~h!$dJnxXua?)ySIgJP*UHz)YvpxvtGs>_`VgH1%3(M91bvD=L!YBB(3kQCkg|>P z4f2ihUGnDyNd&0~GSLZk&oWC#f~o=>ctBAg9svgzl4Zw4lCv9AkVKl_RGq3zH#TIX zi^d*pGNe38ug)A@y|`|E-O1o`((3Z59d`hYYD(43hxjB*C{wUro0|#%PRZU*R%gbM zm2z`NoMu-;G?d|SC27~ajX+zBlzC#DLS|I;lExMgZKEPCkb0rhqCNvmI+o1zG>(?f zr>Vlo9Y%U5`W5{KsL(#B0skHP7*o-{9;1>Cb22H!PS-LzO1@s%)O~0_ph)fQThSlP z%FY)IkQ{#vBaAW2=rIQ>T)-lhunJVU2Hdl3LVHxB5&_KYfKinFC}>Y^jR#x@V2h-3 z9iE(DUDwdj98ibMFviZ#_Stby-YjpDZiJFb7I~}8R2fD< z(3TuHV8B69b4Roc5i>A%b$v&CJD{#}wA+6%&8kB5eUdVj-nb8BOaM8`u5Nb6T5(@G zgTvi|fC?XlkH*JLZ35X+W-i|$-(F>i{%hlM;QrGTJS)Q(>%lQ_0t5Zc}27eRaFzDom2P{DC>t;e zZJt0uR`tTo4v~RW@wmE%hGcEd)wHsBRaD|ml%JGWy@_jZElz@yHwRd1F0R8Tq3dxy zo{t-VbQAoZ!eEt(vm%ir*JA(_0EvnR1DovBL`IJVU8>7FFmoJIW4o*`A&Rvf9!%t0DTQAmeZB8s}$iO+*&U5*m?0(>E@W13Z+PSWH+Dp?P> z%)%r9C@;v*{|{)DOYo)Oa)AbG#g|a=vHYvjl%eO1>)lA_=yb51|uf$j3)p(8kio9LkA@7u5m3M8#*WhdM zbr3PE!>#gb^6T;&@|*Ho^4k=E=$q4_Laa);R#}~zlWcCgWN;j&A-Gcu>l$0qZ9`U? zMim6C>@V7P?zyeGl8(wwK4%AY>!6?JoHc#v&*xM5_aHM^);ZJ*Ar z=gs&QrVOetK;A7mlsb>Wq}=H|m+bSmVMv?P=d%~?!gnhd6lT7UGV}fNJMyYm@k3zq zsO5VYKLT;wV^h+ZY6$Q}<@e?H+w$fDwU7FZgzMp z44FY^FdXu`<9-Mad|@b{^MoAXgx~G(L}C$_+a2@;LT+U+uj4nAl0#)`opg9Bev^)X zmLnc=Pua4!GtPc?)!xG&L-Yu-_6PVw{E_^H{H6Sr{PhOB8-D`f_h<4q@(=O>`A^w1 zc~o+Co2yF0$ac@Q-SeHQc(>H$o-)2}&fHAnESYcdkCe=Jcn|&_{~&)We<$yezu&<0 zhQQ!yrY~5|^C7&^Q$8)32p&Dn+dG9QzGVUxG4{Pq-wt|}sm<&F2&dto@ zpm<_QsKlvj0xJj47v?m{^iEApcAN)ThACTz|CE1R$0GSBIvyi9DebfEh+0`5ZE0l% zR+N91_q96?teVYZ3fHq5R?F(-U*uoq-`2BwrY~!he~0PqmCI&8cO8~>I{nm9-e`c2 z83?f&M8CPEHrc-Fy@oD5v2%dP7PIz42$ce%?(%*h^oLy5u?dii`Yq?#K5T!e&|v$r z{Qyomnmq;}l`_^r5JM24BbXqTAgF)k2@(hrX{DoCImhwcl zlA;zMk91>wk_JR=8KAh5ifwacXnE>kTUTa1nI@%jvxANR3AWmRk$H zMQ(HID;|C)0qk>UC_KK$8D2c)6VN@(6Y$L0W?J9o#&I9e)UuRREJsAPDxK zpgdqRn4_X;dJR1{m(8ZKGY@&X3ZAYe$UvVqQnRgx=dd3^hwHXx6dQMZSm+2vl!p2L*g%r0Y3VNYdGV`Y}Gr?Y3UXR>Fp zXA@LRkd>ekf@}oY2`VM18$sO(>OoLXf_mM|o{P?5m&3mc*bCW<*o#3ce#ovMsJDDR zK~DO^MUb1I%L!TonsNg{cPQK0R`%PjpSpyMz~U<|I8EeLG%u;o1xNlCkvK9k^3QOU z!{H|fp*8;q4EkHdCA&#$p|&45XB|O(*0Jje>ibtRaRZf!8`+x(>PL{HLndxvw^FXY zmEFv4A?PTAjwa}s_3UlH*mn?gEM@GnuB!RYLR5ER4*NgALZRTHuvyT(hpVpoa zIJ_K9e>Bt4RGJ+rZ=lg$b{(E&w{>#j*cTxDWnU!7(=o|c*w-QcWw*0C*q!XF>@M~- zg1iLz2=WsYASg&sXe0Xu`zHGq{CkJ#P0&Dsh7&YC^YbzgE2?f3#Q>zUQEaD>7u=v6 z4+TsEz~H%oaOY?l+$xa7BS+TR$$iRx3863h8T&c=1wqFVG>D+V>)EeBrp_Vgco3iS z>DUe%{u;6lH4PRi+6W>NTLSs&x|-~jj1$HF2!1d76G1}`8}+ikvcI7OyO-UEo8b2Y z?4Jb12pS6o*=Q(1C&-^>K9447Tu$J~Ar8~Pk>lx5K^sSC=txjlKL0<&!#EXsaRaC3 zG@OAgG+65$(|@`zvR}pblYtarqWlmo`T@<9+o{FRGi}Y!KS8G_H_+leQfO zn=Id=WJnEc4$!8Av4_2sLB+V<6crke9#8=s6$f=K@?1JJ>?Od+RYYgStvD`TB1P;{6B!VUrG=-q4 zN8s7RGUs7AuoEDh<9#_xJ=Nh<1u8CW~rMNWLOwbI1W)f6I z(1`?rKQoJ<8iHyGN)j}ipgA{l3l)EvJDFR`EraA@Z|*dvFF|t^x0#@Nf|e4r43rVb z<%NnDW9?uBWsCZ^$It*iA9@pFqtSH@kilpI>@Y}H41uGKuqo; z(C6I61l7?r7~~|m6_f`r<1VNF2s()%a5w2sc@-S9ACTpdHQcpKVJmkvcMU=F32JEN zuH)7c)JUK-27FsnUc$<(bdUVs$_a*ZCk2ANe3Cbzmx4@D|aXO5ZrCt?Hsr$z`ZR5 zEnLst#of)_L(n3EmJoDu217e!oth3BWCeuLupI;bqG4s@l2~1uKG;#62ASYPo=Df# zCTCTr`Y)pA-*#{h!Ky#ZJp%dO{w*y~|F@($nI3B}Ouh4?-N9>y-+fKDdw1V?mE|^q z7H2RJ?lJE1i2zDYD37&rk3qhYdlEW$>d+1beD?PEMUNz|yLIcdkp+AD{01HL8d|$H z;~VvQWKZV*EN;#{$886{hkKrTfqRjAiF=uQg`iUiI+dW)2$BgRP&}}a+X2%2Dzk@s zjeDIyDFF!ZnQ|pT=Mi*4#2ntrIwQs*jg9>W8B6R;jsI2XG+88ui z-LMc2Z~_+tB$C^qg2lHz08)MMqX2k7r9h@sPkFxh;1**+BS3*>GTm#)CudEbaMt9B zSyuUw+fAA6Bkp5@&LZgSR_+rH^f@ce|?Al3(Sd*!UgS~$DbI~euHBLNSbKXnJ(kYbO89Fcf50OxiS0k<#a3OEC< z4FB*vFF*j(uBF=S9_XB!d9A!iLA1l)q#b#@xTbpXdgd<9|iFs503VRbv*bf z*Jpy3!{dGY2@ntR-^4HAQ+%2SeSR}Rw-9tIL7NHM zLeN%%Ku_NO{{weQ$%k?r{;(L||2^Q2KaYoct}O;Vznnjxzkt7xzlgt>zl6V(U%_9- zU(T=OSMgWypfK(t=x&0*e&0*beFWW4&;tZLNYFzBJxtIe1U*X7V*~+6@C5B`HNS?x zn!kp6qWoI;*UGQwH}Kc<8~JOfU-~3LuM=@QfkZd;NX1v_k6lEBi@y>{AW{!{D}%Ka zL46vevK2d(4P_uXNUc~~5sPv!^3~({j=Fla8Zt|6R z=OTbnJhg>*F}DPV|M?4f;#mV?)b;>DTO$&bLO z=VcCsr8A8nAzqttT>1Oai<@wYe~^EOf0%!Sf0TcWf1H1Ue^P#qpr;6WhM;W(Jx|b! z1iehqc7k>iw2RuF_T)0ZO$nDFhWz)!*t|3CoeJ&esti;A724?mVJO#G0=~@=Ci{j$@P#S^J+bj$75`*v zs@njNHf)-@l!s38U4`aLRfd{>g=QbOGeaBxhl=h2@D74xa~etk;J^s{(s_A5QYgPt zWtjONQa*BCwm(QaSKACjW=j z4=QKxsQqd?+cdZH3V*MVd+lEkR;Bv^%72OwwM|c{D&l|Q4=`mn@jvsw@W1lE@xSwX z`F;F;{ttrQAm~kk-XiF2g5DwMU4q^t=zW4dxQYK$U<4#!ffYD`7X(2h=tF`L!7+ks z2sfGtdx>Z#;w41P_+Lsz#E}3q`l<%Sx>Om)g7bdxVdsHxt)ij2DLuEbxh?8Zn!^#W z7AR1r`M&}5S4UmLG}nv)fRQN$v(r2?stg1F0a!OCbCfh$k;(UU1ht({U>^qQYb%;o z#*#HgT}KC_e>K4DU@~vyp&2SresDZ|NmO~QN(a@BGva?j4Z0uF!^HB6^2n(8c<6@qk?|`8FM6?6@Ckl`z9Q&rc@;ed-(CflP6DzF z4q2ustnHMbgJN4v=@XRdeBn3R`;1I#821qfS!So5pwg;=KG92_yF zI^X5;(E5Cr*A?vVE9)Ng2ii;Yh2a83Z|j9}VT1q?+z$l(NYGE~(bK{hVJwBmekSOb zf3!q@y8{1U6^q)&>-A;E>v0Flx(9+VT4kOS={zS9^lNsWQ#13-a{COSnoeb=P$is5 z(C-B8C1@X=N{vuUr?Q`5<{wSvPVy**IeWTTI2j^xVF|%_ov@T(E?30mi~wS`%22-8v6w=eBMM9(4YjMK?!HV;daNIK3^yjj|Jgf3z?Ex;SAv{ zO8QKK`E|nC1PgS*&2t-}tRr)Zt})dXC1f91E?ht#IG5dV-H9cnCyZ z1V^Ag3i`JsGiY)qw2Cb02$}!}1U#?p^*QOWv*U~B*3GJGPM0^(F(?s+Rmmp6bd;MN zPp1LZPab@czWFUtm!hvmN;fq^HZ%n&=8~})eE$d(@@wD)7+`$iSW3rWvI}s7un90d z;YQ&mf(-;4TZNm2TL?B0+@E64r@Fm~kShQ$j0=V!`{fM=6YwUvc+?SeJ7XcA)8qF! zqYgOh0mZJ#S_eJa;((eszuWD0IRb$opncxB+Yy1Cbp$+-pfeWtgq@z~KWpUte?0(K zcA0Jy0IY8nZWrz#*i3MKt8kZaH^Bu2J2H9x^ug87=5`;ZW6`snJs>>66y6{_C_E%Q zEIcASDm*4UP9TMgEd&=4TuiW);1YsuH!!`0r{Lc+@NXL}$RpTJe{`cips0b8K1`kl zQ0&|k)1jG`PARN^C;xP4t_PgGMgLYRNO%=eLBcMAOWRUG!W#hC(&7R7H&Z=8D+pF) zSNMJ5Bf4E52;kIqC%8we@UgI);GP8cp=Mb z$6bEXvVB8G+Rxm5|LJ=_?sp1J`-mL0(hi5@^DS}ddz?L2}+XCEsOd;MeC75gx~ z#eVSbXhn8iRBS!;#~>=Z87nM0fV0K^1iRZfTXg*uXNy7cI6CZ*IFMj3!M;{;kT{rN zKf$4YjkCpw7^g&{+(v=}z}&53f?B>HSi3_wTYW`#X-0|T4#U~v3F3Il*)ZPYGMv3b zoCq8<=}?Z@yZfrXHyk~@?xFmFO;<07fWrmcH5J-UJGAZX8%Hmm`{n3aH-G;9d-Klu zV@fCPnkiN*+yzb>aMxhqE-^vHGI+?~l^vKvb<2%l{<1qTuj9gYAm2!EIFp?c7lP$OKms zd}0Q>#?=Z={g_d`jE~%0qlwd`5hh z;AsR;CwRts@j3B%syt>AT=jpzEW)8oap$4R=qKAX??3Dse(K%#e6=mKxyPK0;(86* zzJ6%it3&qG{leJr<@%RIw?=mO9sg(nIyB6FBM3Il0_<#iY2R5 zBH1LnR4R3ox=THzo>DKVx70`KEA^9(l8%;+k&czhB!|>r8X!3(m*kc_l2`IcekmXY zrI0jGI!+oS4VI3VhDbxDuoRJ^QcQ|V32B%#Tq>7FNF$|D(r9UnG*%iXogj^uDx^wj zf;3T@Bu$p4NK>V0(sXHtG*haQPL!&pSyGKuD~vmX=D(q*J6*rPCx?BGT#78Pb{3Sp?S-Je%OT1fN83J;4nG zgLjuAxS8OE1TQ8S)H&$#Qwf#{KAm6)PR}OzT!NPqd;!6*W0w%Tg5b*uUPbVg1g|Fe zYJ#sNcrC%L1aBZ1B;!VcHxUe>)@FjY5_~(scM=S;crU^C6Z{~-4-@<-!4MccNic{H zNX2smzd-Oy1iwP?4uW4L_%(vxAowkU-y!%tf~O-4 zAnYi@jv?$g!j30wC1EELb~0h75_URaXA<^A!pQ6iwS!&VV4p1RKm(o&rjGh341nS&n4_~!d^hwiwJuOfjWKma>A}6?3ILFP1vgm zdo5wt61J7F8$eE^bEI>n^Q7g{`O*c_h0;aR#nL6xrP2!NGU;+@rL;=ALb_7AN?I+g zk*=1mk*<}llh#V>q*iIYv_ZOF+9=&1-6-88ZIW)5Zjo-4HcMNit z-O@eMz0!Ts{n7){gVICN!_p(tqtav2ZaY?hX z>ZR(f>Z9tb>POg(guQ{VHxj7EXEzb{X2RY=*jov^nLrIbyOprF5%zWh_4n+ZguRQf zcN6v=!rn{R`v`kKVILsugM@vEu#XV-QNlh(pmd&nlCVz^_G!XCL)d2tyN$5V5%zfk zh4Ac4gngN?uMl=SVRsOACt+VD>@EUD@a*e^eS@%X680^^zD?M72>UK!-y`e?g#D1P z9}#vpVLu`4r-c2Cu%8nsL1(`t>{o>Sny}vx_FKY!N7y}t{hqKt5cWp`Mda+yg#CrE zzY+F#!tN#PKEm!N>>q?ZK%j7(V+e-`hY80Ljw2jTIDv2?;UvPT2&X2ThHzQ})!kek z;q-(v5Y9+A6XDE+%O_j`;R*?7AzTsRiV0^WTnXW9gtHT_lyKb$*PU=Z2-lNvy$IKv zaD52ZmvH?EcNF1{CfqTEJC<-|gmVzCKj8)t&Pg~IfwE)HLpU$te1!88E-w{4fEPB42T5$Zm%;Ib$EO+c(H2S<%RDbh$S2` zcOc;p`&`j*G}Z~tSGg1_P#)S9Dxm}bOu?Yj;r7Si9WMSz*b#{WFcpbLBi=ATzJ9;6 zGZnu=rLJpK0-i+B?eoG6B2ECdT=AeIl<-F#F^@YKin+XS5;W17%5e&nd0nI8j|CDj zS3KefxxFx#2t9BbaKY3=d2pn+2muSQT4!_qQ3&lMCgvZr6^QEd(sI+vAiYw@f`4TRd!wGB) zD;7vN!ZBwA@Y0As0>=z|@Qodv=Q2g10`IEn%3NI0gf|p%0~32PKj^Fyp}52CiX?(@ zcPJi=<;crSg$mT-bcIUD=XZv~A(tZ<_R%* zM#TrfbO2r~=y3Twu=~M~#}R_K*Wr!^A^=c3LlJKdkI`3J!tS5aH7d?fC>n{v8=+m% z2+Squ^g1H`n8)D_yZq5uBI=5`bEI;jv!{0xL$}K#&MWy#asJ)p;)GD^%bZT34hp z5{oC6+%+n0H@ukL8*_n~1oaQ^ZVQ5aamO8rkkbh!%@^_I zYM9jul}o!uB^vX4d_iE{pr6Wq7z)`V9KskI@c0NGGA)bIW zayo%ZI1vK}337RHP&r{wI1&b4?mUm16e?GAjY>G?bGbc`&kgzLo-j+`=sH&V%8PK=v`}4MJ@~j-9+ip|YlHRKk(C z$LS6O+j_>_REV9xXp;O6mvtW3F%lEKlbZyH}xdP1mTzouQz|6O1`P-vfU|eULB> zdtvS3p}60fh=+ag9R7Mpp>kapsRZGh3<6-mf^K-(p*KR=i^^u)8v)Y>zD+ccqxBzG zsK8h9bcM%Y>A@EQ&E*FM2AdDAegs@T2WXtI)9vwi+%9+L6?;aZvc79n!eMX7<8U`Z*65<74A7Thc*bTba6$r&C2Rq}j91rPjg~HAMoI(P&*5?7e7z)rR&j$<^ zbitR-L5SoJBog6RIM+M+K%sDJ*H$Xw4~Cpxm;$_Z2j=0YHYMnTAk3NhHoG`DE;;-0 zi9%&d*QmHb9H2zO<#4(D;7)nHWMben``x)3|2u`s9bKaWVi5I*AYz3;jN1AbP=RR00ls835plUf zu~?2>`AMO2SJ$WnTwy=NzA;DK6#^<$SfD&4L<49c$>Fcx6)N|1jf&F?U+oxz z?=SQP5)ey*+wX|D5}=hmVNhZ4HHVQ{j$JvRP`R&bR3aee;V|egSD1SD9^iwpD;{(B ze8G6k3+_a~+nLAItU~31u2J#8puLd*%peMJsu%Q$1HOL71rrJf+);nj?{;-wF}0*n zd8lhtyq-Y76N5k^9EGFmUS9;}5_AR~P8vyqE)E0|oi)B%r%-vMYgD3fkj9wD>45Lt z1S)hcFpa1q3c3@lfj{a_bk_K4lS1XOu2BIOA2tAjM^7S=fS3jLCFBA=2)RKKJDpCj z`<-p1+M-Z-qH9zlF?SR+j2nVS7t96x7>HQ_h4BH96!j-!L1&_~y;s{5Do=HdN(jDd zC>ruY#T)qhG?oOrLN&e<`gi+WE(-B<(lF{C3YBNNMkN{y`N4ky9}j>eh$dj;BVe~c z_5)xf;~+`y&e4RruR>*8*QhvsF(05HV0yv3fEDw>ic!gN#==lG7L3tXC3K#P^416Z zd>5(26E0`m5BwVg#Kr4{=p`HkV8IU#hCdE*zQ^4;mQ*{HxxCmlD&Xja-N7)(q7RrD zU>)GGC>UnoWN?iWz{{Q-PWCBOUjFBHG7|TN65)6lYT-PYsEB@TEhtWiiJftfO?M;* zbyE*iC~WWAJiO7U+aCrOAHEzHsD!~64~D?EaCtx(1L_LJmpMB>M4_^?Yg8iC)r!%O z4!(#Nd@wJ?U4kJ8M3o?pzF;(-;~lAE3YA@5qXH(x4J!|BsvE*@h$bR5GD^UEINV^n z0zS~oIddsjsJz}aD((QdT@gS-Aq)cx=B8gP4AM@4D?i|sF%RGhow<07Lgme_QSl~% zJ}+2ahtKZ^S#d((0ik?ce_T#?TPqe-UtN2UdqYwFzB!#;0Z1daG%ea@cMi?5;;Sm z@_yH-1ig_MtzrSk9-J7M3jjOdwm85HCjvn)MCG2&c$j*YLgmA*Q30SJ2H%DZZ*z$S zVPB%)zeJp%XFP$ZD-iYsU7-Fu@BSQx%Ew)!5|6li@D+0)AaNJ0m=l6yK-?1$OL~1S zfck>raA#jkU9V92q-#`yfZIc=#S38_ghyTwc*s$>!M6B)akncFg)lFNlNTsdKI7x>5?>REXFVMh>@4(OUd9H6CyC*qEGUa^G=l`pzR1>^xxk)Y2J4O1^E1_53K zwgf;pUohZ=9ZN)VZ2nS(%2!>Z;zP{&&W+08XYn25^Ww;er}#fT}yM*jdV4zUvy5a0FN^1}Y^K2U2wL zU@jC+j>iGj^Sa~laOYT3yjRM3iQy zJn+%$_Cs*y_W>lgb~#6u2vFAP|eNR-Nb-01+{B?=}W0eP34-G5r4qUstIh*rQ6 zg7YchbA#M_0Sga90vCw-eSpS7ZqlCJcynyaTM89p*Qh`&Pt%hDM<7lka39okMEnqq zC1L=S{eSGe2YeLO-he#`yE~L*dv=p80Sq7lf%F1WQX!O3LT@o70fIm<2~9NAsklX0W1{na!w`)d+ePOeEwM(CJS~PbKQ-CvoVnE>eE1Nr`Em6o!)- ztaY(?pG5RZV+x!B1>u>gvizLfq+C{wbzZqolj3fklzc`GY)HvvZ5>x(Y@ElM2>nV% z|0&rixjA_SX}XyFr6wir???UY0u-E=PN$XWDk3EVF+P2smROj{f^=$nUSYO=R^(ev zg133)U_~N7KaYhX_6g9dpo>YL5?sn613)9CV}CdJ=8Da?DMvnMGdF|&~E z2!Z_-SQLXw+8d^QXJac*aA#IjZs}mSkX#tDciU-DJL~g z3fc#EH#y$ zOd$uF(KlzdKb2UWtFy6onv}%mNlD4fVPwjz9@A~IEIGIeQ+|mB49HVC5+OGwTW4b( zH7PxsCxyAl%tCr9tVm*Bs6p6uKuurY~Q z#DYwEuS^^!vOz41sTW!^T`#;%4^2vH^Q15)qM5?7uqiA#H=SNT@5&|yLVj)*Pm|KCc~Y1xpxLGKm6E~KJ993qz7)`&vGmR!p=1_Bb$b!p4A7+XZl08!WOjok z(=$#^OQ$C;cZ+5vXS0KgK}BH!qcawGn(AeUCZ%ulq{wb?78`jIGw3VHo<*R?CujSz zXhC^_zCbT!geE1Yc~Wv{<=H=%otP(=C(~GF#o8E_B{3;lK%|vTWt$=w~CT4ZJH`|=5Ng32UDM>kUM=J}uOoGU< zi(GFlU>%$;Wg)$B)~M2(&b+soqe&UkJShak45s*4HIn8PsIja(!WtErN~eQnUtgBF znv|ixs8HU6V4rc~T1H_e|4qNH$YS zOJ-gyH!C$=u2QjGD35nC3w1SiwkD;dc~WxaCYN-%Xq3j*@U(oaEjv3aCy~8qNm)$r z$&F-9y>f{rWmNN|WTfTOrXZj&g^moZ7VSQ-n!FDPvnJG71g_)j0 zTm@U8!Om+s^WJ8KCS^kNq$Fj_joWl5Xh?|3S!_nmWnqC>L!4ymLP~OmF7jWkNtx6< zDU7s0cj zQZ{Q+s+uXKko{=+eDg&v}CaI#5l^@|Y%NR`aB!GTP3_rx^LN1c6Z!Gx2O8W5q3r9bOCv zbpG;`CZ)Q0QZkd+}toxcvl0J$gSvE=`6a^yG+&hVV>8doYp)kj4N{sDNkZv zDla$@{pC?~Oep7M(-hE2PG^F@X^p+CNvUm~lyq7|Rt&Ne*@7mc7fo4qcADm z6^$BuAMuuCxeceOC4Zwy5zUi=irJOQE{lxRT%x7iiOx)A5;ewl3#^T|EOc{9KWI`G zH&04_8dHkN^jtDh<>$=!);;|UR;hE7lIW*pr)IK_+_WseXj0B?o|K$I_MxynDKRfw zPQ0RW@1x1>cEarY# z%xbFrD3d1Tg62udV@|1nouP^NKWi_%bi@b381Ls4X3`RhlCtq`G+Ea>Zt4UeWJSj}+FmqX0AlIhx zmsHwH)^%twQ4T{trZ9BxheUPIq+HQFDd{vL=}d*O48r0o>-zL9*@w;?RyI9L*5aA> zZE9mDX;Q9go|L3aMs?Zj9YK_A_n8=F!@JyY#k;$iG;l=Grc%0VQm$#9lx(>>khgQN zEQ*mvPlowxxnz~e8zORB2Y%F4N~$L1y5>nC_VV%(EizphIg>=coCz5E$xMy$r9b|f z)wDkrm8D6!p?Ol+K+CE-8|$+(0<#Ldw?l|zx{T>}b`a4;&(=xltw~wcJSlV@Gnq{y zP_lkNpPy;PEZK45l|Uv*3+P?w_VY#MYEl}SCxzFkSZ0x}SiWrcb6BLNDPR$Xm(r4W zk+Oh2t4;ONUz4(?c~S@=jO>{bVU`;0C(B*1S#;0X#z|vL=axQ3Qz?TrDK|Aw3X2l- zD;TaOWlJuhJP1Glp zMxCNhD37YpCrpc)u1`2MYNkG+I%%XLxL>l0Q*t=1>3iCU{qSRZw> zK4D|jZG2oTijQtQa`?zqYwg@=(>Q6UQl9q9N3`ogI=d_CZvEYxqqgW19*BBSpYTZ3 zWAgcYo@47@KmVzyr}cL~7xlb8;iaf;`h-`bw(ApKkJ_nE*cJ7bK4Ev%dw&)(t`b_`aFQdNFCwvpd(b!ED_I=b3`h-JKKkE~Ii~3!c5FHuaLZ4uY zR`m(3qub~c%+VHof<4-yPjE%^3GOBti}pl&^$GFOZS@H!L?`GIIz^wTPv{ceRiDs3 zI*Hnh?jc&d_m{O9ogST`zdJj+r#_)~G{+A$ME4ObKKkp&ylFPna1!OP??&`ZRq)T{K4n zHGSxU=!N=(Gou&j6ZmSLK4D2TpFnQK`vS|6LDS4Hb%Q}mi>eQb(eAFYo~(Ho<0)#q_j^d0(yyQA;XCv1tnPoMB$^h5fD zN23WyO>1In^ppC8XQH3gC%h2-(jUS?FknQ#8m*5$(XU7Aqfhj%XnpjF-W~m}{^=h? zf2dFRBzliNVP7=o-Zsth7tvqp6TXiAMxXFq^!NINpP~=x6Ml{6#D^viHAk2u^$A9^ zNuSWl+*+RyZ8qx@Y-YPYA;#>|C&ZaO`UJl@UZ2q3e1blqqq&nl;UsexeL|wSyFMY= zoT5)iH|xWTIoqrcFXrCnKKfg7%(?o60&}50p~yT?pD@H+tWOwj9-&VdWge|h7-t@@ zPncw$tWPL2Pthk-ny2a$s?0O=3A4Ir@Zi z&FAS8E-+uHPq@UqOrLO>`Eq^273M4T3D=mf)hFCwzEPjhU~beW++<#-Pq@XrL7#A& z`F4H6o#wmr3HO>e>l5xbKcG)|*!+k-;c@d5`h=&V0o4+xC zYd&cH&iuXk2lJ2SpUj6iQ0h_hujb#34_OpTgeB6_!noC9G(OBhQo>Ly48w#xxA!?= z7%2>+g<-5Pj2DK9!Z2AFP7#JF!cZX$Q-xu=Fw6*?K=pz!%oK*%@_Z`89AT&t9I$Jc zCr_p_Ea1c{@?>dgX=Q0`X=919L|e=ji^XcOS?m^v#c7GLxGZiFkv3M;$i{BD& zX=`a`X>U2fl3?M3W~U3o6~eGt7+X-WTVH_)rwZgbe7;g~9yM%GOFn%nI z-wBgZn4H4YL6}m6DNk@711BEvk$=8gZ@OQYo)e~B!n9BKy)uh7qBpQoe7p|K;#{;T zoNRRTGo&oq*xtaJ@u+`&4#}vxD$dFpUo|K2FJZ?SS+o(pftBNbUp7rv(Ja10@@EHf zS+udffi>iUzbe~M4z>>*@^$2dq#nmQ2g{<3@C~dS|NFAYV-^15+;NLG);F+*e0&Zz ztL>3rSyxsyGjtl2_Fpn5;d;|%01T`wm;6;d&Z(@at>)CSCe>I~UEAcp4p%g%eT7TX zW&sSWF3GQ&lmovMxLW!voV01{R+GX$C?^#pxv- zK0!{O9G-#ES_W8?p7{@JAncU0Q29#^A2`;uCc?8YLCXRQ*8lp5x8R9#ITe9pPkQjr zoH`D?ol|#oLpw#2%p&%Pe^~g#=5=iEt<(gujy>gH3(~x|X_W@fc|3XvXJ~?1+CDyR z96X<}sq{Y$ht-;F7Q9bAE`<)t4%%DOEc}^w)oD3kG5nN&n1jDz+~EejP!r4Q_`g0z zuSc-ZfrB6eXG4aIJ4+MCGWkDs;UiYuv{tnka+K;0&%$CY3#_64`&M1E@BRz_tOs$v zCX1Eyf2!0Y#noE%riK1fYraGi%hGzqKeVO8Vw*G-e@pb`nrPPBkB<`{Zi|CmwXn8W zJ66hag@qZ74VEh{S6QyMTw}S`a-HRR%MF$rEvtm#3}HA^7#0b`S;BC(Fw_f!5QcMv zVX-hQ*hx29G;1EDarZU|8{g#~lRD#Oi)Lj{&l5;kX0K zeZp{F@VEoZgU34V!19FUDaF-j*=l)G7|s`l3mPp?Tb>by3x#31Jno>~u$g5G<$-v! zD>zMS?##N{!DS1p=hhX3j-Nc{NkPGhB+HAIS2(%A@{(nnGNEj{_hD+91UNvm7 zFt}O9u?50#sT9{EbQEMo<>AvWL-I#fFU+s1okMbCmDE+&RBpHIQY@CY!cQZZx2x`zx9%C#X6bFVs?5DD^Ei!QyJa`m zz8ik+Y3<5)zWHs@-rI+KeBo{1<-E?dp(^;m@-g*nSZDc&;~s?Jvc|}9mOb^K3d7}L z2Nu|zCx`C(%<`qY>yU+G6PF3Y%46IWy5*o%QMzrkd}sOI@`L3^%TJa=mY*%ZSbnwq zX8Bzht`vr=gyCvoxJDSR6^84C;d)`XK^Sfn2K;*UMr(xTyjG*tWL2##l@8X{(tR63 z?t7Ck+$juq3B%pY!W!=R-{-=eIaX4$z2GsdvJuXiyMTkc%W4*Cf03PA`i=kn!%eJl zc)Qgj42?l=xBBsRYrM6sL>Sfx!&>dP{z_ZKYHQ1&l~_AjPr|UQCtCSh`Z{4)-)QY( z<$LKj3&ZWl&F!s8)->GSnruz6rV7I?!mvRYHmGswfuFVaC37kZf`BH z4v_U+C@XoBJm|$*6!Ek$+;NnHUTj&0guVxq!2d#O8fFcy+Snp452cN=jt#^FE5~vY z6Yk~c6KfGipYYH>o;(%0WwMoJnGIGtP1Z8&6l=M)!dhvaYMo|fDho}}THG%T4+z79 z!tjtVJS+^42*abo@Yn|Hj9@sJO*oiCFsPBi;PFr}pv@o{ydopPtN;5V!T$jFu9xmj z-|C5wdoPjheQv~3i7;%fr!D2D{z~f?Azfc=y;NG%Gzq{S{V--tc_Lz6SJo;3d2iltT$QLNh{nY z3>=qyy3FL?Y_tCZ{e|t;+wk$*!+kvSt8uZZKNXqQlnfX=(O5q!5D)I;+PlK9t+{pQ z=$GOP-@UTeRR^z78l7BgeLGYMo2~b2?t5RreYZ>Zy;i#KbARf-k6M{&-(Y>r`ndH8 z>sITN)~BpbTc5E$Yh|i>hcGaCy;B(85QaB}VV5wxB@Ay1!#l#TdxQ0bp!>dTeZ~4J z?)#c_-*-dq`=Ky=DGXmp_dW1`?tA?E;?@tO`%=sAh1~ZO>AricpGt(`ePQ@O`^ABW zTDyfzo?YKh2axn`0V&K&Ocgzk#x?|q%-al1`g}~Z#$#Ss90>Ka0fekd-CoxhYhJXZ~Z%c{fz^oq%+!Dac%4HYj1z% zv(qb*2b^%uE%EzWWWVqx*9Pm^7HzXiS6pYa5W`)!ImL?`P+~${~()Uqk=CR zBcv|}&TX{y!~ku*gyCS&0BwCRKwDp1KZ!7WCk)?fzZl?a)()Xe_O}hBT($wWB4PMZ z7=CKB4YCauhC{;e>v3yCY{P9MrH~P_4f$ENA+}MG3o~vH4(x2DgwZ150)+Lian#Q%(=Rz5oZ97fVJ?DzE7#GRV%r;66=za;ye(09@ zw#5w1Yzu4)ZKvDLu$^gJWIM}twyoYKZ0880K^Tp~Xc9(M7+VTsD`9LcjBSK5N*Gb9 zd82Jf&;ZUioMF3A9wceI1QRe?f+k?JiHI|V(H}TV(is20FQET_YGh`+Ub-Gjw4ozY zZL4uT?J(w}9LH>1Zo(G_O1wup;DX8WM+QC!aUknLgHBf=OfjB&!~Sz~+5_BbwQ^a`Wze?Uv|EH3<9 zxC_@mbYB0~WkWx`Kd<=E!Fjvt1MdDJ*S-{f?dz>yUeq&t;A23pvpD2u-h4Ca|>>`X^H`?|Gz5h$R|3JX|zm?wKE#&>(1Kyvld4IwG zd4GAOJm5Vzi&*>r@}aZ#NW9<9mW{+fKtQ!VwzM<`XAK!D0`%dZp z_GGS23BR_-r*Ds#|7hW=+wPb!DCR)NACI`dJ>7n?(rukR!=7o+vS-_S+ItCOPhsTu zWMS+rjD3W$?>c*LrGve%;SJ@0Jy#g}$(~q_h`6|Zq%h{yr>F1o8kG{BbUUnA8bV~nd zV+@@UgR{dd|T zP7PJko%TS7hP={WJUL!Z=+RXCFWJ`P%-S zY}&rDe``M|j8(!oLl{q8WB=a%gLI#n!Z_=HK-2ac?)ZDSJ5JfL=J)rri#jiT_rUj} z;Oy(9>pCJ8i=##OwI_VLv7v5Q{}HR^Uo+5|;C|?c>pD!1R@AdYm9ASYUDwfCy6&8# zxUTimpxZjE4hJ_G4ms=rx2?f#9j&F?&iOO9b;LP3;ITSq%bd&db5 z`V@7-I9C|w3FCZWTp)}Kh4FM@JVO}I6vjmx933^cb(~~4Lw0)|iPCM)3b`#k`ZKgX z@5TS;w#R=8)6omJbJLtBKzQ=M~N1>xgnp1zr0AUoucuu2Zpo8|2hVp{r=C+Pu z4rY$lIEFh$I7)FZ}=80cD-guxOYty3K zUegYoF)84-Wn4QY{Mv4=-Oqh-X}|X_`2B;oKYL+dbQ8C&bWE2XxX!_(6eIZyr3cQC z9!NiHa_Fe(!IiV8)lIjxP7V2OwWCJ!+tUJmd&x2G3f;26!JO4U^4s8am4oT3fZtv$ z{r1w3-(G&$Z?BVn8|Z5}mP=#1Ocx=D5PzRw9fnW3APGnXZfqpP)iC~J2(=-z}I z>b;K5(onAv#%s0ind1Qr;K6VMSW!7?Ddbl2j5u{^@HkA`1+TmG&q zN5%KQ>8`7uJ7N2+Co;YZ&2u}RaL7GAgzF~*YQO#%nGfas1qW|xY%o3Sc**pT=@HYT zj#nMqO^=x#H$5RD-NJaIh;)g_SP{uo&?;eEEh0T4(kmi;8ys&0!}Z&acO1J3*Y8QS zH-yxFlQ7;cjGJU1`wpphMnGZvbL4iYA)KB*t87lGH6i@ZO~&%oQ>WHe*8QEG7@C3Y zkp}juFow2EIQB~e`^@pVL>SiyBL*gaF|fz1hEQ#M>-b)p*g?m4!njTt*Ec$TaQrBY zHw)v&<7Z;OIu*^tn8C)SZ$V4WNNHjl!cDA0-(c39s?9&j6p@|R zIHR1=(!_2RMs1J9cBf6TIPKwPbM^DzpExdVNcWZb(^t65&V5{(m@|fJUE$X*-gELT zw$h@kbI!m0_3L-VpU$-j{W=F1MIH*}$m@&`Sew%yu(msoQI60p9h|&NyRm7X-r4n7 zz52VGcI-DglQolbraLp7na(VwgR`eJI=MM1T3XtD!uYr_J|T=R%l~>sv%9}Nn;ftl zC(T}iv!5{D8w^d(JS@kV?<|lA<7Q!`y5%p!z!tr&&QQ??JLSmPImB5kjQ0!U1C7pM z&f&uNpfEmi+)T$g+Br@N8DqdW>DF z6~@Q3=;WM%Ih-194(@wa|M*z)!uit~&xKD*9pLcNQe zXUTaM#_Lj{&k5s8fr*y?$SliW&6WJ~d6-bSFK}KgwRfTOB4K=97++{K>~XRu@_b=@ z@eh+G6Q@_#)(x&M4@}gIt(vXP28Eup(s?CQE>0rGwxdqD*jjWBU3$G!wwZ=Q&Km28k*AH9EIBUlzuwY?SIN!u?{cZZNuXRY?PFL_*)NQrHO6#^=_rh}?_V7^BZ%n|)BPw$5Yr^c8EXrr5yWJ1ZFcyz2e#kS`I*a$N+0}f zzV#x*+hs?LAm-$lzOt;nr4ca8I;SzFpEQE6k7fj&pS0G5>ZpH=T;VqyiYW@{{vf)K z=_hsn^`GfJW?0OGf2jM3LEWEXHN{Lp_Z3q2--mSnlbm)nMWTCCizB+9E_Kh0%nu>m z&y>2K<=iR}MtWZa0QrmVyM#(r7c*bh#@v{B!gxp+e{PIf5JMyKi!lCv{4{@7%sEHX z{Nk7;QuDtG<8MLDpN}>#2-oKF>paPKw9W6e<&-VUmhbF>ouK)Px%QIqYhT`0`?SL| zL|p&FmM+V$Ui5ww%`cBxAV_Cv-M$_Fd3xg zSE70D{NuUy>Z54>J~VGK1vPJKB{d&8PHNtMMDq_x%|9$m zYDn{sNzFfw=0TWRO3ep;(fpuL7N3oILDt4|G0zKAYhh~B81rJxOTrW-OqS!P`Ry?~ z(fl9#+A(j$you&bl)@a;{5xoKcepmc`TeU6Q8S7kx_y7Sdw=qrw1DQ{=h_d#uRU=8 zw8=Mb8LV#pD5c}zGwY``(fr3TpGpm`i(v(ZSG=uKgZrcgZDAU;wO$vhp)X_PrZPO9 zFoE73$G9tW%MY$d>G92f1<2Jx)4NO6rrKR?q~2pfdXEcDwVO^jtag_LwY#jsCZ901J#LD3b#iq< z@w~nAl6-&VVr}2i8p9S>H&-Gm7bd?j#n*S+?n+WDuHD(-8vPFyKPIU72|>k|O2u~!DW0+EDS^paQ_>N| zmrKQ02vet!;-{hbKiw$fn(dPNu3XhFOuw@*oz&>6an%Y_7hy^~ehOdUI#VkAW%ZWl~E0I9%a<*H3us#GL~oZdhVj_iFU0TLTI|k878P zU;BLD&XcaYw&3^rxtV80biC`7CJMjMCD#yL7a7(GQ+FBTUCY=aXXQFbxTm;w>dYvHYpukboPZQHKg!N*{?In zwQy6;5slv~HNIJx(n1=)AC0>na6KpyrgUM-(0+0C!Pb*PnS8<}r?FgHT~7*AmM~>E zx}J7DBTPMoskhX;C%?R~RsM=>R!=@!vwG6l z&|N!SZylvi;CkEjj_-CbO<{O7K>e;T)s4OIk)kGb}f zaC>p~om|~8DmV4cAs5cSb=!ScbFH;!C?oq^pKH4RETH>bqP*)B**2W~r@H^f^((q} zed{{t`p)&e>j&45uAf|oTtB;h5vF`$DiEeZVd^hT1B9tam<9^dAYmFTOhY!fe$#aC zjATwdkO%5u5u;4kL^kFtWvG@;z$lUL2Q z2foa~wubQMb4}6gc~!Mle3gW45#dkaUBITVGjh%GnsNNIBIV4jn_gXmpzxVbxk03` zdS*q{Y(5qcUR(2P%H|BNnq65N7$du#xREGoZ-Lvnuj}6vADCsoy*^?eoD?+{oh(PN1c$jYWt(o5#7DG_T=zu9~rY_ z|FYzwYi{eX>&=0?P9s2v#^&z6?p*1H>)h-BqMe$68@ltQ8%_*!LtFlxR!>Mj1KmRc z?&}^LaNp8n+!wl~#69_+wbgFgX8E4ByWH`PyOMuSlkQsOJ2k^v?fm&G=k3f5Y{i z0(!rcYnO*#Yg>2ws*_F|8uO^~(ZHf2J1_Je*82)KQ*cu6?5IQUr_%Jgua-^k%y7N; zv{*xLM!9cvH)wia9ngFAG42ah>v}gI`P$&V*?o(9gL|XHPL=G+fTy`K@%`=Zdooauu9i`vJ3LEZhF z^dLs5XNElJrDJt>-8g2~)i=Ek16Y9{0QM4>YrVKVY^( zhMkXOxH>1?Y<(AmEO4)zzQ!8&KKFk2XTr2Zn9dcZ^VYb(aDOR%j%e^dU>V;19X|Yh zxDWR@?b%x`C-u8?<%vI^@a~?@69fMK6W1OJzxMm2SK@bE*DwCUn<{1vx$`Hwa) z*Vw?C-61z?b_}ZJGW?%aT(H(+O|kZW@qT`+Lv!9(x8aP~IJvGK>yz#)H^0cWF4HpH zSD3ESMp^3*2kzK*m|ZLb<4b~O7u(@jW*6HvmNvXGwp%PM_@%xJotb+O}6&;;c`>?GdLG~Fl_ zw2BSErbfA5MUVKn?+N~Q`+kqLmi5Oy!Lie0PnFuMik%@$tA(kd(YcI0!NwQ0J;6;^ zs*binIJPEsuDrcAmNsgQFs*HjofkV_m~IlLb@kmwR8{D1qBmSMXL|MQ%I-7EYNl0o zuUI&{Y*tlyRqf2O*%gy3D(6+P-JfqVOslS07_yVIVg&*WhhpmkzIijg8JjJG+KRBC z7P@6=?B%qn|6r3^?24dUUS&1KUW3lBlTNuI+~%=xx0x{IzMf~-5B=rFB@f=X?yebftMK;Ntz7$L__dw; z-g=-z;JVo70?pDrQiCr_4c;53L0i%m>)KEqZI69j z)BKKr=I=YkeW6?4j^(SO|48$n1~vbg)|-m`N^1Urkmesb+?#slh~mGMia#h!4~7)~ zgH-&Fu|G+K=^E|FpqIcTnma77ZDdJOpglFV~uf!IHNEJ33??pKvBb3vH-c}^ zyD3TSo&UilSN8kP@r#AF9a1IHweYY4UQWk5vCV~=_TzK#jDo#p*)U^8zXfxDvp8l%fj?ZW8B!dal-Vf zFzq;QJ|8zZZVD<4pW}|Jh^s_-!n9qOUZYu!n~qwl!qu|pk(nb_ttzg)zi&c^y>FDw z38-);*Uk#R_RMcL4RY`7w{FvygRDiZ8v8d<;heZyS=Q5}{k0tEpVmE5zWt|w;%OM-j?ys9nmYG}p1%#c>9!Ycxbx}6y4_6_zcub@S=J||;y(^3 z{uvo~J~>+88Mx0D@jHd{~?)OTke?~g}Cx5E@H{$rH^gmhHiu)+2`%mNc z#_dD*pGn>C59$63!6rP@4^sC(9?|_*Quhah>9dgTiT`Wk4n{1EJ1FPrK9_SQpV$9} z<|C(t3iWf`Z?ZOiiThQUm__}nG46MdB1}Bx+v67NJw{JUS##?=YM_1n8n5xRmR|Es zSk2ish2|OGT*Aa&5@N-71PV`_V)1yw?PcdfRUdSCrufc#$BbKB zpK-$-(q24%u8j}Bc5eB2`#t9VqaWTMxAf_EAKAsV3H{h7DgS4S>>aEvPlBgYzyv%U z119j(F$xm8rJJWaryh6`h3V%8PY+@GRrh6VPg>xsi>6;nt)mwtE|BxGiM8dk63eHT z&F1Ww#A#(S`TE?#o;`Y$md>xPIh6_9^6EJYdyJ~(lf*sB>UayUtb&go&#RnS%~y(h zgny|xl#fgg?N6g8%abik%-84wZkh=M*;8c}j)ay1`Q>)Hc%n646Av#CE!mO*i?jr~D2y%NpJ1 zOj#iRHT0-TkKD8Gnd+G))F`1wH+rf(GlXgusz*N5QY*hImtPfF58+dDXumS_mBk|& z3q7pbGhcD7XUnYi65Np&;10k27JTn*y=Q@(<@KBai-=aLHc_D3iBhVYDAg^nn&1&c zDbG1V4Slb`b1qRzF2j1x*S=RERGXaGwM8rq<>3+!pPFj$EEB4u!LwYb&cD?th3;PI zk?$`4@#QPeH6Hoyq8cMqm)7ohZX`CW3Xctu-P%}&bu38UG~(jTQHgi855$p1u3Zy; z?b_6ncirE!pw-$zX?fr8{+QRVLsK}Ob)LY^{zD#i_EVBrnh%fM*{{Zh2f?x1O0D^U zH*34&g4$}6&U1n`+I?8%w6fZ{wMW*Y5`%vUdYD$_cgiZ4mKAImJaS=Fohu^q4!;%d zx!=PHFO8lDJP!&rUa0LFJr8>x5$Xv-?W2!aTLTeGZCh$xtczHI7?3y*f2yn@UNs{` z^{ruCUCZL!5$o$cl_O3wQp=`JDKDQ=nO2rEB_(@GYI)Msw92XJ<(1h}E2eS=c5+f# zlhf8b&uTeq{~yj-@|5z7-hh8;QY$Nexn8t zZCUpQ&o@G4^FgWg#Ab%f?>#@skokk>N1-MOHM!Aq$n&#MQ-s>H*&)-bcVW@>`47#lZiLp+g1j^452c#l@poX1jXX*5FXTa9W0rY zc0$pD&HGwh;`#mebQu7>Cvt7)@M~i_y%qoN%lVDx{W$b&Z^V@>ONIiVx2so9^%@R& z*~&x!%$A*}jhNw40${0icr&73arNxNs+pA~3+cklI(8`ZrW5(R6#3-(EC0Tg1fr-{ ze&b3XG`)Si{iLgA(WPqe<_NVfp;G&-wKv~ec)0uH?Jv}RNB%U>JN&P^I^Gf964}-9 zj+R{=o_$HMt8=OB>V%|>_m(QIwcZKdiQY-x$wH;Kl_yksTLo*qr+CY}Q@rIuEfnfR zq0$3jm8I00qH~SFSq8z+zlRQa2z&;BTz0DJIEaOR(oOoi zZdYne(mzmk!)oP83bK_u`iTm`dY$2&g%x>Em7N=^HM-F|+gmNv0YaT{G*1mxRIT?6 ze9~Lzo$H5bmAK`5Q@Lb=?b4wFA) zWiRGv8z!`_V_KKYh=_JEmS@$OVs(V?#}S8CW@Q zVBwYLUA=Ga>deW#0}*d6*WMIen}kcsF}DY7uWkAmWXq z%jdmKcKODKcloZM+j9JaUPu{tdpAq3xkq}Yh>$%!<5RO>C$UL8QJEQlPKPoy{`!M6rq+idbfM&=}Zx7rL={= z+&+i*{rUF0kdQaM?;Oqac6;BIrbk4q2sU^hqSB+h3^U@#Up{{Rsv(oFJ~?gHjGNo+ z4XA$)*M1tV{>L|Z?yo(+cz;9XYf1ATc>bg&>fi7ELaKkA_j5Vzp-z?R|4OQVTDbZn zl0un3=>0)6n(qTfQ+15HLbv?tldHqt-=ua=mD=@1_#%ZmQ>fw?G0|u8nbEFK^|kc1 z^0oH0@kRMi*DRsV7AnR;mQNFEjZkaX`z$gh`t0D8L%TXHD9)GaJ^TM3K{3z=@U^86 z;A2=z_H$)+C(q322;yRb=u^t~;Mv5WWJ)pa7`rAFU7zTHB-S*VX`?se+^~kB^!xgnm8e=s4ODOxF_`X!S-R#@r`_#ABx6ilV z_nGf=-xor?MW`EuibdZl)Z2u5yHGa?^^Ti;U-=IBzV?0N`_^~R_nq&1q24LfyM>A_ zHw%@(ai35h7V0B`xunoUdJ)6%BU5^{6KbpH)|6MuRLs{}5jd%_=@C1zb80H5@@2uw z3T+TSxT>~}L)Yr2PpU1S$tIlH(-O;SDmg^Zsy!)jCKm*gEI377AzsDodJY#&lAHDA zWMts)+x-!WCE{s+i^Ze1Ch_=jp2 zmQ>cQ^{WxLL~K;9_>;c}OxxcoVqJs3wNUS=Us`I7?aq;Eb7ylD)jli%vM>5Rzl^t%XK{urU&-{5x( z^#NJsmdXWl*r3G1f~@j-RVKeL{Uus1FVA9w=tSMt{3TS(foy^~T^PUK42p+2=zswzAV&N{-@fAVpc5vyohzH>z8UOEe!noU#Toq zeFOc2^wn4FA0{<5RH!dC_=gL1+n;M{l;2PD@erTW!~g@US0k%Wt(;w4HFI>?%(zDlpf1Q7>f1Xg^6zVRazO}}` zz`xK>@APeXd%83kKuHEN)hC zXOnM*<}TE34BXf0X!q%_mjXf=uJ@m-bX)5e{&W0`BW@DvJ3=MszFU9ZTK{?erT+8% z7YLOp^ACjjp-?}PIyz=!5qr2U$CTBTPj9FFa_%c@jG+lM{c)&=Tbk{DHv0RQ`!5?) zT++R+YE~r;UCwGV%Kol9b<3Rd~o$|_h*IB1kr@QG0O2=!B01vmNE`Pcht zg7yk^pHTM;^|SwI6%dApMGW_Es-ID6CG&OKmdZrTzG|4DQQL`G zUE7m`)Iy1+p~UUMC*38V)LZ|gp(U({vs1I_!{$qE{oz@`w9upOm5=J2#Q(CAQ<4&s zl9JOh^YU|2QVOzCl9CG!i!LpznU#?iy6e7X-4#;F1OCU9Za4WK^grZ(*#C(CQK8cK zeks(ignB@zU*F_^-2a4stN%&=Q$l4?<6EH~6zX?E{a$M4Z|AYpdeYx54zIQSD}9t& zduFE=q-CWSBqt_kW@RR(Wh5sj=BDLmB&H^1WEB=>W#?xm=LIXVwrbYgnPr@Zr`6@Z z)?mOTUJ6NeB-Oj^_yR>KG zd$u$ZL+X3SzejQ1;@|Cm*Z-dXeg6mk5B(qcKlbBTKMM6Hp&kztT(=@wqRz}H2F3my^gv- zcmA+V*G;Xi;gim;rpZOMW2$NxT*#4uRr^Cp{-DuiHC22o`45i>4Lay6Yggvfluxg! zqeC>ergGfjn@g=RO(g^t3FJI>m}EkHc}-!w4~(3 zf{g5>#Pq_Pg2deX{G`NO>09ZUDH%C=8KVMTH8J#zl#K34DX6mG6Fr5gnohEs-g^U7t&DY_-;qZyww!nJ-&zBve=RmyX#ZSm3AE?m3G_XQx!{m zT70@}6D#Nz549QEOqnBl_DM-QA|hKDj3%}9*belRrDn!;D3i9ze~s)=S3PHJhia`O zZ&xDDTTMT>xP+EpjsUcaUTxWGWKDH-U46tRYh0TshtuajsY_DNUMDB?&M6u=Xz-Ad zF=Ho|j_NRb?#!95MVl>Fn|*9?Nw9n!%H=?NVsQzld)HJ=nOj#`OP49OzQwqbx#i`R zm2|gXi*cFVabB7CQ!5wNj_04j3^l~o8yYtru3p;3M=v!~DcS_m01>VNId_tkMv9N!=t==xBdU<;2$?|>f>Y5|NOA37W zuOk=EsXX!*t70{6FCIE<_;^jDywOoPR{Ph=K+PZC3Dr1k1S-+ShTJi7)a2rl;BYXf zrlxG+nvtVNpzFZDV{BSkWu*s-!X4wr6Mg?!yNwe@93_>jCru98M&qOr{O2i*Z0${~ ztwOWI#wpx>lys$~VpBZK%b%pAD49x;GFTa|lql88T;)vVT;*cr8s$3W24xiu&mGEr z%2wqmWry;CvRC<9`7NSVgf${A!WR)A(Ke!eL_);L5qS}lB2JGuGeSfx zj<_&lS;X>)%Oh4sTpe+3#Ptz3My!t59I-3nV8l-mKS%r;@q45xvQ=c8$mmFGq&?CZ z>55E?ERGx&IU;gQ!ovnCzZ1|8(GH6&##S5SrWze3TcS9~sW zcZAN~@qOZb@%`d+0s+(`T6&40q9yUvzb-y6a!-6gd|~7s_Qkf0=l%7TZAHs=Y>oX* zM%5FJ9sZJVi*P^ zU?fz+G^m18VHQ-wX;2GuVLn_6H^MgfK~d~kFdpW?WpFv%1$V<^@H)H!yWnlu4etT@ zwUb}_C-5okgWnXzL6P~$x#BQ@38*JWYlsHQ(;& zs2|7I@GX1?Z8FEOitzsGC<^cizI+yUq{=0SKE9tH9pvlY;7%$KY% zsz4rGJpg&GWJraRp+BHU7kO|^0d(g&4^{yAaZwkp>)-}JzUu+t*{=8DL-<%x+}A)O zya3yQdWk&+u!Y#Of#=2E0-J&S#gac>_EKV>f@k0r;J(-$K-pv81o9qBoySq1yc49v zWkVqp!5}CG@*Ot1Z?^X-UMr--SXd5E!bghYi-1hve&2;~A5cEui?9tSqmMHBcEQ`Q8{UHt z;AchgCjj|mMo96`gaxn|&V{8w9{kAjQ`i0mSO@Fj9(WiYg~wqlAlHvfKQjHu^pkHt z_3D2UK7qZkAHIOEfO>9M4s)Ou<^uBDoepQhRj?Y+al5sO(w@3%KNYF~ducx#xVL=` zoB@mAY(+_+ZWE@!G~n61oGHn2`q!N-U6Q6 z}Jg?jR@Hp%O z^4je)z&5*Ko81n=_wXa2heRiw1jtJq2U7vNN}L7NPzUp10h|tN;aPYUw!;oUuZh%6 zB61QxhcDp=Md^+%y2k-}?oK^*Zwu`q0Xo8ofb8yFArW#R9|~ar41~c@4CJpn_0fGa zkk9TDfO_b@5gq{a*B$vi&`A$u_Q0-sa8D2XuSY$s1a#ix7NCxLJOW$cWuPoQUIX&p z<99_#G6Q-@$_DB`iTY1MW)d=!CP68nkE9iFJ)qm92B7|v?tlk@`cLBdN#rXDyGnWm zsQ;vcbS)g<1InJ94t*g9@}K}NhwI=@*bLZ4^6P*NCR5MJA1g{q7f1!}O`*P0MnDCi zw-oFl1)EDjPbriw1s$dAgg4<6_y)*F%J+aBq+karzrb&blG+BM!2&iQucN^ctX~;@L zR$5OO4cJ@S#emPGJqDB`jXFpppH!QY9tj38K}%>2QNZ)ktw8;zcLi)Ky$2)%b(YS} z>3x7YPp8h)DNp(c7zxNt9}8tL8|DCIOs@k07r+&O-=$vz=qY_Kd=1!qI`x?00_rCN zTgjLKGl6Gi@T?4;mBF(z=D}h(7nTCg&A13IflJ{sz^*g)D@rEyp4lHxgVk^w+zy-I zPIv*Rm&`BVOZW=NPbT%0`MaWIQCC^iRTjF-iyk0-wS@_*GGQVOPDVvtH3)0UIE904uB2r9G!@_#b_MHLL zVc%*XPkraY0yqN}!P$Vl_a%>g@t?j=0d>|F`|3x1^rH^?QOZvZ>U`4RBb zoZl2BHv$Y`18g9dJm!+eTqR@VH4a5cLO>putR4U3+S-md_aE%=&xWoG{8OZGVFnU@EKr7 z1qT$RFa~0Qd=$n*dq5Y3o!}%OPlY{T01Sd+7zQI?6pVpWpbBO}HPpadm=8-}DO?B_ z!!>X{tOD{@cstwycLDh~DmYfPVX%0sHEY zef4((_SK(y?cWYEp(mUS=)6BV?~l&=Q>XpWd4I~)e+W>n{*8Hmjij~k3Rcj zQv-O`0InTC83!x_Y;nNzfX)YS%>eQ^fIJTP7|{KI1Mn?;4?n@r@GFr2q9`E0MdY`L z{1*A3Et~+_I~Tdl`gX47wGbg;(KS_z3pEKKK&8hJ%2NLDb*i z)?fu}cQExexFd9iuFxHL{@~uwA4URpI(RhTBZDhp2F!vvfSnGW50rl}^*i_+xD2j@ zhv8`;PlL(RVDvX68alyA&<&6`1bIVJAszYva)%6rAutR|fIJMD0>~dS9mvBF4!aF@Gxci$^g5jM;irIrC9o7O1oAMPJPaof!^y*N@-Un{3?~o6 z$-{8+F#KlN2)6?|8@?T0hd1GEkaFIG58-3@6wux9FW>-t3*>M3k8ns)M#Ml0^o3G5 z4|w(n$~9sqaD9mi@h}|9VG*o^8{jUu2e73Q>ZRmi;NFt0Kz)=v2QLD7EkVyE@51{) z{z^Vkl#$rT$YMZGBS*j};29&I2kslW4Y+SqcjyJZfxM2&1?ptfCb$#shRsZ^`v7|w zjXjJ`ge15OZiLmqJ)^0|G3a6p*N&lFW7j&-~%e`aK_gLhPJsHT~Sn@Zv00zJyD2CxM62`zdSPbZHoE-{b9y|mG6=gi-9nUr6 zr@}0l1GT_4;}=0aaR2!8;C#3Ot^#ak{Plp&$NvC70r?;Qo1#o0ZcV6!nNSTiFc;2* zv*8>#7qG_(*x3YhH{o;m4zQI8ziP`C6C?R&7w87*&=b(h#C|}zCsN*tly@TKomc`B zVG>|p6OlFX0$2_!;7TCR6IVkcpqq)vp11*CfG-tg61tj%yh*8$0oi~pOv(r9cv63$ zUMEd}$$)=Ps(@30x|>AZO``55QPxSwo3sVqf{$P?dPQ0Wj5DFF#7 zDG})g8FB&%mF^n4yE_J$bHC@DeO>qV!=8E8TEDga^X1*|WkY#3G-tz)Fki!#w8o4L z&DhY44b9lF6TcF}cJ74G$lHzVtC8#))xmeyNPdl8;dS1^`;B&n(D*&f*SI;YFk55q zH}-zxj+nFYK|Ndv3O;h-%ESu)Qdrk9Fh@zCBBxY_Zx2Emsf?YMW ztEPSE#{dQ~j^CNhUo2)Bv8?7F*0GVz+zz3c_nX;QGdVW zH`~H3wXo9`pYSD(@!hm&j%-?VLcc9!-eL&9GLrG=xrLrvOhwNv^w}bwZRo9q@2bT) zl6V|K%M4^CJGsb50Se;=T2?}qEoIsAIpo>$HT2o?9rW7rV?Lu9U*qmux|5ba;7(fh zLar@;V;sL@r!D2$avJv9axqI;5kjjfsMYENyxq#%t>n;34z1K^x4MircWYS8_R%02DY+9+?YBFx8)sYasNqv4Sv({1M#Vxn?_o8)0D&r1Y%dWNTTG!=s0)Jp{t!2_$9GK8-)Qiq1Lq66~yS{`4k@wIoq_TJaaSj850vIp;deVC)1 z;1oAQ_$Gu9?YoJVOZ02lcl0~F&qsX97c|0cMz_Fs73~+Jzojkui0(``deDnL_|Br; zL-b&VF@n+9Q?x9iWf5&h(QYGpCUcmNy+oTM+8oibBw(KCwfu+gFM2a>J=%QH=8HC8 zw7jC_6)mr5^F^QK0`?ny4f96ZbF@82+jDes2yNU&nGB)dN#3@9qeWw2RO_zPI89xT;eJ>xXnG1 zcpSpF5u_pw>B&S^vXhHE3N{6J5BqAx!)h#~ySNPc4+zcYy` zOlKBzS->Keu$(wnvYLNb$40iWon7o@KZiKV2~Km4i(KJ4x46p#9))0a;ro=NCLI~c zLL@nfA|C}QLUBq`hVnc`6{_su{tRR=!x+J6#xj9Fn9MY0GKcy6#bTBbO9HD{%YSTOGuzn79uhgo5sq_;vs~aZ z*SN_Y?(>l35Zb2T3DS~*%w!`6xyefb3R8?HDNQ*lQkiPhpcZv_o(8U4ODpBRcd9n|SyR~_7Chuf&rL7fhA>*z*0 zeug?7)#=y-`|7wFbvmlkaeWA#WY?)A>U2`4Qw4rxFzR$tr_)Hza1(Vpsnh8}2%TT& z6V&OfPG|Smc@cl3PG@yGyM-=Mlt7&>>U6P}EyXf@B;%-r<*$6hH-?8T;?h_LinKp@9-WU z@G-Ml#tPzC8AA806rd1AC{9Ou)0h4Xx)Hm~p+Z}2t~nZrC5U_ZT5 zk&W!+#D03UrW0N0Mh~`gn4=u$R0uy+r#>(660b6WS*Y`qIzKH8p?3hx8o@39d2Ri-ZL^i!wbi;Q6!>hx2m-&`Jt&_4s}^jD{UB+d99 zb^5E*zccQ<|1S2h5BvGqo&Wp{wRo227|tL3$rPq@FN6V4kcM<*I|&IFn&j!f$9vL%AF7fr9_=U>I_QH=X`}agVY)H9c$QzI)l_1 zv^RubN>L4Ueo^NaclOH=#-q+J>ija9TOkaNK%K$r3{K0Zv_zf3>I`nfDz>1`V08xX z4q?cXR6(5~>I|vLFN{TWo?+!sxt|M4i#2G9T~}pYS>Ji6eo(Srft--|Ltn6r%*C=t_Uo8KcgaAspi}>Woomj5{0q zBJZKjSarsJ%3M~U&RBKEy0dYS6hfVG>Wq7m&h$l{aq5iwg(F-go&BShdLA0nOKze^g^A9>P-BZ1Dr*jiRw(e62c$P@doPrq0S%g zGo2XJ`9qyQVndjekvym~Nu5cBX-iMknWWC7ek5`lbtb7Z=~4)P*5NhO`BR-g-(@O) zq0XP`{JA`Y$?3_BI+N9zT#)bRjyjXoncRoHoJ5_;>P)^6!j#&)f;v;wnesN1S%5lI z)S0q0gsExCi8@o&nVO$AbVHq~>P-EK-5f`qsp?EU7s9lfyo5T_)S31slbDA()6|)^ zIE3k`$&NbH)tR1`Z|H(L)76>&BRe^YI@8seekOz&HFyDaW~ejcbtW>Ootf&)?7?;pqs~lqW}XURR(0y5&Mb9iy~+e;q0TIIW-Sb1b_%kf z&TMsN=b{xIP-nI}vwvVK2T^CXI_?uVQwH3>daMV zZVp<|4t3_LGq)?7*^fGN)tP%Ngn5;zi#qevnfD@Nn1(v@)S0KiyvHHT&wx7f)tMhj zGrmWi`RdH?%trR1&U|&|9|>W>Q#^}03)ETAfYD4rodxPFn9aiw7N$d;h3YKKN)x_C zorUTw?8tidpw2>d79I-WuL{&coxjxis~#iy6LtPl=dYPO2w_ng)LEp?qRccRnl`Al zNUcTozNjZ|Y|&2)Wf;FQf=PIL(PXCb7j9}%3`8GNKIaP>@(pH=F>}m! zbjQpwW{&B_5X>B7=9uC9fth2>93#(|g_t?U%rT2ujhSQ29P=+bF>{QWWA<_qGsl=Y z<}9}`Ym9xwJR&)S#S!G72t_GI2`W>Cs#K>w4S0c6!R@M-{Qu!rx!obo4%N3 z@d!pTnkh_W8Z%go{ViU~a{k5s7O!IiW?8(CL=Iq<#TU5DHEwV#ge7KKl8Q9g-;#`E z#%xO}P?4uF+mgCG$McwN$=kfcySz_hn$VQyw51*G>4@2u^uc^f`ZJPIm~Y7#%(i4Y zGnj?hmMp`3OJZ5add#tC^)}ZS(e$~vIe}!d)VJHb1nObX4v1d7PO)R_P4APUFe7XE&G{){D%E48_RfRV!maw znac|7Z&@4(Y{bmV%)D$X2Ql+9GcP;JCCt3c%*)*BvPZ#xmyndCCN1eHKpDzXjtbPG zHqT;r%U|aW-o(8w|AH@RhxN^C}Q6W6d1vhGY9-=2$bw z4&*n?9Bby-@yx``v1X2)iyMyBY^<4M6WEBEW6d19m4lc$*37X-xrCWx%^Z7;N0>R* z%yAK9#>{bMj*FxaW{xv+oE^tKg_+~b9B0RI&tv8|GsoF++`E`L&dhOk9M=>x$C)|K zj^o;6<~TFQ*>PN7%p7OtI6ICTjhW-j95;>`m^seOadsTH95ct6InIvbHelvBGsoF+ z+yTrSXXZFNj=PAN}0`fcr(Y_aeNcZ9B<}$JC1LMnd8kIZ^!X{Fmt?_sW%n5d!PyjP0m^s0Y6DnZl1T!btaY9|poM7ezJ5G2TGbfli!HyFeW99@iC)ja9 zTg;qb<^(%V=#7~Z%$#7y2_rFcf|(QSIAI!QPB3$V9VaZs%n4>ru;YYvm^s1B33i;2 z7{bb&M3E1@uhjR-;*_Ke<?bbgqc>_ z;mWW02K}v^%K{d$1bwcwmz677%|GaQrTwhj!ghAC7rR<{h@+g~H0Q9dm3FoAI=8sX z1MKYYKnmRF-`U7cF68`oPkJHazunW{NAT9)m$}M~5LTIQ)n}M#RYUA*)of(E%1y1( z`zpP!&Wrq3m!>T4VD)IGU`MNG;P=+(XN~(=qn|bUS(5`jtQo>EhGV~L^}N>4to1W% z^}JTkYy0B=zSa$__4eAaB!}>ioBSs;S@E;~w4wu@=)w;i=L**_*FU#I_}5JTw#IJ$ zZNv8*;)d79umstzj}2i%Q7Y1amw1&oc$@do*9LVqsI$RtHrUMuGjHgM znK$&{N9=cl{ciY~fedCSzcQaKTn}NR>^44)TsGRt#!39mAx?7+_p$LZcSsIlQv@k_ zf;6Nf7rw1c1u4RlxQk8YsK7hOds8=lqAz;eGzk0MG>S2d=XbVY7n}1Slg%>OT#pxc zlh5$pX8YT0=FR5aY~IZS7{V|{;N8traf_SnW%CN+G2>?6-RA$;z$OlGmd7D%G2@n0 zm~TsZGLaQ|Z87T>eQn84A*%BXwRo1g=yi);x4g`2yn!8Vd6)Om_ZB^F(bJZ_JP2WH zVf^gYZ!yExd2D1CerD@_4sr_n*m{kd+~Gb+=v{PSn|`-Nk`sUTwiUqp+lo;FeQj$_ zTe{$Ow)Mnrw#jhYFAT+Qwv8l#t08Qc!S+1Z@AhY?jXAfQYrFThx1s~?aeH@uq%U^8 z-5qZC?)LFa#7?%GfBO=a;_u@2IM%a;?U;RgB6hR=Fh{w9S$F7VhkfkGg`RfU#}510 z;a+x>pcG}PKz&~1WnSfV-a_9yKEU_8<5NE8OS&tKW z`_5R_vKdR*xr5#8#SA;mu+x3*yo4-w-rz0|ctmmtyUehwF8bT$o7wd~dfTP1T@7hW zQ`#cuUG}xhzIN$n*HDHtmI?g9WTqm|UEbgA26mUGI`h=5Yd9UkKK<^?g1+|YYoFchD}>wMC&PVu*jJHCe2TpG$z|VC^tx{^`#HhY z5E8wYX#T`Va+4QxCKjbQCGl=z4QkNnFQ<#Ps_Un1Sp7;O97Ph1B z{qo(f-~IaCf08qt<2sK+IFJo@bwI`k%J3}DQIEHHpO5&IhWNG)G@}I__=&N&`2+6n zz-07vU?y{z&tEKN8L`Opz;5(*Adv&;@qiu==<$F|56JYu1uk(Vgo7E--@!Nej*)ol zpr1XIj#9jY-VR0MXAZSPkB55Dp8>e}LxUO0uS`Uaho&6Cd+ zndg*wPW9v``qH1-tYbF^(f=v^pVI%Si(KJ4x6u13yE~ncLKMaBPV4Ekot?I`)9RmA z`*c%U@HKKh{T=pox)*&=`}9c0GJ!vsid;|omQK$h7QLN5#8FOgnsZ!4Z>RNkT5qTC z@*spWS%@SDay=v0Gx;cpT+itFj9kx@q%<--(;PcKqvn}y+z#QazZGZAb@n6L)14o2 z&u3+Kb_l~txW+Sr1Y+w^xkmdQE>|r1KvCH#EIL4z8 zF68Gq)V$D-xoqcZ2p2P>?~7G=nwseU;tRZq{x7=yi|=EGi|+rTU0!TWG~dyI&UB+Y zlSp7AJJ^lhF3R)baoqjIvs~a(2$yp560MNQC7E3Mm680;Y`k}A6=uF<-b?1al*nP+ z?WGfV_mVwbx)Z|Xl%ypCX1tsobuLGd7yG(go!2nu<##aS<&XH3FK9$lTJRP6y4(x* zdD(ql?$1C5GZZ~v*7Ie%ygY_K@x5K1!7LUL$4XY?Rxhu^J}=wnW%FD%&*fvB#w=c|dXqS5xo=>GAjJY8J9lnDXf9s-CXu>8f0>%Jr&Tuj=WlT(8RYs$8$i z^=bz?qra;^Als|5z1j!4ULC+M3_-6~7hvaC?}u=x<(l1IbK}3U7x;B7v^--YX~h(;gR+aa6lvbk<|*Lxy|>*LYOb-i3) zj$68JXV>lQx?8&LmacDP3)|Vn1uk=y>*(#e-mc5>`a|sXh8}OEA~iC-Q3ZbkZq&t2 zZ!|!^H$FtaH$JB!O=w0-zM>=Du*)00=!ZStu*Vzvzah^XTRDi_ZpiJ1+-_Xt3bMO# zBZQm!xv8IOSJ2l@echDh%?|v`AbgWI|71RYu^79& z8A}4Ikl9U{-Q39@64B>PJHC08TOr)a&QrWg8_acUEEDnmt*OjnF7EY~TDR=y)+Tmy zoKu{|UT)dTEql53kmL|?bbL{nN|54XSJTiW7V zx;+rR-X6j*Mlc$^-yVHVJG@9F*CcXXj2W0`>7?k(b9?Dw8b z@2P#y&EJ#hz0;h-t=_ZSdr3SFK^x(ICL+m66a}#7`$f^${VF_%zV7SmzP|43>waUJ z(UR6gqqqC*_?bZrMz;55d*2P*cLVpwq38R5@F%jpKbHlV;r?P0_>T>2W*a-%jco6` zk^APke-XLfzsVg8bl+?b3Q~)gd5d?^{{#I$Xo#D5AlC=2(E9^DKlqUmj7Dz{^z`6w z)}a0awIA$5t`ClKf>WH~HurcCLQ)zsk_EXYNpelv ziknS3jCqpWNs?TXE^`gJCYkMF6xFE5E4+^WAL{?%Cwz`vA2vbn5B2=8D}xz^-X7}d z;R@nW|DoCscOch?`#H!Fj&YS6B!}=Q6*7I49+^JMMhFtr;9@+ULeLd<oD7+y_n~bc^)0-4ClGTl@K1A;c*3OA=Ae%;8q{MhD;y3iN|i@@n?KN3*6{qeLmLb zW4%58gGDT14QfB$$QE`Y*T?Slv1}jP>0{r@<9j>`L98J;C25gsawcS(>^_sr@*MSf zkyo(qWcyCmTe9Ag^_Hx+WcQk^ujGz&Mz+a6Alu}h=!2e<^_(o*WZ5Q%7TFG@}z;>CG^v zFr65d6UR!{@*nHj$bJrSo-17E7WYZwafpZrWFbG5sLIpS5DbfY^x`I$is!3+^&nanh1GKcvrWD$#5jd>zAu$dj~W*=sYxDz5$ zhCdY>Q4NCouuxCcT*2%6u&W*SZD%E3gYk$q~3=)Q(xr=FQ5oyzs6Md%DXWFVfk3Q0V zL^IS*E7!E&(jL1?YgcLAP+E7Ab}&Pkz@JQI269b1pTE#o+BIxLUupG~R$poLmG&Ap zxx;-PqPKJ@$c?*6m!E9Xt^EZ3h&q3T&#-p6zH0Q9xjJ}VI zddzqod(8NdfPCcDg(AG^#Xt4#9AB%4gtsKK+?Q>Oa7Kok1nyUp|) z?qT6;#iLx%e0d{B%*KBEy$v9By@ zWa+?93}qzd&SLH?f8gCL=FVd7EOwV=8M`@&?Y*)E~T(dpEF0&=md)b)LZt z*`MWg-okf~U8dPv@D=*b-VXg{@4^rCpg$v-!E9n!#V+>ZMzf#eB3HPHT(jRpwmH&~ z3q9q~Qw}}lsLIpSL{B-MqdqV43SZI~{pFBtj+V$aM>O9e+Z^rDYYy4w=!IL&q1PM( z7|wVm@+VW#e~wwmHpe{NN{+vAD>?pUBU{+cPRx)ql0wKdXKBh&k?P1aXKm{8Jg@Q* z`pl`%oO;XIo6(GAI%?;%*PMT`loiC0fSu;t#vzV#inClot~swG+gvHgLUBq`hVnc` z74()%Z@KiAOK-XAp|4z@@;S22)d<<la2b2D#>v zYc9Fwnu57<#jqT+L?O@IMJYiknOX9 zs)*jA^cJPJs9MzFJwD`P~kkDl|q%v-$62k1MG ze)H%zkACyCLce+1@gpP9R~|X%nakg-L7sWGa*$)lGtU_=BF{Y6xDg`qMv$3o{=WUFy*Q zzmwnhk-r}U_ys%7Z>RZx!%p+-KmTfEmj5s^%P+J1GRuFSOSr-OGAkgn0;xz%S~6hH z0_H5BmjZGuV8;atQ;ZT+;A!+%z@7`Z%>vJ%#{w_#E@m%a-vvJ7OB&IeVd%5KIDTgm zzNZ4SnTIS2EFzBe9OMXRxfLP`+C@RTC}_6@?X_Sbic%7}6)cDT3OqT^IBn7o5Q?mZDz4mB_8&e{5hgGAyWvf^sZ)loKJMkX;t)KqtEJLx?Dxf-K0a zu-hmcMLuL#xC(kH>`n^5$Sb_gn>68D{`W>texffwGl(IKXCnG8Jca4#yRbbMmRaF- zoDUI2?68RWi@b(+i+Hz4Tl89_Gu`Npxr;1j1@ZjNKX|9eM$BDA=0)UP)O{3n3q{?0 z(Ol#~?nUKZv?%VOXjR-pQTI@^7Ik=zdbru5W-j_3AMy#G(~!pW!TlDsm!jq@YR;nO zEIJeO6`jvt#Gu!r_EPi^M>)ss5K$~G8IXA~c^4~;TP;?KGL)wx`YJXYb&9D|Or2u# zEVhNc?8n>1j&PAHTt^PY?s7jw6qm7oCs0K3jM#T^`!1e?B2=If@+dBk;x%~|SrmVR zw~<5f5BQkR_ySoJ{|0+1ZcoL#(Su$LWH4^H_y|TLtKxbtuCEdWd514)N(;WC4d2tA zj{M9h+BNxPcOGpu|>ounYN=NaO&rcrqVmd$KV<@(1?wr2RbU zcc0wGPWG@5e@9EEAv4*KS;^e!zohI+7N;boc^7w4avJlALoX%uQd0erXZhcIk3vML z^w>iwdnlEYyyT}4-YxYMRp`%1#xeo@mXbxO8OWm4TvoCPHA)@k7$-Tyc`k94$04G0 zAO)#NLpm~IzS3DKfbXNU@1wLkEL|SGl-5h>YS@424*bLbeqkuX(PL>nmeynG$>_7R zU6qc--b-(1Kl&>U59`>7KFb{9D94dw8U2tv#HV~gGg{J`Xuf3(`Y)$eIkn11kOQ|`UXJC<lWl`Q7<46u>s$Eh4WnEc8I7b-->drDA$T5 zD1$62+FiwJJWWk%^BQj=>x#0jDC>$18NhHxA?J$Yn89r3vVcV_W;I)J2NicC!-@wu z%rRtF@ftU|gL|mx9{hW%BA$}xQ~G*J7EcXj1v0Oc72idr{1l=Ha;>EQN;S}bCH+^D zZ>4w9ea{HC`dIyuUv^bJV!nBRatJ8r?He3#Pc_6_?Pu;LUxspa)Q(7r?Q)_e1+?{ z?aF$qtgkBBkw+C-RCxopT%`>^GKAll&P?1*75k~8MiqOh;>{}Fs^YCG53qx(xp^9W zSM^@iH*up?-{DIdK+;g=k@=*XiSMz;U`w;uB))PHco5~{MP`BDj)*iVnxH>=TG^(|~;2S;!V)z4vX)vs_HeOLcq&yPdI(*d{m z^e6npP{uHxiTsJ$PtRoma(Q|Q%dwZI_i-ac)G%)inbs&l1t)XrWb!)_tfLo|xo@a7Xn4&bMHE!$~JwM}j zo{`lvCy>E2mqSF&DD+v=-;A17sLnIgrY^7Y25)0uHQh%|`P9^V&3@>m=JgO!%bc~^ z(1{=E%`c2%3}&b`n|Un7o@%XO6T6XJtyA3P0gpmNZTqO568Br%{npM%79!De?UIzC z92KcdHEK|cXE8%bk+Y&xDBjFY_Ajt%n4G|4e;SL&njJzAPrX3yej(=ZKM1v{e|Nc*^6#ws^ R1wZ_M|NQ^|JEFm~{{wq0MD+jw literal 106217 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%0AFx1NIlyY<9wJx9Y> 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 rJwFmp2IqrNr&h?j({@)9e2SMogrP diff --git a/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist index 29372fd..53d0216 100644 --- a/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ SessionStatusLiveExtension.xcscheme_^#shared#^_ orderHint - 1 + 0 diff --git a/ios/OpenClimb/ContentView.swift b/ios/OpenClimb/ContentView.swift index 276c850..d99571e 100644 --- a/ios/OpenClimb/ContentView.swift +++ b/ios/OpenClimb/ContentView.swift @@ -4,6 +4,7 @@ struct ContentView: View { @StateObject private var dataManager = ClimbingDataManager() @State private var selectedTab = 0 @Environment(\.scenePhase) private var scenePhase + @State private var notificationObservers: [NSObjectProtocol] = [] var body: some View { TabView(selection: $selectedTab) { @@ -43,11 +44,23 @@ struct ContentView: View { .tag(4) } .environmentObject(dataManager) - .onChange(of: scenePhase) { - if scenePhase == .active { - dataManager.onAppBecomeActive() + .onChange(of: scenePhase) { oldPhase, newPhase in + if newPhase == .active { + // Add slight delay to ensure app is fully loaded + Task { + try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds + dataManager.onAppBecomeActive() + } + } else if newPhase == .background { + dataManager.onAppEnterBackground() } } + .onAppear { + setupNotificationObservers() + } + .onDisappear { + removeNotificationObservers() + } .overlay(alignment: .top) { if let message = dataManager.successMessage { SuccessMessageView(message: message) @@ -62,6 +75,44 @@ struct ContentView: View { } } } + + private func setupNotificationObservers() { + // Listen for when the app will enter foreground + let willEnterForegroundObserver = NotificationCenter.default.addObserver( + forName: UIApplication.willEnterForegroundNotification, + object: nil, + queue: .main + ) { _ in + print("๐Ÿ“ฑ App will enter foreground - preparing Live Activity check") + Task { + // Small delay to ensure app is fully active + try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds + await dataManager.onAppBecomeActive() + } + } + + // Listen for when the app becomes active + let didBecomeActiveObserver = NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { _ in + print("๐Ÿ“ฑ App did become active - checking Live Activity status") + Task { + try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds + dataManager.onAppBecomeActive() + } + } + + notificationObservers = [willEnterForegroundObserver, didBecomeActiveObserver] + } + + private func removeNotificationObservers() { + for observer in notificationObservers { + NotificationCenter.default.removeObserver(observer) + } + notificationObservers.removeAll() + } } struct SuccessMessageView: View { diff --git a/ios/OpenClimb/Utils/ImageManager.swift b/ios/OpenClimb/Utils/ImageManager.swift index eb59c94..a8d74db 100644 --- a/ios/OpenClimb/Utils/ImageManager.swift +++ b/ios/OpenClimb/Utils/ImageManager.swift @@ -1,4 +1,3 @@ - import Foundation import SwiftUI @@ -522,7 +521,7 @@ class ImageManager { } } - private func getFullPath(from relativePath: String) -> String { + func getFullPath(from relativePath: String) -> String { // If it's already a full path, check if it's legacy and needs migration if relativePath.hasPrefix("/") { // If it's pointing to legacy Documents directory, redirect to new location diff --git a/ios/OpenClimb/ViewModels/ClimbingDataManager.swift b/ios/OpenClimb/ViewModels/ClimbingDataManager.swift index 8370854..cbd55fa 100644 --- a/ios/OpenClimb/ViewModels/ClimbingDataManager.swift +++ b/ios/OpenClimb/ViewModels/ClimbingDataManager.swift @@ -7,6 +7,10 @@ import UniformTypeIdentifiers import WidgetKit #endif +#if canImport(ActivityKit) + import ActivityKit +#endif + @MainActor class ClimbingDataManager: ObservableObject { @@ -23,6 +27,7 @@ class ClimbingDataManager: ObservableObject { private let sharedUserDefaults = UserDefaults(suiteName: "group.com.atridad.OpenClimb") private let encoder = JSONEncoder() private let decoder = JSONDecoder() + private var liveActivityObserver: NSObjectProtocol? private enum Keys { static let gyms = "openclimb_gyms" @@ -57,6 +62,7 @@ class ClimbingDataManager: ObservableObject { _ = ImageManager.shared loadAllData() migrateImagePaths() + setupLiveActivityNotifications() Task { try? await Task.sleep(nanoseconds: 2_000_000_000) @@ -67,6 +73,12 @@ class ClimbingDataManager: ObservableObject { } } + deinit { + if let observer = liveActivityObserver { + NotificationCenter.default.removeObserver(observer) + } + } + private func loadAllData() { loadGyms() loadProblems() @@ -463,6 +475,7 @@ class ClimbingDataManager: ObservableObject { let exportData = ClimbDataExport( exportedAt: dateFormatter.string(from: Date()), + version: "1.0", gyms: gyms.map { AndroidGym(from: $0) }, problems: problems.map { AndroidProblem(from: $0) }, sessions: sessions.map { AndroidClimbSession(from: $0) }, @@ -471,13 +484,21 @@ class ClimbingDataManager: ObservableObject { // Collect referenced image paths let referencedImagePaths = collectReferencedImagePaths() + print("๐ŸŽฏ Starting export with \(referencedImagePaths.count) images") - return try ZipUtils.createExportZip( + let zipData = try ZipUtils.createExportZip( exportData: exportData, referencedImagePaths: referencedImagePaths ) + + print("โœ… Export completed successfully") + successMessage = "Export completed with \(referencedImagePaths.count) images" + clearMessageAfterDelay() + return zipData } catch { - setError("Export failed: \(error.localizedDescription)") + let errorMessage = "Export failed: \(error.localizedDescription)" + print("โŒ \(errorMessage)") + setError(errorMessage) return nil } } @@ -565,16 +586,18 @@ class ClimbingDataManager: ObservableObject { struct ClimbDataExport: Codable { let exportedAt: String + let version: String let gyms: [AndroidGym] let problems: [AndroidProblem] let sessions: [AndroidClimbSession] let attempts: [AndroidAttempt] init( - exportedAt: String, gyms: [AndroidGym], problems: [AndroidProblem], + exportedAt: String, version: String = "1.0", gyms: [AndroidGym], problems: [AndroidProblem], sessions: [AndroidClimbSession], attempts: [AndroidAttempt] ) { self.exportedAt = exportedAt + self.version = version self.gyms = gyms self.problems = problems self.sessions = sessions @@ -588,6 +611,7 @@ struct AndroidGym: Codable { let location: String? let supportedClimbTypes: [ClimbType] let difficultySystems: [DifficultySystem] + let customDifficultyGrades: [String] let notes: String? let createdAt: String let updatedAt: String @@ -598,6 +622,7 @@ struct AndroidGym: Codable { self.location = gym.location self.supportedClimbTypes = gym.supportedClimbTypes self.difficultySystems = gym.difficultySystems + self.customDifficultyGrades = gym.customDifficultyGrades self.notes = gym.notes let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" @@ -607,13 +632,15 @@ struct AndroidGym: Codable { init( id: String, name: String, location: String?, supportedClimbTypes: [ClimbType], - difficultySystems: [DifficultySystem], notes: String?, createdAt: String, updatedAt: String + difficultySystems: [DifficultySystem], customDifficultyGrades: [String] = [], + notes: String?, createdAt: String, updatedAt: String ) { self.id = id self.name = name self.location = location self.supportedClimbTypes = supportedClimbTypes self.difficultySystems = difficultySystems + self.customDifficultyGrades = customDifficultyGrades self.notes = notes self.createdAt = createdAt self.updatedAt = updatedAt @@ -633,7 +660,7 @@ struct AndroidGym: Codable { location: location, supportedClimbTypes: supportedClimbTypes, difficultySystems: difficultySystems, - customDifficultyGrades: [], + customDifficultyGrades: customDifficultyGrades, notes: notes, createdAt: createdDate, updatedAt: updatedDate @@ -648,7 +675,13 @@ struct AndroidProblem: Codable { let description: String? let climbType: ClimbType let difficulty: DifficultyGrade + let setter: String? + let tags: [String] + let location: String? let imagePaths: [String]? + let isActive: Bool + let dateSet: String? + let notes: String? let createdAt: String let updatedAt: String @@ -659,16 +692,26 @@ struct AndroidProblem: Codable { self.description = problem.description self.climbType = problem.climbType self.difficulty = problem.difficulty + self.setter = problem.setter + self.tags = problem.tags + self.location = problem.location self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths + self.isActive = problem.isActive + self.notes = problem.notes let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" + self.dateSet = problem.dateSet != nil ? formatter.string(from: problem.dateSet!) : nil self.createdAt = formatter.string(from: problem.createdAt) self.updatedAt = formatter.string(from: problem.updatedAt) } init( id: String, gymId: String, name: String?, description: String?, climbType: ClimbType, - difficulty: DifficultyGrade, imagePaths: [String]?, createdAt: String, updatedAt: String + difficulty: DifficultyGrade, setter: String? = nil, tags: [String] = [], + location: String? = nil, + imagePaths: [String]? = nil, isActive: Bool = true, dateSet: String? = nil, + notes: String? = nil, + createdAt: String, updatedAt: String ) { self.id = id self.gymId = gymId @@ -676,7 +719,13 @@ struct AndroidProblem: Codable { self.description = description self.climbType = climbType self.difficulty = difficulty + self.setter = setter + self.tags = tags + self.location = location self.imagePaths = imagePaths + self.isActive = isActive + self.dateSet = dateSet + self.notes = notes self.createdAt = createdAt self.updatedAt = updatedAt } @@ -697,13 +746,13 @@ struct AndroidProblem: Codable { description: description, climbType: climbType, difficulty: difficulty, - setter: nil, - tags: [], - location: nil, + setter: setter, + tags: tags, + location: location, imagePaths: imagePaths ?? [], - isActive: true, - dateSet: nil, - notes: nil, + isActive: isActive, + dateSet: dateSet != nil ? formatter.date(from: dateSet!) : nil, + notes: notes, createdAt: createdDate, updatedAt: updatedDate ) @@ -717,7 +766,13 @@ struct AndroidProblem: Codable { description: self.description, climbType: self.climbType, difficulty: self.difficulty, + setter: self.setter, + tags: self.tags, + location: self.location, imagePaths: newImagePaths.isEmpty ? nil : newImagePaths, + isActive: self.isActive, + dateSet: self.dateSet, + notes: self.notes, createdAt: self.createdAt, updatedAt: self.updatedAt ) @@ -730,8 +785,9 @@ struct AndroidClimbSession: Codable { let date: String let startTime: String? let endTime: String? - let duration: Int? + let duration: Int64? let status: SessionStatus + let notes: String? let createdAt: String let updatedAt: String @@ -743,15 +799,17 @@ struct AndroidClimbSession: Codable { self.date = formatter.string(from: session.date) self.startTime = session.startTime != nil ? formatter.string(from: session.startTime!) : nil self.endTime = session.endTime != nil ? formatter.string(from: session.endTime!) : nil - self.duration = session.duration + self.duration = session.duration != nil ? Int64(session.duration!) : nil self.status = session.status + self.notes = session.notes self.createdAt = formatter.string(from: session.createdAt) self.updatedAt = formatter.string(from: session.updatedAt) } init( id: String, gymId: String, date: String, startTime: String?, endTime: String?, - duration: Int?, status: SessionStatus, createdAt: String, updatedAt: String + duration: Int64?, status: SessionStatus, notes: String? = nil, createdAt: String, + updatedAt: String ) { self.id = id self.gymId = gymId @@ -760,6 +818,7 @@ struct AndroidClimbSession: Codable { self.endTime = endTime self.duration = duration self.status = status + self.notes = notes self.createdAt = createdAt self.updatedAt = updatedAt } @@ -783,9 +842,9 @@ struct AndroidClimbSession: Codable { date: sessionDate, startTime: sessionStartTime, endTime: sessionEndTime, - duration: duration, + duration: duration != nil ? Int(duration!) : nil, status: status, - notes: nil, + notes: notes, createdAt: createdDate, updatedAt: updatedDate ) @@ -799,8 +858,8 @@ struct AndroidAttempt: Codable { let result: AttemptResult let highestHold: String? let notes: String? - let duration: Int? - let restTime: Int? + let duration: Int64? + let restTime: Int64? let timestamp: String let createdAt: String @@ -811,8 +870,8 @@ struct AndroidAttempt: Codable { self.result = attempt.result self.highestHold = attempt.highestHold self.notes = attempt.notes - self.duration = attempt.duration - self.restTime = attempt.restTime + self.duration = attempt.duration != nil ? Int64(attempt.duration!) : nil + self.restTime = attempt.restTime != nil ? Int64(attempt.restTime!) : nil let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" self.timestamp = formatter.string(from: attempt.timestamp) @@ -821,7 +880,7 @@ struct AndroidAttempt: Codable { init( id: String, sessionId: String, problemId: String, result: AttemptResult, - highestHold: String?, notes: String?, duration: Int?, restTime: Int?, + highestHold: String?, notes: String?, duration: Int64?, restTime: Int64?, timestamp: String, createdAt: String ) { self.id = id @@ -853,8 +912,8 @@ struct AndroidAttempt: Codable { result: result, highestHold: highestHold, notes: notes, - duration: duration, - restTime: restTime, + duration: duration != nil ? Int(duration!) : nil, + restTime: restTime != nil ? Int(restTime!) : nil, timestamp: attemptTimestamp, createdAt: createdDate ) @@ -864,9 +923,33 @@ struct AndroidAttempt: Codable { extension ClimbingDataManager { private func collectReferencedImagePaths() -> Set { var imagePaths = Set() + print("๐Ÿ–ผ๏ธ Starting image path collection...") + print("๐Ÿ“Š Total problems: \(problems.count)") + for problem in problems { - imagePaths.formUnion(problem.imagePaths) + if !problem.imagePaths.isEmpty { + print( + "๐Ÿ“ธ Problem '\(problem.name ?? "Unnamed")' has \(problem.imagePaths.count) images" + ) + for imagePath in problem.imagePaths { + print(" - Relative path: \(imagePath)") + let fullPath = ImageManager.shared.getFullPath(from: imagePath) + print(" - Full path: \(fullPath)") + + // Check if file exists + if FileManager.default.fileExists(atPath: fullPath) { + print(" โœ… File exists") + imagePaths.insert(fullPath) + } else { + print(" โŒ File does NOT exist") + // Still add it to let ZipUtils handle the error logging + imagePaths.insert(fullPath) + } + } + } } + + print("๐Ÿ–ผ๏ธ Collected \(imagePaths.count) total image paths for export") return imagePaths } @@ -1046,23 +1129,111 @@ extension ClimbingDataManager { } private func checkAndRestartLiveActivity() async { - guard let activeSession = activeSession else { return } + guard let activeSession = activeSession else { + // No active session, make sure all Live Activities are cleaned up + await LiveActivityManager.shared.endLiveActivity() + return + } + + // Only restart if session is actually active + guard activeSession.status == .active else { + print( + "โš ๏ธ Session exists but is not active (status: \(activeSession.status)), ending Live Activity" + ) + await LiveActivityManager.shared.endLiveActivity() + return + } if let gym = gym(withId: activeSession.gymId) { + print("๐Ÿ” Checking Live Activity for active session at \(gym.name)") + + // First cleanup any dismissed activities + await LiveActivityManager.shared.cleanupDismissedActivities() + + // Then attempt to restart if needed await LiveActivityManager.shared.restartLiveActivityIfNeeded( activeSession: activeSession, gymName: gym.name ) + + // Update with current session data + await updateLiveActivityData() } } /// Call this when app becomes active to check for Live Activity restart func onAppBecomeActive() { + print("๐Ÿ“ฑ App became active - checking Live Activity status") Task { await checkAndRestartLiveActivity() } } + /// Call this when app enters background to update Live Activity + func onAppEnterBackground() { + print("๐Ÿ“ฑ App entering background - updating Live Activity if needed") + Task { + await updateLiveActivityData() + } + } + + /// Setup notifications for Live Activity events + private func setupLiveActivityNotifications() { + liveActivityObserver = NotificationCenter.default.addObserver( + forName: .liveActivityDismissed, + object: nil, + queue: .main + ) { [weak self] _ in + print("๐Ÿ”” Received Live Activity dismissed notification - attempting restart") + Task { @MainActor in + await self?.handleLiveActivityDismissed() + } + } + } + + /// Handle Live Activity being dismissed by user + private func handleLiveActivityDismissed() async { + guard let activeSession = activeSession, + activeSession.status == .active, + let gym = gym(withId: activeSession.gymId) + else { + return + } + + print("๐Ÿ”„ Attempting to restart dismissed Live Activity for \(gym.name)") + + // Wait a bit before restarting to avoid frequency limits + try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + + await LiveActivityManager.shared.startLiveActivity( + for: activeSession, + gymName: gym.name + ) + + // Update with current data + await updateLiveActivityData() + } + + /// Update Live Activity with current session statistics + private func updateLiveActivityData() async { + guard let activeSession = activeSession, + activeSession.status == .active + else { return } + + let elapsed = Date().timeIntervalSince(activeSession.startTime ?? activeSession.date) + let sessionAttempts = attempts.filter { $0.sessionId == activeSession.id } + let totalAttempts = sessionAttempts.count + let completedProblems = Set( + sessionAttempts.filter { $0.result.isSuccessful }.map { $0.problemId } + ).count + + await LiveActivityManager.shared.updateLiveActivity( + elapsed: elapsed, + totalAttempts: totalAttempts, + completedProblems: completedProblems + ) + } + /// Update Live Activity with current session data private func updateLiveActivityForActiveSession() { guard let activeSession = activeSession, diff --git a/ios/OpenClimb/ViewModels/LiveActivityManager.swift b/ios/OpenClimb/ViewModels/LiveActivityManager.swift index 2598e42..c6e7992 100644 --- a/ios/OpenClimb/ViewModels/LiveActivityManager.swift +++ b/ios/OpenClimb/ViewModels/LiveActivityManager.swift @@ -1,12 +1,22 @@ import ActivityKit import Foundation +extension Notification.Name { + static let liveActivityDismissed = Notification.Name("liveActivityDismissed") +} + @MainActor final class LiveActivityManager { static let shared = LiveActivityManager() private init() {} private var currentActivity: Activity? + private var healthCheckTimer: Timer? + private var lastHealthCheck: Date = Date() + + deinit { + healthCheckTimer?.invalidate() + } /// Check if there's an active session and restart Live Activity if needed func restartLiveActivityIfNeeded(activeSession: ClimbSession?, gymName: String?) async { @@ -18,13 +28,31 @@ final class LiveActivityManager { return } - // Check if we already have a running Live Activity - if currentActivity != nil { - print("โ„น๏ธ Live Activity already running") + // Check if we have a tracked Live Activity that's still actually running + if let currentActivity = currentActivity { + let activities = Activity.activities + let isStillActive = activities.contains { $0.id == currentActivity.id } + + if isStillActive { + print("โ„น๏ธ Live Activity still running: \(currentActivity.id)") + return + } else { + print( + "โš ๏ธ Tracked Live Activity \(currentActivity.id) was dismissed, clearing reference" + ) + self.currentActivity = nil + } + } + + // Check if there are ANY active Live Activities for this session + let existingActivities = Activity.activities + if let existingActivity = existingActivities.first { + print("โ„น๏ธ Found existing Live Activity: \(existingActivity.id), using it") + self.currentActivity = existingActivity return } - print("๐Ÿ”„ Restarting Live Activity for existing session") + print("๐Ÿ”„ No Live Activity found, restarting for existing session") await startLiveActivity(for: activeSession, gymName: gymName) } @@ -34,10 +62,17 @@ final class LiveActivityManager { await endLiveActivity() + // Start health checks once we have an active session + startHealthChecks() + + // Calculate elapsed time if session already started + let startTime = session.startTime ?? session.date + let elapsed = Date().timeIntervalSince(startTime) + let attributes = SessionActivityAttributes( - gymName: gymName, startTime: session.startTime ?? session.date) + gymName: gymName, startTime: startTime) let initialContentState = SessionActivityAttributes.ContentState( - elapsed: 0, + elapsed: elapsed, totalAttempts: 0, completedProblems: 0 ) @@ -59,6 +94,8 @@ final class LiveActivityManager { print("Authorization error - check Live Activity permissions in Settings") } else if error.localizedDescription.contains("content") { print("Content error - check ActivityAttributes structure") + } else if error.localizedDescription.contains("frequencyLimited") { + print("Frequency limited - too many Live Activities started recently") } } } @@ -66,11 +103,23 @@ final class LiveActivityManager { /// Call this to update the Live Activity with new session progress func updateLiveActivity(elapsed: TimeInterval, totalAttempts: Int, completedProblems: Int) async { - guard let currentActivity else { + guard let currentActivity = currentActivity else { print("โš ๏ธ No current activity to update") return } + // Verify the activity is still valid before updating + let activities = Activity.activities + let isStillActive = activities.contains { $0.id == currentActivity.id } + + if !isStillActive { + print( + "โš ๏ธ Tracked Live Activity \(currentActivity.id) is no longer active, clearing reference" + ) + self.currentActivity = nil + return + } + print( "๐Ÿ”„ Updating Live Activity - Attempts: \(totalAttempts), Completed: \(completedProblems)" ) @@ -81,12 +130,21 @@ final class LiveActivityManager { completedProblems: completedProblems ) - await currentActivity.update(.init(state: updatedContentState, staleDate: nil)) - print("โœ… Live Activity updated successfully") + do { + await currentActivity.update(.init(state: updatedContentState, staleDate: nil)) + print("โœ… Live Activity updated successfully") + } catch { + print("โŒ Failed to update Live Activity: \(error)") + // If update fails, the activity might have been dismissed + self.currentActivity = nil + } } /// Call this when a ClimbSession ends to end the Live Activity func endLiveActivity() async { + // Stop health checks first + stopHealthChecks() + // First end the tracked activity if it exists if let currentActivity { print("๐Ÿ”ด Ending tracked Live Activity: \(currentActivity.id)") @@ -115,18 +173,92 @@ final class LiveActivityManager { func checkLiveActivityAvailability() -> String { let authorizationInfo = ActivityAuthorizationInfo() let status = authorizationInfo.areActivitiesEnabled + let allActivities = Activity.activities let message = """ Live Activity Status: โ€ข Enabled: \(status) โ€ข Authorization: \(authorizationInfo.areActivitiesEnabled ? "Granted" : "Denied/Unknown") - โ€ข Current Activity: \(currentActivity?.id.description ?? "None") + โ€ข Tracked Activity: \(currentActivity?.id.description ?? "None") + โ€ข All Active Activities: \(allActivities.count) """ print(message) return message } + /// Force check and cleanup dismissed Live Activities + func cleanupDismissedActivities() async { + let activities = Activity.activities + + if let currentActivity = currentActivity { + let isStillActive = activities.contains { $0.id == currentActivity.id } + if !isStillActive { + print("๐Ÿงน Cleaning up dismissed Live Activity: \(currentActivity.id)") + self.currentActivity = nil + } + } + } + + /// Start periodic health checks for Live Activity + func startHealthChecks() { + stopHealthChecks() // Stop any existing timer + + print("๐Ÿฉบ Starting Live Activity health checks") + healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { + [weak self] _ in + Task { @MainActor in + await self?.performHealthCheck() + } + } + } + + /// Stop periodic health checks + func stopHealthChecks() { + healthCheckTimer?.invalidate() + healthCheckTimer = nil + print("๐Ÿ›‘ Stopped Live Activity health checks") + } + + /// Perform a health check on the current Live Activity + private func performHealthCheck() async { + guard let currentActivity = currentActivity else { return } + + let now = Date() + let timeSinceLastCheck = now.timeIntervalSince(lastHealthCheck) + + // Only perform health check if it's been at least 25 seconds + guard timeSinceLastCheck >= 25 else { return } + + print("๐Ÿฉบ Performing Live Activity health check") + lastHealthCheck = now + + let activities = Activity.activities + let isStillActive = activities.contains { $0.id == currentActivity.id } + + if !isStillActive { + print("๐Ÿ’” Health check failed - Live Activity was dismissed") + self.currentActivity = nil + + // Notify that we need to restart + NotificationCenter.default.post( + name: .liveActivityDismissed, + object: nil + ) + } else { + print("โœ… Live Activity health check passed") + } + } + + /// Get the current activity status for debugging + func getCurrentActivityStatus() -> String { + let activities = Activity.activities + let trackedStatus = currentActivity != nil ? "Tracked" : "None" + let actualCount = activities.count + + return "Status: \(trackedStatus) | Active Count: \(actualCount)" + } + /// Start periodic updates for Live Activity func startPeriodicUpdates(for session: ClimbSession, totalAttempts: Int, completedProblems: Int) { diff --git a/ios/OpenClimb/Views/AnalyticsView.swift b/ios/OpenClimb/Views/AnalyticsView.swift index 828182a..4d4874b 100644 --- a/ios/OpenClimb/Views/AnalyticsView.swift +++ b/ios/OpenClimb/Views/AnalyticsView.swift @@ -105,9 +105,26 @@ struct ProgressChartSection: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var selectedSystem: DifficultySystem = .vScale @State private var showAllTime: Bool = true + @State private var cachedGradeCountData: [GradeCount] = [] + @State private var lastCalculationDate: Date = Date.distantPast + @State private var lastDataHash: Int = 0 private var gradeCountData: [GradeCount] { - calculateGradeCounts() + let currentHash = + dataManager.problems.count + dataManager.attempts.count + (showAllTime ? 1 : 0) + let now = Date() + + // Recalculate only if data changed or cache is older than 30 seconds + if currentHash != lastDataHash || now.timeIntervalSince(lastCalculationDate) > 30 { + let newData = calculateGradeCounts() + DispatchQueue.main.async { + self.cachedGradeCountData = newData + self.lastCalculationDate = now + self.lastDataHash = currentHash + } + } + + return cachedGradeCountData.isEmpty ? calculateGradeCounts() : cachedGradeCountData } private var usedSystems: [DifficultySystem] { diff --git a/ios/OpenClimb/Views/Detail/SessionDetailView.swift b/ios/OpenClimb/Views/Detail/SessionDetailView.swift index 192be69..b4ad000 100644 --- a/ios/OpenClimb/Views/Detail/SessionDetailView.swift +++ b/ios/OpenClimb/Views/Detail/SessionDetailView.swift @@ -15,6 +15,18 @@ struct SessionDetailView: View { dataManager.session(withId: sessionId) } + private func startTimer() { + // Update every 5 seconds instead of 1 second for better performance + timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in + currentTime = Date() + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + private var gym: Gym? { guard let session = session else { return nil } return dataManager.gym(withId: session.gymId) @@ -35,7 +47,7 @@ struct SessionDetailView: View { calculateSessionStats() } - private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + @State private var timer: Timer? var body: some View { ScrollView { @@ -57,8 +69,11 @@ struct SessionDetailView: View { } .padding() } - .onReceive(timer) { _ in - currentTime = Date() + .onAppear { + startTimer() + } + .onDisappear { + stopTimer() } .navigationTitle("Session Details") .navigationBarTitleDisplayMode(.inline) @@ -153,46 +168,14 @@ struct SessionDetailView: View { let uniqueProblems = Set(attempts.map { $0.problemId }) let completedProblems = Set(successfulAttempts.map { $0.problemId }) - let attemptedProblems = uniqueProblems.compactMap { dataManager.problem(withId: $0) } - let boulderProblems = attemptedProblems.filter { $0.climbType == .boulder } - let ropeProblems = attemptedProblems.filter { $0.climbType == .rope } - - let boulderRange = gradeRange(for: boulderProblems) - let ropeRange = gradeRange(for: ropeProblems) - return SessionStats( totalAttempts: attempts.count, successfulAttempts: successfulAttempts.count, uniqueProblemsAttempted: uniqueProblems.count, - uniqueProblemsCompleted: completedProblems.count, - boulderRange: boulderRange, - ropeRange: ropeRange + uniqueProblemsCompleted: completedProblems.count ) } - private func gradeRange(for problems: [Problem]) -> String? { - guard !problems.isEmpty else { return nil } - let difficulties = problems.map { $0.difficulty } - - // Group by difficulty system first - let groupedBySystem = Dictionary(grouping: difficulties) { $0.system } - - // For each system, find the range - let ranges = groupedBySystem.compactMap { (system, difficulties) -> String? in - let sortedDifficulties = difficulties.sorted() - guard let min = sortedDifficulties.first, let max = sortedDifficulties.last else { - return nil - } - - if min == max { - return min.grade - } else { - return "\(min.grade) - \(max.grade)" - } - } - - return ranges.joined(separator: ", ") - } } struct SessionHeaderCard: View { @@ -300,19 +283,6 @@ struct SessionStatsCard: View { StatItem(label: "Successful", value: "\(stats.successfulAttempts)") StatItem(label: "Completed", value: "\(stats.uniqueProblemsCompleted)") } - - // Grade ranges - VStack(alignment: .leading, spacing: 8) { - if let boulderRange = stats.boulderRange, let ropeRange = stats.ropeRange { - HStack { - StatItem(label: "Boulder Range", value: boulderRange) - StatItem(label: "Rope Range", value: ropeRange) - } - } else if let singleRange = stats.boulderRange ?? stats.ropeRange { - StatItem(label: "Grade Range", value: singleRange) - .frame(maxWidth: .infinity, alignment: .center) - } - } } } .padding() @@ -504,8 +474,6 @@ struct SessionStats { let successfulAttempts: Int let uniqueProblemsAttempted: Int let uniqueProblemsCompleted: Int - let boulderRange: String? - let ropeRange: String? } #Preview { diff --git a/ios/OpenClimb/Views/ProblemsView.swift b/ios/OpenClimb/Views/ProblemsView.swift index 7014935..36b3fd2 100644 --- a/ios/OpenClimb/Views/ProblemsView.swift +++ b/ios/OpenClimb/Views/ProblemsView.swift @@ -1,4 +1,3 @@ - import SwiftUI struct ProblemsView: View { @@ -286,7 +285,7 @@ struct ProblemRow: View { if !problem.imagePaths.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { + LazyHStack(spacing: 8) { ForEach(problem.imagePaths.prefix(3), id: \.self) { imagePath in ProblemImageView(imagePath: imagePath) } @@ -372,6 +371,13 @@ struct ProblemImageView: View { @State private var isLoading = true @State private var hasFailed = false + private static var imageCache: NSCache = { + let cache = NSCache() + cache.countLimit = 100 + cache.totalCostLimit = 50 * 1024 * 1024 // 50MB + return cache + }() + var body: some View { Group { if let uiImage = uiImage { @@ -412,10 +418,22 @@ struct ProblemImageView: View { return } + let cacheKey = NSString(string: imagePath) + + // Check cache first + if let cachedImage = Self.imageCache.object(forKey: cacheKey) { + self.uiImage = cachedImage + self.isLoading = false + return + } + DispatchQueue.global(qos: .userInitiated).async { if let data = ImageManager.shared.loadImageData(fromPath: imagePath), let image = UIImage(data: data) { + // Cache the image + Self.imageCache.setObject(image, forKey: cacheKey) + DispatchQueue.main.async { self.uiImage = image self.isLoading = false diff --git a/ios/OpenClimb/Views/SessionsView.swift b/ios/OpenClimb/Views/SessionsView.swift index dbb9f5f..a9df99b 100644 --- a/ios/OpenClimb/Views/SessionsView.swift +++ b/ios/OpenClimb/Views/SessionsView.swift @@ -114,7 +114,7 @@ struct ActiveSessionBanner: View { @State private var currentTime = Date() @State private var navigateToDetail = false - private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + @State private var timer: Timer? var body: some View { HStack { @@ -162,8 +162,11 @@ struct ActiveSessionBanner: View { .fill(.green.opacity(0.1)) .stroke(.green.opacity(0.3), lineWidth: 1) ) - .onReceive(timer) { _ in - currentTime = Date() + .onAppear { + startTimer() + } + .onDisappear { + stopTimer() } .background( NavigationLink( @@ -190,6 +193,17 @@ struct ActiveSessionBanner: View { return String(format: "%ds", seconds) } } + + private func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in + currentTime = Date() + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } } struct SessionRow: View { diff --git a/ios/OpenClimb/Views/SettingsView.swift b/ios/OpenClimb/Views/SettingsView.swift index be5a7ff..79a832e 100644 --- a/ios/OpenClimb/Views/SettingsView.swift +++ b/ios/OpenClimb/Views/SettingsView.swift @@ -164,60 +164,70 @@ struct ExportDataView: View { let data: Data @Environment(\.dismiss) private var dismiss @State private var tempFileURL: URL? + @State private var isCreatingFile = true var body: some View { NavigationView { - VStack(spacing: 20) { - Image(systemName: "square.and.arrow.up") - .font(.system(size: 60)) - .foregroundColor(.blue) + VStack(spacing: 30) { + if isCreatingFile { + // Loading state - more prominent + VStack(spacing: 20) { + ProgressView() + .scaleEffect(1.5) + .tint(.blue) - Text("Export Data") - .font(.title) - .fontWeight(.bold) + Text("Preparing Your Export") + .font(.title2) + .fontWeight(.semibold) - Text( - "Your climbing data has been prepared for export. Use the share button below to save or send your data." - ) - .multilineTextAlignment(.center) - .padding(.horizontal) - - if let fileURL = tempFileURL { - ShareLink( - item: fileURL, - preview: SharePreview( - "OpenClimb Data Export", - image: Image("MountainsIcon")) - ) { - Label("Share Data", systemImage: "square.and.arrow.up") - .font(.headline) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding() - .background( - RoundedRectangle(cornerRadius: 12) - .fill(.blue) - ) + Text("Creating ZIP file with your climbing data and images...") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) } - .padding(.horizontal) - .buttonStyle(.plain) + .frame(maxWidth: .infinity, maxHeight: .infinity) } else { - Button(action: {}) { - Label("Preparing Export...", systemImage: "hourglass") - .font(.headline) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding() - .background( - RoundedRectangle(cornerRadius: 12) - .fill(.gray) - ) - } - .disabled(true) - .padding(.horizontal) - } + // Ready state + VStack(spacing: 20) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 60)) + .foregroundColor(.green) - Spacer() + Text("Export Ready!") + .font(.title) + .fontWeight(.bold) + + Text( + "Your climbing data has been prepared for export. Use the share button below to save or send your data." + ) + .multilineTextAlignment(.center) + .padding(.horizontal) + + if let fileURL = tempFileURL { + ShareLink( + item: fileURL, + preview: SharePreview( + "OpenClimb Data Export", + image: Image("MountainsIcon")) + ) { + Label("Share Data", systemImage: "square.and.arrow.up") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.blue) + ) + } + .padding(.horizontal) + .buttonStyle(.plain) + } + } + + Spacer() + } } .padding() .navigationTitle("Export") @@ -259,6 +269,9 @@ struct ExportDataView: View { ).first else { print("Could not access Documents directory") + DispatchQueue.main.async { + self.isCreatingFile = false + } return } let fileURL = documentsURL.appendingPathComponent(filename) @@ -268,9 +281,13 @@ struct ExportDataView: View { DispatchQueue.main.async { self.tempFileURL = fileURL + self.isCreatingFile = false } } catch { print("Failed to create export file: \(error)") + DispatchQueue.main.async { + self.isCreatingFile = false + } } } }