Compare commits

...

1 Commits
0.4.3 ... 0.4.4

Author SHA1 Message Date
89f1e350b3 0.4.4 - Cleaned up range views 2025-08-17 01:29:15 -06:00
3 changed files with 124 additions and 65 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.atridad.openclimb" applicationId = "com.atridad.openclimb"
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = 35
versionCode = 10 versionCode = 11
versionName = "0.4.3" versionName = "0.4.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -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 } val grades = attemptedProblems.map { it.difficulty }
if (grades.isNotEmpty()) { if (grades.isNotEmpty()) {
// Separate boulder and rope problems
val boulderProblems = attemptedProblems.filter { it.climbType == ClimbType.BOULDER } val boulderProblems = attemptedProblems.filter { it.climbType == ClimbType.BOULDER }
val ropeProblems = attemptedProblems.filter { it.climbType == ClimbType.ROPE } val ropeProblems = attemptedProblems.filter { it.climbType == ClimbType.ROPE }
val gradeRange = when { val boulderRange = if (boulderProblems.isNotEmpty()) {
boulderProblems.isNotEmpty() && ropeProblems.isNotEmpty() -> { val boulderGrades = boulderProblems.map { it.difficulty }
val boulderRange = if (boulderProblems.isNotEmpty()) { val sorted = boulderGrades.sortedWith { a, b -> a.compareTo(b) }
val boulderGrades = boulderProblems.map { it.difficulty } "${sorted.first().grade} - ${sorted.last().grade}"
val sortedBoulderGrades = boulderGrades.sortedWith { a, b -> a.compareTo(b) } } else null
"${sortedBoulderGrades.first().grade} - ${sortedBoulderGrades.last().grade}"
} else null
val ropeRange = if (ropeProblems.isNotEmpty()) { val ropeRange = if (ropeProblems.isNotEmpty()) {
val ropeGrades = ropeProblems.map { it.difficulty } val ropeGrades = ropeProblems.map { it.difficulty }
val sortedRopeGrades = ropeGrades.sortedWith { a, b -> a.compareTo(b) } val sorted = ropeGrades.sortedWith { a, b -> a.compareTo(b) }
"${sortedRopeGrades.first().grade} - ${sortedRopeGrades.last().grade}" "${sorted.first().grade} - ${sorted.last().grade}"
} else null } else null
when { if (boulderRange != null && ropeRange != null) {
boulderRange != null && ropeRange != null -> "$boulderRange / $ropeRange" Row(
boulderRange != null -> boulderRange modifier = Modifier.fillMaxWidth(),
ropeRange != null -> ropeRange horizontalArrangement = Arrangement.SpaceEvenly
else -> "N/A" ) {
} StatItem(label = "Boulder Range", value = boulderRange)
StatItem(label = "Rope Range", value = ropeRange)
} }
else -> { } else {
val sortedGrades = grades.sortedWith { a, b -> a.compareTo(b) } Row(
"${sortedGrades.first().grade} - ${sortedGrades.last().grade}" modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
StatItem(
label = "Grade Range",
value = boulderRange ?: ropeRange ?: "N/A"
)
} }
} }
StatItem(
label = "Grade Range",
value = gradeRange
)
} else { } else {
StatItem( Row(
label = "Grade Range", modifier = Modifier.fillMaxWidth(),
value = "N/A" horizontalArrangement = Arrangement.Center
) ) {
StatItem(
label = "Grade Range",
value = "N/A"
)
}
} }
} }
} }

View File

