diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a477080..d06020f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.atridad.openclimb" minSdk = 31 targetSdk = 35 - versionCode = 10 - versionName = "0.4.3" + versionCode = 11 + versionName = "0.4.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt b/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt index 8a6b689..7cabcf5 100644 --- a/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt +++ b/app/src/main/java/com/atridad/openclimb/ui/screens/DetailScreens.kt @@ -412,49 +412,53 @@ fun SessionDetailScreen(sessionId: String, viewModel: ClimbViewModel, onNavigate } - // Show grade range if available + // Show grade range(s) with better layout val grades = attemptedProblems.map { it.difficulty } if (grades.isNotEmpty()) { - // Separate boulder and rope problems val boulderProblems = attemptedProblems.filter { it.climbType == ClimbType.BOULDER } val ropeProblems = attemptedProblems.filter { it.climbType == ClimbType.ROPE } - - val gradeRange = when { - boulderProblems.isNotEmpty() && ropeProblems.isNotEmpty() -> { - val boulderRange = if (boulderProblems.isNotEmpty()) { - val boulderGrades = boulderProblems.map { it.difficulty } - val sortedBoulderGrades = boulderGrades.sortedWith { a, b -> a.compareTo(b) } - "${sortedBoulderGrades.first().grade} - ${sortedBoulderGrades.last().grade}" - } else null - - val ropeRange = if (ropeProblems.isNotEmpty()) { - val ropeGrades = ropeProblems.map { it.difficulty } - val sortedRopeGrades = ropeGrades.sortedWith { a, b -> a.compareTo(b) } - "${sortedRopeGrades.first().grade} - ${sortedRopeGrades.last().grade}" - } else null - - when { - boulderRange != null && ropeRange != null -> "$boulderRange / $ropeRange" - boulderRange != null -> boulderRange - ropeRange != null -> ropeRange - else -> "N/A" - } + + val boulderRange = if (boulderProblems.isNotEmpty()) { + val boulderGrades = boulderProblems.map { it.difficulty } + val sorted = boulderGrades.sortedWith { a, b -> a.compareTo(b) } + "${sorted.first().grade} - ${sorted.last().grade}" + } else null + + val ropeRange = if (ropeProblems.isNotEmpty()) { + val ropeGrades = ropeProblems.map { it.difficulty } + val sorted = ropeGrades.sortedWith { a, b -> a.compareTo(b) } + "${sorted.first().grade} - ${sorted.last().grade}" + } else null + + if (boulderRange != null && ropeRange != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + StatItem(label = "Boulder Range", value = boulderRange) + StatItem(label = "Rope Range", value = ropeRange) } - else -> { - val sortedGrades = grades.sortedWith { a, b -> a.compareTo(b) } - "${sortedGrades.first().grade} - ${sortedGrades.last().grade}" + } else { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + StatItem( + label = "Grade Range", + value = boulderRange ?: ropeRange ?: "N/A" + ) } } - - StatItem( - label = "Grade Range", - value = gradeRange - ) } else { - StatItem( - label = "Grade Range", - value = "N/A" - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + StatItem( + label = "Grade Range", + value = "N/A" + ) + } } } } diff --git a/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt b/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt index 098499c..4d16cfa 100644 --- a/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt +++ b/app/src/main/java/com/atridad/openclimb/utils/SessionShareUtils.kt @@ -172,8 +172,8 @@ object SessionShareUtils { stats: SessionStats ): File? { return try { - val width = 1080 - val height = 1350 + val width = 1242 // 3:4 aspect at higher resolution for better fit + val height = 1656 val bitmap = createBitmap(width, height) val canvas = Canvas(bitmap) @@ -227,7 +227,7 @@ object SessionShareUtils { } // Draw main card background - val cardRect = RectF(60f, 200f, width - 60f, height - 100f) + val cardRect = RectF(60f, 200f, width - 60f, height - 120f) canvas.drawRoundRect(cardRect, 40f, 40f, cardPaint) // Draw content @@ -248,31 +248,48 @@ object SessionShareUtils { // Stats grid val statsStartY = yPosition val columnWidth = width / 2f + val columnMaxTextWidth = columnWidth - 120f // Left column stats var leftY = statsStartY - drawStatItem(canvas, columnWidth / 2f, leftY, "Attempts", stats.totalAttempts.toString(), statLabelPaint, statValuePaint) - leftY += 140f - drawStatItem(canvas, columnWidth / 2f, leftY, "Problems", stats.uniqueProblemsAttempted.toString(), statLabelPaint, statValuePaint) - leftY += 140f - drawStatItem(canvas, columnWidth / 2f, leftY, "Duration", stats.sessionDuration, statLabelPaint, statValuePaint) + 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) + leftY += 120f + drawStatItemFitting(canvas, columnWidth / 2f, leftY, "Duration", stats.sessionDuration, statLabelPaint, statValuePaint, columnMaxTextWidth) // Right column stats var rightY = statsStartY - drawStatItem(canvas, width - columnWidth / 2f, rightY, "Successful", stats.successfulAttempts.toString(), statLabelPaint, statValuePaint) - rightY += 140f - drawStatItem(canvas, width - columnWidth / 2f, rightY, "Completed", stats.uniqueProblemsCompleted.toString(), statLabelPaint, statValuePaint) - rightY += 140f + drawStatItemFitting(canvas, width - columnWidth / 2f, rightY, "Successful", stats.successfulAttempts.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 -> - drawStatItem(canvas, width - columnWidth / 2f, rightY, "Top Grade", grade, statLabelPaint, statValuePaint) + 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 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) + } 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) } // Success rate arc - if (stats.totalAttempts > 0) { - val successRate = (stats.successfulAttempts.toFloat() / stats.totalAttempts) * 100 - drawSuccessRateArc(canvas, width / 2f, height - 280f, successRate, statLabelPaint, statValuePaint) - } + val successRate = if (stats.totalAttempts > 0) { + (stats.successfulAttempts.toFloat() / stats.totalAttempts) * 100f + } else 0f + drawSuccessRateArc(canvas, width / 2f, height - 300f, successRate, statLabelPaint, statValuePaint) // App branding val brandingPaint = Paint().apply { @@ -320,6 +337,41 @@ object SessionShareUtils { 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. + */ + private fun drawStatItemFitting( + canvas: Canvas, + x: Float, + y: Float, + label: String, + value: String, + labelPaint: Paint, + valuePaint: Paint, + maxTextWidth: Float + ) { + val tempPaint = Paint(valuePaint) + var textSize = tempPaint.textSize + var textWidth = tempPaint.measureText(value) + while (textWidth > maxTextWidth && textSize > 36f) { + textSize -= 2f + tempPaint.textSize = textSize + textWidth = tempPaint.measureText(value) + } + 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. + */ + private fun gradeRangeForProblems(problems: List): String? { + if (problems.isEmpty()) return null + val grades = problems.map { it.difficulty } + val sorted = grades.sortedWith { a, b -> a.compareTo(b) } + return "${sorted.first().grade} - ${sorted.last().grade}" + } + private fun drawSuccessRateArc( canvas: Canvas, centerX: Float, @@ -328,18 +380,18 @@ object SessionShareUtils { labelPaint: Paint, valuePaint: Paint ) { - val radius = 80f - val strokeWidth = 16f - + val radius = 70f + val strokeWidth = 14f + // Background arc val bgPaint = Paint().apply { - color = "#40FFFFFF".toColorInt() + color = "#30FFFFFF".toColorInt() style = Paint.Style.STROKE this.strokeWidth = strokeWidth isAntiAlias = true strokeCap = Paint.Cap.ROUND } - + // Success arc val successPaint = Paint().apply { color = "#4CAF50".toColorInt() @@ -348,20 +400,23 @@ object SessionShareUtils { isAntiAlias = true strokeCap = Paint.Cap.ROUND } - + val rect = RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius) - + // Draw background arc (full circle) canvas.drawArc(rect, -90f, 360f, false, bgPaint) - + // Draw success arc val sweepAngle = (successRate / 100f) * 360f canvas.drawArc(rect, -90f, sweepAngle, false, successPaint) - + // Draw percentage text val percentText = "${successRate.roundToInt()}%" - canvas.drawText(percentText, centerX, centerY + 10f, valuePaint) - canvas.drawText("Success Rate", centerX, centerY + 60f, labelPaint) + canvas.drawText(percentText, centerX, centerY + 8f, valuePaint) + + // Draw label below the arc (outside the ring) for better readability + val belowLabelPaint = Paint(labelPaint) + canvas.drawText("Success Rate", centerX, centerY + radius + 36f, belowLabelPaint) } private fun formatSessionDate(dateString: String): String {