Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c07186a7df
|
|||
|
15a5e217a5
|
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.atridad.openclimb"
|
applicationId = "com.atridad.openclimb"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 7
|
versionCode = 9
|
||||||
versionName = "0.4.0"
|
versionName = "0.4.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,40 +35,32 @@ object ImageUtils {
|
|||||||
*/
|
*/
|
||||||
fun saveImageFromUri(context: Context, imageUri: Uri): String? {
|
fun saveImageFromUri(context: Context, imageUri: Uri): String? {
|
||||||
return try {
|
return try {
|
||||||
val inputStream = context.contentResolver.openInputStream(imageUri)
|
// Decode bitmap from a fresh stream to avoid mark/reset dependency
|
||||||
inputStream?.use { input ->
|
val originalBitmap = context.contentResolver.openInputStream(imageUri)?.use { input ->
|
||||||
// Decode with options to get EXIF data
|
BitmapFactory.decodeStream(input)
|
||||||
val options = BitmapFactory.Options().apply {
|
} ?: return null
|
||||||
inJustDecodeBounds = true
|
|
||||||
}
|
val orientedBitmap = correctImageOrientation(context, imageUri, originalBitmap)
|
||||||
input.reset()
|
val compressedBitmap = compressImage(orientedBitmap)
|
||||||
BitmapFactory.decodeStream(input, null, options)
|
|
||||||
|
// Generate unique filename
|
||||||
// Reset stream and decode with proper orientation
|
val filename = "${UUID.randomUUID()}.jpg"
|
||||||
input.reset()
|
val imageFile = File(getImagesDirectory(context), filename)
|
||||||
val originalBitmap = BitmapFactory.decodeStream(input)
|
|
||||||
val orientedBitmap = correctImageOrientation(context, imageUri, originalBitmap)
|
// Save compressed image
|
||||||
val compressedBitmap = compressImage(orientedBitmap)
|
FileOutputStream(imageFile).use { output ->
|
||||||
|
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, IMAGE_QUALITY, output)
|
||||||
// Generate unique filename
|
|
||||||
val filename = "${UUID.randomUUID()}.jpg"
|
|
||||||
val imageFile = File(getImagesDirectory(context), filename)
|
|
||||||
|
|
||||||
// Save compressed image
|
|
||||||
FileOutputStream(imageFile).use { output ->
|
|
||||||
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, IMAGE_QUALITY, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up bitmaps
|
|
||||||
originalBitmap.recycle()
|
|
||||||
if (orientedBitmap != originalBitmap) {
|
|
||||||
orientedBitmap.recycle()
|
|
||||||
}
|
|
||||||
compressedBitmap.recycle()
|
|
||||||
|
|
||||||
// Return relative path
|
|
||||||
"$IMAGES_DIR/$filename"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up bitmaps
|
||||||
|
originalBitmap.recycle()
|
||||||
|
if (orientedBitmap != originalBitmap) {
|
||||||
|
orientedBitmap.recycle()
|
||||||
|
}
|
||||||
|
compressedBitmap.recycle()
|
||||||
|
|
||||||
|
// Return relative path
|
||||||
|
"$IMAGES_DIR/$filename"
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ object SessionShareUtils {
|
|||||||
val uniqueProblemsCompleted: Int,
|
val uniqueProblemsCompleted: Int,
|
||||||
val averageGrade: String?,
|
val averageGrade: String?,
|
||||||
val sessionDuration: String,
|
val sessionDuration: String,
|
||||||
val topResult: AttemptResult?
|
val topResult: AttemptResult?,
|
||||||
|
val topGrade: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
fun calculateSessionStats(
|
fun calculateSessionStats(
|
||||||
@@ -58,6 +59,19 @@ object SessionShareUtils {
|
|||||||
else -> null
|
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 duration = if (session.duration != null) "${session.duration}m" else "Unknown"
|
||||||
val topResult = attempts.maxByOrNull {
|
val topResult = attempts.maxByOrNull {
|
||||||
when (it.result) {
|
when (it.result) {
|
||||||
@@ -76,7 +90,8 @@ object SessionShareUtils {
|
|||||||
uniqueProblemsCompleted = uniqueCompletedProblems.size,
|
uniqueProblemsCompleted = uniqueCompletedProblems.size,
|
||||||
averageGrade = averageGrade,
|
averageGrade = averageGrade,
|
||||||
sessionDuration = duration,
|
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)
|
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Completed", stats.uniqueProblemsCompleted.toString(), statLabelPaint, statValuePaint)
|
||||||
rightY += 140f
|
rightY += 140f
|
||||||
|
|
||||||
stats.averageGrade?.let { grade ->
|
stats.topGrade?.let { grade ->
|
||||||
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Avg Grade", grade, statLabelPaint, statValuePaint)
|
drawStatItem(canvas, width - columnWidth / 2f, rightY, "Top Grade", grade, statLabelPaint, statValuePaint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success rate arc
|
// Success rate arc
|
||||||
@@ -383,4 +398,48 @@ object SessionShareUtils {
|
|||||||
e.printStackTrace()
|
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