Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0537da79e4
|
|||
|
4804049274
|
|||
|
8db6ed0e82
|
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user