0.3.3
This commit is contained in:
@@ -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.
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.atridad.openclimb",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 4,
|
||||
"versionName": "0.3.1",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"baselineProfiles": [
|
||||
{
|
||||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/app-release.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/app-release.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minSdkVersionForDexing": 31
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -282,26 +216,7 @@ 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