Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c07186a7df
|
|||
|
15a5e217a5
|
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.atridad.openclimb"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 7
|
||||
versionName = "0.4.0"
|
||||
versionCode = 9
|
||||
versionName = "0.4.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -35,18 +35,11 @@ object ImageUtils {
|
||||
*/
|
||||
fun saveImageFromUri(context: Context, imageUri: Uri): String? {
|
||||
return try {
|
||||
val inputStream = context.contentResolver.openInputStream(imageUri)
|
||||
inputStream?.use { input ->
|
||||
// Decode with options to get EXIF data
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
input.reset()
|
||||
BitmapFactory.decodeStream(input, null, options)
|
||||
// Decode bitmap from a fresh stream to avoid mark/reset dependency
|
||||
val originalBitmap = context.contentResolver.openInputStream(imageUri)?.use { input ->
|
||||
BitmapFactory.decodeStream(input)
|
||||
} ?: return null
|
||||
|
||||
// Reset stream and decode with proper orientation
|
||||
input.reset()
|
||||
val originalBitmap = BitmapFactory.decodeStream(input)
|
||||
val orientedBitmap = correctImageOrientation(context, imageUri, originalBitmap)
|
||||
val compressedBitmap = compressImage(orientedBitmap)
|
||||
|
||||
@@ -68,7 +61,6 @@ object ImageUtils {
|
||||
|
||||
// Return relative path
|
||||
"$IMAGES_DIR/$filename"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
||||
@@ -24,7 +24,8 @@ object SessionShareUtils {
|
||||
val uniqueProblemsCompleted: Int,
|
||||
val averageGrade: String?,
|
||||
val sessionDuration: String,
|
||||
val topResult: AttemptResult?
|
||||
val topResult: AttemptResult?,
|
||||
val topGrade: String?
|
||||
)
|
||||
|
||||
fun calculateSessionStats(
|
||||
@@ -58,6 +59,19 @@ object SessionShareUtils {
|
||||
else -> null
|
||||
}
|
||||
|
||||
// Determine highest achieved grade (only from completed problems: SUCCESS or FLASH)
|
||||
val completedProblems = problems.filter { it.id in uniqueCompletedProblems }
|
||||
val completedBoulder = completedProblems.filter { it.climbType == ClimbType.BOULDER }
|
||||
val completedRope = completedProblems.filter { it.climbType == ClimbType.ROPE }
|
||||
val topBoulder = highestGradeForProblems(completedBoulder)
|
||||
val topRope = highestGradeForProblems(completedRope)
|
||||
val topGrade = when {
|
||||
topBoulder != null && topRope != null -> "$topBoulder / $topRope"
|
||||
topBoulder != null -> topBoulder
|
||||
topRope != null -> topRope
|
||||
else -> null
|
||||
}
|
||||
|
||||
val duration = if (session.duration != null) "${session.duration}m" else "Unknown"
|
||||
val topResult = attempts.maxByOrNull {
|
||||
when (it.result) {
|
||||
@@ -76,7 +90,8 @@ object SessionShareUtils {
|
||||
uniqueProblemsCompleted = uniqueCompletedProblems.size,
|
||||
averageGrade = averageGrade,
|
||||
sessionDuration = duration,
|
||||
topResult = topResult
|
||||
topResult = topResult,
|
||||
topGrade = topGrade
|
||||
)
|
||||
}
|
||||
|
||||
@@ -249,8 +264,8 @@ object SessionShareUtils {
|
||||
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Completed", stats.uniqueProblemsCompleted.toString(), statLabelPaint, statValuePaint)
|
||||
rightY += 140f
|
||||
|
||||
stats.averageGrade?.let { grade ->
|
||||
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Avg Grade", grade, statLabelPaint, statValuePaint)
|
||||
stats.topGrade?.let { grade ->
|
||||
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Top Grade", grade, statLabelPaint, statValuePaint)
|
||||
}
|
||||
|
||||
// Success rate arc
|
||||
@@ -383,4 +398,48 @@ object SessionShareUtils {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest grade string among the given problems, respecting their difficulty system.
|
||||
*/
|
||||
private fun highestGradeForProblems(problems: List<Problem>): String? {
|
||||
if (problems.isEmpty()) return null
|
||||
return problems.maxByOrNull { p -> gradeRank(p.difficulty.system, p.difficulty.grade) }?.difficulty?.grade
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a comparable numeric rank for grades across supported systems.
|
||||
*/
|
||||
private fun gradeRank(system: DifficultySystem, grade: String): Double {
|
||||
return when (system) {
|
||||
DifficultySystem.V_SCALE -> {
|
||||
if (grade == "VB") 0.0 else grade.removePrefix("V").toDoubleOrNull() ?: -1.0
|
||||
}
|
||||
DifficultySystem.FONT -> {
|
||||
val list = DifficultySystem.FONT.getAvailableGrades()
|
||||
val idx = list.indexOf(grade.uppercase())
|
||||
if (idx >= 0) idx.toDouble() else grade.filter { it.isDigit() }.toDoubleOrNull() ?: -1.0
|
||||
}
|
||||
DifficultySystem.YDS -> {
|
||||
// Parse 5.X with optional letter a-d
|
||||
val s = grade.lowercase()
|
||||
if (!s.startsWith("5.")) return -1.0
|
||||
val tail = s.removePrefix("5.")
|
||||
val numberPart = tail.takeWhile { it.isDigit() || it == '.' }
|
||||
val letterPart = tail.drop(numberPart.length).firstOrNull()
|
||||
val base = numberPart.toDoubleOrNull() ?: return -1.0
|
||||
val letterWeight = when (letterPart) {
|
||||
'a' -> 0.0
|
||||
'b' -> 0.1
|
||||
'c' -> 0.2
|
||||
'd' -> 0.3
|
||||
else -> 0.0
|
||||
}
|
||||
base + letterWeight
|
||||
}
|
||||
DifficultySystem.CUSTOM -> {
|
||||
grade.filter { it.isDigit() || it == '.' || it == '-' }.toDoubleOrNull() ?: -1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user