Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7770997fd4
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,7 @@ local.properties
|
|||||||
|
|
||||||
# Log/OS Files
|
# Log/OS Files
|
||||||
*.log
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Android Studio generated files and folders
|
# Android Studio generated files and folders
|
||||||
captures/
|
captures/
|
||||||
@@ -32,4 +33,4 @@ render.experimental.xml
|
|||||||
google-services.json
|
google-services.json
|
||||||
|
|
||||||
# Android Profiling
|
# Android Profiling
|
||||||
*.hprof
|
*.hprof
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.atridad.openclimb"
|
applicationId = "com.atridad.openclimb"
|
||||||
minSdk = 34
|
minSdk = 34
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 21
|
versionCode = 22
|
||||||
versionName = "1.4.0"
|
versionName = "1.4.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import com.atridad.openclimb.ui.theme.OpenClimbTheme
|
|||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private var shortcutAction by mutableStateOf<String?>(null)
|
private var shortcutAction by mutableStateOf<String?>(null)
|
||||||
|
private var lastUsedGymId by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
fun clearShortcutAction() {
|
||||||
|
shortcutAction = null
|
||||||
|
lastUsedGymId = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -21,11 +27,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
shortcutAction = intent?.action
|
shortcutAction = intent?.action
|
||||||
|
lastUsedGymId = intent?.getStringExtra("LAST_USED_GYM_ID")
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
OpenClimbTheme {
|
OpenClimbTheme {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
OpenClimbApp(shortcutAction = shortcutAction)
|
OpenClimbApp(
|
||||||
|
shortcutAction = shortcutAction,
|
||||||
|
lastUsedGymId = lastUsedGymId,
|
||||||
|
onShortcutActionProcessed = { clearShortcutAction() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,5 +47,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
|
|
||||||
shortcutAction = intent.action
|
shortcutAction = intent.action
|
||||||
|
lastUsedGymId = intent.getStringExtra("LAST_USED_GYM_ID")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
package com.atridad.openclimb.data.repository
|
package com.atridad.openclimb.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
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.ZipExportImportUtils
|
import com.atridad.openclimb.utils.ZipExportImportUtils
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class ClimbRepository(
|
class ClimbRepository(database: OpenClimbDatabase, private val context: Context) {
|
||||||
database: OpenClimbDatabase,
|
|
||||||
private val context: Context
|
|
||||||
) {
|
|
||||||
private val gymDao = database.gymDao()
|
private val gymDao = database.gymDao()
|
||||||
private val problemDao = database.problemDao()
|
private val problemDao = database.problemDao()
|
||||||
private val sessionDao = database.climbSessionDao()
|
private val sessionDao = database.climbSessionDao()
|
||||||
private val attemptDao = database.attemptDao()
|
private val attemptDao = database.attemptDao()
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
private val json = Json {
|
prettyPrint = true
|
||||||
prettyPrint = true
|
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gym operations
|
// Gym operations
|
||||||
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms()
|
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms()
|
||||||
suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id)
|
suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id)
|
||||||
@@ -34,7 +28,7 @@ class ClimbRepository(
|
|||||||
suspend fun updateGym(gym: Gym) = gymDao.updateGym(gym)
|
suspend fun updateGym(gym: Gym) = gymDao.updateGym(gym)
|
||||||
suspend fun deleteGym(gym: Gym) = gymDao.deleteGym(gym)
|
suspend fun deleteGym(gym: Gym) = gymDao.deleteGym(gym)
|
||||||
fun searchGyms(query: String): Flow<List<Gym>> = gymDao.searchGyms(query)
|
fun searchGyms(query: String): Flow<List<Gym>> = gymDao.searchGyms(query)
|
||||||
|
|
||||||
// Problem operations
|
// Problem operations
|
||||||
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)
|
||||||
@@ -43,27 +37,36 @@ class ClimbRepository(
|
|||||||
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)
|
||||||
fun searchProblems(query: String): Flow<List<Problem>> = problemDao.searchProblems(query)
|
fun searchProblems(query: String): Flow<List<Problem>> = problemDao.searchProblems(query)
|
||||||
|
|
||||||
// Session operations
|
// Session operations
|
||||||
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)
|
||||||
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
|
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
|
||||||
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
|
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
|
||||||
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)
|
||||||
|
suspend fun getLastUsedGym(): Gym? {
|
||||||
|
val recentSessions = sessionDao.getRecentSessions(1).first()
|
||||||
|
return if (recentSessions.isNotEmpty()) {
|
||||||
|
getGymById(recentSessions.first().gymId)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt operations
|
// Attempt operations
|
||||||
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
||||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> = attemptDao.getAttemptsBySession(sessionId)
|
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
|
||||||
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> = attemptDao.getAttemptsByProblem(problemId)
|
attemptDao.getAttemptsBySession(sessionId)
|
||||||
|
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)
|
||||||
suspend fun updateAttempt(attempt: Attempt) = attemptDao.updateAttempt(attempt)
|
suspend fun updateAttempt(attempt: Attempt) = attemptDao.updateAttempt(attempt)
|
||||||
suspend fun deleteAttempt(attempt: Attempt) = attemptDao.deleteAttempt(attempt)
|
suspend fun deleteAttempt(attempt: Attempt) = attemptDao.deleteAttempt(attempt)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ZIP Export with images - Single format for reliability
|
// ZIP Export with images - Single format for reliability
|
||||||
suspend fun exportAllDataToZip(directory: File? = null): File {
|
suspend fun exportAllDataToZip(directory: File? = null): File {
|
||||||
try {
|
try {
|
||||||
@@ -72,47 +75,58 @@ class ClimbRepository(
|
|||||||
val allProblems = problemDao.getAllProblems().first()
|
val allProblems = problemDao.getAllProblems().first()
|
||||||
val allSessions = sessionDao.getAllSessions().first()
|
val allSessions = sessionDao.getAllSessions().first()
|
||||||
val allAttempts = attemptDao.getAllAttempts().first()
|
val allAttempts = attemptDao.getAllAttempts().first()
|
||||||
|
|
||||||
// Validate data integrity before export
|
// Validate data integrity before export
|
||||||
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
||||||
|
|
||||||
val exportData = ClimbDataExport(
|
val exportData =
|
||||||
exportedAt = LocalDateTime.now().toString(),
|
ClimbDataExport(
|
||||||
version = "1.0",
|
exportedAt = LocalDateTime.now().toString(),
|
||||||
gyms = allGyms,
|
version = "1.0",
|
||||||
problems = allProblems,
|
gyms = allGyms,
|
||||||
sessions = allSessions,
|
problems = allProblems,
|
||||||
attempts = allAttempts
|
sessions = allSessions,
|
||||||
)
|
attempts = allAttempts
|
||||||
|
)
|
||||||
|
|
||||||
// Collect all referenced image paths and validate they exist
|
// Collect all referenced image paths and validate they exist
|
||||||
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
||||||
val validImagePaths = referencedImagePaths.filter { imagePath ->
|
val validImagePaths =
|
||||||
try {
|
referencedImagePaths
|
||||||
val imageFile = com.atridad.openclimb.utils.ImageUtils.getImageFile(context, imagePath)
|
.filter { imagePath ->
|
||||||
imageFile.exists() && imageFile.length() > 0
|
try {
|
||||||
} catch (e: Exception) {
|
val imageFile =
|
||||||
false
|
com.atridad.openclimb.utils.ImageUtils.getImageFile(
|
||||||
}
|
context,
|
||||||
}.toSet()
|
imagePath
|
||||||
|
)
|
||||||
|
imageFile.exists() && imageFile.length() > 0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
|
||||||
// Log any missing images for debugging
|
// Log any missing images for debugging
|
||||||
val missingImages = referencedImagePaths - validImagePaths
|
val missingImages = referencedImagePaths - validImagePaths
|
||||||
if (missingImages.isNotEmpty()) {
|
if (missingImages.isNotEmpty()) {
|
||||||
android.util.Log.w("ClimbRepository", "Some referenced images are missing: $missingImages")
|
android.util.Log.w(
|
||||||
|
"ClimbRepository",
|
||||||
|
"Some referenced images are missing: $missingImages"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ZipExportImportUtils.createExportZip(
|
return ZipExportImportUtils.createExportZip(
|
||||||
context = context,
|
context = context,
|
||||||
exportData = exportData,
|
exportData = exportData,
|
||||||
referencedImagePaths = validImagePaths,
|
referencedImagePaths = validImagePaths,
|
||||||
directory = directory
|
directory = directory
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Exception("Export failed: ${e.message}")
|
throw Exception("Export failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {
|
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {
|
||||||
try {
|
try {
|
||||||
// Collect all data with proper error handling
|
// Collect all data with proper error handling
|
||||||
@@ -120,72 +134,81 @@ class ClimbRepository(
|
|||||||
val allProblems = problemDao.getAllProblems().first()
|
val allProblems = problemDao.getAllProblems().first()
|
||||||
val allSessions = sessionDao.getAllSessions().first()
|
val allSessions = sessionDao.getAllSessions().first()
|
||||||
val allAttempts = attemptDao.getAllAttempts().first()
|
val allAttempts = attemptDao.getAllAttempts().first()
|
||||||
|
|
||||||
// Validate data integrity before export
|
// Validate data integrity before export
|
||||||
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
||||||
|
|
||||||
val exportData = ClimbDataExport(
|
val exportData =
|
||||||
exportedAt = LocalDateTime.now().toString(),
|
ClimbDataExport(
|
||||||
version = "1.0",
|
exportedAt = LocalDateTime.now().toString(),
|
||||||
gyms = allGyms,
|
version = "1.0",
|
||||||
problems = allProblems,
|
gyms = allGyms,
|
||||||
sessions = allSessions,
|
problems = allProblems,
|
||||||
attempts = allAttempts
|
sessions = allSessions,
|
||||||
)
|
attempts = allAttempts
|
||||||
|
)
|
||||||
|
|
||||||
// Collect all referenced image paths and validate they exist
|
// Collect all referenced image paths and validate they exist
|
||||||
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
||||||
val validImagePaths = referencedImagePaths.filter { imagePath ->
|
val validImagePaths =
|
||||||
try {
|
referencedImagePaths
|
||||||
val imageFile = com.atridad.openclimb.utils.ImageUtils.getImageFile(context, imagePath)
|
.filter { imagePath ->
|
||||||
imageFile.exists() && imageFile.length() > 0
|
try {
|
||||||
} catch (e: Exception) {
|
val imageFile =
|
||||||
false
|
com.atridad.openclimb.utils.ImageUtils.getImageFile(
|
||||||
}
|
context,
|
||||||
}.toSet()
|
imagePath
|
||||||
|
)
|
||||||
|
imageFile.exists() && imageFile.length() > 0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
|
||||||
ZipExportImportUtils.createExportZipToUri(
|
ZipExportImportUtils.createExportZipToUri(
|
||||||
context = context,
|
context = context,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
exportData = exportData,
|
exportData = exportData,
|
||||||
referencedImagePaths = validImagePaths
|
referencedImagePaths = validImagePaths
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Exception("Export failed: ${e.message}")
|
throw Exception("Export failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun importDataFromZip(file: File) {
|
suspend fun importDataFromZip(file: File) {
|
||||||
try {
|
try {
|
||||||
// Validate the ZIP file
|
// Validate the ZIP file
|
||||||
if (!file.exists() || file.length() == 0L) {
|
if (!file.exists() || file.length() == 0L) {
|
||||||
throw Exception("Invalid ZIP file: file is empty or doesn't exist")
|
throw Exception("Invalid ZIP file: file is empty or doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and validate the ZIP contents
|
// Extract and validate the ZIP contents
|
||||||
val importResult = ZipExportImportUtils.extractImportZip(context, file)
|
val importResult = ZipExportImportUtils.extractImportZip(context, file)
|
||||||
|
|
||||||
// Validate JSON content
|
// Validate JSON content
|
||||||
if (importResult.jsonContent.isBlank()) {
|
if (importResult.jsonContent.isBlank()) {
|
||||||
throw Exception("Invalid ZIP file: no data.json found or empty content")
|
throw Exception("Invalid ZIP file: no data.json found or empty content")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and validate the data structure
|
// Parse and validate the data structure
|
||||||
val importData = try {
|
val importData =
|
||||||
json.decodeFromString<ClimbDataExport>(importResult.jsonContent)
|
try {
|
||||||
} catch (e: Exception) {
|
json.decodeFromString<ClimbDataExport>(importResult.jsonContent)
|
||||||
throw Exception("Invalid data format: ${e.message}")
|
} catch (e: Exception) {
|
||||||
}
|
throw Exception("Invalid data format: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
// Validate data integrity
|
// Validate data integrity
|
||||||
validateImportData(importData)
|
validateImportData(importData)
|
||||||
|
|
||||||
// Clear existing data to avoid conflicts
|
// Clear existing data to avoid conflicts
|
||||||
attemptDao.deleteAllAttempts()
|
attemptDao.deleteAllAttempts()
|
||||||
sessionDao.deleteAllSessions()
|
sessionDao.deleteAllSessions()
|
||||||
problemDao.deleteAllProblems()
|
problemDao.deleteAllProblems()
|
||||||
gymDao.deleteAllGyms()
|
gymDao.deleteAllGyms()
|
||||||
|
|
||||||
// Import gyms first (problems depend on gyms)
|
// Import gyms first (problems depend on gyms)
|
||||||
importData.gyms.forEach { gym ->
|
importData.gyms.forEach { gym ->
|
||||||
try {
|
try {
|
||||||
@@ -194,13 +217,14 @@ class ClimbRepository(
|
|||||||
throw Exception("Failed to import gym ${gym.name}: ${e.message}")
|
throw Exception("Failed to import gym ${gym.name}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import problems with updated image paths
|
// Import problems with updated image paths
|
||||||
val updatedProblems = ZipExportImportUtils.updateProblemImagePaths(
|
val updatedProblems =
|
||||||
importData.problems,
|
ZipExportImportUtils.updateProblemImagePaths(
|
||||||
importResult.importedImagePaths
|
importData.problems,
|
||||||
)
|
importResult.importedImagePaths
|
||||||
|
)
|
||||||
|
|
||||||
updatedProblems.forEach { problem ->
|
updatedProblems.forEach { problem ->
|
||||||
try {
|
try {
|
||||||
problemDao.insertProblem(problem)
|
problemDao.insertProblem(problem)
|
||||||
@@ -208,7 +232,7 @@ class ClimbRepository(
|
|||||||
throw Exception("Failed to import problem ${problem.name}: ${e.message}")
|
throw Exception("Failed to import problem ${problem.name}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import sessions
|
// Import sessions
|
||||||
importData.sessions.forEach { session ->
|
importData.sessions.forEach { session ->
|
||||||
try {
|
try {
|
||||||
@@ -217,7 +241,7 @@ class ClimbRepository(
|
|||||||
throw Exception("Failed to import session: ${e.message}")
|
throw Exception("Failed to import session: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import attempts last (depends on problems and sessions)
|
// Import attempts last (depends on problems and sessions)
|
||||||
importData.attempts.forEach { attempt ->
|
importData.attempts.forEach { attempt ->
|
||||||
try {
|
try {
|
||||||
@@ -226,61 +250,66 @@ class ClimbRepository(
|
|||||||
throw Exception("Failed to import attempt: ${e.message}")
|
throw Exception("Failed to import attempt: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Exception("Import failed: ${e.message}")
|
throw Exception("Import failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateDataIntegrity(
|
private fun validateDataIntegrity(
|
||||||
gyms: List<Gym>,
|
gyms: List<Gym>,
|
||||||
problems: List<Problem>,
|
problems: List<Problem>,
|
||||||
sessions: List<ClimbSession>,
|
sessions: List<ClimbSession>,
|
||||||
attempts: List<Attempt>
|
attempts: List<Attempt>
|
||||||
) {
|
) {
|
||||||
// Validate that all problems reference valid gyms
|
// Validate that all problems reference valid gyms
|
||||||
val gymIds = gyms.map { it.id }.toSet()
|
val gymIds = gyms.map { it.id }.toSet()
|
||||||
val invalidProblems = problems.filter { it.gymId !in gymIds }
|
val invalidProblems = problems.filter { it.gymId !in gymIds }
|
||||||
if (invalidProblems.isNotEmpty()) {
|
if (invalidProblems.isNotEmpty()) {
|
||||||
throw Exception("Data integrity error: ${invalidProblems.size} problems reference non-existent gyms")
|
throw Exception(
|
||||||
|
"Data integrity error: ${invalidProblems.size} problems reference non-existent gyms"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that all sessions reference valid gyms
|
// Validate that all sessions reference valid gyms
|
||||||
val invalidSessions = sessions.filter { it.gymId !in gymIds }
|
val invalidSessions = sessions.filter { it.gymId !in gymIds }
|
||||||
if (invalidSessions.isNotEmpty()) {
|
if (invalidSessions.isNotEmpty()) {
|
||||||
throw Exception("Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms")
|
throw Exception(
|
||||||
|
"Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that all attempts reference valid problems and sessions
|
// Validate that all attempts reference valid problems and sessions
|
||||||
val problemIds = problems.map { it.id }.toSet()
|
val problemIds = problems.map { it.id }.toSet()
|
||||||
val sessionIds = sessions.map { it.id }.toSet()
|
val sessionIds = sessions.map { it.id }.toSet()
|
||||||
|
|
||||||
val invalidAttempts = attempts.filter {
|
val invalidAttempts =
|
||||||
it.problemId !in problemIds || it.sessionId !in sessionIds
|
attempts.filter { it.problemId !in problemIds || it.sessionId !in sessionIds }
|
||||||
}
|
|
||||||
if (invalidAttempts.isNotEmpty()) {
|
if (invalidAttempts.isNotEmpty()) {
|
||||||
throw Exception("Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions")
|
throw Exception(
|
||||||
|
"Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateImportData(importData: ClimbDataExport) {
|
private fun validateImportData(importData: ClimbDataExport) {
|
||||||
if (importData.gyms.isEmpty()) {
|
if (importData.gyms.isEmpty()) {
|
||||||
throw Exception("Import data is invalid: no gyms found")
|
throw Exception("Import data is invalid: no gyms found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importData.version.isBlank()) {
|
if (importData.version.isBlank()) {
|
||||||
throw Exception("Import data is invalid: no version information")
|
throw Exception("Import data is invalid: no version information")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for reasonable data sizes to prevent malicious imports
|
// Check for reasonable data sizes to prevent malicious imports
|
||||||
if (importData.gyms.size > 1000 ||
|
if (importData.gyms.size > 1000 ||
|
||||||
importData.problems.size > 10000 ||
|
importData.problems.size > 10000 ||
|
||||||
importData.sessions.size > 10000 ||
|
importData.sessions.size > 10000 ||
|
||||||
importData.attempts.size > 100000) {
|
importData.attempts.size > 100000
|
||||||
|
) {
|
||||||
throw Exception("Import data is too large: possible corruption or malicious file")
|
throw Exception("Import data is too large: possible corruption or malicious file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resetAllData() {
|
suspend fun resetAllData() {
|
||||||
try {
|
try {
|
||||||
// Clear all data from database
|
// Clear all data from database
|
||||||
@@ -288,15 +317,14 @@ class ClimbRepository(
|
|||||||
sessionDao.deleteAllSessions()
|
sessionDao.deleteAllSessions()
|
||||||
problemDao.deleteAllProblems()
|
problemDao.deleteAllProblems()
|
||||||
gymDao.deleteAllGyms()
|
gymDao.deleteAllGyms()
|
||||||
|
|
||||||
// Clear all images from storage
|
// Clear all images from storage
|
||||||
clearAllImages()
|
clearAllImages()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Exception("Reset failed: ${e.message}")
|
throw Exception("Reset failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearAllImages() {
|
private fun clearAllImages() {
|
||||||
try {
|
try {
|
||||||
// Get the images directory
|
// Get the images directory
|
||||||
@@ -314,10 +342,10 @@ class ClimbRepository(
|
|||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class ClimbDataExport(
|
data class ClimbDataExport(
|
||||||
val exportedAt: String,
|
val exportedAt: String,
|
||||||
val version: String = "1.0",
|
val version: String = "1.0",
|
||||||
val gyms: List<Gym>,
|
val gyms: List<Gym>,
|
||||||
val problems: List<Problem>,
|
val problems: List<Problem>,
|
||||||
val sessions: List<ClimbSession>,
|
val sessions: List<ClimbSession>,
|
||||||
val attempts: List<Attempt>
|
val attempts: List<Attempt>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,36 +4,33 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
data class BottomNavigationItem(
|
data class BottomNavigationItem(val screen: Screen, val icon: ImageVector, val label: String)
|
||||||
val screen: Screen,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val label: String
|
|
||||||
)
|
|
||||||
|
|
||||||
val bottomNavigationItems = listOf(
|
val bottomNavigationItems =
|
||||||
BottomNavigationItem(
|
listOf(
|
||||||
screen = Screen.Sessions,
|
BottomNavigationItem(
|
||||||
icon = Icons.Default.PlayArrow,
|
screen = Screen.Sessions,
|
||||||
label = "Sessions"
|
icon = Icons.Default.PlayArrow,
|
||||||
),
|
label = "Sessions"
|
||||||
BottomNavigationItem(
|
),
|
||||||
screen = Screen.Problems,
|
BottomNavigationItem(
|
||||||
icon = Icons.Default.Star,
|
screen = Screen.Problems,
|
||||||
label = "Problems"
|
icon = Icons.Default.Star,
|
||||||
),
|
label = "Problems"
|
||||||
BottomNavigationItem(
|
),
|
||||||
screen = Screen.Analytics,
|
BottomNavigationItem(
|
||||||
icon = Icons.Default.Info,
|
screen = Screen.Analytics,
|
||||||
label = "Analytics"
|
icon = Icons.Default.Info,
|
||||||
),
|
label = "Analytics"
|
||||||
BottomNavigationItem(
|
),
|
||||||
screen = Screen.Gyms,
|
BottomNavigationItem(
|
||||||
icon = Icons.Default.LocationOn,
|
screen = Screen.Gyms,
|
||||||
label = "Gyms"
|
icon = Icons.Default.LocationOn,
|
||||||
),
|
label = "Gyms"
|
||||||
BottomNavigationItem(
|
),
|
||||||
screen = Screen.Settings,
|
BottomNavigationItem(
|
||||||
icon = Icons.Default.Settings,
|
screen = Screen.Settings,
|
||||||
label = "Settings"
|
icon = Icons.Default.Settings,
|
||||||
)
|
label = "Settings"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -30,10 +31,16 @@ import com.atridad.openclimb.utils.NotificationPermissionUtils
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OpenClimbApp(shortcutAction: String? = null) {
|
fun OpenClimbApp(
|
||||||
|
shortcutAction: String? = null,
|
||||||
|
lastUsedGymId: String? = null,
|
||||||
|
onShortcutActionProcessed: () -> Unit = {}
|
||||||
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var lastUsedGym by remember { mutableStateOf<com.atridad.openclimb.data.model.Gym?>(null) }
|
||||||
|
|
||||||
val database = remember { OpenClimbDatabase.getDatabase(context) }
|
val database = remember { OpenClimbDatabase.getDatabase(context) }
|
||||||
val repository = remember { ClimbRepository(database, context) }
|
val repository = remember { ClimbRepository(database, context) }
|
||||||
val viewModel: ClimbViewModel = viewModel(factory = ClimbViewModelFactory(repository))
|
val viewModel: ClimbViewModel = viewModel(factory = ClimbViewModelFactory(repository))
|
||||||
@@ -69,11 +76,19 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
|||||||
val activeSession by viewModel.activeSession.collectAsState()
|
val activeSession by viewModel.activeSession.collectAsState()
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
val gyms by viewModel.gyms.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(activeSession, gyms) {
|
// Update last used gym when gyms change
|
||||||
|
LaunchedEffect(gyms) {
|
||||||
|
if (gyms.isNotEmpty() && lastUsedGym == null) {
|
||||||
|
lastUsedGym = viewModel.getLastUsedGym()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(activeSession, gyms, lastUsedGym) {
|
||||||
AppShortcutManager.updateShortcuts(
|
AppShortcutManager.updateShortcuts(
|
||||||
context = context,
|
context = context,
|
||||||
hasActiveSession = activeSession != null,
|
hasActiveSession = activeSession != null,
|
||||||
hasGyms = gyms.isNotEmpty()
|
hasGyms = gyms.isNotEmpty(),
|
||||||
|
lastUsedGym = if (activeSession == null && gyms.size > 1) lastUsedGym else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,22 +99,6 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
|||||||
popUpTo(0) { inclusive = true }
|
popUpTo(0) { inclusive = true }
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeSession == null && gyms.isNotEmpty()) {
|
|
||||||
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
|
||||||
!NotificationPermissionUtils.isNotificationPermissionGranted(
|
|
||||||
context
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
showNotificationPermissionDialog = true
|
|
||||||
} else {
|
|
||||||
if (gyms.size == 1) {
|
|
||||||
viewModel.startSession(context, gyms.first().id)
|
|
||||||
} else {
|
|
||||||
navController.navigate(Screen.AddEditSession())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
AppShortcutManager.ACTION_END_SESSION -> {
|
AppShortcutManager.ACTION_END_SESSION -> {
|
||||||
navController.navigate(Screen.Sessions) {
|
navController.navigate(Screen.Sessions) {
|
||||||
@@ -112,6 +111,62 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process shortcut actions after data is loaded
|
||||||
|
LaunchedEffect(shortcutAction, activeSession, gyms, lastUsedGym) {
|
||||||
|
if (shortcutAction == AppShortcutManager.ACTION_START_SESSION && gyms.isNotEmpty()) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Processing shortcut action: activeSession=$activeSession, gyms.size=${gyms.size}, lastUsedGymId=$lastUsedGymId, lastUsedGym=${lastUsedGym?.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (activeSession == null) {
|
||||||
|
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
||||||
|
!NotificationPermissionUtils.isNotificationPermissionGranted(
|
||||||
|
context
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
android.util.Log.d("OpenClimbApp", "Showing notification permission dialog")
|
||||||
|
showNotificationPermissionDialog = true
|
||||||
|
} else {
|
||||||
|
if (gyms.size == 1) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Starting session with single gym: ${gyms.first().name}"
|
||||||
|
)
|
||||||
|
viewModel.startSession(context, gyms.first().id)
|
||||||
|
} else {
|
||||||
|
// Try to get the last used gym from the intent or fallback to state
|
||||||
|
val targetGym =
|
||||||
|
lastUsedGymId?.let { gymId -> gyms.find { it.id == gymId } }
|
||||||
|
?: lastUsedGym
|
||||||
|
|
||||||
|
if (targetGym != null) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Starting session with target gym: ${targetGym.name}"
|
||||||
|
)
|
||||||
|
viewModel.startSession(context, targetGym.id)
|
||||||
|
} else {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"No target gym found, navigating to selection"
|
||||||
|
)
|
||||||
|
navController.navigate(Screen.AddEditSession())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Active session already exists: ${activeSession?.id}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the shortcut action after processing to prevent repeated execution
|
||||||
|
onShortcutActionProcessed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var fabConfig by remember { mutableStateOf<FabConfig?>(null) }
|
var fabConfig by remember { mutableStateOf<FabConfig?>(null) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -155,6 +210,8 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
|||||||
if (gyms.size == 1) {
|
if (gyms.size == 1) {
|
||||||
viewModel.startSession(context, gyms.first().id)
|
viewModel.startSession(context, gyms.first().id)
|
||||||
} else {
|
} else {
|
||||||
|
// Always show gym selection for FAB when
|
||||||
|
// multiple gyms
|
||||||
navController.navigate(Screen.AddEditSession())
|
navController.navigate(Screen.AddEditSession())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +230,6 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable<Screen.Problems> {
|
composable<Screen.Problems> {
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
|
||||||
LaunchedEffect(gyms) {
|
LaunchedEffect(gyms) {
|
||||||
fabConfig =
|
fabConfig =
|
||||||
if (gyms.isNotEmpty()) {
|
if (gyms.isNotEmpty()) {
|
||||||
|
|||||||
@@ -190,12 +190,18 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() {
|
|||||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
||||||
repository.getSessionsByGym(gymId)
|
repository.getSessionsByGym(gymId)
|
||||||
|
|
||||||
|
// Get last used gym for shortcut functionality
|
||||||
|
suspend fun getLastUsedGym(): Gym? = repository.getLastUsedGym()
|
||||||
|
|
||||||
// Active session management
|
// Active session management
|
||||||
fun startSession(context: Context, gymId: String, notes: String? = null) {
|
fun startSession(context: Context, gymId: String, notes: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
android.util.Log.d("ClimbViewModel", "startSession called with gymId: $gymId")
|
||||||
|
|
||||||
if (!com.atridad.openclimb.utils.NotificationPermissionUtils
|
if (!com.atridad.openclimb.utils.NotificationPermissionUtils
|
||||||
.isNotificationPermissionGranted(context)
|
.isNotificationPermissionGranted(context)
|
||||||
) {
|
) {
|
||||||
|
android.util.Log.d("ClimbViewModel", "Notification permission not granted")
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
error =
|
error =
|
||||||
@@ -206,6 +212,10 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() {
|
|||||||
|
|
||||||
val existingActive = repository.getActiveSession()
|
val existingActive = repository.getActiveSession()
|
||||||
if (existingActive != null) {
|
if (existingActive != null) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"ClimbViewModel",
|
||||||
|
"Active session already exists: ${existingActive.id}"
|
||||||
|
)
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
error = "There's already an active session. Please end it first."
|
error = "There's already an active session. Please end it first."
|
||||||
@@ -213,15 +223,21 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("ClimbViewModel", "Creating new session")
|
||||||
val newSession = ClimbSession.create(gymId = gymId, notes = notes)
|
val newSession = ClimbSession.create(gymId = gymId, notes = notes)
|
||||||
repository.insertSession(newSession)
|
repository.insertSession(newSession)
|
||||||
|
|
||||||
|
android.util.Log.d(
|
||||||
|
"ClimbViewModel",
|
||||||
|
"Starting tracking service for session: ${newSession.id}"
|
||||||
|
)
|
||||||
// Start the tracking service
|
// Start the tracking service
|
||||||
val serviceIntent = SessionTrackingService.createStartIntent(context, newSession.id)
|
val serviceIntent = SessionTrackingService.createStartIntent(context, newSession.id)
|
||||||
context.startForegroundService(serviceIntent)
|
context.startForegroundService(serviceIntent)
|
||||||
|
|
||||||
ClimbStatsWidgetProvider.updateAllWidgets(context)
|
ClimbStatsWidgetProvider.updateAllWidgets(context)
|
||||||
|
|
||||||
|
android.util.Log.d("ClimbViewModel", "Session started successfully")
|
||||||
_uiState.value = _uiState.value.copy(message = "Session started successfully!")
|
_uiState.value = _uiState.value.copy(message = "Session started successfully!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ object AppShortcutManager {
|
|||||||
const val ACTION_END_SESSION = "com.atridad.openclimb.action.END_SESSION"
|
const val ACTION_END_SESSION = "com.atridad.openclimb.action.END_SESSION"
|
||||||
|
|
||||||
/** Updates the app shortcuts based on current session state */
|
/** Updates the app shortcuts based on current session state */
|
||||||
fun updateShortcuts(context: Context, hasActiveSession: Boolean, hasGyms: Boolean) {
|
fun updateShortcuts(
|
||||||
|
context: Context,
|
||||||
|
hasActiveSession: Boolean,
|
||||||
|
hasGyms: Boolean,
|
||||||
|
lastUsedGym: com.atridad.openclimb.data.model.Gym? = null
|
||||||
|
) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
|
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
|
||||||
|
|
||||||
@@ -30,7 +35,7 @@ object AppShortcutManager {
|
|||||||
shortcuts.add(createEndSessionShortcut(context))
|
shortcuts.add(createEndSessionShortcut(context))
|
||||||
} else if (hasGyms) {
|
} else if (hasGyms) {
|
||||||
// Show "Start Session" shortcut when no active session but gyms exist
|
// Show "Start Session" shortcut when no active session but gyms exist
|
||||||
shortcuts.add(createStartSessionShortcut(context))
|
shortcuts.add(createStartSessionShortcut(context, lastUsedGym))
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcutManager.dynamicShortcuts = shortcuts
|
shortcutManager.dynamicShortcuts = shortcuts
|
||||||
@@ -38,16 +43,34 @@ object AppShortcutManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||||
private fun createStartSessionShortcut(context: Context): ShortcutInfo {
|
private fun createStartSessionShortcut(
|
||||||
|
context: Context,
|
||||||
|
lastUsedGym: com.atridad.openclimb.data.model.Gym? = null
|
||||||
|
): ShortcutInfo {
|
||||||
val startIntent =
|
val startIntent =
|
||||||
Intent(context, MainActivity::class.java).apply {
|
Intent(context, MainActivity::class.java).apply {
|
||||||
action = ACTION_START_SESSION
|
action = ACTION_START_SESSION
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
lastUsedGym?.let { gym -> putExtra("LAST_USED_GYM_ID", gym.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val shortLabel =
|
||||||
|
if (lastUsedGym != null) {
|
||||||
|
"Start at ${lastUsedGym.name}"
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.shortcut_start_session_short)
|
||||||
|
}
|
||||||
|
|
||||||
|
val longLabel =
|
||||||
|
if (lastUsedGym != null) {
|
||||||
|
"Start a new climbing session at ${lastUsedGym.name}"
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.shortcut_start_session_long)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ShortcutInfo.Builder(context, SHORTCUT_START_SESSION)
|
return ShortcutInfo.Builder(context, SHORTCUT_START_SESSION)
|
||||||
.setShortLabel(context.getString(R.string.shortcut_start_session_short))
|
.setShortLabel(shortLabel)
|
||||||
.setLongLabel(context.getString(R.string.shortcut_start_session_long))
|
.setLongLabel(longLabel)
|
||||||
.setIcon(Icon.createWithResource(context, R.drawable.ic_play_arrow_24))
|
.setIcon(Icon.createWithResource(context, R.drawable.ic_play_arrow_24))
|
||||||
.setIntent(startIntent)
|
.setIntent(startIntent)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
Reference in New Issue
Block a user