0.3.2 - Optimizations
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,15 +14,15 @@ android {
|
|||||||
applicationId = "com.atridad.openclimb"
|
applicationId = "com.atridad.openclimb"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 4
|
versionCode = 5
|
||||||
versionName = "0.3.1"
|
versionName = "0.3.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ data class ClimbSession(
|
|||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: String,
|
val id: String,
|
||||||
val gymId: String,
|
val gymId: String,
|
||||||
val date: String, // ISO date string
|
val date: String,
|
||||||
val startTime: String? = null, // When session was started
|
val startTime: String? = null,
|
||||||
val endTime: String? = null, // When session was completed
|
val endTime: String? = null,
|
||||||
val duration: Long? = null, // Duration in minutes (calculated when completed)
|
val duration: Long? = null,
|
||||||
val status: SessionStatus = SessionStatus.ACTIVE,
|
val status: SessionStatus = SessionStatus.ACTIVE,
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ enum class ClimbType {
|
|||||||
BOULDER;
|
BOULDER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the display name for the UI
|
* Get the display name
|
||||||
*/
|
*/
|
||||||
fun getDisplayName(): String = when (this) {
|
fun getDisplayName(): String = when (this) {
|
||||||
ROPE -> "Rope"
|
ROPE -> "Rope"
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class DifficultySystem {
|
enum class DifficultySystem {
|
||||||
// Bouldering systems
|
// Bouldering
|
||||||
V_SCALE, // V-Scale (VB - V17)
|
V_SCALE, // V-Scale (VB - V17)
|
||||||
FONT, // Fontainebleau (3 - 8C+)
|
FONT, // Fontainebleau (3 - 8C+)
|
||||||
|
|
||||||
// Rope climbing systems
|
// Rope
|
||||||
YDS, // Yosemite Decimal System (5.0 - 5.15d)
|
YDS, // Yosemite Decimal System (5.0 - 5.15d)
|
||||||
|
|
||||||
// Custom system for gyms that use their own colors/naming
|
// Custom difficulty systems
|
||||||
CUSTOM;
|
CUSTOM;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,22 +39,22 @@ enum class DifficultySystem {
|
|||||||
fun isRopeSystem(): Boolean = when (this) {
|
fun isRopeSystem(): Boolean = when (this) {
|
||||||
YDS -> true
|
YDS -> true
|
||||||
V_SCALE, FONT -> false
|
V_SCALE, FONT -> false
|
||||||
CUSTOM -> true // Custom is available for all
|
CUSTOM -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get available grades for this difficulty system
|
* Get available grades for this system
|
||||||
*/
|
*/
|
||||||
fun getAvailableGrades(): List<String> = when (this) {
|
fun getAvailableGrades(): List<String> = when (this) {
|
||||||
V_SCALE -> listOf("VB", "V0", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "V12", "V13", "V14", "V15", "V16", "V17")
|
V_SCALE -> listOf("VB", "V0", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "V12", "V13", "V14", "V15", "V16", "V17")
|
||||||
FONT -> listOf("3", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6A+", "6B", "6B+", "6C", "6C+", "7A", "7A+", "7B", "7B+", "7C", "7C+", "8A", "8A+", "8B", "8B+", "8C", "8C+")
|
FONT -> listOf("3", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6A+", "6B", "6B+", "6C", "6C+", "7A", "7A+", "7B", "7B+", "7C", "7C+", "8A", "8A+", "8B", "8B+", "8C", "8C+")
|
||||||
YDS -> listOf("5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "5.10a", "5.10b", "5.10c", "5.10d", "5.11a", "5.11b", "5.11c", "5.11d", "5.12a", "5.12b", "5.12c", "5.12d", "5.13a", "5.13b", "5.13c", "5.13d", "5.14a", "5.14b", "5.14c", "5.14d", "5.15a", "5.15b", "5.15c", "5.15d")
|
YDS -> listOf("5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "5.10a", "5.10b", "5.10c", "5.10d", "5.11a", "5.11b", "5.11c", "5.11d", "5.12a", "5.12b", "5.12c", "5.12d", "5.13a", "5.13b", "5.13c", "5.13d", "5.14a", "5.14b", "5.14c", "5.14d", "5.15a", "5.15b", "5.15c", "5.15d")
|
||||||
CUSTOM -> emptyList() // Custom allows free text input
|
CUSTOM -> emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Get all difficulty systems available for a specific climb type
|
* Get all difficulty systems based on type
|
||||||
*/
|
*/
|
||||||
fun getSystemsForClimbType(climbType: ClimbType): List<DifficultySystem> = when (climbType) {
|
fun getSystemsForClimbType(climbType: ClimbType): List<DifficultySystem> = when (climbType) {
|
||||||
ClimbType.BOULDER -> entries.filter { it.isBoulderingSystem() }
|
ClimbType.BOULDER -> entries.filter { it.isBoulderingSystem() }
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ data class Gym(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val location: String? = null,
|
val location: String? = null,
|
||||||
val supportedClimbTypes: List<ClimbType>,
|
val supportedClimbTypes: List<ClimbType>,
|
||||||
val difficultySystems: List<DifficultySystem>, // What systems this gym uses
|
val difficultySystems: List<DifficultySystem>,
|
||||||
val customDifficultyGrades: List<String> = emptyList(), // For gyms using colors/custom names
|
val customDifficultyGrades: List<String> = emptyList(),
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
val createdAt: String, // ISO string format for serialization
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ data class Problem(
|
|||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val climbType: ClimbType,
|
val climbType: ClimbType,
|
||||||
val difficulty: DifficultyGrade,
|
val difficulty: DifficultyGrade,
|
||||||
val setter: String? = null, // Route setter name
|
val setter: String? = null,
|
||||||
val tags: List<String> = emptyList(), // e.g., "overhang", "slab", "crimpy"
|
val tags: List<String> = emptyList(),
|
||||||
val location: String? = null, // Wall section, area in gym
|
val location: String? = null,
|
||||||
val imagePaths: List<String> = emptyList(), // Local file paths to photos
|
val imagePaths: List<String> = emptyList(),
|
||||||
val isActive: Boolean = true, // Whether the problem is still up
|
val isActive: Boolean = true,
|
||||||
val dateSet: String? = null, // When the problem was set
|
val dateSet: String? = null,
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.atridad.openclimb.data.model
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ProblemProgress(
|
|
||||||
val problemId: String,
|
|
||||||
val totalAttempts: Int,
|
|
||||||
val successfulAttempts: Int,
|
|
||||||
val firstAttemptDate: String,
|
|
||||||
val lastAttemptDate: String,
|
|
||||||
val bestResult: AttemptResult,
|
|
||||||
val averageAttempts: Double,
|
|
||||||
val successRate: Double,
|
|
||||||
val personalBest: String? = null, // Highest hold or completion details
|
|
||||||
val notes: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SessionSummary(
|
|
||||||
val sessionId: String,
|
|
||||||
val date: String,
|
|
||||||
val totalAttempts: Int,
|
|
||||||
val successfulAttempts: Int,
|
|
||||||
val uniqueProblems: Int,
|
|
||||||
val avgDifficulty: Double,
|
|
||||||
val maxDifficulty: DifficultyGrade,
|
|
||||||
val climbTypes: List<ClimbType>,
|
|
||||||
val duration: Long?, // in minutes
|
|
||||||
val notes: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ClimbingStats(
|
|
||||||
val totalSessions: Int,
|
|
||||||
val totalAttempts: Int,
|
|
||||||
val totalSuccesses: Int,
|
|
||||||
val overallSuccessRate: Double,
|
|
||||||
val uniqueProblemsAttempted: Int,
|
|
||||||
val uniqueProblemsCompleted: Int,
|
|
||||||
val averageSessionDuration: Double, // in minutes
|
|
||||||
val favoriteGym: String?,
|
|
||||||
val mostAttemptedDifficulty: DifficultyGrade?,
|
|
||||||
val currentStreak: Int, // consecutive sessions
|
|
||||||
val longestStreak: Int,
|
|
||||||
val firstClimbDate: String?,
|
|
||||||
val lastClimbDate: String?,
|
|
||||||
val improvementTrend: String? = null // "improving", "stable", "declining"
|
|
||||||
)
|
|
||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import com.atridad.openclimb.data.database.OpenClimbDatabase
|
import com.atridad.openclimb.data.database.OpenClimbDatabase
|
||||||
import com.atridad.openclimb.data.model.*
|
import com.atridad.openclimb.data.model.*
|
||||||
import com.atridad.openclimb.utils.ImageUtils
|
|
||||||
import com.atridad.openclimb.utils.ZipExportImportUtils
|
import com.atridad.openclimb.utils.ZipExportImportUtils
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -14,7 +13,7 @@ import java.io.File
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
class ClimbRepository(
|
class ClimbRepository(
|
||||||
private val database: OpenClimbDatabase,
|
database: OpenClimbDatabase,
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) {
|
) {
|
||||||
private val gymDao = database.gymDao()
|
private val gymDao = database.gymDao()
|
||||||
@@ -40,7 +39,6 @@ class ClimbRepository(
|
|||||||
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
|
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
|
||||||
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
|
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
|
||||||
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
|
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
|
||||||
fun getActiveProblems(): Flow<List<Problem>> = problemDao.getActiveProblems()
|
|
||||||
suspend fun insertProblem(problem: Problem) = problemDao.insertProblem(problem)
|
suspend fun insertProblem(problem: Problem) = problemDao.insertProblem(problem)
|
||||||
suspend fun updateProblem(problem: Problem) = problemDao.updateProblem(problem)
|
suspend fun updateProblem(problem: Problem) = problemDao.updateProblem(problem)
|
||||||
suspend fun deleteProblem(problem: Problem) = problemDao.deleteProblem(problem)
|
suspend fun deleteProblem(problem: Problem) = problemDao.deleteProblem(problem)
|
||||||
@@ -50,17 +48,14 @@ class ClimbRepository(
|
|||||||
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
||||||
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
||||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> = sessionDao.getSessionsByGym(gymId)
|
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> = sessionDao.getSessionsByGym(gymId)
|
||||||
fun getRecentSessions(limit: Int = 10): Flow<List<ClimbSession>> = sessionDao.getRecentSessions(limit)
|
|
||||||
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
|
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
|
||||||
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
|
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
|
||||||
fun getSessionsByStatus(status: SessionStatus): Flow<List<ClimbSession>> = sessionDao.getSessionsByStatus(status)
|
|
||||||
suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session)
|
suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session)
|
||||||
suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session)
|
suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session)
|
||||||
suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session)
|
suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session)
|
||||||
|
|
||||||
// Attempt operations
|
// Attempt operations
|
||||||
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
||||||
suspend fun getAttemptById(id: String): Attempt? = attemptDao.getAttemptById(id)
|
|
||||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> = attemptDao.getAttemptsBySession(sessionId)
|
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> = attemptDao.getAttemptsBySession(sessionId)
|
||||||
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> = attemptDao.getAttemptsByProblem(problemId)
|
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> = attemptDao.getAttemptsByProblem(problemId)
|
||||||
suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt)
|
suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt)
|
||||||
@@ -69,7 +64,7 @@ class ClimbRepository(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// JSON Export functionality
|
// JSON Export
|
||||||
suspend fun exportAllDataToJson(directory: File? = null): File {
|
suspend fun exportAllDataToJson(directory: File? = null): File {
|
||||||
val exportDir = directory ?: File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "OpenClimb")
|
val exportDir = directory ?: File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "OpenClimb")
|
||||||
if (!exportDir.exists()) {
|
if (!exportDir.exists()) {
|
||||||
@@ -124,12 +119,12 @@ class ClimbRepository(
|
|||||||
val jsonContent = file.readText()
|
val jsonContent = file.readText()
|
||||||
val importData = json.decodeFromString<ClimbDataExport>(jsonContent)
|
val importData = json.decodeFromString<ClimbDataExport>(jsonContent)
|
||||||
|
|
||||||
// Import gyms (replace if exists due to primary key constraint)
|
// Import gyms
|
||||||
importData.gyms.forEach { gym ->
|
importData.gyms.forEach { gym ->
|
||||||
try {
|
try {
|
||||||
gymDao.insertGym(gym)
|
gymDao.insertGym(gym)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// If insertion fails due to primary key conflict, update instead
|
// If insertion fails, update instead
|
||||||
gymDao.updateGym(gym)
|
gymDao.updateGym(gym)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +161,7 @@ class ClimbRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZIP Export functionality with images
|
// ZIP Export with images
|
||||||
suspend fun exportAllDataToZip(directory: File? = null): File {
|
suspend fun exportAllDataToZip(directory: File? = null): File {
|
||||||
val allGyms = gymDao.getAllGyms().first()
|
val allGyms = gymDao.getAllGyms().first()
|
||||||
val allProblems = problemDao.getAllProblems().first()
|
val allProblems = problemDao.getAllProblems().first()
|
||||||
@@ -206,7 +201,7 @@ class ClimbRepository(
|
|||||||
attempts = attempts
|
attempts = attempts
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collect all referenced image paths
|
// Collect all image paths
|
||||||
val referencedImagePaths = problems.flatMap { it.imagePaths }.toSet()
|
val referencedImagePaths = problems.flatMap { it.imagePaths }.toSet()
|
||||||
|
|
||||||
ZipExportImportUtils.createExportZipToUri(
|
ZipExportImportUtils.createExportZipToUri(
|
||||||
@@ -228,12 +223,12 @@ class ClimbRepository(
|
|||||||
importResult.importedImagePaths
|
importResult.importedImagePaths
|
||||||
)
|
)
|
||||||
|
|
||||||
// Import gyms (replace if exists due to primary key constraint)
|
// Import gyms
|
||||||
importData.gyms.forEach { gym ->
|
importData.gyms.forEach { gym ->
|
||||||
try {
|
try {
|
||||||
gymDao.insertGym(gym)
|
gymDao.insertGym(gym)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// If insertion fails due to primary key conflict, update instead
|
// If insertion fails update instead
|
||||||
gymDao.updateGym(gym)
|
gymDao.updateGym(gym)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.app.PendingIntent
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.atridad.openclimb.MainActivity
|
import com.atridad.openclimb.MainActivity
|
||||||
@@ -16,7 +15,6 @@ import com.atridad.openclimb.data.repository.ClimbRepository
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
class SessionTrackingService : Service() {
|
class SessionTrackingService : Service() {
|
||||||
@@ -113,31 +111,32 @@ class SessionTrackingService : Service() {
|
|||||||
remainingMinutes > 0 -> "${remainingMinutes}m"
|
remainingMinutes > 0 -> "${remainingMinutes}m"
|
||||||
else -> "< 1m"
|
else -> "< 1m"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
"Active"
|
"Active"
|
||||||
}
|
}
|
||||||
} ?: "Active"
|
} ?: "Active"
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle("OpenClimb Session Active")
|
.setContentTitle("Climbing Session Active")
|
||||||
.setContentText("${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts")
|
.setContentText("${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts")
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
.setSmallIcon(R.drawable.ic_mountains)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setContentIntent(createOpenAppIntent())
|
.setContentIntent(createOpenAppIntent())
|
||||||
.addAction(
|
.addAction(
|
||||||
R.drawable.ic_launcher_foreground,
|
R.drawable.ic_mountains,
|
||||||
"Open Session",
|
"Open Session",
|
||||||
createOpenAppIntent()
|
createOpenAppIntent()
|
||||||
)
|
)
|
||||||
.addAction(
|
.addAction(
|
||||||
R.drawable.ic_launcher_foreground,
|
android.R.drawable.ic_menu_close_clear_cancel,
|
||||||
"End Session",
|
"End Session",
|
||||||
createStopIntent()
|
createStopIntent()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
// Handle errors gracefully
|
// Handle errors gracefully
|
||||||
stopSessionTracking()
|
stopSessionTracking()
|
||||||
}
|
}
|
||||||
@@ -166,19 +165,17 @@ class SessionTrackingService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val channel = NotificationChannel(
|
||||||
val channel = NotificationChannel(
|
CHANNEL_ID,
|
||||||
CHANNEL_ID,
|
"Session Tracking",
|
||||||
"Session Tracking",
|
NotificationManager.IMPORTANCE_LOW
|
||||||
NotificationManager.IMPORTANCE_LOW
|
).apply {
|
||||||
).apply {
|
description = "Shows active climbing session information"
|
||||||
description = "Shows active climbing session information"
|
setShowBadge(false)
|
||||||
setShowBadge(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -86,9 +86,6 @@ fun OpenClimbApp() {
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateToSessionDetail = { sessionId ->
|
onNavigateToSessionDetail = { sessionId ->
|
||||||
navController.navigate(Screen.SessionDetail(sessionId))
|
navController.navigate(Screen.SessionDetail(sessionId))
|
||||||
},
|
|
||||||
onNavigateToAddSession = { gymId ->
|
|
||||||
navController.navigate(Screen.AddEditSession(gymId = gymId))
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -112,9 +109,6 @@ fun OpenClimbApp() {
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateToProblemDetail = { problemId ->
|
onNavigateToProblemDetail = { problemId ->
|
||||||
navController.navigate(Screen.ProblemDetail(problemId))
|
navController.navigate(Screen.ProblemDetail(problemId))
|
||||||
},
|
|
||||||
onNavigateToAddProblem = { gymId ->
|
|
||||||
navController.navigate(Screen.AddEditProblem(gymId = gymId))
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -140,9 +134,6 @@ fun OpenClimbApp() {
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateToGymDetail = { gymId ->
|
onNavigateToGymDetail = { gymId ->
|
||||||
navController.navigate(Screen.GymDetail(gymId))
|
navController.navigate(Screen.GymDetail(gymId))
|
||||||
},
|
|
||||||
onNavigateToAddGym = {
|
|
||||||
navController.navigate(Screen.AddEditGym())
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.rounded.Close
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.atridad.openclimb.data.model.ClimbSession
|
import com.atridad.openclimb.data.model.ClimbSession
|
||||||
@@ -95,80 +93,6 @@ fun ActiveSessionBanner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StartSessionButton(
|
|
||||||
gyms: List<Gym>,
|
|
||||||
onStartSession: (String) -> Unit
|
|
||||||
) {
|
|
||||||
var showGymSelection by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (gyms.isEmpty()) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "No gyms available",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Add a gym first to start a session",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(
|
|
||||||
onClick = { showGymSelection = true },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.PlayArrow, contentDescription = null)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Start Session")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showGymSelection) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showGymSelection = false },
|
|
||||||
title = { Text("Select Gym") },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
gyms.forEach { gym ->
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onStartSession(gym.id)
|
|
||||||
showGymSelection = false
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = gym.name,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { showGymSelection = false }) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateDuration(startTimeString: String): String {
|
private fun calculateDuration(startTimeString: String): String {
|
||||||
return try {
|
return try {
|
||||||
val startTime = LocalDateTime.parse(startTimeString)
|
val startTime = LocalDateTime.parse(startTimeString)
|
||||||
@@ -182,7 +106,7 @@ private fun calculateDuration(startTimeString: String): String {
|
|||||||
remainingMinutes > 0 -> "${remainingMinutes}m"
|
remainingMinutes > 0 -> "${remainingMinutes}m"
|
||||||
else -> "< 1m"
|
else -> "< 1m"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
"Active"
|
"Active"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ fun FullscreenImageViewer(
|
|||||||
LaunchedEffect(pagerState.currentPage) {
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
thumbnailListState.animateScrollToItem(
|
thumbnailListState.animateScrollToItem(
|
||||||
index = pagerState.currentPage,
|
index = pagerState.currentPage,
|
||||||
scrollOffset = -200 // Center the item
|
scrollOffset = -200
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.atridad.openclimb.ui.components
|
package com.atridad.openclimb.ui.components
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -20,7 +19,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.atridad.openclimb.utils.ImageUtils
|
import com.atridad.openclimb.utils.ImageUtils
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImagePicker(
|
fun ImagePicker(
|
||||||
@@ -41,7 +39,7 @@ fun ImagePicker(
|
|||||||
val remainingSlots = maxImages - currentCount
|
val remainingSlots = maxImages - currentCount
|
||||||
val urisToProcess = uris.take(remainingSlots)
|
val urisToProcess = uris.take(remainingSlots)
|
||||||
|
|
||||||
// Process each selected image
|
// Process images
|
||||||
val newImagePaths = mutableListOf<String>()
|
val newImagePaths = mutableListOf<String>()
|
||||||
urisToProcess.forEach { uri ->
|
urisToProcess.forEach { uri ->
|
||||||
val imagePath = ImageUtils.saveImageFromUri(context, uri)
|
val imagePath = ImageUtils.saveImageFromUri(context, uri)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.selection.selectable
|
|||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -81,7 +82,7 @@ fun AddEditGymScreen(
|
|||||||
title = { Text(if (isEditing) "Edit Gym" else "Add Gym") },
|
title = { Text(if (isEditing) "Edit Gym" else "Add Gym") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -90,7 +91,7 @@ fun AddEditGymScreen(
|
|||||||
val gym = Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
|
val gym = Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
viewModel.updateGym(gym.copy(id = gymId!!))
|
viewModel.updateGym(gym.copy(id = gymId))
|
||||||
} else {
|
} else {
|
||||||
viewModel.addGym(gym)
|
viewModel.addGym(gym)
|
||||||
}
|
}
|
||||||
@@ -291,7 +292,7 @@ fun AddEditProblemScreen(
|
|||||||
|
|
||||||
val availableClimbTypes = selectedGym?.supportedClimbTypes ?: ClimbType.entries.toList()
|
val availableClimbTypes = selectedGym?.supportedClimbTypes ?: ClimbType.entries.toList()
|
||||||
val availableDifficultySystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
|
val availableDifficultySystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
|
||||||
selectedGym?.difficultySystems?.contains(system) ?: true
|
selectedGym?.difficultySystems?.contains(system) != false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-select climb type if there's only one available
|
// Auto-select climb type if there's only one available
|
||||||
@@ -329,7 +330,7 @@ fun AddEditProblemScreen(
|
|||||||
title = { Text(if (isEditing) "Edit Problem" else "Add Problem") },
|
title = { Text(if (isEditing) "Edit Problem" else "Add Problem") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -359,7 +360,7 @@ fun AddEditProblemScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
viewModel.updateProblem(problem.copy(id = problemId!!))
|
viewModel.updateProblem(problem.copy(id = problemId))
|
||||||
} else {
|
} else {
|
||||||
viewModel.addProblem(problem)
|
viewModel.addProblem(problem)
|
||||||
}
|
}
|
||||||
@@ -646,7 +647,7 @@ fun AddEditProblemScreen(
|
|||||||
label = { Text("Tags (Optional)") },
|
label = { Text("Tags (Optional)") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
placeholder = { Text("e.g., overhang, crimpy, dynamic (comma-separated)") }
|
placeholder = { Text("e.g., crimpy, dynamic (comma-separated)") }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -736,7 +737,7 @@ fun AddEditSessionScreen(
|
|||||||
title = { Text(if (isEditing) "Edit Session" else "Add Session") },
|
title = { Text(if (isEditing) "Edit Session" else "Add Session") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -749,7 +750,7 @@ fun AddEditSessionScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
viewModel.updateSession(session.copy(id = sessionId!!))
|
viewModel.updateSession(session.copy(id = sessionId))
|
||||||
} else {
|
} else {
|
||||||
viewModel.addSession(session)
|
viewModel.addSession(session)
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
@@ -87,7 +86,7 @@ fun SessionDetailScreen(
|
|||||||
title = { Text("Session Details") },
|
title = { Text("Session Details") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -403,7 +402,7 @@ fun ProblemDetailScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
var showImageViewer by remember { mutableStateOf(false) }
|
var showImageViewer by remember { mutableStateOf(false) }
|
||||||
var selectedImageIndex by remember { mutableStateOf(0) }
|
var selectedImageIndex by remember { mutableIntStateOf(0) }
|
||||||
val attempts by viewModel.getAttemptsByProblem(problemId).collectAsState(initial = emptyList())
|
val attempts by viewModel.getAttemptsByProblem(problemId).collectAsState(initial = emptyList())
|
||||||
val sessions by viewModel.sessions.collectAsState()
|
val sessions by viewModel.sessions.collectAsState()
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
val gyms by viewModel.gyms.collectAsState()
|
||||||
@@ -436,7 +435,7 @@ fun ProblemDetailScreen(
|
|||||||
title = { Text("Problem Details") },
|
title = { Text("Problem Details") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -770,7 +769,7 @@ fun GymDetailScreen(
|
|||||||
title = { Text(gym?.name ?: "Gym Details") },
|
title = { Text(gym?.name ?: "Gym Details") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -1087,7 +1086,7 @@ fun GymDetailScreen(
|
|||||||
supportingContent = {
|
supportingContent = {
|
||||||
val dateTime = try {
|
val dateTime = try {
|
||||||
LocalDateTime.parse(session.date)
|
LocalDateTime.parse(session.date)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val formattedDate = dateTime?.format(
|
val formattedDate = dateTime?.format(
|
||||||
@@ -1348,42 +1347,11 @@ private fun formatDate(dateString: String): String {
|
|||||||
val date = LocalDateTime.parse(dateString, formatter)
|
val date = LocalDateTime.parse(dateString, formatter)
|
||||||
val displayFormatter = DateTimeFormatter.ofPattern("MMM dd, yyyy")
|
val displayFormatter = DateTimeFormatter.ofPattern("MMM dd, yyyy")
|
||||||
date.format(displayFormatter)
|
date.format(displayFormatter)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
dateString.take(10) // Fallback to just the date part
|
dateString.take(10) // Fallback to just the date part
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatTime(timeString: String): String {
|
|
||||||
return try {
|
|
||||||
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
|
||||||
val time = LocalDateTime.parse(timeString, formatter)
|
|
||||||
val displayFormatter = DateTimeFormatter.ofPattern("h:mm a")
|
|
||||||
time.format(displayFormatter)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
timeString.take(8) // Fallback to time part
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateSessionDuration(startTime: String, endTime: String): String {
|
|
||||||
return try {
|
|
||||||
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
|
||||||
val start = LocalDateTime.parse(startTime, formatter)
|
|
||||||
val end = LocalDateTime.parse(endTime, formatter)
|
|
||||||
val duration = java.time.Duration.between(start, end)
|
|
||||||
|
|
||||||
val hours = duration.toHours()
|
|
||||||
val minutes = duration.toMinutes() % 60
|
|
||||||
|
|
||||||
when {
|
|
||||||
hours > 0 -> "${hours}h ${minutes}m"
|
|
||||||
minutes > 0 -> "${minutes}m"
|
|
||||||
else -> "< 1m"
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EnhancedAddAttemptDialog(
|
fun EnhancedAddAttemptDialog(
|
||||||
@@ -1896,28 +1864,5 @@ fun EnhancedAddAttemptDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StatisticItem(
|
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
valueColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primary
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = valueColor
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ package com.atridad.openclimb.ui.screens
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.atridad.openclimb.R
|
import com.atridad.openclimb.R
|
||||||
import com.atridad.openclimb.data.model.Gym
|
import com.atridad.openclimb.data.model.Gym
|
||||||
@@ -21,8 +18,7 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun GymsScreen(
|
fun GymsScreen(
|
||||||
viewModel: ClimbViewModel,
|
viewModel: ClimbViewModel,
|
||||||
onNavigateToGymDetail: (String) -> Unit,
|
onNavigateToGymDetail: (String) -> Unit
|
||||||
onNavigateToAddGym: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
val gyms by viewModel.gyms.collectAsState()
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.atridad.openclimb.R
|
import com.atridad.openclimb.R
|
||||||
import com.atridad.openclimb.data.model.ClimbType
|
import com.atridad.openclimb.data.model.ClimbType
|
||||||
@@ -26,14 +23,13 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun ProblemsScreen(
|
fun ProblemsScreen(
|
||||||
viewModel: ClimbViewModel,
|
viewModel: ClimbViewModel,
|
||||||
onNavigateToProblemDetail: (String) -> Unit,
|
onNavigateToProblemDetail: (String) -> Unit
|
||||||
onNavigateToAddProblem: (String?) -> Unit
|
|
||||||
) {
|
) {
|
||||||
val problems by viewModel.problems.collectAsState()
|
val problems by viewModel.problems.collectAsState()
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
val gyms by viewModel.gyms.collectAsState()
|
||||||
var showImageViewer by remember { mutableStateOf(false) }
|
var showImageViewer by remember { mutableStateOf(false) }
|
||||||
var selectedImagePaths by remember { mutableStateOf<List<String>>(emptyList()) }
|
var selectedImagePaths by remember { mutableStateOf<List<String>>(emptyList()) }
|
||||||
var selectedImageIndex by remember { mutableStateOf(0) }
|
var selectedImageIndex by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
var selectedClimbType by remember { mutableStateOf<ClimbType?>(null) }
|
var selectedClimbType by remember { mutableStateOf<ClimbType?>(null) }
|
||||||
@@ -41,8 +37,8 @@ fun ProblemsScreen(
|
|||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
val filteredProblems = problems.filter { problem ->
|
val filteredProblems = problems.filter { problem ->
|
||||||
val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } ?: true
|
val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } != false
|
||||||
val gymMatch = selectedGym?.let { it.id == problem.gymId } ?: true
|
val gymMatch = selectedGym?.let { it.id == problem.gymId } != false
|
||||||
climbTypeMatch && gymMatch
|
climbTypeMatch && gymMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.atridad.openclimb.ui.screens
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -26,13 +28,13 @@ import java.time.format.DateTimeFormatter
|
|||||||
@Composable
|
@Composable
|
||||||
fun SessionsScreen(
|
fun SessionsScreen(
|
||||||
viewModel: ClimbViewModel,
|
viewModel: ClimbViewModel,
|
||||||
onNavigateToSessionDetail: (String) -> Unit,
|
onNavigateToSessionDetail: (String) -> Unit
|
||||||
onNavigateToAddSession: (String?) -> Unit
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sessions by viewModel.sessions.collectAsState()
|
val sessions by viewModel.sessions.collectAsState()
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
val gyms by viewModel.gyms.collectAsState()
|
||||||
val activeSession by viewModel.activeSession.collectAsState()
|
val activeSession by viewModel.activeSession.collectAsState()
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
// Filter out active sessions from regular session list
|
// Filter out active sessions from regular session list
|
||||||
val completedSessions = sessions.filter { it.status == SessionStatus.COMPLETED }
|
val completedSessions = sessions.filter { it.status == SessionStatus.COMPLETED }
|
||||||
@@ -103,6 +105,79 @@ fun SessionsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show UI state messages and errors
|
||||||
|
uiState.message?.let { message ->
|
||||||
|
LaunchedEffect(message) {
|
||||||
|
kotlinx.coroutines.delay(5000)
|
||||||
|
viewModel.clearMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uiState.error?.let { error ->
|
||||||
|
LaunchedEffect(error) {
|
||||||
|
kotlinx.coroutines.delay(5000)
|
||||||
|
viewModel.clearError()
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -141,7 +216,7 @@ fun SessionCard(
|
|||||||
|
|
||||||
session.duration?.let { duration ->
|
session.duration?.let { duration ->
|
||||||
Text(
|
Text(
|
||||||
text = "Duration: ${duration} minutes",
|
text = "Duration: $duration minutes",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -203,7 +278,7 @@ private fun formatDate(dateString: String): String {
|
|||||||
return try {
|
return try {
|
||||||
val date = LocalDateTime.parse(dateString.split("T")[0] + "T00:00:00")
|
val date = LocalDateTime.parse(dateString.split("T")[0] + "T00:00:00")
|
||||||
date.format(DateTimeFormatter.ofPattern("MMM dd, yyyy"))
|
date.format(DateTimeFormatter.ofPattern("MMM dd, yyyy"))
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
dateString
|
dateString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.atridad.openclimb.ui.screens
|
|||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import android.os.Environment
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|||||||
@@ -63,13 +63,4 @@ val ClimbNeutralVariant30 = Color(0xFF484848)
|
|||||||
val ClimbNeutralVariant50 = Color(0xFF797979)
|
val ClimbNeutralVariant50 = Color(0xFF797979)
|
||||||
val ClimbNeutralVariant60 = Color(0xFF939393)
|
val ClimbNeutralVariant60 = Color(0xFF939393)
|
||||||
val ClimbNeutralVariant80 = Color(0xFFC7C7C7)
|
val ClimbNeutralVariant80 = Color(0xFFC7C7C7)
|
||||||
val ClimbNeutralVariant90 = Color(0xFFE3E3E3)
|
val ClimbNeutralVariant90 = Color(0xFFE3E3E3)
|
||||||
|
|
||||||
// Legacy colors for backward compatibility
|
|
||||||
val Purple80 = ClimbOrange80
|
|
||||||
val PurpleGrey80 = ClimbGrey80
|
|
||||||
val Pink80 = ClimbBlue80
|
|
||||||
|
|
||||||
val Purple40 = ClimbOrange40
|
|
||||||
val PurpleGrey40 = ClimbGrey40
|
|
||||||
val Pink40 = ClimbBlue40
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.atridad.openclimb.ui.theme
|
package com.atridad.openclimb.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
@@ -10,7 +9,6 @@ import androidx.compose.material3.dynamicLightColorScheme
|
|||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
@@ -98,7 +96,7 @@ fun OpenClimbTheme(
|
|||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
dynamicColor && true -> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.atridad.openclimb.utils
|
package com.atridad.openclimb.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import androidx.core.graphics.scale
|
||||||
|
|
||||||
object ImageUtils {
|
object ImageUtils {
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ object ImageUtils {
|
|||||||
/**
|
/**
|
||||||
* Compresses and resizes an image bitmap
|
* Compresses and resizes an image bitmap
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("UseKtx")
|
||||||
private fun compressImage(original: Bitmap): Bitmap {
|
private fun compressImage(original: Bitmap): Bitmap {
|
||||||
val width = original.width
|
val width = original.width
|
||||||
val height = original.height
|
val height = original.height
|
||||||
@@ -79,7 +81,7 @@ object ImageUtils {
|
|||||||
return if (scaleFactor < 1f) {
|
return if (scaleFactor < 1f) {
|
||||||
val newWidth = (width * scaleFactor).toInt()
|
val newWidth = (width * scaleFactor).toInt()
|
||||||
val newHeight = (height * scaleFactor).toInt()
|
val newHeight = (height * scaleFactor).toInt()
|
||||||
Bitmap.createScaledBitmap(original, newWidth, newHeight, true)
|
original.scale(newWidth, newHeight)
|
||||||
} else {
|
} else {
|
||||||
original
|
original
|
||||||
}
|
}
|
||||||
@@ -110,30 +112,7 @@ object ImageUtils {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies an image file to export directory
|
|
||||||
* @param context Android context
|
|
||||||
* @param relativePath The relative path of the image
|
|
||||||
* @param exportDir The directory to copy to
|
|
||||||
* @return The filename in the export directory, null if failed
|
|
||||||
*/
|
|
||||||
fun copyImageForExport(context: Context, relativePath: String, exportDir: File): String? {
|
|
||||||
return try {
|
|
||||||
val sourceFile = getImageFile(context, relativePath)
|
|
||||||
if (!sourceFile.exists()) return null
|
|
||||||
|
|
||||||
val filename = sourceFile.name
|
|
||||||
val destFile = File(exportDir, filename)
|
|
||||||
|
|
||||||
sourceFile.copyTo(destFile, overwrite = true)
|
|
||||||
filename
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports an image file from the import directory
|
* Imports an image file from the import directory
|
||||||
* @param context Android context
|
* @param context Android context
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import java.io.FileOutputStream
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
|
||||||
object SessionShareUtils {
|
object SessionShareUtils {
|
||||||
|
|
||||||
@@ -75,31 +77,7 @@ object SessionShareUtils {
|
|||||||
topResult = topResult
|
topResult = topResult
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateDuration(startTime: String?, endTime: String?): String {
|
|
||||||
return try {
|
|
||||||
if (startTime != null && endTime != null) {
|
|
||||||
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
|
||||||
val start = LocalDateTime.parse(startTime, formatter)
|
|
||||||
val end = LocalDateTime.parse(endTime, formatter)
|
|
||||||
val duration = java.time.Duration.between(start, end)
|
|
||||||
|
|
||||||
val hours = duration.toHours()
|
|
||||||
val minutes = duration.toMinutes() % 60
|
|
||||||
|
|
||||||
when {
|
|
||||||
hours > 0 -> "${hours}h ${minutes}m"
|
|
||||||
minutes > 0 -> "${minutes}m"
|
|
||||||
else -> "< 1m"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
"Unknown"
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateShareCard(
|
fun generateShareCard(
|
||||||
context: Context,
|
context: Context,
|
||||||
session: ClimbSession,
|
session: ClimbSession,
|
||||||
@@ -110,14 +88,14 @@ object SessionShareUtils {
|
|||||||
val width = 1080
|
val width = 1080
|
||||||
val height = 1350
|
val height = 1350
|
||||||
|
|
||||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
val bitmap = createBitmap(width, height)
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
|
|
||||||
val gradientDrawable = GradientDrawable(
|
val gradientDrawable = GradientDrawable(
|
||||||
GradientDrawable.Orientation.TOP_BOTTOM,
|
GradientDrawable.Orientation.TOP_BOTTOM,
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
Color.parseColor("#667eea"),
|
"#667eea".toColorInt(),
|
||||||
Color.parseColor("#764ba2")
|
"#764ba2".toColorInt()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
gradientDrawable.setBounds(0, 0, width, height)
|
gradientDrawable.setBounds(0, 0, width, height)
|
||||||
@@ -133,7 +111,7 @@ object SessionShareUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val subtitlePaint = Paint().apply {
|
val subtitlePaint = Paint().apply {
|
||||||
color = Color.parseColor("#E8E8E8")
|
color = "#E8E8E8".toColorInt()
|
||||||
textSize = 48f
|
textSize = 48f
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
@@ -141,7 +119,7 @@ object SessionShareUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val statLabelPaint = Paint().apply {
|
val statLabelPaint = Paint().apply {
|
||||||
color = Color.parseColor("#B8B8B8")
|
color = "#B8B8B8".toColorInt()
|
||||||
textSize = 36f
|
textSize = 36f
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
@@ -157,7 +135,7 @@ object SessionShareUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val cardPaint = Paint().apply {
|
val cardPaint = Paint().apply {
|
||||||
color = Color.parseColor("#40FFFFFF")
|
color = "#40FFFFFF".toColorInt()
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +189,7 @@ object SessionShareUtils {
|
|||||||
|
|
||||||
// App branding
|
// App branding
|
||||||
val brandingPaint = Paint().apply {
|
val brandingPaint = Paint().apply {
|
||||||
color = Color.parseColor("#80FFFFFF")
|
color = "#80FFFFFF".toColorInt()
|
||||||
textSize = 32f
|
textSize = 32f
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
@@ -268,7 +246,7 @@ object SessionShareUtils {
|
|||||||
|
|
||||||
// Background arc
|
// Background arc
|
||||||
val bgPaint = Paint().apply {
|
val bgPaint = Paint().apply {
|
||||||
color = Color.parseColor("#40FFFFFF")
|
color = "#40FFFFFF".toColorInt()
|
||||||
style = Paint.Style.STROKE
|
style = Paint.Style.STROKE
|
||||||
this.strokeWidth = strokeWidth
|
this.strokeWidth = strokeWidth
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
@@ -277,7 +255,7 @@ object SessionShareUtils {
|
|||||||
|
|
||||||
// Success arc
|
// Success arc
|
||||||
val successPaint = Paint().apply {
|
val successPaint = Paint().apply {
|
||||||
color = Color.parseColor("#4CAF50")
|
color = "#4CAF50".toColorInt()
|
||||||
style = Paint.Style.STROKE
|
style = Paint.Style.STROKE
|
||||||
this.strokeWidth = strokeWidth
|
this.strokeWidth = strokeWidth
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
@@ -305,7 +283,7 @@ object SessionShareUtils {
|
|||||||
val date = LocalDateTime.parse(dateString, formatter)
|
val date = LocalDateTime.parse(dateString, formatter)
|
||||||
val displayFormatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy")
|
val displayFormatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy")
|
||||||
date.format(displayFormatter)
|
date.format(displayFormatter)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
dateString.take(10)
|
dateString.take(10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.atridad.openclimb.utils
|
package com.atridad.openclimb.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@@ -179,14 +178,7 @@ object ZipExportImportUtils {
|
|||||||
|
|
||||||
return ImportResult(jsonContent, importedImagePaths)
|
return ImportResult(jsonContent, importedImagePaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to determine if a file is a ZIP file based on extension
|
|
||||||
*/
|
|
||||||
fun isZipFile(filename: String): Boolean {
|
|
||||||
return filename.lowercase().endsWith(".zip")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates image paths in a problem list after import
|
* Updates image paths in a problem list after import
|
||||||
* This function maps the old image paths to the new ones after import
|
* This function maps the old image paths to the new ones after import
|
||||||
|
|||||||
Reference in New Issue
Block a user