Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
89f1e350b3
|
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user