Compare commits

...

3 Commits
1.2.0 ... 1.3.1

Author SHA1 Message Date
0537da79e4 1.3.1 - Graphing Fixes Cont'd 2025-08-31 19:05:18 -06:00
4804049274 1.3.1 - Graphing Fixes Cont'd 2025-08-31 19:03:43 -06:00
8db6ed0e82 1.3.0 - Graphing Fixes 2025-08-28 00:18:54 -06:00
6 changed files with 72 additions and 116 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.atridad.openclimb"
minSdk = 34
targetSdk = 36
versionCode = 18
versionName = "1.2.0"
versionCode = 20
versionName = "1.3.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -4,6 +4,7 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -31,12 +32,12 @@ data class ChartDataPoint(
* Configuration for chart styling
*/
data class ChartStyle(
val lineColor: Color = Color(0xFF6366F1),
val fillColor: Color = Color(0x336366F1),
val lineColor: Color,
val fillColor: Color,
val lineWidth: Float = 3f,
val gridColor: Color = Color(0xFFE5E7EB),
val textColor: Color = Color(0xFF374151),
val backgroundColor: Color = Color.White
val gridColor: Color,
val textColor: Color,
val backgroundColor: Color
)
/**
@@ -46,7 +47,13 @@ data class ChartStyle(
fun LineChart(
data: List<ChartDataPoint>,
modifier: Modifier = Modifier,
style: ChartStyle = ChartStyle(),
style: ChartStyle = ChartStyle(
lineColor = MaterialTheme.colorScheme.primary,
fillColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
gridColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
backgroundColor = MaterialTheme.colorScheme.surface
),
showGrid: Boolean = true,
xAxisFormatter: (Float) -> String = { it.toString() },
yAxisFormatter: (Float) -> String = { it.toString() }
@@ -136,18 +143,27 @@ fun LineChart(
)
}
// Draw data points
// Draw data points - more pronounced
screenPoints.forEach { point ->
// Draw outer circle (larger)
drawCircle(
color = style.lineColor,
radius = style.lineWidth * 1.5f,
radius = 8f,
center = point
)
// Draw inner circle (white center)
drawCircle(
color = style.backgroundColor,
radius = style.lineWidth * 0.8f,
radius = 5f,
center = point
)
// Draw border for better visibility
drawCircle(
color = style.lineColor,
radius = 8f,
center = point,
style = Stroke(width = 2f)
)
}
}
}
@@ -190,18 +206,7 @@ private fun DrawScope.drawGrid(
strokeWidth = 1.dp.toPx()
)
// Draw label
val text = xAxisFormatter(sessionNum.toFloat())
val textSize = textMeasurer.measure(text, textStyle)
drawText(
textMeasurer = textMeasurer,
text = text,
style = textStyle,
topLeft = Offset(
x - textSize.size.width / 2f,
padding + chartHeight + 8.dp.toPx()
)
)
// X-axis labels removed per user request
}
}

View File

@@ -543,11 +543,18 @@ fun AddEditProblemScreen(
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
OutlinedTextField(
value = difficultyGrade,
onValueChange = { difficultyGrade = it },
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 custom grade") }
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) }

View File

@@ -132,14 +132,12 @@ fun ProgressChartCard(
progressData: List<ProgressDataPoint>,
problems: List<com.atridad.openclimb.data.model.Problem>,
) {
// Find all grading systems that have been used
val usedSystems = remember(problems) {
problems.map { it.difficulty.system }.distinct().filter { system ->
problems.any { it.difficulty.system == system }
}
// Find all grading systems that have been used in the progress data
val usedSystems = remember(progressData) {
progressData.map { it.difficultySystem }.distinct()
}
var selectedSystem by remember {
var selectedSystem by remember(usedSystems) {
mutableStateOf(usedSystems.firstOrNull() ?: DifficultySystem.V_SCALE)
}
var expanded by remember { mutableStateOf(false) }
@@ -456,7 +454,10 @@ fun gradeToNumeric(system: DifficultySystem, grade: String): Int {
else -> 0
}
}
DifficultySystem.CUSTOM -> 0
DifficultySystem.CUSTOM -> {
// Custom grades are numeric strings, so parse them directly
grade.toIntOrNull() ?: 0
}
}
}

View File

@@ -3,6 +3,7 @@ package com.atridad.openclimb.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@@ -25,6 +26,7 @@ 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.window.Dialog
import androidx.lifecycle.viewModelScope
@@ -35,7 +37,6 @@ import com.atridad.openclimb.ui.theme.CustomIcons
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -291,8 +292,8 @@ fun SessionDetailScreen(
// Show stop icon for active sessions, delete icon for completed sessions
if (session?.status == SessionStatus.ACTIVE) {
IconButton(onClick = {
session?.let { s ->
IconButton(onClick = {
session.let { s ->
viewModel.endSession(context, s.id)
onNavigateBack()
}
@@ -916,11 +917,6 @@ fun GymDetailScreen(
problems.any { problem -> problem.id == attempt.problemId }
}
val successfulAttempts =
gymAttempts.filter { it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH) }
val uniqueProblemsClimbed = gymAttempts.map { it.problemId }.toSet().size
val totalSessions = sessions.size
val activeSessions = sessions.count { it.status == SessionStatus.ACTIVE }
@@ -1556,76 +1552,6 @@ private fun formatDate(dateString: String): String {
}
}
/**
* Calculate average grade for a specific set of problems, respecting their difficulty systems
*/
private fun calculateAverageGrade(problems: List<Problem>): String? {
if (problems.isEmpty()) return null
// Group problems by difficulty system
val problemsBySystem = problems.groupBy { it.difficulty.system }
val averages = mutableListOf<String>()
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()
}
}
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()
}
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
}
if (gradeValues.isNotEmpty()) {
val avg = gradeValues.average()
averages.add("5.${String.format("%.1f", avg)}")
}
}
DifficultySystem.CUSTOM -> {
// For custom systems, try to extract numeric values
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))
}
}
}
}
return if (averages.isNotEmpty()) {
if (averages.size == 1) {
averages.first()
} else {
averages.joinToString(" / ")
}
} else null
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EnhancedAddAttemptDialog(
@@ -1835,8 +1761,12 @@ fun EnhancedAddAttemptDialog(
color = MaterialTheme.colorScheme.onSurface
)
TextButton(onClick = { showCreateProblem = false }) {
Text("← Back", color = MaterialTheme.colorScheme.primary)
IconButton(onClick = { showCreateProblem = false }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -1930,9 +1860,14 @@ fun EnhancedAddAttemptDialog(
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
OutlinedTextField(
value = newProblemGrade,
onValueChange = { newProblemGrade = it },
onValueChange = { newValue ->
// Only allow integers for custom scales
if (newValue.isEmpty() || newValue.all { it.isDigit() }) {
newProblemGrade = newValue
}
},
label = { Text("Grade *") },
placeholder = { Text("Enter custom grade") },
placeholder = { Text("Enter numeric grade (e.g. 5, 10, 15)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors =
@@ -1943,6 +1878,7 @@ fun EnhancedAddAttemptDialog(
MaterialTheme.colorScheme.outline
),
isError = newProblemGrade.isBlank(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
supportingText =
if (newProblemGrade.isBlank()) {
{
@@ -1951,7 +1887,14 @@ fun EnhancedAddAttemptDialog(
color = MaterialTheme.colorScheme.error
)
}
} else null
} else {
{
Text(
"Custom grades must be whole numbers",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
)
} else {
var expanded by remember { mutableStateOf(false) }

View File

@@ -1,5 +1,5 @@
[versions]
agp = "8.12.1"
agp = "8.12.2"
kotlin = "2.2.10"
coreKtx = "1.17.0"
junit = "4.13.2"