From 5d1748765f9d7d7e38203f68f2bd41cce6c1c320 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Sat, 27 Sep 2025 18:47:09 -0600 Subject: [PATCH] New builds for iOS 1.0.3 and Android 1.5.1 --- .../openclimb/data/database/dao/ProblemDao.kt | 72 +- .../atridad/openclimb/data/model/Problem.kt | 102 +- .../data/repository/ClimbRepository.kt | 6 +- .../openclimb/ui/screens/AddEditScreens.kt | 986 +++++++++--------- .../openclimb/ui/screens/DetailScreens.kt | 9 - .../openclimb/ui/screens/ProblemsScreen.kt | 322 +++--- .../openclimb/utils/SessionShareUtils.kt | 529 ++++++---- ios/OpenClimb.xcodeproj/project.pbxproj | 8 +- .../UserInterfaceState.xcuserstate | Bin 114205 -> 115786 bytes .../xcschemes/xcschememanagement.plist | 15 +- ios/OpenClimb/Models/DataModels.swift | 18 +- .../ViewModels/ClimbingDataManager.swift | 12 +- .../Views/AddEdit/AddAttemptView.swift | 6 - .../Views/AddEdit/AddEditProblemView.swift | 54 +- .../Views/Detail/GymDetailView.swift | 5 +- .../Views/Detail/ProblemDetailView.swift | 11 +- .../Views/Detail/SessionDetailView.swift | 1 - ios/OpenClimb/Views/GymsView.swift | 6 +- ios/OpenClimb/Views/ProblemsView.swift | 29 +- 19 files changed, 1157 insertions(+), 1034 deletions(-) diff --git a/android/app/src/main/java/com/atridad/openclimb/data/database/dao/ProblemDao.kt b/android/app/src/main/java/com/atridad/openclimb/data/database/dao/ProblemDao.kt index 19697e4..6546258 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/database/dao/ProblemDao.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/database/dao/ProblemDao.kt @@ -7,62 +7,58 @@ import kotlinx.coroutines.flow.Flow @Dao interface ProblemDao { - + @Query("SELECT * FROM problems ORDER BY updatedAt DESC") fun getAllProblems(): Flow> - - @Query("SELECT * FROM problems WHERE id = :id") - suspend fun getProblemById(id: String): Problem? - + + @Query("SELECT * FROM problems WHERE id = :id") suspend fun getProblemById(id: String): Problem? + @Query("SELECT * FROM problems WHERE gymId = :gymId ORDER BY updatedAt DESC") fun getProblemsByGym(gymId: String): Flow> - + @Query("SELECT * FROM problems WHERE climbType = :climbType ORDER BY updatedAt DESC") fun getProblemsByClimbType(climbType: ClimbType): Flow> - - @Query("SELECT * FROM problems WHERE gymId = :gymId AND climbType = :climbType ORDER BY updatedAt DESC") + + @Query( + "SELECT * FROM problems WHERE gymId = :gymId AND climbType = :climbType ORDER BY updatedAt DESC" + ) fun getProblemsByGymAndType(gymId: String, climbType: ClimbType): Flow> - + @Query("SELECT * FROM problems WHERE isActive = 1 ORDER BY updatedAt DESC") fun getActiveProblems(): Flow> - + @Query("SELECT * FROM problems WHERE gymId = :gymId AND isActive = 1 ORDER BY updatedAt DESC") fun getActiveProblemsByGym(gymId: String): Flow> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertProblem(problem: Problem) - + + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertProblem(problem: Problem) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertProblems(problems: List) - - @Update - suspend fun updateProblem(problem: Problem) - - @Delete - suspend fun deleteProblem(problem: Problem) - - @Query("DELETE FROM problems WHERE id = :id") - suspend fun deleteProblemById(id: String) - + + @Update suspend fun updateProblem(problem: Problem) + + @Delete suspend fun deleteProblem(problem: Problem) + + @Query("DELETE FROM problems WHERE id = :id") suspend fun deleteProblemById(id: String) + @Query("SELECT COUNT(*) FROM problems WHERE gymId = :gymId") suspend fun getProblemsCountByGym(gymId: String): Int - + @Query("SELECT COUNT(*) FROM problems WHERE isActive = 1") suspend fun getActiveProblemsCount(): Int - - @Query(""" - SELECT * FROM problems - WHERE (name LIKE '%' || :searchQuery || '%' + + @Query( + """ + SELECT * FROM problems + WHERE (name LIKE '%' || :searchQuery || '%' OR description LIKE '%' || :searchQuery || '%' - OR location LIKE '%' || :searchQuery || '%' - OR setter LIKE '%' || :searchQuery || '%') + OR location LIKE '%' || :searchQuery || '%') ORDER BY updatedAt DESC - """) + """ + ) fun searchProblems(searchQuery: String): Flow> - - @Query("SELECT COUNT(*) FROM problems") - suspend fun getProblemsCount(): Int - - @Query("DELETE FROM problems") - suspend fun deleteAllProblems() + + @Query("SELECT COUNT(*) FROM problems") suspend fun getProblemsCount(): Int + + @Query("DELETE FROM problems") suspend fun deleteAllProblems() } diff --git a/android/app/src/main/java/com/atridad/openclimb/data/model/Problem.kt b/android/app/src/main/java/com/atridad/openclimb/data/model/Problem.kt index 80463e9..1116f7f 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/model/Problem.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/model/Problem.kt @@ -4,71 +4,67 @@ import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey -import kotlinx.serialization.Serializable import java.time.LocalDateTime +import kotlinx.serialization.Serializable @Entity( - tableName = "problems", - foreignKeys = [ - ForeignKey( - entity = Gym::class, - parentColumns = ["id"], - childColumns = ["gymId"], - onDelete = ForeignKey.CASCADE - ) - ], - indices = [Index(value = ["gymId"])] + tableName = "problems", + foreignKeys = + [ + ForeignKey( + entity = Gym::class, + parentColumns = ["id"], + childColumns = ["gymId"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index(value = ["gymId"])] ) @Serializable data class Problem( - @PrimaryKey - val id: String, - val gymId: String, - val name: String? = null, - val description: String? = null, - val climbType: ClimbType, - val difficulty: DifficultyGrade, - val setter: String? = null, - val tags: List = emptyList(), - val location: String? = null, - val imagePaths: List = emptyList(), - val isActive: Boolean = true, - val dateSet: String? = null, - val notes: String? = null, - val createdAt: String, - val updatedAt: String + @PrimaryKey val id: String, + val gymId: String, + val name: String? = null, + val description: String? = null, + val climbType: ClimbType, + val difficulty: DifficultyGrade, + val tags: List = emptyList(), + val location: String? = null, + val imagePaths: List = emptyList(), + val isActive: Boolean = true, + val dateSet: String? = null, + val notes: String? = null, + val createdAt: String, + val updatedAt: String ) { companion object { fun create( - gymId: String, - name: String? = null, - description: String? = null, - climbType: ClimbType, - difficulty: DifficultyGrade, - setter: String? = null, - tags: List = emptyList(), - location: String? = null, - imagePaths: List = emptyList(), - dateSet: String? = null, - notes: String? = null + gymId: String, + name: String? = null, + description: String? = null, + climbType: ClimbType, + difficulty: DifficultyGrade, + tags: List = emptyList(), + location: String? = null, + imagePaths: List = emptyList(), + dateSet: String? = null, + notes: String? = null ): Problem { val now = LocalDateTime.now().toString() return Problem( - id = java.util.UUID.randomUUID().toString(), - gymId = gymId, - name = name, - description = description, - climbType = climbType, - difficulty = difficulty, - setter = setter, - tags = tags, - location = location, - imagePaths = imagePaths, - isActive = true, - dateSet = dateSet, - notes = notes, - createdAt = now, - updatedAt = now + id = java.util.UUID.randomUUID().toString(), + gymId = gymId, + name = name, + description = description, + climbType = climbType, + difficulty = difficulty, + tags = tags, + location = location, + imagePaths = imagePaths, + isActive = true, + dateSet = dateSet, + notes = notes, + createdAt = now, + updatedAt = now ) } } diff --git a/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt b/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt index 0670e30..e4d3e0d 100644 --- a/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt +++ b/android/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt @@ -82,7 +82,7 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context) val exportData = ClimbDataExport( exportedAt = LocalDateTime.now().toString(), - version = "1.0", + version = "2.0", gyms = allGyms, problems = allProblems, sessions = allSessions, @@ -141,7 +141,7 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context) val exportData = ClimbDataExport( exportedAt = LocalDateTime.now().toString(), - version = "1.0", + version = "2.0", gyms = allGyms, problems = allProblems, sessions = allSessions, @@ -343,7 +343,7 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context) @kotlinx.serialization.Serializable data class ClimbDataExport( val exportedAt: String, - val version: String = "1.0", + val version: String = "2.0", val gyms: List, val problems: List, val sessions: List, diff --git a/android/app/src/main/java/com/atridad/openclimb/ui/screens/AddEditScreens.kt b/android/app/src/main/java/com/atridad/openclimb/ui/screens/AddEditScreens.kt index abadcea..8fea3d0 100644 --- a/android/app/src/main/java/com/atridad/openclimb/ui/screens/AddEditScreens.kt +++ b/android/app/src/main/java/com/atridad/openclimb/ui/screens/AddEditScreens.kt @@ -12,46 +12,44 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.compose.ui.platform.LocalContext import com.atridad.openclimb.data.model.* import com.atridad.openclimb.ui.components.ImagePicker import com.atridad.openclimb.ui.viewmodel.ClimbViewModel -import kotlinx.coroutines.flow.first import java.time.LocalDateTime +import kotlinx.coroutines.flow.first @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AddEditGymScreen( - gymId: String?, - viewModel: ClimbViewModel, - onNavigateBack: () -> Unit -) { +fun AddEditGymScreen(gymId: String?, viewModel: ClimbViewModel, onNavigateBack: () -> Unit) { var name by remember { mutableStateOf("") } var location by remember { mutableStateOf("") } var notes by remember { mutableStateOf("") } var selectedClimbTypes by remember { mutableStateOf(setOf()) } var selectedDifficultySystems by remember { mutableStateOf(setOf()) } - + val isEditing = gymId != null - + // Calculate available difficulty systems based on selected climb types - val availableDifficultySystems = if (selectedClimbTypes.isEmpty()) { - emptyList() - } else { - selectedClimbTypes.flatMap { climbType -> - DifficultySystem.getSystemsForClimbType(climbType) - }.distinct() - } - + val availableDifficultySystems = + if (selectedClimbTypes.isEmpty()) { + emptyList() + } else { + selectedClimbTypes + .flatMap { climbType -> DifficultySystem.getSystemsForClimbType(climbType) } + .distinct() + } + // Reset selected difficulty systems when available systems change LaunchedEffect(availableDifficultySystems) { - selectedDifficultySystems = selectedDifficultySystems.filter { it in availableDifficultySystems }.toSet() + selectedDifficultySystems = + selectedDifficultySystems.filter { it in availableDifficultySystems }.toSet() } - + // Load existing gym data for editing LaunchedEffect(gymId) { if (gymId != null) { @@ -65,96 +63,105 @@ fun AddEditGymScreen( } } } - + Scaffold( - topBar = { - TopAppBar( - title = { Text(if (isEditing) "Edit Gym" else "Add Gym") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - actions = { - TextButton( - onClick = { - val gym = Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes) - - if (isEditing) { - viewModel.updateGym(gym.copy(id = gymId!!)) - } else { - viewModel.addGym(gym) + topBar = { + TopAppBar( + title = { Text(if (isEditing) "Edit Gym" else "Add Gym") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) } - onNavigateBack() }, - enabled = name.isNotBlank() && selectedClimbTypes.isNotEmpty() && selectedDifficultySystems.isNotEmpty() - ) { - Text("Save") - } - } - ) - } + actions = { + TextButton( + onClick = { + val gym = + Gym.create( + name, + location, + selectedClimbTypes.toList(), + selectedDifficultySystems.toList(), + notes = notes + ) + + if (isEditing) { + viewModel.updateGym(gym.copy(id = gymId!!)) + } else { + viewModel.addGym(gym) + } + onNavigateBack() + }, + enabled = + name.isNotBlank() && + selectedClimbTypes.isNotEmpty() && + selectedDifficultySystems.isNotEmpty() + ) { Text("Save") } + } + ) + } ) { paddingValues -> Column( - 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) ) { // Name field OutlinedTextField( - value = name, - onValueChange = { name = it }, - label = { Text("Gym Name") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true + value = name, + onValueChange = { name = it }, + label = { Text("Gym Name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true ) - + // Location field OutlinedTextField( - value = location, - onValueChange = { location = it }, - label = { Text("Location (Optional)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true + value = location, + onValueChange = { location = it }, + label = { Text("Location (Optional)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true ) - + // Climb Types - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Supported Climb Types", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Supported Climb Types", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(8.dp)) - + ClimbType.entries.forEach { climbType -> Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = climbType in selectedClimbTypes, - onClick = { - selectedClimbTypes = if (climbType in selectedClimbTypes) { - selectedClimbTypes - climbType - } else { - selectedClimbTypes + climbType - } - }, - role = Role.Checkbox - ) + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth() + .selectable( + selected = climbType in selectedClimbTypes, + onClick = { + selectedClimbTypes = + if (climbType in + selectedClimbTypes + ) { + selectedClimbTypes - + climbType + } else { + selectedClimbTypes + + climbType + } + }, + role = Role.Checkbox + ) ) { Checkbox( - checked = climbType in selectedClimbTypes, - onCheckedChange = null + checked = climbType in selectedClimbTypes, + onCheckedChange = null ) Spacer(modifier = Modifier.width(8.dp)) Text(climbType.getDisplayName()) @@ -162,50 +169,54 @@ fun AddEditGymScreen( } } } - + // Difficulty Systems - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Difficulty Systems", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Difficulty Systems", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(8.dp)) - + if (selectedClimbTypes.isEmpty()) { Text( - text = "Select climb types first to see available difficulty systems", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp) + text = + "Select climb types first to see available difficulty systems", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 8.dp) ) } else { availableDifficultySystems.forEach { system -> Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = system in selectedDifficultySystems, - onClick = { - selectedDifficultySystems = if (system in selectedDifficultySystems) { - selectedDifficultySystems - system - } else { - selectedDifficultySystems + system - } - }, - role = Role.Checkbox - ) + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth() + .selectable( + selected = + system in + selectedDifficultySystems, + onClick = { + selectedDifficultySystems = + if (system in + selectedDifficultySystems + ) { + selectedDifficultySystems - + system + } else { + selectedDifficultySystems + + system + } + }, + role = Role.Checkbox + ) ) { Checkbox( - checked = system in selectedDifficultySystems, - onCheckedChange = null + checked = system in selectedDifficultySystems, + onCheckedChange = null ) Spacer(modifier = Modifier.width(8.dp)) Text(system.getDisplayName()) @@ -214,14 +225,14 @@ fun AddEditGymScreen( } } } - + // Notes field OutlinedTextField( - value = notes, - onValueChange = { notes = it }, - label = { Text("Notes (Optional)") }, - modifier = Modifier.fillMaxWidth(), - minLines = 3 + value = notes, + onValueChange = { notes = it }, + label = { Text("Notes (Optional)") }, + modifier = Modifier.fillMaxWidth(), + minLines = 3 ) } } @@ -230,28 +241,30 @@ fun AddEditGymScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddEditProblemScreen( - problemId: String?, - gymId: String?, - viewModel: ClimbViewModel, - onNavigateBack: () -> Unit + problemId: String?, + gymId: String?, + viewModel: ClimbViewModel, + onNavigateBack: () -> Unit ) { val isEditing = problemId != null val gyms by viewModel.gyms.collectAsState() - + // Problem form state - var selectedGym by remember { mutableStateOf(gymId?.let { id -> gyms.find { it.id == id } }) } + var selectedGym by remember { + mutableStateOf(gymId?.let { id -> gyms.find { it.id == id } }) + } var problemName by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } var selectedClimbType by remember { mutableStateOf(ClimbType.BOULDER) } var selectedDifficultySystem by remember { mutableStateOf(DifficultySystem.V_SCALE) } var difficultyGrade by remember { mutableStateOf("") } - var setter by remember { mutableStateOf("") } + var location by remember { mutableStateOf("") } var tags by remember { mutableStateOf("") } var notes by remember { mutableStateOf("") } var isActive by remember { mutableStateOf(true) } var imagePaths by remember { mutableStateOf>(emptyList()) } - + // Load existing problem data for editing LaunchedEffect(problemId) { if (problemId != null) { @@ -262,7 +275,7 @@ fun AddEditProblemScreen( selectedClimbType = p.climbType selectedDifficultySystem = p.difficulty.system difficultyGrade = p.difficulty.grade - setter = p.setter ?: "" + location = p.location ?: "" tags = p.tags.joinToString(", ") notes = p.notes ?: "" @@ -272,39 +285,42 @@ fun AddEditProblemScreen( } } } - + LaunchedEffect(gymId, gyms) { if (gymId != null && selectedGym == null) { selectedGym = gyms.find { it.id == gymId } } } - + val availableClimbTypes = selectedGym?.supportedClimbTypes ?: ClimbType.entries.toList() - val availableDifficultySystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system -> - selectedGym?.difficultySystems?.contains(system) != false - } - + val availableDifficultySystems = + DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system -> + selectedGym?.difficultySystems?.contains(system) != false + } + // Auto-select climb type if there's only one available LaunchedEffect(availableClimbTypes) { if (availableClimbTypes.size == 1 && selectedClimbType != availableClimbTypes.first()) { selectedClimbType = availableClimbTypes.first() } } - + // Auto-select or reset difficulty system based on climb type LaunchedEffect(selectedClimbType, availableDifficultySystems) { when { // If current system is not compatible, select the first available one selectedDifficultySystem !in availableDifficultySystems -> { - selectedDifficultySystem = availableDifficultySystems.firstOrNull() ?: DifficultySystem.CUSTOM + selectedDifficultySystem = + availableDifficultySystems.firstOrNull() ?: DifficultySystem.CUSTOM } // If there's only one available system and nothing is selected, auto-select it - availableDifficultySystems.size == 1 && selectedDifficultySystem != availableDifficultySystems.first() -> { + availableDifficultySystems.size == 1 && + selectedDifficultySystem != availableDifficultySystems.first() -> { selectedDifficultySystem = availableDifficultySystems.first() } } } - + // Reset grade when difficulty system changes (unless it's a valid grade for the new system) LaunchedEffect(selectedDifficultySystem) { val availableGrades = selectedDifficultySystem.getAvailableGrades() @@ -312,96 +328,108 @@ fun AddEditProblemScreen( difficultyGrade = "" } } - + Scaffold( - topBar = { - TopAppBar( - title = { Text(if (isEditing) "Edit Problem" else "Add Problem") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - actions = { - TextButton( - onClick = { - selectedGym?.let { gym -> - val difficulty = DifficultyGrade( - system = selectedDifficultySystem, - grade = difficultyGrade, - numericValue = when (selectedDifficultySystem) { - DifficultySystem.V_SCALE -> difficultyGrade.removePrefix("V").toIntOrNull() ?: 0 - else -> difficultyGrade.hashCode() % 100 // Simple mapping for other systems - } + topBar = { + TopAppBar( + title = { Text(if (isEditing) "Edit Problem" else "Add Problem") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" ) - - val problem = Problem.create( - gymId = gym.id, - name = problemName.ifBlank { null }, - description = description.ifBlank { null }, - climbType = selectedClimbType, - difficulty = difficulty, - setter = setter.ifBlank { null }, - tags = tags.split(",").map { it.trim() }.filter { it.isNotBlank() }, - location = location.ifBlank { null }, - imagePaths = imagePaths, - notes = notes.ifBlank { null } - ) - - if (isEditing) { - viewModel.updateProblem(problem.copy(id = problemId!!)) - } else { - viewModel.addProblem(problem) - } - onNavigateBack() } }, - enabled = selectedGym != null && difficultyGrade.isNotBlank() - ) { - Text("Save") - } - } - ) - } + actions = { + TextButton( + onClick = { + selectedGym?.let { gym -> + val difficulty = + DifficultyGrade( + system = selectedDifficultySystem, + grade = difficultyGrade, + numericValue = + when (selectedDifficultySystem + ) { + DifficultySystem.V_SCALE -> + difficultyGrade + .removePrefix( + "V" + ) + .toIntOrNull() + ?: 0 + else -> + difficultyGrade + .hashCode() % + 100 // Simple mapping for other systems + } + ) + + val problem = + Problem.create( + gymId = gym.id, + name = problemName.ifBlank { null }, + description = + description.ifBlank { null }, + climbType = selectedClimbType, + difficulty = difficulty, + tags = + tags.split(",") + .map { it.trim() } + .filter { + it.isNotBlank() + }, + location = location.ifBlank { null }, + imagePaths = imagePaths, + notes = notes.ifBlank { null } + ) + + if (isEditing) { + viewModel.updateProblem( + problem.copy(id = problemId!!) + ) + } else { + viewModel.addProblem(problem) + } + onNavigateBack() + } + }, + enabled = selectedGym != null && difficultyGrade.isNotBlank() + ) { Text("Save") } + } + ) + } ) { 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) ) { // Gym Selection item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Select Gym", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Select Gym", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(8.dp)) - + if (gyms.isEmpty()) { Text( - text = "No gyms available. Add a gym first.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error + text = "No gyms available. Add a gym first.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error ) } else { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { items(gyms) { gym -> FilterChip( - onClick = { selectedGym = gym }, - label = { Text(gym.name) }, - selected = selectedGym?.id == gym.id + onClick = { selectedGym = gym }, + label = { Text(gym.name) }, + selected = selectedGym?.id == gym.id ) } } @@ -409,92 +437,74 @@ fun AddEditProblemScreen( } } } - + // Basic Problem Info item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Problem Details", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Problem Details", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(16.dp)) - + OutlinedTextField( - value = problemName, - onValueChange = { problemName = it }, - label = { Text("Problem Name (Optional)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - placeholder = { Text("e.g., 'The Overhang Monster', 'Yellow V4'") } + value = problemName, + onValueChange = { problemName = it }, + label = { Text("Problem Name (Optional)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("e.g., 'The Overhang Monster', 'Yellow V4'") } ) - + Spacer(modifier = Modifier.height(8.dp)) - + OutlinedTextField( - value = description, - onValueChange = { description = it }, - label = { Text("Description (Optional)") }, - modifier = Modifier.fillMaxWidth(), - minLines = 2, - placeholder = { Text("Describe the problem, holds, style, etc.") } + value = description, + onValueChange = { description = it }, + label = { Text("Description (Optional)") }, + modifier = Modifier.fillMaxWidth(), + minLines = 2, + placeholder = { Text("Describe the problem, holds, style, etc.") } ) - + Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = setter, - onValueChange = { setter = it }, - label = { Text("Route Setter (Optional)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - + Spacer(modifier = Modifier.height(8.dp)) - + OutlinedTextField( - value = location, - onValueChange = { location = it }, - label = { Text("Location (Optional)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - placeholder = { Text("e.g., 'Cave area', 'Wall 3', 'Right side'") } + value = location, + onValueChange = { location = it }, + label = { Text("Location (Optional)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("e.g., 'Cave area', 'Wall 3', 'Right side'") } ) } } } - + // Climb Type if (selectedGym != null) { item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Climb Type", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Climb Type", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(8.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { availableClimbTypes.forEach { climbType -> FilterChip( - onClick = { selectedClimbType = climbType }, - label = { Text(climbType.getDisplayName()) }, - selected = selectedClimbType == climbType + onClick = { selectedClimbType = climbType }, + label = { Text(climbType.getDisplayName()) }, + selected = selectedClimbType == climbType ) } } @@ -502,91 +512,102 @@ fun AddEditProblemScreen( } } } - + // Difficulty if (selectedGym != null) { item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Difficulty", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Difficulty", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(8.dp)) - + Text( - text = "Difficulty System", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium + text = "Difficulty System", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium ) - - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { items(availableDifficultySystems) { system -> FilterChip( - onClick = { selectedDifficultySystem = system }, - label = { Text(system.getDisplayName()) }, - selected = selectedDifficultySystem == system + onClick = { selectedDifficultySystem = system }, + label = { Text(system.getDisplayName()) }, + selected = selectedDifficultySystem == system ) } } - + Spacer(modifier = Modifier.height(8.dp)) - + if (selectedDifficultySystem == DifficultySystem.CUSTOM) { OutlinedTextField( - value = difficultyGrade, - onValueChange = { newValue -> - // Only allow integers for custom scales - if (newValue.isEmpty() || newValue.all { it.isDigit() }) { - difficultyGrade = newValue - } - }, - label = { Text("Grade *") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - placeholder = { Text("Enter numeric grade (e.g. 5, 10, 15)") }, - supportingText = { Text("Custom grades must be whole numbers") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + value = difficultyGrade, + onValueChange = { newValue -> + // Only allow integers for custom scales + if (newValue.isEmpty() || newValue.all { it.isDigit() } + ) { + difficultyGrade = newValue + } + }, + label = { Text("Grade *") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { + Text("Enter numeric grade (e.g. 5, 10, 15)") + }, + supportingText = { + Text("Custom grades must be whole numbers") + }, + keyboardOptions = + KeyboardOptions(keyboardType = KeyboardType.Number) ) } else { var expanded by remember { mutableStateOf(false) } val availableGrades = selectedDifficultySystem.getAvailableGrades() - + ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - modifier = Modifier.fillMaxWidth() + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth() ) { OutlinedTextField( - value = difficultyGrade, - 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() + value = difficultyGrade, + 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() ) ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } + expanded = expanded, + onDismissRequest = { expanded = false } ) { availableGrades.forEach { grade -> DropdownMenuItem( - text = { Text(grade) }, - onClick = { - difficultyGrade = grade - expanded = false - } + text = { Text(grade) }, + onClick = { + difficultyGrade = grade + expanded = false + } ) } } @@ -596,87 +617,76 @@ fun AddEditProblemScreen( } } } - + // Images Section item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Photos", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Photos", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(16.dp)) - + ImagePicker( - imageUris = imagePaths, - onImagesChanged = { imagePaths = it }, - maxImages = 5 + imageUris = imagePaths, + onImagesChanged = { imagePaths = it }, + maxImages = 5 ) } } } item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Additional Info", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Additional Info", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(16.dp)) - + OutlinedTextField( - value = tags, - onValueChange = { tags = it }, - label = { Text("Tags (Optional)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - placeholder = { Text("e.g., crimpy, dynamic (comma-separated)") } + value = tags, + onValueChange = { tags = it }, + label = { Text("Tags (Optional)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("e.g., crimpy, dynamic (comma-separated)") } ) - + Spacer(modifier = Modifier.height(8.dp)) - + OutlinedTextField( - value = notes, - onValueChange = { notes = it }, - label = { Text("Notes (Optional)") }, - modifier = Modifier.fillMaxWidth(), - minLines = 3, - placeholder = { Text("Any additional notes about this problem") } + value = notes, + onValueChange = { notes = it }, + label = { Text("Notes (Optional)") }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + placeholder = { Text("Any additional notes about this problem") } ) - + Spacer(modifier = Modifier.height(16.dp)) - + Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = isActive, - onClick = { isActive = !isActive }, - role = Role.Checkbox - ) + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth() + .selectable( + selected = isActive, + onClick = { isActive = !isActive }, + role = Role.Checkbox + ) ) { - Checkbox( - checked = isActive, - onCheckedChange = null - ) + Checkbox(checked = isActive, onCheckedChange = null) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Problem is currently active", - style = MaterialTheme.typography.bodyMedium + text = "Problem is currently active", + style = MaterialTheme.typography.bodyMedium ) } } @@ -689,21 +699,23 @@ fun AddEditProblemScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddEditSessionScreen( - sessionId: String?, - gymId: String?, - viewModel: ClimbViewModel, - onNavigateBack: () -> Unit + sessionId: String?, + gymId: String?, + viewModel: ClimbViewModel, + onNavigateBack: () -> Unit ) { val isEditing = sessionId != null val gyms by viewModel.gyms.collectAsState() val context = LocalContext.current // Session form state - var selectedGym by remember { mutableStateOf(gymId?.let { id -> gyms.find { it.id == id } }) } + var selectedGym by remember { + mutableStateOf(gymId?.let { id -> gyms.find { it.id == id } }) + } var sessionDate by remember { mutableStateOf(LocalDateTime.now().toLocalDate().toString()) } var duration by remember { mutableStateOf("") } var sessionNotes by remember { mutableStateOf("") } - + // Load existing session data for editing LaunchedEffect(sessionId) { if (sessionId != null) { @@ -716,84 +728,86 @@ fun AddEditSessionScreen( } } } - + LaunchedEffect(gymId, gyms) { if (gymId != null && selectedGym == null) { selectedGym = gyms.find { it.id == gymId } } } - + Scaffold( - topBar = { - TopAppBar( - title = { Text(if (isEditing) "Edit Session" else "Add Session") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - actions = { - TextButton( - onClick = { - selectedGym?.let { gym -> - if (isEditing) { - val session = ClimbSession.create( - gymId = gym.id, - notes = sessionNotes.ifBlank { null } - ) - viewModel.updateSession(session.copy(id = sessionId!!)) - } else { - viewModel.startSession(context, gym.id, sessionNotes.ifBlank { null }) - } - onNavigateBack() + topBar = { + TopAppBar( + title = { Text(if (isEditing) "Edit Session" else "Add Session") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) } }, - enabled = selectedGym != null - ) { - Text("Save") - } - } - ) - } + actions = { + TextButton( + onClick = { + selectedGym?.let { gym -> + if (isEditing) { + val session = + ClimbSession.create( + gymId = gym.id, + notes = + sessionNotes.ifBlank { + null + } + ) + viewModel.updateSession( + session.copy(id = sessionId!!) + ) + } else { + viewModel.startSession( + context, + gym.id, + sessionNotes.ifBlank { null } + ) + } + onNavigateBack() + } + }, + enabled = selectedGym != null + ) { Text("Save") } + } + ) + } ) { 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) ) { // Gym Selection item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Select Gym", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Select Gym", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(8.dp)) - + if (gyms.isEmpty()) { Text( - text = "No gyms available. Add a gym first.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error + text = "No gyms available. Add a gym first.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error ) } else { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { items(gyms) { gym -> FilterChip( - onClick = { selectedGym = gym }, - label = { Text(gym.name) }, - selected = selectedGym?.id == gym.id + onClick = { selectedGym = gym }, + label = { Text(gym.name) }, + selected = selectedGym?.id == gym.id ) } } @@ -801,50 +815,47 @@ fun AddEditSessionScreen( } } } - + // Session Details item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Session Details", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Session Details", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(16.dp)) - + OutlinedTextField( - value = sessionDate, - onValueChange = { sessionDate = it }, - label = { Text("Date (YYYY-MM-DD)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true + value = sessionDate, + onValueChange = { sessionDate = it }, + label = { Text("Date (YYYY-MM-DD)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true ) - + Spacer(modifier = Modifier.height(8.dp)) - + OutlinedTextField( - value = duration, - onValueChange = { duration = it }, - label = { Text("Duration (minutes)") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + value = duration, + onValueChange = { duration = it }, + label = { Text("Duration (minutes)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = + KeyboardOptions(keyboardType = KeyboardType.Number) ) - + Spacer(modifier = Modifier.height(8.dp)) - + OutlinedTextField( - value = sessionNotes, - onValueChange = { sessionNotes = it }, - label = { Text("Session Notes (Optional)") }, - modifier = Modifier.fillMaxWidth(), - minLines = 3 + value = sessionNotes, + onValueChange = { sessionNotes = it }, + label = { Text("Session Notes (Optional)") }, + modifier = Modifier.fillMaxWidth(), + minLines = 3 ) } } @@ -852,6 +863,3 @@ fun AddEditSessionScreen( } } } - - - 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 2e40d07..2a5ac9b 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 @@ -693,15 +693,6 @@ 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 - ) - } - if (problem?.tags?.isNotEmpty() == true) { Spacer(modifier = Modifier.height(12.dp)) LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { diff --git a/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt b/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt index 80084e1..d7de626 100644 --- a/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt +++ b/android/app/src/main/java/com/atridad/openclimb/ui/screens/ProblemsScreen.kt @@ -21,181 +21,181 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ProblemsScreen( - viewModel: ClimbViewModel, - onNavigateToProblemDetail: (String) -> Unit -) { +fun ProblemsScreen(viewModel: ClimbViewModel, onNavigateToProblemDetail: (String) -> Unit) { val problems by viewModel.problems.collectAsState() val gyms by viewModel.gyms.collectAsState() var showImageViewer by remember { mutableStateOf(false) } var selectedImagePaths by remember { mutableStateOf>(emptyList()) } var selectedImageIndex by remember { mutableIntStateOf(0) } - + // Filter state var selectedClimbType by remember { mutableStateOf(null) } var selectedGym by remember { mutableStateOf(null) } - + // Apply filters - val filteredProblems = problems.filter { problem -> - val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } != false - val gymMatch = selectedGym?.let { it.id == problem.gymId } != false - climbTypeMatch && gymMatch - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { + val filteredProblems = + problems.filter { problem -> + val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } != false + val gymMatch = selectedGym?.let { it.id == problem.gymId } != false + climbTypeMatch && gymMatch + } + + // Separate active and inactive problems + val activeProblems = filteredProblems.filter { it.isActive } + val inactiveProblems = filteredProblems.filter { !it.isActive } + val sortedProblems = activeProblems + inactiveProblems + + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Icon( - painter = painterResource(id = R.drawable.ic_mountains), - contentDescription = "OpenClimb Logo", - modifier = Modifier.size(32.dp), - tint = MaterialTheme.colorScheme.primary + painter = painterResource(id = R.drawable.ic_mountains), + contentDescription = "OpenClimb Logo", + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colorScheme.primary ) Text( - text = "Problems & Routes", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold + text = "Problems & Routes", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold ) } - + Spacer(modifier = Modifier.height(16.dp)) - + // Filters Section if (problems.isNotEmpty()) { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Filters", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = "Filters", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(12.dp)) - + // Climb Type Filter Text( - text = "Climb Type", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium + text = "Climb Type", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium ) - + Spacer(modifier = Modifier.height(8.dp)) - - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { item { FilterChip( - onClick = { selectedClimbType = null }, - label = { Text("All Types") }, - selected = selectedClimbType == null + onClick = { selectedClimbType = null }, + label = { Text("All Types") }, + selected = selectedClimbType == null ) } items(ClimbType.entries) { climbType -> FilterChip( - onClick = { selectedClimbType = climbType }, - label = { Text(climbType.getDisplayName()) }, - selected = selectedClimbType == climbType + onClick = { selectedClimbType = climbType }, + label = { Text(climbType.getDisplayName()) }, + selected = selectedClimbType == climbType ) } } - + Spacer(modifier = Modifier.height(12.dp)) - + // Gym Filter Text( - text = "Gym", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium + text = "Gym", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium ) - + Spacer(modifier = Modifier.height(8.dp)) - - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { item { FilterChip( - onClick = { selectedGym = null }, - label = { Text("All Gyms") }, - selected = selectedGym == null + onClick = { selectedGym = null }, + label = { Text("All Gyms") }, + selected = selectedGym == null ) } items(gyms) { gym -> FilterChip( - onClick = { selectedGym = gym }, - label = { Text(gym.name) }, - selected = selectedGym?.id == gym.id + onClick = { selectedGym = gym }, + label = { Text(gym.name) }, + selected = selectedGym?.id == gym.id ) } } - + // Filter result count if (selectedClimbType != null || selectedGym != null) { Spacer(modifier = Modifier.height(12.dp)) Text( - text = "Showing ${filteredProblems.size} of ${problems.size} problems", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = + "Showing ${filteredProblems.size} of ${problems.size} problems (${activeProblems.size} active, ${inactiveProblems.size} reset)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } - + Spacer(modifier = Modifier.height(16.dp)) } - + if (filteredProblems.isEmpty()) { EmptyStateMessage( - title = if (problems.isEmpty()) { - if (gyms.isEmpty()) "No Gyms Available" else "No Problems Yet" - } else { - "No Problems Match Filters" - }, - message = if (problems.isEmpty()) { - if (gyms.isEmpty()) "Add a gym first to start tracking problems and routes!" else "Start tracking your favorite problems and routes!" - } else { - "Try adjusting your filters to see more problems." - }, - onActionClick = { }, - actionText = "" + title = + if (problems.isEmpty()) { + if (gyms.isEmpty()) "No Gyms Available" else "No Problems Yet" + } else { + "No Problems Match Filters" + }, + message = + if (problems.isEmpty()) { + if (gyms.isEmpty()) + "Add a gym first to start tracking problems and routes!" + else "Start tracking your favorite problems and routes!" + } else { + "Try adjusting your filters to see more problems." + }, + onActionClick = {}, + actionText = "" ) } else { LazyColumn { - items(filteredProblems) { problem -> + items(sortedProblems) { problem -> ProblemCard( - problem = problem, - gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym", - onClick = { onNavigateToProblemDetail(problem.id) }, - onImageClick = { imagePaths, index -> - selectedImagePaths = imagePaths - selectedImageIndex = index - showImageViewer = true - } + problem = problem, + gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym", + onClick = { onNavigateToProblemDetail(problem.id) }, + onImageClick = { imagePaths, index -> + selectedImagePaths = imagePaths + selectedImageIndex = index + showImageViewer = true + }, + onToggleActive = { + val updatedProblem = problem.copy(isActive = !problem.isActive) + viewModel.updateProblem(updatedProblem) + } ) Spacer(modifier = Modifier.height(8.dp)) } } } } - + // Fullscreen Image Viewer if (showImageViewer && selectedImagePaths.isNotEmpty()) { FullscreenImageViewer( - imagePaths = selectedImagePaths, - initialIndex = selectedImageIndex, - onDismiss = { showImageViewer = false } + imagePaths = selectedImagePaths, + initialIndex = selectedImageIndex, + onDismiss = { showImageViewer = false } ) } } @@ -203,97 +203,117 @@ fun ProblemsScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ProblemCard( - problem: Problem, - gymName: String, - onClick: () -> Unit, - onImageClick: ((List, Int) -> Unit)? = null + problem: Problem, + gymName: String, + onClick: () -> Unit, + onImageClick: ((List, Int) -> Unit)? = null, + onToggleActive: (() -> Unit)? = null ) { - Card( - onClick = onClick, - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { + Card(onClick = onClick, modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top ) { Column(modifier = Modifier.weight(1f)) { Text( - text = problem.name ?: "Unnamed Problem", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + text = problem.name ?: "Unnamed Problem", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = + if (problem.isActive) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) ) - + Text( - text = gymName, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = gymName, + style = MaterialTheme.typography.bodyMedium, + color = + MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = if (problem.isActive) 1f else 0.6f + ) ) } - + Column(horizontalAlignment = Alignment.End) { Text( - text = problem.difficulty.grade, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = problem.difficulty.grade, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary ) - + Text( - text = problem.climbType.getDisplayName(), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = problem.climbType.getDisplayName(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } - + problem.location?.let { location -> Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Location: $location", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Location: $location", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - + if (problem.tags.isNotEmpty()) { Spacer(modifier = Modifier.height(8.dp)) Row { problem.tags.take(3).forEach { tag -> AssistChip( - onClick = { }, - label = { Text(tag) }, - modifier = Modifier.padding(end = 4.dp) + onClick = {}, + label = { Text(tag) }, + modifier = Modifier.padding(end = 4.dp) ) } } } - + // Display images if any if (problem.imagePaths.isNotEmpty()) { Spacer(modifier = Modifier.height(8.dp)) ImageDisplay( - imagePaths = problem.imagePaths.take(3), // Show max 3 images in list - imageSize = 60, - onImageClick = { index -> - onImageClick?.invoke(problem.imagePaths, index) - } + imagePaths = problem.imagePaths.take(3), // Show max 3 images in list + imageSize = 60, + onImageClick = { index -> onImageClick?.invoke(problem.imagePaths, index) } ) } - + if (!problem.isActive) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Inactive", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error + text = "Reset / No Longer Set", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.tertiary, + fontWeight = FontWeight.Medium ) } + + // Toggle active button + if (onToggleActive != null) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton( + onClick = onToggleActive, + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = + if (problem.isActive) + MaterialTheme.colorScheme.tertiary + else MaterialTheme.colorScheme.primary + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = if (problem.isActive) "Mark as Reset" else "Mark as Active", + style = MaterialTheme.typography.bodySmall + ) + } + } } } } diff --git a/android/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt b/android/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt index ad32149..fb6748e 100644 --- a/android/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt +++ b/android/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt @@ -5,139 +5,146 @@ import android.content.Intent import android.graphics.* import android.graphics.drawable.GradientDrawable import androidx.core.content.FileProvider +import androidx.core.graphics.createBitmap +import androidx.core.graphics.toColorInt import com.atridad.openclimb.data.model.* import java.io.File import java.io.FileOutputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.math.roundToInt -import androidx.core.graphics.createBitmap -import androidx.core.graphics.toColorInt object SessionShareUtils { - + data class SessionStats( - val totalAttempts: Int, - val successfulAttempts: Int, - val problems: List, - val uniqueProblemsAttempted: Int, - val uniqueProblemsCompleted: Int, - val averageGrade: String?, - val sessionDuration: String, - val topResult: AttemptResult?, - val topGrade: String? + val totalAttempts: Int, + val successfulAttempts: Int, + val problems: List, + val uniqueProblemsAttempted: Int, + val uniqueProblemsCompleted: Int, + val averageGrade: String?, + val sessionDuration: String, + val topResult: AttemptResult?, + val topGrade: String? ) - + fun calculateSessionStats( - session: ClimbSession, - attempts: List, - problems: List + session: ClimbSession, + attempts: List, + problems: List ): SessionStats { - val successfulResults = listOf( - AttemptResult.SUCCESS, - AttemptResult.FLASH - ) - + val successfulResults = listOf(AttemptResult.SUCCESS, AttemptResult.FLASH) + val successfulAttempts = attempts.filter { it.result in successfulResults } val uniqueProblems = attempts.map { it.problemId }.distinct() val uniqueCompletedProblems = successfulAttempts.map { it.problemId }.distinct() - + val attemptedProblems = problems.filter { it.id in uniqueProblems } - + // Calculate separate averages for different climbing types and difficulty systems val boulderProblems = attemptedProblems.filter { it.climbType == ClimbType.BOULDER } val ropeProblems = attemptedProblems.filter { it.climbType == ClimbType.ROPE } - + val boulderAverage = calculateAverageGrade(boulderProblems, "Boulder") val ropeAverage = calculateAverageGrade(ropeProblems, "Rope") - + // Combine averages for display - val averageGrade = when { - boulderAverage != null && ropeAverage != null -> "$boulderAverage / $ropeAverage" - boulderAverage != null -> boulderAverage - ropeAverage != null -> ropeAverage - else -> null - } - + val averageGrade = + when { + boulderAverage != null && ropeAverage != null -> + "$boulderAverage / $ropeAverage" + boulderAverage != null -> boulderAverage + ropeAverage != null -> ropeAverage + else -> null + } + // Determine highest achieved grade (only from completed problems: SUCCESS or FLASH) val completedProblems = problems.filter { it.id in uniqueCompletedProblems } val completedBoulder = completedProblems.filter { it.climbType == ClimbType.BOULDER } val completedRope = completedProblems.filter { it.climbType == ClimbType.ROPE } val topBoulder = highestGradeForProblems(completedBoulder) val topRope = highestGradeForProblems(completedRope) - val topGrade = when { - topBoulder != null && topRope != null -> "$topBoulder / $topRope" - topBoulder != null -> topBoulder - topRope != null -> topRope - else -> null - } - + val topGrade = + when { + topBoulder != null && topRope != null -> "$topBoulder / $topRope" + topBoulder != null -> topBoulder + topRope != null -> topRope + else -> null + } + val duration = if (session.duration != null) "${session.duration}m" else "Unknown" - val topResult = attempts.maxByOrNull { - when (it.result) { - AttemptResult.FLASH -> 3 - AttemptResult.SUCCESS -> 2 - AttemptResult.FALL -> 1 - else -> 0 - } - }?.result - + val topResult = + attempts + .maxByOrNull { + when (it.result) { + AttemptResult.FLASH -> 3 + AttemptResult.SUCCESS -> 2 + AttemptResult.FALL -> 1 + else -> 0 + } + } + ?.result + return SessionStats( - totalAttempts = attempts.size, - successfulAttempts = successfulAttempts.size, - problems = attemptedProblems, - uniqueProblemsAttempted = uniqueProblems.size, - uniqueProblemsCompleted = uniqueCompletedProblems.size, - averageGrade = averageGrade, - sessionDuration = duration, - topResult = topResult, - topGrade = topGrade + totalAttempts = attempts.size, + successfulAttempts = successfulAttempts.size, + problems = attemptedProblems, + uniqueProblemsAttempted = uniqueProblems.size, + uniqueProblemsCompleted = uniqueCompletedProblems.size, + averageGrade = averageGrade, + sessionDuration = duration, + topResult = topResult, + topGrade = topGrade ) } - + /** * Calculate average grade for a specific set of problems, respecting their difficulty systems */ private fun calculateAverageGrade(problems: List, climbingType: String): String? { if (problems.isEmpty()) return null - + // Group problems by difficulty system val problemsBySystem = problems.groupBy { it.difficulty.system } - + val averages = mutableListOf() - + problemsBySystem.forEach { (system, systemProblems) -> when (system) { DifficultySystem.V_SCALE -> { - val gradeValues = systemProblems.mapNotNull { problem -> - when { - problem.difficulty.grade == "VB" -> 0 - else -> problem.difficulty.grade.removePrefix("V").toIntOrNull() - } - } + val gradeValues = + systemProblems.mapNotNull { problem -> + when { + problem.difficulty.grade == "VB" -> 0 + else -> problem.difficulty.grade.removePrefix("V").toIntOrNull() + } + } if (gradeValues.isNotEmpty()) { val avg = gradeValues.average().roundToInt() averages.add(if (avg == 0) "VB" else "V$avg") } } DifficultySystem.FONT -> { - val gradeValues = systemProblems.mapNotNull { problem -> - // Extract numeric part from Font grades (e.g., "6A" -> 6, "7C+" -> 7) - problem.difficulty.grade.filter { it.isDigit() }.toIntOrNull() - } + val gradeValues = + systemProblems.mapNotNull { problem -> + // Extract numeric part from Font grades (e.g., "6A" -> 6, "7C+" -> + // 7) + problem.difficulty.grade.filter { it.isDigit() }.toIntOrNull() + } if (gradeValues.isNotEmpty()) { val avg = gradeValues.average().roundToInt() averages.add("$avg") } } DifficultySystem.YDS -> { - val gradeValues = systemProblems.mapNotNull { problem -> - // Extract numeric part from YDS grades (e.g., "5.10a" -> 5.10) - val grade = problem.difficulty.grade - if (grade.startsWith("5.")) { - grade.substring(2).toDoubleOrNull() - } else null - } + val gradeValues = + systemProblems.mapNotNull { problem -> + // Extract numeric part from YDS grades (e.g., "5.10a" -> 5.10) + val grade = problem.difficulty.grade + if (grade.startsWith("5.")) { + grade.substring(2).toDoubleOrNull() + } else null + } if (gradeValues.isNotEmpty()) { val avg = gradeValues.average() averages.add("5.${String.format("%.1f", avg)}") @@ -145,9 +152,13 @@ object SessionShareUtils { } DifficultySystem.CUSTOM -> { // For custom systems, try to extract numeric values - val gradeValues = systemProblems.mapNotNull { problem -> - problem.difficulty.grade.filter { it.isDigit() || it == '.' || it == '-' }.toDoubleOrNull() - } + val gradeValues = + systemProblems.mapNotNull { problem -> + problem.difficulty + .grade + .filter { it.isDigit() || it == '.' || it == '-' } + .toDoubleOrNull() + } if (gradeValues.isNotEmpty()) { val avg = gradeValues.average() averages.add(String.format("%.1f", avg)) @@ -155,7 +166,7 @@ object SessionShareUtils { } } } - + return if (averages.isNotEmpty()) { if (averages.size == 1) { averages.first() @@ -166,185 +177,262 @@ object SessionShareUtils { } fun generateShareCard( - context: Context, - session: ClimbSession, - gym: Gym, - stats: SessionStats + context: Context, + session: ClimbSession, + gym: Gym, + stats: SessionStats ): File? { return try { val width = 1242 // 3:4 aspect at higher resolution for better fit val height = 1656 - + val bitmap = createBitmap(width, height) val canvas = Canvas(bitmap) - - val gradientDrawable = GradientDrawable( - GradientDrawable.Orientation.TOP_BOTTOM, - intArrayOf( - "#667eea".toColorInt(), - "#764ba2".toColorInt() - ) - ) + + val gradientDrawable = + GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + intArrayOf("#667eea".toColorInt(), "#764ba2".toColorInt()) + ) gradientDrawable.setBounds(0, 0, width, height) gradientDrawable.draw(canvas) - + // Setup paint objects - val titlePaint = Paint().apply { - color = Color.WHITE - textSize = 72f - typeface = Typeface.DEFAULT_BOLD - isAntiAlias = true - textAlign = Paint.Align.CENTER - } - - val subtitlePaint = Paint().apply { - color = "#E8E8E8".toColorInt() - textSize = 48f - typeface = Typeface.DEFAULT - isAntiAlias = true - textAlign = Paint.Align.CENTER - } - - val statLabelPaint = Paint().apply { - color = "#B8B8B8".toColorInt() - textSize = 36f - typeface = Typeface.DEFAULT - isAntiAlias = true - textAlign = Paint.Align.CENTER - } - - val statValuePaint = Paint().apply { - color = Color.WHITE - textSize = 64f - typeface = Typeface.DEFAULT_BOLD - isAntiAlias = true - textAlign = Paint.Align.CENTER - } - - val cardPaint = Paint().apply { - color = "#40FFFFFF".toColorInt() - isAntiAlias = true - } - + val titlePaint = + Paint().apply { + color = Color.WHITE + textSize = 72f + typeface = Typeface.DEFAULT_BOLD + isAntiAlias = true + textAlign = Paint.Align.CENTER + } + + val subtitlePaint = + Paint().apply { + color = "#E8E8E8".toColorInt() + textSize = 48f + typeface = Typeface.DEFAULT + isAntiAlias = true + textAlign = Paint.Align.CENTER + } + + val statLabelPaint = + Paint().apply { + color = "#B8B8B8".toColorInt() + textSize = 36f + typeface = Typeface.DEFAULT + isAntiAlias = true + textAlign = Paint.Align.CENTER + } + + val statValuePaint = + Paint().apply { + color = Color.WHITE + textSize = 64f + typeface = Typeface.DEFAULT_BOLD + isAntiAlias = true + textAlign = Paint.Align.CENTER + } + + val cardPaint = + Paint().apply { + color = "#40FFFFFF".toColorInt() + isAntiAlias = true + } + // Draw main card background val cardRect = RectF(60f, 200f, width - 60f, height - 120f) canvas.drawRoundRect(cardRect, 40f, 40f, cardPaint) - + // Draw content var yPosition = 300f - + // Title canvas.drawText("Climbing Session", width / 2f, yPosition, titlePaint) yPosition += 80f - + // Gym and date canvas.drawText(gym.name, width / 2f, yPosition, subtitlePaint) yPosition += 60f - + val dateText = formatSessionDate(session.date) canvas.drawText(dateText, width / 2f, yPosition, subtitlePaint) yPosition += 120f - + // Stats grid val statsStartY = yPosition val columnWidth = width / 2f val columnMaxTextWidth = columnWidth - 120f - + // Left column stats var leftY = statsStartY - drawStatItemFitting(canvas, columnWidth / 2f, leftY, "Attempts", stats.totalAttempts.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth) + drawStatItemFitting( + canvas, + columnWidth / 2f, + leftY, + "Attempts", + stats.totalAttempts.toString(), + statLabelPaint, + statValuePaint, + columnMaxTextWidth + ) leftY += 120f - drawStatItemFitting(canvas, columnWidth / 2f, leftY, "Problems", stats.uniqueProblemsAttempted.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth) + drawStatItemFitting( + canvas, + columnWidth / 2f, + leftY, + "Problems", + stats.uniqueProblemsAttempted.toString(), + statLabelPaint, + statValuePaint, + columnMaxTextWidth + ) leftY += 120f - drawStatItemFitting(canvas, columnWidth / 2f, leftY, "Duration", stats.sessionDuration, statLabelPaint, statValuePaint, columnMaxTextWidth) - + drawStatItemFitting( + canvas, + columnWidth / 2f, + leftY, + "Duration", + stats.sessionDuration, + statLabelPaint, + statValuePaint, + columnMaxTextWidth + ) + // Right column stats var rightY = statsStartY - drawStatItemFitting(canvas, width - columnWidth / 2f, rightY, "Successful", stats.successfulAttempts.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth) + drawStatItemFitting( + canvas, + width - columnWidth / 2f, + rightY, + "Completed", + stats.uniqueProblemsCompleted.toString(), + statLabelPaint, + statValuePaint, + columnMaxTextWidth + ) rightY += 120f - drawStatItemFitting(canvas, width - columnWidth / 2f, rightY, "Completed", stats.uniqueProblemsCompleted.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth) - rightY += 120f - + var rightYAfter = rightY stats.topGrade?.let { grade -> - drawStatItemFitting(canvas, width - columnWidth / 2f, rightY, "Top Grade", grade, statLabelPaint, statValuePaint, columnMaxTextWidth) + drawStatItemFitting( + canvas, + width - columnWidth / 2f, + rightY, + "Top Grade", + grade, + statLabelPaint, + statValuePaint, + columnMaxTextWidth + ) rightYAfter += 120f } - + // Grade range(s) - val boulderRange = gradeRangeForProblems(stats.problems.filter { it.climbType == ClimbType.BOULDER }) - val ropeRange = gradeRangeForProblems(stats.problems.filter { it.climbType == ClimbType.ROPE }) + val boulderRange = + gradeRangeForProblems( + stats.problems.filter { it.climbType == ClimbType.BOULDER } + ) + val ropeRange = + gradeRangeForProblems(stats.problems.filter { it.climbType == ClimbType.ROPE }) val rangesY = kotlin.math.max(leftY, rightYAfter) + 120f if (boulderRange != null && ropeRange != null) { // Two evenly spaced items - drawStatItemFitting(canvas, columnWidth / 2f, rangesY, "Boulder Range", boulderRange, statLabelPaint, statValuePaint, columnMaxTextWidth) - drawStatItemFitting(canvas, width - columnWidth / 2f, rangesY, "Rope Range", ropeRange, statLabelPaint, statValuePaint, columnMaxTextWidth) + drawStatItemFitting( + canvas, + columnWidth / 2f, + rangesY, + "Boulder Range", + boulderRange, + statLabelPaint, + statValuePaint, + columnMaxTextWidth + ) + drawStatItemFitting( + canvas, + width - columnWidth / 2f, + rangesY, + "Rope Range", + ropeRange, + statLabelPaint, + statValuePaint, + columnMaxTextWidth + ) } else if (boulderRange != null || ropeRange != null) { // Single centered item val singleRange = boulderRange ?: ropeRange ?: "" - drawStatItemFitting(canvas, width / 2f, rangesY, "Grade Range", singleRange, statLabelPaint, statValuePaint, width - 200f) + drawStatItemFitting( + canvas, + width / 2f, + rangesY, + "Grade Range", + singleRange, + statLabelPaint, + statValuePaint, + width - 200f + ) } - - // App branding - val brandingPaint = Paint().apply { - color = "#80FFFFFF".toColorInt() - textSize = 32f - typeface = Typeface.DEFAULT - isAntiAlias = true - textAlign = Paint.Align.CENTER - } + val brandingPaint = + Paint().apply { + color = "#80FFFFFF".toColorInt() + textSize = 32f + typeface = Typeface.DEFAULT + isAntiAlias = true + textAlign = Paint.Align.CENTER + } canvas.drawText("OpenClimb", width / 2f, height - 40f, brandingPaint) - + // Save to file val shareDir = File(context.cacheDir, "shares") if (!shareDir.exists()) { shareDir.mkdirs() } - + val filename = "session_${session.id}_${System.currentTimeMillis()}.png" val file = File(shareDir, filename) - + val outputStream = FileOutputStream(file) bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) outputStream.flush() outputStream.close() - + bitmap.recycle() - + file } catch (e: Exception) { e.printStackTrace() null } } - + private fun drawStatItem( - canvas: Canvas, - x: Float, - y: Float, - label: String, - value: String, - labelPaint: Paint, - valuePaint: Paint + canvas: Canvas, + x: Float, + y: Float, + label: String, + value: String, + labelPaint: Paint, + valuePaint: Paint ) { canvas.drawText(value, x, y, valuePaint) canvas.drawText(label, x, y + 50f, labelPaint) } - + /** - * Draws a stat item while fitting the value text to a max width by reducing text size if needed. + * Draws a stat item while fitting the value text to a max width by reducing text size if + * needed. */ private fun drawStatItemFitting( - canvas: Canvas, - x: Float, - y: Float, - label: String, - value: String, - labelPaint: Paint, - valuePaint: Paint, - maxTextWidth: Float + canvas: Canvas, + x: Float, + y: Float, + label: String, + value: String, + labelPaint: Paint, + valuePaint: Paint, + maxTextWidth: Float ) { val tempPaint = Paint(valuePaint) var textSize = tempPaint.textSize @@ -357,7 +445,7 @@ object SessionShareUtils { canvas.drawText(value, x, y, tempPaint) canvas.drawText(label, x, y + 50f, labelPaint) } - + /** * Returns a range string like "X - Y" for the given problems, based on their difficulty grades. */ @@ -367,9 +455,7 @@ object SessionShareUtils { val sorted = grades.sortedWith { a, b -> a.compareTo(b) } return "${sorted.first().grade} - ${sorted.last().grade}" } - - private fun formatSessionDate(dateString: String): String { return try { val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME @@ -380,23 +466,28 @@ object SessionShareUtils { dateString.take(10) } } - + fun shareSessionCard(context: Context, imageFile: File) { try { - val uri = FileProvider.getUriForFile( - context, - "${context.packageName}.fileprovider", - imageFile - ) - - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "image/png" - putExtra(Intent.EXTRA_STREAM, uri) - putExtra(Intent.EXTRA_TEXT, "Check out my climbing session! 🧗‍♀️ #OpenClimb") - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - + val uri = + FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + imageFile + ) + + val shareIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + putExtra( + Intent.EXTRA_TEXT, + "Check out my climbing session! 🧗‍♀️ #OpenClimb" + ) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val chooser = Intent.createChooser(shareIntent, "Share Session") chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(chooser) @@ -406,16 +497,18 @@ object SessionShareUtils { } /** - * Returns the highest grade string among the given problems, respecting their difficulty system. + * Returns the highest grade string among the given problems, respecting their difficulty + * system. */ private fun highestGradeForProblems(problems: List): String? { if (problems.isEmpty()) return null - return problems.maxByOrNull { p -> gradeRank(p.difficulty.system, p.difficulty.grade) }?.difficulty?.grade + return problems + .maxByOrNull { p -> gradeRank(p.difficulty.system, p.difficulty.grade) } + ?.difficulty + ?.grade } - /** - * Produces a comparable numeric rank for grades across supported systems. - */ + /** Produces a comparable numeric rank for grades across supported systems. */ private fun gradeRank(system: DifficultySystem, grade: String): Double { return when (system) { DifficultySystem.V_SCALE -> { @@ -424,7 +517,8 @@ object SessionShareUtils { DifficultySystem.FONT -> { val list = DifficultySystem.FONT.getAvailableGrades() val idx = list.indexOf(grade.uppercase()) - if (idx >= 0) idx.toDouble() else grade.filter { it.isDigit() }.toDoubleOrNull() ?: -1.0 + if (idx >= 0) idx.toDouble() + else grade.filter { it.isDigit() }.toDoubleOrNull() ?: -1.0 } DifficultySystem.YDS -> { // Parse 5.X with optional letter a-d @@ -434,13 +528,14 @@ object SessionShareUtils { val numberPart = tail.takeWhile { it.isDigit() || it == '.' } val letterPart = tail.drop(numberPart.length).firstOrNull() val base = numberPart.toDoubleOrNull() ?: return -1.0 - val letterWeight = when (letterPart) { - 'a' -> 0.0 - 'b' -> 0.1 - 'c' -> 0.2 - 'd' -> 0.3 - else -> 0.0 - } + val letterWeight = + when (letterPart) { + 'a' -> 0.0 + 'b' -> 0.1 + 'c' -> 0.2 + 'd' -> 0.3 + else -> 0.0 + } base + letterWeight } DifficultySystem.CUSTOM -> { diff --git a/ios/OpenClimb.xcodeproj/project.pbxproj b/ios/OpenClimb.xcodeproj/project.pbxproj index d01e414..f8e7793 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 = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -437,7 +437,7 @@ CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 4BC9Y2LL4B; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -479,7 +479,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -509,7 +509,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; 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 10fa4a89987b53863ed7fb4282d4b59246ae2be6..47954733372f6fcc439c6e32711f1e7955cf2ae5 100644 GIT binary patch literal 115786 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º+UswgIg{Yhgwm^6BzfFq^ZfZJ-t{Ghm1)z->#_ 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 U@&EnL82|rA|G$4Cn(qC706~p)KL7v# literal 114205 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

>=5b-uO1DqGLAo^QRty25&y^&0Cs>-E-K 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 R1wZ_M|NQ^|JEFm~{{wq0MD+jw diff --git a/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist index 53d0216..7dfcbe0 100644 --- a/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/ios/OpenClimb.xcodeproj/xcuserdata/atridad.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ OpenClimb.xcscheme_^#shared#^_ orderHint - 0 + 1 SessionStatusLiveExtension.xcscheme_^#shared#^_ @@ -15,5 +15,18 @@ 0 + SuppressBuildableAutocreation + + D24C19672E75002A0045894C + + primary + + + D2FE948A2E78FEE0008CDB25 + + primary + + + diff --git a/ios/OpenClimb/Models/DataModels.swift b/ios/OpenClimb/Models/DataModels.swift index c395279..a514538 100644 --- a/ios/OpenClimb/Models/DataModels.swift +++ b/ios/OpenClimb/Models/DataModels.swift @@ -260,7 +260,7 @@ struct Problem: Identifiable, Codable, Hashable { let description: String? let climbType: ClimbType let difficulty: DifficultyGrade - let setter: String? + let tags: [String] let location: String? let imagePaths: [String] @@ -272,7 +272,7 @@ struct Problem: Identifiable, Codable, Hashable { init( gymId: UUID, name: String? = nil, description: String? = nil, climbType: ClimbType, - difficulty: DifficultyGrade, setter: String? = nil, tags: [String] = [], + difficulty: DifficultyGrade, tags: [String] = [], location: String? = nil, imagePaths: [String] = [], dateSet: Date? = nil, notes: String? = nil ) { @@ -282,7 +282,7 @@ struct Problem: Identifiable, Codable, Hashable { self.description = description self.climbType = climbType self.difficulty = difficulty - self.setter = setter + self.tags = tags self.location = location self.imagePaths = imagePaths @@ -296,7 +296,7 @@ struct Problem: Identifiable, Codable, Hashable { func updated( name: String? = nil, description: String? = nil, climbType: ClimbType? = nil, - difficulty: DifficultyGrade? = nil, setter: String? = nil, tags: [String]? = nil, + difficulty: DifficultyGrade? = nil, tags: [String]? = nil, location: String? = nil, imagePaths: [String]? = nil, isActive: Bool? = nil, dateSet: Date? = nil, notes: String? = nil ) -> Problem { @@ -307,7 +307,7 @@ struct Problem: Identifiable, Codable, Hashable { description: description ?? self.description, climbType: climbType ?? self.climbType, difficulty: difficulty ?? self.difficulty, - setter: setter ?? self.setter, + tags: tags ?? self.tags, location: location ?? self.location, imagePaths: imagePaths ?? self.imagePaths, @@ -321,7 +321,7 @@ struct Problem: Identifiable, Codable, Hashable { private init( id: UUID, gymId: UUID, name: String?, description: String?, climbType: ClimbType, - difficulty: DifficultyGrade, setter: String?, tags: [String], location: String?, + difficulty: DifficultyGrade, tags: [String], location: String?, imagePaths: [String], isActive: Bool, dateSet: Date?, notes: String?, createdAt: Date, updatedAt: Date ) { @@ -331,7 +331,7 @@ struct Problem: Identifiable, Codable, Hashable { self.description = description self.climbType = climbType self.difficulty = difficulty - self.setter = setter + self.tags = tags self.location = location self.imagePaths = imagePaths @@ -344,7 +344,7 @@ struct Problem: Identifiable, Codable, Hashable { static func fromImport( id: UUID, gymId: UUID, name: String?, description: String?, climbType: ClimbType, - difficulty: DifficultyGrade, setter: String?, tags: [String], location: String?, + difficulty: DifficultyGrade, tags: [String], location: String?, imagePaths: [String], isActive: Bool, dateSet: Date?, notes: String?, createdAt: Date, updatedAt: Date ) -> Problem { @@ -355,7 +355,7 @@ struct Problem: Identifiable, Codable, Hashable { description: description, climbType: climbType, difficulty: difficulty, - setter: setter, + tags: tags, location: location, imagePaths: imagePaths, diff --git a/ios/OpenClimb/ViewModels/ClimbingDataManager.swift b/ios/OpenClimb/ViewModels/ClimbingDataManager.swift index cbd55fa..112f549 100644 --- a/ios/OpenClimb/ViewModels/ClimbingDataManager.swift +++ b/ios/OpenClimb/ViewModels/ClimbingDataManager.swift @@ -475,7 +475,7 @@ class ClimbingDataManager: ObservableObject { let exportData = ClimbDataExport( exportedAt: dateFormatter.string(from: Date()), - version: "1.0", + version: "2.0", gyms: gyms.map { AndroidGym(from: $0) }, problems: problems.map { AndroidProblem(from: $0) }, sessions: sessions.map { AndroidClimbSession(from: $0) }, @@ -593,7 +593,7 @@ struct ClimbDataExport: Codable { let attempts: [AndroidAttempt] init( - exportedAt: String, version: String = "1.0", gyms: [AndroidGym], problems: [AndroidProblem], + exportedAt: String, version: String = "2.0", gyms: [AndroidGym], problems: [AndroidProblem], sessions: [AndroidClimbSession], attempts: [AndroidAttempt] ) { self.exportedAt = exportedAt @@ -675,7 +675,6 @@ struct AndroidProblem: Codable { let description: String? let climbType: ClimbType let difficulty: DifficultyGrade - let setter: String? let tags: [String] let location: String? let imagePaths: [String]? @@ -692,7 +691,6 @@ 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 @@ -707,7 +705,7 @@ struct AndroidProblem: Codable { init( id: String, gymId: String, name: String?, description: String?, climbType: ClimbType, - difficulty: DifficultyGrade, setter: String? = nil, tags: [String] = [], + difficulty: DifficultyGrade, tags: [String] = [], location: String? = nil, imagePaths: [String]? = nil, isActive: Bool = true, dateSet: String? = nil, notes: String? = nil, @@ -719,7 +717,6 @@ struct AndroidProblem: Codable { self.description = description self.climbType = climbType self.difficulty = difficulty - self.setter = setter self.tags = tags self.location = location self.imagePaths = imagePaths @@ -746,7 +743,6 @@ struct AndroidProblem: Codable { description: description, climbType: climbType, difficulty: difficulty, - setter: setter, tags: tags, location: location, imagePaths: imagePaths ?? [], @@ -766,7 +762,6 @@ 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, @@ -1331,7 +1326,6 @@ extension ClimbingDataManager { description: "Technical overhang with small holds", climbType: .boulder, difficulty: DifficultyGrade(system: .vScale, grade: "V4"), - setter: "John Doe", tags: ["technical", "overhang"], location: "Cave area" ) diff --git a/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift b/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift index 336b58f..3c3afae 100644 --- a/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift +++ b/ios/OpenClimb/Views/AddEdit/AddAttemptView.swift @@ -635,12 +635,6 @@ struct ProblemExpandedView: View { .foregroundColor(.secondary) } - if let setter = problem.setter, !setter.isEmpty { - Label(setter, systemImage: "person") - .font(.subheadline) - .foregroundColor(.secondary) - } - if let description = problem.description, !description.isEmpty { Text(description) .font(.body) diff --git a/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift b/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift index 811f64e..78e91ec 100644 --- a/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift +++ b/ios/OpenClimb/Views/AddEdit/AddEditProblemView.swift @@ -13,7 +13,6 @@ struct AddEditProblemView: View { @State private var selectedClimbType: ClimbType = .boulder @State private var selectedDifficultySystem: DifficultySystem = .vScale @State private var difficultyGrade = "" - @State private var setter = "" @State private var location = "" @State private var tags = "" @State private var notes = "" @@ -63,7 +62,7 @@ struct AddEditProblemView: View { PhotosSection() ClimbTypeSection() DifficultySection() - LocationAndSetterSection() + LocationSection() TagsSection() AdditionalInfoSection() } @@ -158,7 +157,6 @@ struct AddEditProblemView: View { ) } - TextField("Route Setter (Optional)", text: $setter) } } @@ -281,7 +279,7 @@ struct AddEditProblemView: View { } @ViewBuilder - private func LocationAndSetterSection() -> some View { + private func LocationSection() -> some View { Section("Location & Details") { TextField( "Location (Optional)", text: $location, prompt: Text("e.g., 'Cave area', 'Wall 3'")) @@ -334,25 +332,28 @@ struct AddEditProblemView: View { HStack(spacing: 12) { ForEach(imageData.indices, id: \.self) { index in if let uiImage = UIImage(data: imageData[index]) { - Image(uiImage: uiImage) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 80, height: 80) - .clipped() - .cornerRadius(8) - .overlay(alignment: .topTrailing) { - Button(action: { - imageData.remove(at: index) - if index < imagePaths.count { - imagePaths.remove(at: index) - } - }) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.red) - .background(Circle().fill(.white)) + ZStack(alignment: .topTrailing) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 80, height: 80) + .clipped() + .cornerRadius(8) + + Button(action: { + imageData.remove(at: index) + if index < imagePaths.count { + imagePaths.remove(at: index) } - .offset(x: 8, y: -8) + }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + .background(Circle().fill(.white)) + .font(.system(size: 18)) } + .offset(x: 4, y: -4) + } + .frame(width: 88, height: 88) // Extra space for button } else { RoundedRectangle(cornerRadius: 8) .fill(.gray.opacity(0.3)) @@ -365,6 +366,7 @@ struct AddEditProblemView: View { } } .padding(.horizontal, 1) + .padding(.vertical, 8) } } } @@ -410,7 +412,7 @@ struct AddEditProblemView: View { selectedClimbType = problem.climbType selectedDifficultySystem = problem.difficulty.system difficultyGrade = problem.difficulty.grade - setter = problem.setter ?? "" + location = problem.location ?? "" tags = problem.tags.joined(separator: ", ") notes = problem.notes ?? "" @@ -420,7 +422,7 @@ struct AddEditProblemView: View { // Load image data for preview imageData = [] for imagePath in problem.imagePaths { - if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)) { + if let data = ImageManager.shared.loadImageData(fromPath: imagePath) { imageData.append(data) } } @@ -479,7 +481,7 @@ struct AddEditProblemView: View { let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedDescription = description.trimmingCharacters(in: .whitespacesAndNewlines) - let trimmedSetter = setter.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedLocation = location.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedNotes = notes.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedTags = tags.split(separator: ",").map { @@ -494,7 +496,7 @@ struct AddEditProblemView: View { description: trimmedDescription.isEmpty ? nil : trimmedDescription, climbType: selectedClimbType, difficulty: difficulty, - setter: trimmedSetter.isEmpty ? nil : trimmedSetter, + tags: trimmedTags, location: trimmedLocation.isEmpty ? nil : trimmedLocation, imagePaths: imagePaths, @@ -510,7 +512,7 @@ struct AddEditProblemView: View { description: trimmedDescription.isEmpty ? nil : trimmedDescription, climbType: selectedClimbType, difficulty: difficulty, - setter: trimmedSetter.isEmpty ? nil : trimmedSetter, + tags: trimmedTags, location: trimmedLocation.isEmpty ? nil : trimmedLocation, imagePaths: imagePaths, diff --git a/ios/OpenClimb/Views/Detail/GymDetailView.swift b/ios/OpenClimb/Views/Detail/GymDetailView.swift index 3b6e57f..dd86fe7 100644 --- a/ios/OpenClimb/Views/Detail/GymDetailView.swift +++ b/ios/OpenClimb/Views/Detail/GymDetailView.swift @@ -1,4 +1,3 @@ - import SwiftUI struct GymDetailView: View { @@ -60,8 +59,10 @@ struct GymDetailView: View { ToolbarItemGroup(placement: .navigationBarTrailing) { if gym != nil { Menu { - Button("Edit Gym") { + Button { // Navigate to edit view + } label: { + Label("Edit Gym", systemImage: "pencil") } Button(role: .destructive) { diff --git a/ios/OpenClimb/Views/Detail/ProblemDetailView.swift b/ios/OpenClimb/Views/Detail/ProblemDetailView.swift index 62dc277..fe38fa1 100644 --- a/ios/OpenClimb/Views/Detail/ProblemDetailView.swift +++ b/ios/OpenClimb/Views/Detail/ProblemDetailView.swift @@ -1,4 +1,3 @@ - import SwiftUI struct ProblemDetailView: View { @@ -64,8 +63,10 @@ struct ProblemDetailView: View { ToolbarItemGroup(placement: .navigationBarTrailing) { if problem != nil { Menu { - Button("Edit Problem") { + Button { showingEditProblem = true + } label: { + Label("Edit Problem", systemImage: "pencil") } Button(role: .destructive) { @@ -167,12 +168,6 @@ struct ProblemHeaderCard: View { .font(.body) } - if let setter = problem.setter, !setter.isEmpty { - Text("Set by: \(setter)") - .font(.subheadline) - .foregroundColor(.secondary) - } - if !problem.tags.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { diff --git a/ios/OpenClimb/Views/Detail/SessionDetailView.swift b/ios/OpenClimb/Views/Detail/SessionDetailView.swift index b4ad000..84a8e4d 100644 --- a/ios/OpenClimb/Views/Detail/SessionDetailView.swift +++ b/ios/OpenClimb/Views/Detail/SessionDetailView.swift @@ -280,7 +280,6 @@ struct SessionStatsCard: View { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) { StatItem(label: "Total Attempts", value: "\(stats.totalAttempts)") StatItem(label: "Problems", value: "\(stats.uniqueProblemsAttempted)") - StatItem(label: "Successful", value: "\(stats.successfulAttempts)") StatItem(label: "Completed", value: "\(stats.uniqueProblemsCompleted)") } } diff --git a/ios/OpenClimb/Views/GymsView.swift b/ios/OpenClimb/Views/GymsView.swift index 078dd4f..31c895a 100644 --- a/ios/OpenClimb/Views/GymsView.swift +++ b/ios/OpenClimb/Views/GymsView.swift @@ -1,4 +1,3 @@ - import SwiftUI struct GymsView: View { @@ -49,7 +48,10 @@ struct GymsList: View { Button { gymToEdit = gym } label: { - Label("Edit", systemImage: "pencil") + HStack { + Image(systemName: "pencil") + Text("Edit") + } } .tint(.blue) } diff --git a/ios/OpenClimb/Views/ProblemsView.swift b/ios/OpenClimb/Views/ProblemsView.swift index 36b3fd2..da4bd6b 100644 --- a/ios/OpenClimb/Views/ProblemsView.swift +++ b/ios/OpenClimb/Views/ProblemsView.swift @@ -13,10 +13,9 @@ struct ProblemsView: View { // Apply search filter if !searchText.isEmpty { filtered = filtered.filter { problem in - (problem.name?.localizedCaseInsensitiveContains(searchText) ?? false) + return problem.name?.localizedCaseInsensitiveContains(searchText) ?? false || (problem.description?.localizedCaseInsensitiveContains(searchText) ?? false) || (problem.location?.localizedCaseInsensitiveContains(searchText) ?? false) - || (problem.setter?.localizedCaseInsensitiveContains(searchText) ?? false) || problem.tags.contains { $0.localizedCaseInsensitiveContains(searchText) } } } @@ -31,7 +30,11 @@ struct ProblemsView: View { filtered = filtered.filter { $0.gymId == gym.id } } - return filtered.sorted { $0.updatedAt > $1.updatedAt } + // Separate active and inactive problems + let active = filtered.filter { $0.isActive }.sorted { $0.updatedAt > $1.updatedAt } + let inactive = filtered.filter { !$0.isActive }.sorted { $0.updatedAt > $1.updatedAt } + + return active + inactive } var body: some View { @@ -195,10 +198,23 @@ struct ProblemsList: View { Label("Delete", systemImage: "trash") } + Button { + let updatedProblem = problem.updated(isActive: !problem.isActive) + dataManager.updateProblem(updatedProblem) + } label: { + Label( + problem.isActive ? "Mark as Reset" : "Mark as Active", + systemImage: problem.isActive ? "xmark.circle" : "checkmark.circle") + } + .tint(.orange) + Button { problemToEdit = problem } label: { - Label("Edit", systemImage: "pencil") + HStack { + Image(systemName: "pencil") + Text("Edit") + } } .tint(.blue) } @@ -239,6 +255,7 @@ struct ProblemRow: View { Text(problem.name ?? "Unnamed Problem") .font(.headline) .fontWeight(.semibold) + .foregroundColor(problem.isActive ? .primary : .secondary) Text(gym?.name ?? "Unknown Gym") .font(.subheadline) @@ -295,9 +312,9 @@ struct ProblemRow: View { } if !problem.isActive { - Text("Inactive") + Text("Reset / No Longer Set") .font(.caption) - .foregroundColor(.red) + .foregroundColor(.orange) .fontWeight(.medium) } }