0.3.3
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.
Binary file not shown.
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.atridad.openclimb"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 5
|
||||
versionName = "0.3.2"
|
||||
versionCode = 6
|
||||
versionName = "0.3.3"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,8 +11,8 @@
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 4,
|
||||
"versionName": "0.3.1",
|
||||
"versionCode": 6,
|
||||
"versionName": "0.3.3",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -9,12 +9,10 @@ import java.time.LocalDateTime
|
||||
|
||||
@Serializable
|
||||
enum class AttemptResult {
|
||||
SUCCESS, // Completed the problem/route
|
||||
FALL, // Fell but made progress
|
||||
NO_PROGRESS, // Couldn't make meaningful progress
|
||||
FLASH, // Completed on first try
|
||||
REDPOINT, // Completed after previous attempts
|
||||
ONSIGHT // Completed on first try without prior knowledge
|
||||
SUCCESS,
|
||||
FALL,
|
||||
NO_PROGRESS,
|
||||
FLASH,
|
||||
}
|
||||
|
||||
@Entity(
|
||||
|
||||
@@ -65,7 +65,7 @@ data class ClimbSession(
|
||||
val start = LocalDateTime.parse(startTime)
|
||||
val end = LocalDateTime.parse(endTime)
|
||||
java.time.Duration.between(start, end).toMinutes()
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
} else null
|
||||
|
||||
@@ -123,7 +123,7 @@ class ClimbRepository(
|
||||
importData.gyms.forEach { gym ->
|
||||
try {
|
||||
gymDao.insertGym(gym)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
// If insertion fails, update instead
|
||||
gymDao.updateGym(gym)
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class ClimbRepository(
|
||||
importData.problems.forEach { problem ->
|
||||
try {
|
||||
problemDao.insertProblem(problem)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
problemDao.updateProblem(problem)
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ class ClimbRepository(
|
||||
importData.sessions.forEach { session ->
|
||||
try {
|
||||
sessionDao.insertSession(session)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
sessionDao.updateSession(session)
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ class ClimbRepository(
|
||||
importData.attempts.forEach { attempt ->
|
||||
try {
|
||||
attemptDao.insertAttempt(attempt)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
attemptDao.updateAttempt(attempt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,10 @@ class SessionTrackingService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createStopIntent(context: Context): Intent {
|
||||
fun createStopIntent(context: Context, sessionId: String): Intent {
|
||||
return Intent(context, SessionTrackingService::class.java).apply {
|
||||
action = ACTION_STOP_SESSION
|
||||
putExtra(EXTRA_SESSION_ID, sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +64,21 @@ class SessionTrackingService : Service() {
|
||||
}
|
||||
}
|
||||
ACTION_STOP_SESSION -> {
|
||||
stopSessionTracking()
|
||||
val sessionId = intent.getStringExtra(EXTRA_SESSION_ID)
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val targetSession = when {
|
||||
sessionId != null -> repository.getSessionById(sessionId)
|
||||
else -> repository.getActiveSession()
|
||||
}
|
||||
if (targetSession != null && targetSession.status == com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
|
||||
val completed = with(com.atridad.openclimb.data.model.ClimbSession) { targetSession.complete() }
|
||||
repository.updateSession(completed)
|
||||
}
|
||||
} finally {
|
||||
stopSessionTracking()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
@@ -131,7 +146,7 @@ class SessionTrackingService : Service() {
|
||||
.addAction(
|
||||
android.R.drawable.ic_menu_close_clear_cancel,
|
||||
"End Session",
|
||||
createStopIntent()
|
||||
createStopPendingIntent(sessionId)
|
||||
)
|
||||
.build()
|
||||
|
||||
@@ -154,8 +169,8 @@ class SessionTrackingService : Service() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun createStopIntent(): PendingIntent {
|
||||
val intent = createStopIntent(this)
|
||||
private fun createStopPendingIntent(sessionId: String): PendingIntent {
|
||||
val intent = createStopIntent(this, sessionId)
|
||||
return PendingIntent.getService(
|
||||
this,
|
||||
1,
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModelFactory
|
||||
fun OpenClimbApp() {
|
||||
val navController = rememberNavController()
|
||||
val context = LocalContext.current
|
||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
val database = remember { OpenClimbDatabase.getDatabase(context) }
|
||||
val repository = remember { ClimbRepository(database, context) }
|
||||
@@ -148,6 +147,7 @@ fun OpenClimbApp() {
|
||||
// Detail screens
|
||||
composable<Screen.SessionDetail> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Screen.SessionDetail>()
|
||||
LaunchedEffect(Unit) { fabConfig = null }
|
||||
SessionDetailScreen(
|
||||
sessionId = args.sessionId,
|
||||
viewModel = viewModel,
|
||||
@@ -160,6 +160,7 @@ fun OpenClimbApp() {
|
||||
|
||||
composable<Screen.ProblemDetail> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Screen.ProblemDetail>()
|
||||
LaunchedEffect(Unit) { fabConfig = null }
|
||||
ProblemDetailScreen(
|
||||
problemId = args.problemId,
|
||||
viewModel = viewModel,
|
||||
@@ -172,6 +173,7 @@ fun OpenClimbApp() {
|
||||
|
||||
composable<Screen.GymDetail> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Screen.GymDetail>()
|
||||
LaunchedEffect(Unit) { fabConfig = null }
|
||||
GymDetailScreen(
|
||||
gymId = args.gymId,
|
||||
viewModel = viewModel,
|
||||
@@ -185,6 +187,7 @@ fun OpenClimbApp() {
|
||||
|
||||
composable<Screen.AddEditGym> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Screen.AddEditGym>()
|
||||
LaunchedEffect(Unit) { fabConfig = null }
|
||||
AddEditGymScreen(
|
||||
gymId = args.gymId,
|
||||
viewModel = viewModel,
|
||||
@@ -194,6 +197,7 @@ fun OpenClimbApp() {
|
||||
|
||||
composable<Screen.AddEditProblem> { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<Screen.AddEditProblem>()
|
||||
LaunchedEffect(Unit) { fabConfig = null }
|
||||
AddEditProblemScreen(
|
||||
problemId = args.problemId,
|
||||
gymId = args.gymId,
|
||||
|
||||
@@ -948,8 +948,7 @@ fun AddEditSessionScreen(
|
||||
text = "Result: ${attempt.result.name.lowercase().replaceFirstChar { it.uppercase() }}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = when (attempt.result) {
|
||||
AttemptResult.SUCCESS, AttemptResult.FLASH,
|
||||
AttemptResult.REDPOINT, AttemptResult.ONSIGHT -> MaterialTheme.colorScheme.primary
|
||||
AttemptResult.SUCCESS, AttemptResult.FLASH -> MaterialTheme.colorScheme.primary
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ fun SessionDetailScreen(
|
||||
|
||||
// Calculate stats
|
||||
val successfulAttempts = attempts.filter {
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT)
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
|
||||
}
|
||||
val uniqueProblems = attempts.map { it.problemId }.distinct()
|
||||
val attemptedProblems = problems.filter { it.id in uniqueProblems }
|
||||
@@ -71,9 +71,7 @@ fun SessionDetailScreen(
|
||||
}.sortedByDescending { attempt ->
|
||||
// Sort by result priority, then by timestamp
|
||||
when (attempt.first.result) {
|
||||
AttemptResult.ONSIGHT -> 5
|
||||
AttemptResult.FLASH -> 4
|
||||
AttemptResult.REDPOINT -> 3
|
||||
AttemptResult.FLASH -> 3
|
||||
AttemptResult.SUCCESS -> 2
|
||||
AttemptResult.FALL -> 1
|
||||
else -> 0
|
||||
@@ -130,8 +128,7 @@ fun SessionDetailScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
// Show FAB only for active sessions (those without duration)
|
||||
if (session?.duration == null) {
|
||||
if (session?.status == SessionStatus.ACTIVE) {
|
||||
FloatingActionButton(
|
||||
onClick = { showAddAttemptDialog = true }
|
||||
) {
|
||||
@@ -418,7 +415,7 @@ fun ProblemDetailScreen(
|
||||
|
||||
// Calculate stats
|
||||
val successfulAttempts = attempts.filter {
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT)
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
|
||||
}
|
||||
val successRate = if (attempts.isNotEmpty()) {
|
||||
(successfulAttempts.size.toDouble() / attempts.size * 100).toInt()
|
||||
@@ -750,7 +747,7 @@ fun GymDetailScreen(
|
||||
}
|
||||
|
||||
val successfulAttempts = gymAttempts.filter {
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT)
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
|
||||
}
|
||||
|
||||
val successRate = if (gymAttempts.isNotEmpty()) {
|
||||
@@ -984,7 +981,7 @@ fun GymDetailScreen(
|
||||
problems.sortedByDescending { it.createdAt }.take(5).forEach { problem ->
|
||||
val problemAttempts = gymAttempts.filter { it.problemId == problem.id }
|
||||
val problemSuccessful = problemAttempts.any {
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT)
|
||||
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
|
||||
}
|
||||
|
||||
Card(
|
||||
@@ -1264,13 +1261,13 @@ fun AttemptHistoryCard(
|
||||
@Composable
|
||||
fun AttemptResultBadge(result: AttemptResult) {
|
||||
val backgroundColor = when (result) {
|
||||
AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT -> MaterialTheme.colorScheme.primaryContainer
|
||||
AttemptResult.SUCCESS, AttemptResult.FLASH -> MaterialTheme.colorScheme.primaryContainer
|
||||
AttemptResult.FALL -> MaterialTheme.colorScheme.secondaryContainer
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||
}
|
||||
|
||||
val textColor = when (result) {
|
||||
AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
AttemptResult.SUCCESS, AttemptResult.FLASH -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
AttemptResult.FALL -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
|
||||
@@ -175,8 +175,8 @@ class ClimbViewModel(
|
||||
val completedSession = with(ClimbSession) { session.complete() }
|
||||
repository.updateSession(completedSession)
|
||||
|
||||
// Stop the tracking service
|
||||
val serviceIntent = SessionTrackingService.createStopIntent(context)
|
||||
// Stop the tracking service, passing the session id so service can finalize if needed
|
||||
val serviceIntent = SessionTrackingService.createStopIntent(context, sessionId)
|
||||
context.startService(serviceIntent)
|
||||
|
||||
_uiState.value = _uiState.value.copy(
|
||||
@@ -186,32 +186,6 @@ class ClimbViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseSession(sessionId: String) {
|
||||
viewModelScope.launch {
|
||||
val session = repository.getSessionById(sessionId)
|
||||
if (session != null && session.status == SessionStatus.ACTIVE) {
|
||||
val pausedSession = session.copy(
|
||||
status = SessionStatus.PAUSED,
|
||||
updatedAt = java.time.LocalDateTime.now().toString()
|
||||
)
|
||||
repository.updateSession(pausedSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeSession(sessionId: String) {
|
||||
viewModelScope.launch {
|
||||
val session = repository.getSessionById(sessionId)
|
||||
if (session != null && session.status == SessionStatus.PAUSED) {
|
||||
val resumedSession = session.copy(
|
||||
status = SessionStatus.ACTIVE,
|
||||
updatedAt = java.time.LocalDateTime.now().toString()
|
||||
)
|
||||
repository.updateSession(resumedSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt operations
|
||||
fun addAttempt(attempt: Attempt) {
|
||||
viewModelScope.launch {
|
||||
@@ -219,52 +193,12 @@ class ClimbViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAttempt(attempt: Attempt) {
|
||||
viewModelScope.launch {
|
||||
repository.updateAttempt(attempt)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAttempt(attempt: Attempt) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteAttempt(attempt)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
|
||||
repository.getAttemptsBySession(sessionId)
|
||||
|
||||
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> =
|
||||
repository.getAttemptsByProblem(problemId)
|
||||
|
||||
|
||||
|
||||
// Analytics operations
|
||||
// fun getProblemProgress(problemId: String): Flow<ProblemProgress?> =
|
||||
// repository.getProblemProgress(problemId)
|
||||
|
||||
// fun getSessionSummary(sessionId: String): Flow<SessionSummary?> =
|
||||
// repository.getSessionSummary(sessionId)
|
||||
|
||||
// Export operations
|
||||
fun exportData(context: Context, directory: File? = null) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
val exportFile = repository.exportAllDataToJson(directory)
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
message = "Data exported to: ${exportFile.absolutePath}"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
error = "Export failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDataToUri(context: Context, uri: android.net.Uri) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
@@ -283,25 +217,6 @@ class ClimbViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
// ZIP Export operations with images
|
||||
fun exportDataToZip(context: Context, directory: File? = null) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
val exportFile = repository.exportAllDataToZip(directory)
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
message = "Data with images exported to: ${exportFile.absolutePath}"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
error = "Export failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDataToZipUri(context: Context, uri: android.net.Uri) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
@@ -358,10 +273,6 @@ class ClimbViewModel(
|
||||
_uiState.value = _uiState.value.copy(error = message)
|
||||
}
|
||||
|
||||
// Search operations
|
||||
fun searchGyms(query: String): Flow<List<Gym>> = repository.searchGyms(query)
|
||||
fun searchProblems(query: String): Flow<List<Problem>> = repository.searchProblems(query)
|
||||
|
||||
// Share operations
|
||||
suspend fun generateSessionShareCard(
|
||||
context: Context,
|
||||
|
||||
@@ -34,9 +34,7 @@ object SessionShareUtils {
|
||||
): SessionStats {
|
||||
val successfulResults = listOf(
|
||||
AttemptResult.SUCCESS,
|
||||
AttemptResult.FLASH,
|
||||
AttemptResult.REDPOINT,
|
||||
AttemptResult.ONSIGHT
|
||||
AttemptResult.FLASH
|
||||
)
|
||||
|
||||
val successfulAttempts = attempts.filter { it.result in successfulResults }
|
||||
@@ -57,9 +55,7 @@ object SessionShareUtils {
|
||||
val duration = if (session.duration != null) "${session.duration}m" else "Unknown"
|
||||
val topResult = attempts.maxByOrNull {
|
||||
when (it.result) {
|
||||
AttemptResult.ONSIGHT -> 5
|
||||
AttemptResult.FLASH -> 4
|
||||
AttemptResult.REDPOINT -> 3
|
||||
AttemptResult.FLASH -> 3
|
||||
AttemptResult.SUCCESS -> 2
|
||||
AttemptResult.FALL -> 1
|
||||
else -> 0
|
||||
|
||||
Reference in New Issue
Block a user