@@ -172,8 +172,8 @@ object SessionShareUtils {
stats: SessionStats stats: SessionStats
): File? { ): File? {
return try { return try {
val width = 1080 val width = 1242 // 3:4 aspect at higher resolution for better fit
val height = 1350 val height = 1656
val bitmap = createBitmap(width, height) val bitmap = createBitmap(width, height)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
@@ -227,7 +227,7 @@ object SessionShareUtils {
} }
// Draw main card background // 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) canvas.drawRoundRect(cardRect, 40f, 40f, cardPaint)
// Draw content // Draw content
@@ -248,31 +248,48 @@ object SessionShareUtils {
// Stats grid // Stats grid
val statsStartY = yPosition val statsStartY = yPosition
val columnWidth = width / 2f val columnWidth = width / 2f
val columnMaxTextWidth = columnWidth - 120f
// Left column stats // Left column stats
var leftY = statsStartY var leftY = statsStartY
drawStatItem(canvas, columnWidth / 2f, leftY, "Attempts", stats.totalAttempts.toString(), statLabelPaint, statValuePaint) drawStatItemFitting(canvas, columnWidth / 2f, leftY, "Attempts", stats.totalAttempts.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth)
leftY += 140f leftY += 120f
drawStatItem(canvas, columnWidth / 2f, leftY, "Problems", stats.uniqueProblemsAttempted.toString(), statLabelPaint, statValuePaint) drawStatItemFitting(canvas, columnWidth / 2f, leftY, "Problems", stats.uniqueProblemsAttempted.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth)
leftY += 140f leftY += 120f
drawStatItem(canvas, columnWidth / 2f, leftY, "Duration", stats.sessionDuration, statLabelPaint, statValuePaint) drawStatItemFitting(canvas, columnWidth / 2f, leftY, "Duration", stats.sessionDuration, statLabelPaint, statValuePaint, columnMaxTextWidth)
// Right column stats // Right column stats
var rightY = statsStartY var rightY = statsStartY
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Successful", stats.successfulAttempts.toString(), statLabelPaint, statValuePaint) drawStatItemFitting(canvas, width - columnWidth / 2f, rightY, "Successful", stats.successfulAttempts.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth)
rightY += 140f rightY += 120f
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Completed", stats.uniqueProblemsCompleted.toString(), statLabelPaint, statValuePaint) drawStatItemFitting(canvas, width - columnWidth / 2f, rightY, "Completed", stats.uniqueProblemsCompleted.toString(), statLabelPaint, statValuePaint, columnMaxTextWidth)
rightY += 140f rightY += 120f
var rightYAfter = rightY
stats.topGrade?.let { grade -> 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 // Success rate arc
if (stats.totalAttempts > 0) { val successRate = if (stats.totalAttempts > 0) {
val successRate = (stats.successfulAttempts.toFloat() / stats.totalAttempts) * 100 (stats.successfulAttempts.toFloat() / stats.totalAttempts) * 100f
drawSuccessRateArc(canvas, width / 2f, height - 280f, successRate, statLabelPaint, statValuePaint) } else 0f
} drawSuccessRateArc(canvas, width / 2f, height - 300f, successRate, statLabelPaint, statValuePaint)
// App branding // App branding
val brandingPaint = Paint().apply { val brandingPaint = Paint().apply {
@@ -320,6 +337,41 @@ object SessionShareUtils {
canvas.drawText(label, x, y + 50f, labelPaint) 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<Problem>): 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( private fun drawSuccessRateArc(
canvas: Canvas, canvas: Canvas,
centerX: Float, centerX: Float,
@@ -328,12 +380,12 @@ object SessionShareUtils {
labelPaint: Paint, labelPaint: Paint,
valuePaint: Paint valuePaint: Paint
) { ) {
val radius = 80f val radius = 70f
val strokeWidth = 16f val strokeWidth = 14f
// Background arc // Background arc
val bgPaint = Paint().apply { val bgPaint = Paint().apply {
color = "#40FFFFFF".toColorInt() color = "#30FFFFFF".toColorInt()
style = Paint.Style.STROKE style = Paint.Style.STROKE
this.strokeWidth = strokeWidth this.strokeWidth = strokeWidth
isAntiAlias = true isAntiAlias = true
@@ -360,8 +412,11 @@ object SessionShareUtils {
// Draw percentage text // Draw percentage text
val percentText = "${successRate.roundToInt()}%" val percentText = "${successRate.roundToInt()}%"
canvas.drawText(percentText, centerX, centerY + 10f, valuePaint) canvas.drawText(percentText, centerX, centerY + 8f, valuePaint)
canvas.drawText("Success Rate", centerX, centerY + 60f, labelPaint)
// 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 { private fun formatSessionDate(dateString: String): String {