0.3.0 - Filtering and Better Scales

This commit is contained in:
2025-08-15 19:30:50 -06:00
parent 10984226d9
commit 491a122c01
15 changed files with 446 additions and 110 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -14,8 +14,8 @@ android {
applicationId = "com.atridad.openclimb"
minSdk = 31
targetSdk = 35
versionCode = 2
versionName = "0.2.0"
versionCode = 3
versionName = "0.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -5,5 +5,13 @@ import kotlinx.serialization.Serializable
@Serializable
enum class ClimbType {
ROPE,
BOULDER
BOULDER;
/**
* Get the display name for the UI
*/
fun getDisplayName(): String = when (this) {
ROPE -> "Rope"
BOULDER -> "Bouldering"
}
}
@@ -4,23 +4,68 @@ import kotlinx.serialization.Serializable
@Serializable
enum class DifficultySystem {
// Rope climbing systems
YDS, // Yosemite Decimal System (5.1 - 5.15d)
FRENCH, // French system (3 - 9c+)
UIAA, // UIAA system (I - XII+)
BRITISH, // British system (Mod - E11)
// Bouldering systems
V_SCALE, // V-Scale (VB - V17)
FONT, // Fontainebleau (3 - 9A+)
FONT, // Fontainebleau (3 - 8C+)
// Rope climbing systems
YDS, // Yosemite Decimal System (5.0 - 5.15d)
// Custom system for gyms that use their own colors/naming
CUSTOM
CUSTOM;
/**
* Get the display name for the UI
*/
fun getDisplayName(): String = when (this) {
V_SCALE -> "V Scale"
FONT -> "Font Scale"
YDS -> "YDS (Yosemite)"
CUSTOM -> "Custom"
}
/**
* Check if this system is for bouldering
*/
fun isBoulderingSystem(): Boolean = when (this) {
V_SCALE, FONT -> true
YDS -> false
CUSTOM -> true // Custom is available for all
}
/**
* Check if this system is for rope climbing
*/
fun isRopeSystem(): Boolean = when (this) {
YDS -> true
V_SCALE, FONT -> false
CUSTOM -> true // Custom is available for all
}
/**
* Get available grades for this difficulty system
*/
fun getAvailableGrades(): List<String> = when (this) {
V_SCALE -> listOf("VB", "V0", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "V12", "V13", "V14", "V15", "V16", "V17")
FONT -> listOf("3", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6A+", "6B", "6B+", "6C", "6C+", "7A", "7A+", "7B", "7B+", "7C", "7C+", "8A", "8A+", "8B", "8B+", "8C", "8C+")
YDS -> listOf("5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "5.10a", "5.10b", "5.10c", "5.10d", "5.11a", "5.11b", "5.11c", "5.11d", "5.12a", "5.12b", "5.12c", "5.12d", "5.13a", "5.13b", "5.13c", "5.13d", "5.14a", "5.14b", "5.14c", "5.14d", "5.15a", "5.15b", "5.15c", "5.15d")
CUSTOM -> emptyList() // Custom allows free text input
}
companion object {
/**
* Get all difficulty systems available for a specific climb type
*/
fun getSystemsForClimbType(climbType: ClimbType): List<DifficultySystem> = when (climbType) {
ClimbType.BOULDER -> entries.filter { it.isBoulderingSystem() }
ClimbType.ROPE -> entries.filter { it.isRopeSystem() }
}
}
}
@Serializable
data class DifficultyGrade(
val system: DifficultySystem,
val grade: String,
val numericValue: Int // For comparison and analytics
val numericValue: Int
)
@@ -28,7 +28,6 @@ fun OpenClimbApp() {
val navController = rememberNavController()
val context = LocalContext.current
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = currentBackStackEntry?.destination?.route
val database = remember { OpenClimbDatabase.getDatabase(context) }
val repository = remember { ClimbRepository(database, context) }
@@ -247,17 +246,15 @@ fun OpenClimbBottomNavigation(navController: NavHostController) {
selected = isSelected,
onClick = {
navController.navigate(item.screen) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(Screen.Sessions) {
saveState = true
// Clear the entire back stack and go to the selected tab's root screen
popUpTo(0) {
inclusive = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
// Don't restore state - always start fresh when switching tabs
restoreState = false
}
}
)
@@ -47,6 +47,34 @@ fun AddEditGymScreen(
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()
}
// Reset selected difficulty systems when available systems change
LaunchedEffect(availableDifficultySystems) {
selectedDifficultySystems = selectedDifficultySystems.filter { it in availableDifficultySystems }.toSet()
}
// Load existing gym data for editing
LaunchedEffect(gymId) {
if (gymId != null) {
val existingGym = viewModel.getGymById(gymId).first()
existingGym?.let { gym ->
name = gym.name
location = gym.location ?: ""
notes = gym.notes ?: ""
selectedClimbTypes = gym.supportedClimbTypes.toSet()
selectedDifficultySystems = gym.difficultySystems.toSet()
}
}
}
Scaffold(
topBar = {
TopAppBar(
@@ -59,20 +87,16 @@ fun AddEditGymScreen(
actions = {
TextButton(
onClick = {
val gym = if (isEditing) {
Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
} else {
Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
}
val gym = Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
if (isEditing) {
viewModel.updateGym(gym)
viewModel.updateGym(gym.copy(id = gymId!!))
} else {
viewModel.addGym(gym)
}
onNavigateBack()
},
enabled = name.isNotBlank() && selectedClimbTypes.isNotEmpty()
enabled = name.isNotBlank() && selectedClimbTypes.isNotEmpty() && selectedDifficultySystems.isNotEmpty()
) {
Text("Save")
}
@@ -142,7 +166,7 @@ fun AddEditGymScreen(
onCheckedChange = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(climbType.name.lowercase().replaceFirstChar { it.uppercase() })
Text(climbType.getDisplayName())
}
}
}
@@ -163,29 +187,38 @@ fun AddEditGymScreen(
Spacer(modifier = Modifier.height(8.dp))
DifficultySystem.entries.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
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)
)
} 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
)
) {
Checkbox(
checked = system in selectedDifficultySystems,
onCheckedChange = null
)
) {
Checkbox(
checked = system in selectedDifficultySystems,
onCheckedChange = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(system.name)
Spacer(modifier = Modifier.width(8.dp))
Text(system.getDisplayName())
}
}
}
}
@@ -244,6 +277,8 @@ fun AddEditProblemScreen(
notes = p.notes ?: ""
isActive = p.isActive
imagePaths = p.imagePaths
// Set the selected gym for the existing problem
selectedGym = gyms.find { it.id == p.gymId }
}
}
}
@@ -254,8 +289,39 @@ fun AddEditProblemScreen(
}
}
val availableDifficultySystems = selectedGym?.difficultySystems ?: DifficultySystem.entries.toList()
val availableClimbTypes = selectedGym?.supportedClimbTypes ?: ClimbType.entries.toList()
val availableDifficultySystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
selectedGym?.difficultySystems?.contains(system) ?: true
}
// 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
}
// If there's only one available system and nothing is selected, auto-select it
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()
if (availableGrades.isNotEmpty() && difficultyGrade !in availableGrades) {
difficultyGrade = ""
}
}
Scaffold(
topBar = {
@@ -437,7 +503,7 @@ fun AddEditProblemScreen(
availableClimbTypes.forEach { climbType ->
FilterChip(
onClick = { selectedClimbType = climbType },
label = { Text(climbType.name.lowercase().replaceFirstChar { it.uppercase() }) },
label = { Text(climbType.getDisplayName()) },
selected = selectedClimbType == climbType
)
}
@@ -476,7 +542,7 @@ fun AddEditProblemScreen(
items(availableDifficultySystems) { system ->
FilterChip(
onClick = { selectedDifficultySystem = system },
label = { Text(system.name) },
label = { Text(system.getDisplayName()) },
selected = selectedDifficultySystem == system
)
}
@@ -484,23 +550,51 @@ fun AddEditProblemScreen(
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = difficultyGrade,
onValueChange = { difficultyGrade = it },
label = { Text("Grade *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
placeholder = {
Text(when (selectedDifficultySystem) {
DifficultySystem.V_SCALE -> "e.g., V0, V4, V10"
DifficultySystem.FONT -> "e.g., 3, 6A+, 8B"
DifficultySystem.YDS -> "e.g., 5.8, 5.12a"
DifficultySystem.FRENCH -> "e.g., 6a, 7c+"
DifficultySystem.CUSTOM -> "Custom grade"
else -> "Enter grade"
})
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
OutlinedTextField(
value = difficultyGrade,
onValueChange = { difficultyGrade = it },
label = { Text("Grade *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
placeholder = { Text("Enter custom grade") }
)
} else {
var expanded by remember { mutableStateOf(false) }
val availableGrades = selectedDifficultySystem.getAvailableGrades()
ExposedDropdownMenuBox(
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()
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
availableGrades.forEach { grade ->
DropdownMenuItem(
text = { Text(grade) },
onClick = {
difficultyGrade = grade
expanded = false
}
)
}
}
}
)
}
}
}
}
@@ -617,6 +711,19 @@ fun AddEditSessionScreen(
var attempts by remember { mutableStateOf(listOf<AttemptInput>()) }
var showAddAttemptDialog by remember { mutableStateOf(false) }
// Load existing session data for editing
LaunchedEffect(sessionId) {
if (sessionId != null) {
val existingSession = viewModel.getSessionById(sessionId).first()
existingSession?.let { session ->
selectedGym = gyms.find { it.id == session.gymId }
sessionDate = session.date.split("T")[0] // Extract date part
duration = session.duration?.toString() ?: ""
sessionNotes = session.notes ?: ""
}
}
}
LaunchedEffect(gymId, gyms) {
if (gymId != null && selectedGym == null) {
selectedGym = gyms.find { it.id == gymId }
@@ -830,7 +937,7 @@ fun AddEditSessionScreen(
problem?.difficulty?.let { difficulty ->
Text(
text = "${difficulty.system.name}: ${difficulty.grade}",
text = "${difficulty.system.getDisplayName()}: ${difficulty.grade}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
@@ -956,7 +1063,7 @@ fun AddAttemptDialog(
fontWeight = FontWeight.Medium
)
Text(
text = "${problem.difficulty.system.name}: ${problem.difficulty.grade}",
text = "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
@@ -481,7 +481,7 @@ fun ProblemDetailScreen(
Column {
problem?.let { p ->
Text(
text = "${p.difficulty.system.name}: ${p.difficulty.grade}",
text = "${p.difficulty.system.getDisplayName()}: ${p.difficulty.grade}",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
@@ -490,7 +490,7 @@ fun ProblemDetailScreen(
problem?.let { p ->
Text(
text = p.climbType.name.lowercase().replaceFirstChar { it.uppercase() },
text = p.climbType.getDisplayName(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -1314,7 +1314,7 @@ fun SessionAttemptCard(
)
Text(
text = "${problem.difficulty.system.name}: ${problem.difficulty.grade}",
text = "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
@@ -1406,6 +1406,39 @@ fun EnhancedAddAttemptDialog(
var selectedClimbType by remember { mutableStateOf(ClimbType.BOULDER) }
var selectedDifficultySystem by remember { mutableStateOf(gym.difficultySystems.firstOrNull() ?: DifficultySystem.V_SCALE) }
// Auto-select climb type if there's only one available
LaunchedEffect(gym.supportedClimbTypes) {
if (gym.supportedClimbTypes.size == 1 && selectedClimbType != gym.supportedClimbTypes.first()) {
selectedClimbType = gym.supportedClimbTypes.first()
}
}
// Auto-select difficulty system if there's only one available for the selected climb type
LaunchedEffect(selectedClimbType, gym.difficultySystems) {
val availableSystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
gym.difficultySystems.contains(system)
}
when {
// If current system is not compatible, select the first available one
selectedDifficultySystem !in availableSystems -> {
selectedDifficultySystem = availableSystems.firstOrNull() ?: gym.difficultySystems.firstOrNull() ?: DifficultySystem.CUSTOM
}
// If there's only one available system, auto-select it
availableSystems.size == 1 && selectedDifficultySystem != availableSystems.first() -> {
selectedDifficultySystem = availableSystems.first()
}
}
}
// Reset grade when difficulty system changes
LaunchedEffect(selectedDifficultySystem) {
val availableGrades = selectedDifficultySystem.getAvailableGrades()
if (availableGrades.isNotEmpty() && newProblemGrade !in availableGrades) {
newProblemGrade = ""
}
}
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier
@@ -1509,7 +1542,7 @@ fun EnhancedAddAttemptDialog(
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${problem.difficulty.system.name}: ${problem.difficulty.grade}",
text = "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}",
style = MaterialTheme.typography.bodyMedium,
color = if (isSelected)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
@@ -1584,7 +1617,7 @@ fun EnhancedAddAttemptDialog(
onClick = { selectedClimbType = climbType },
label = {
Text(
climbType.name.lowercase().replaceFirstChar { it.uppercase() },
climbType.getDisplayName(),
fontWeight = FontWeight.Medium
)
},
@@ -1611,12 +1644,15 @@ fun EnhancedAddAttemptDialog(
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(gym.difficultySystems) { system ->
val availableSystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
gym.difficultySystems.contains(system)
}
items(availableSystems) { system ->
FilterChip(
onClick = { selectedDifficultySystem = system },
label = {
Text(
system.name,
system.getDisplayName(),
fontWeight = FontWeight.Medium
)
},
@@ -1630,31 +1666,63 @@ fun EnhancedAddAttemptDialog(
}
}
OutlinedTextField(
value = newProblemGrade,
onValueChange = { newProblemGrade = it },
label = { Text("Grade *") },
placeholder = {
Text(when (selectedDifficultySystem) {
DifficultySystem.V_SCALE -> "e.g., V0, V4, V10"
DifficultySystem.FONT -> "e.g., 3, 6A+, 8B"
DifficultySystem.YDS -> "e.g., 5.8, 5.12a"
DifficultySystem.FRENCH -> "e.g., 6a, 7c+"
DifficultySystem.CUSTOM -> "Custom grade"
else -> "Enter grade"
})
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
),
isError = newProblemGrade.isBlank(),
supportingText = if (newProblemGrade.isBlank()) {
{ Text("Grade is required", color = MaterialTheme.colorScheme.error) }
} else null
)
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
OutlinedTextField(
value = newProblemGrade,
onValueChange = { newProblemGrade = it },
label = { Text("Grade *") },
placeholder = { Text("Enter custom grade") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
),
isError = newProblemGrade.isBlank(),
supportingText = if (newProblemGrade.isBlank()) {
{ Text("Grade is required", color = MaterialTheme.colorScheme.error) }
} else null
)
} else {
var expanded by remember { mutableStateOf(false) }
val availableGrades = selectedDifficultySystem.getAvailableGrades()
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = newProblemGrade,
onValueChange = { },
readOnly = true,
label = { Text("Grade *") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier
.menuAnchor()
.fillMaxWidth(),
isError = newProblemGrade.isBlank(),
supportingText = if (newProblemGrade.isBlank()) {
{ Text("Grade is required", color = MaterialTheme.colorScheme.error) }
} else null
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
availableGrades.forEach { grade ->
DropdownMenuItem(
text = { Text(grade) },
onClick = {
newProblemGrade = grade
expanded = false
}
)
}
}
}
}
}
}
}
@@ -109,7 +109,7 @@ fun GymCard(
AssistChip(
onClick = { },
label = {
Text(climbType.name.lowercase().replaceFirstChar { it.uppercase() })
Text(climbType.getDisplayName())
},
modifier = Modifier.padding(end = 4.dp)
)
@@ -119,7 +119,7 @@ fun GymCard(
if (gym.difficultySystems.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Systems: ${gym.difficultySystems.joinToString(", ") { it.name }}",
text = "Systems: ${gym.difficultySystems.joinToString(", ") { it.getDisplayName() }}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -2,6 +2,7 @@ package com.atridad.openclimb.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
@@ -14,6 +15,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.atridad.openclimb.R
import com.atridad.openclimb.data.model.ClimbType
import com.atridad.openclimb.data.model.Gym
import com.atridad.openclimb.data.model.Problem
import com.atridad.openclimb.ui.components.FullscreenImageViewer
import com.atridad.openclimb.ui.components.ImageDisplay
@@ -32,6 +35,17 @@ fun ProblemsScreen(
var selectedImagePaths by remember { mutableStateOf<List<String>>(emptyList()) }
var selectedImageIndex by remember { mutableStateOf(0) }
// Filter state
var selectedClimbType by remember { mutableStateOf<ClimbType?>(null) }
var selectedGym by remember { mutableStateOf<Gym?>(null) }
// Apply filters
val filteredProblems = problems.filter { problem ->
val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } ?: true
val gymMatch = selectedGym?.let { it.id == problem.gymId } ?: true
climbTypeMatch && gymMatch
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -57,16 +71,113 @@ fun ProblemsScreen(
Spacer(modifier = Modifier.height(16.dp))
if (problems.isEmpty()) {
// Filters Section
if (problems.isNotEmpty()) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
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
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
FilterChip(
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
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Gym Filter
Text(
text = "Gym",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
FilterChip(
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
)
}
}
// 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
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
if (filteredProblems.isEmpty()) {
EmptyStateMessage(
title = if (gyms.isEmpty()) "No Gyms Available" else "No Problems Yet",
message = if (gyms.isEmpty()) "Add a gym first to start tracking problems and routes!" else "Start tracking your favorite problems and routes!",
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(problems) { problem ->
items(filteredProblems) { problem ->
ProblemCard(
problem = problem,
gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym",
@@ -138,7 +249,7 @@ fun ProblemCard(
)
Text(
text = problem.climbType.name.lowercase().replaceFirstChar { it.uppercase() },
text = problem.climbType.getDisplayName(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)