1.3.0 - Graphing Fixes
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.atridad.openclimb"
|
applicationId = "com.atridad.openclimb"
|
||||||
minSdk = 34
|
minSdk = 34
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 18
|
versionCode = 19
|
||||||
versionName = "1.2.0"
|
versionName = "1.3.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.Canvas
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -31,12 +32,12 @@ data class ChartDataPoint(
|
|||||||
* Configuration for chart styling
|
* Configuration for chart styling
|
||||||
*/
|
*/
|
||||||
data class ChartStyle(
|
data class ChartStyle(
|
||||||
val lineColor: Color = Color(0xFF6366F1),
|
val lineColor: Color,
|
||||||
val fillColor: Color = Color(0x336366F1),
|
val fillColor: Color,
|
||||||
val lineWidth: Float = 3f,
|
val lineWidth: Float = 3f,
|
||||||
val gridColor: Color = Color(0xFFE5E7EB),
|
val gridColor: Color,
|
||||||
val textColor: Color = Color(0xFF374151),
|
val textColor: Color,
|
||||||
val backgroundColor: Color = Color.White
|
val backgroundColor: Color
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +47,13 @@ data class ChartStyle(
|
|||||||
fun LineChart(
|
fun LineChart(
|
||||||
data: List<ChartDataPoint>,
|
data: List<ChartDataPoint>,
|
||||||
modifier: Modifier = Modifier,
|
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,
|
showGrid: Boolean = true,
|
||||||
xAxisFormatter: (Float) -> String = { it.toString() },
|
xAxisFormatter: (Float) -> String = { it.toString() },
|
||||||
yAxisFormatter: (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 ->
|
screenPoints.forEach { point ->
|
||||||
|
// Draw outer circle (larger)
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = style.lineColor,
|
color = style.lineColor,
|
||||||
radius = style.lineWidth * 1.5f,
|
radius = 8f,
|
||||||
center = point
|
center = point
|
||||||
)
|
)
|
||||||
|
// Draw inner circle (white center)
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = style.backgroundColor,
|
color = style.backgroundColor,
|
||||||
radius = style.lineWidth * 0.8f,
|
radius = 5f,
|
||||||
center = point
|
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()
|
strokeWidth = 1.dp.toPx()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Draw label
|
// X-axis labels removed per user request
|
||||||
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()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -543,11 +543,18 @@ fun AddEditProblemScreen(
|
|||||||
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
|
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = difficultyGrade,
|
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 *") },
|
label = { Text("Grade *") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
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 {
|
} else {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|||||||
@@ -456,7 +456,10 @@ fun gradeToNumeric(system: DifficultySystem, grade: String): Int {
|
|||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DifficultySystem.CUSTOM -> 0
|
DifficultySystem.CUSTOM -> {
|
||||||
|
// Custom grades are numeric strings, so parse them directly
|
||||||
|
grade.toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.atridad.openclimb.ui.screens
|
|||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -35,7 +37,6 @@ import com.atridad.openclimb.ui.theme.CustomIcons
|
|||||||
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
|
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -291,8 +292,8 @@ fun SessionDetailScreen(
|
|||||||
|
|
||||||
// Show stop icon for active sessions, delete icon for completed sessions
|
// Show stop icon for active sessions, delete icon for completed sessions
|
||||||
if (session?.status == SessionStatus.ACTIVE) {
|
if (session?.status == SessionStatus.ACTIVE) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
session?.let { s ->
|
session.let { s ->
|
||||||
viewModel.endSession(context, s.id)
|
viewModel.endSession(context, s.id)
|
||||||
onNavigateBack()
|
onNavigateBack()
|
||||||
}
|
}
|
||||||
@@ -916,11 +917,6 @@ fun GymDetailScreen(
|
|||||||
problems.any { problem -> problem.id == attempt.problemId }
|
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 uniqueProblemsClimbed = gymAttempts.map { it.problemId }.toSet().size
|
||||||
val totalSessions = sessions.size
|
val totalSessions = sessions.size
|
||||||
val activeSessions = sessions.count { it.status == SessionStatus.ACTIVE }
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EnhancedAddAttemptDialog(
|
fun EnhancedAddAttemptDialog(
|
||||||
@@ -1835,8 +1761,12 @@ fun EnhancedAddAttemptDialog(
|
|||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
TextButton(onClick = { showCreateProblem = false }) {
|
IconButton(onClick = { showCreateProblem = false }) {
|
||||||
Text("← Back", color = MaterialTheme.colorScheme.primary)
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1930,9 +1860,14 @@ fun EnhancedAddAttemptDialog(
|
|||||||
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
|
if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = newProblemGrade,
|
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 *") },
|
label = { Text("Grade *") },
|
||||||
placeholder = { Text("Enter custom grade") },
|
placeholder = { Text("Enter numeric grade (e.g. 5, 10, 15)") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
colors =
|
colors =
|
||||||
@@ -1943,6 +1878,7 @@ fun EnhancedAddAttemptDialog(
|
|||||||
MaterialTheme.colorScheme.outline
|
MaterialTheme.colorScheme.outline
|
||||||
),
|
),
|
||||||
isError = newProblemGrade.isBlank(),
|
isError = newProblemGrade.isBlank(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
supportingText =
|
supportingText =
|
||||||
if (newProblemGrade.isBlank()) {
|
if (newProblemGrade.isBlank()) {
|
||||||
{
|
{
|
||||||
@@ -1951,7 +1887,14 @@ fun EnhancedAddAttemptDialog(
|
|||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else null
|
} else {
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
"Custom grades must be whole numbers",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|||||||
Reference in New Issue
Block a user