332 lines
13 KiB
Kotlin
332 lines
13 KiB
Kotlin
package com.atridad.openclimb.ui
|
|
|
|
import android.Manifest
|
|
import android.content.pm.PackageManager
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.Add
|
|
import androidx.compose.material.icons.filled.PlayArrow
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
import androidx.navigation.NavHostController
|
|
import androidx.navigation.compose.NavHost
|
|
import androidx.navigation.compose.composable
|
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
import androidx.navigation.compose.rememberNavController
|
|
import androidx.navigation.toRoute
|
|
import com.atridad.openclimb.data.database.OpenClimbDatabase
|
|
import com.atridad.openclimb.data.repository.ClimbRepository
|
|
import com.atridad.openclimb.navigation.Screen
|
|
import com.atridad.openclimb.navigation.bottomNavigationItems
|
|
import com.atridad.openclimb.ui.components.NotificationPermissionDialog
|
|
import com.atridad.openclimb.ui.screens.*
|
|
import com.atridad.openclimb.ui.theme.OpenClimbTheme
|
|
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
|
|
import com.atridad.openclimb.ui.viewmodel.ClimbViewModelFactory
|
|
import com.atridad.openclimb.utils.NotificationPermissionUtils
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun OpenClimbApp() {
|
|
val navController = rememberNavController()
|
|
val context = LocalContext.current
|
|
|
|
val database = remember { OpenClimbDatabase.getDatabase(context) }
|
|
val repository = remember { ClimbRepository(database, context) }
|
|
val viewModel: ClimbViewModel = viewModel(
|
|
factory = ClimbViewModelFactory(repository)
|
|
)
|
|
|
|
// Notification permission state
|
|
var showNotificationPermissionDialog by remember { mutableStateOf(false) }
|
|
var hasCheckedNotificationPermission by remember { mutableStateOf(false) }
|
|
|
|
// Permission launcher
|
|
val permissionLauncher = rememberLauncherForActivityResult(
|
|
contract = ActivityResultContracts.RequestPermission()
|
|
) { isGranted: Boolean ->
|
|
// Handle permission result
|
|
if (isGranted) {
|
|
// Permission granted, continue
|
|
} else {
|
|
// Permission denied, show dialog again later
|
|
showNotificationPermissionDialog = false
|
|
}
|
|
}
|
|
|
|
// Check notification permission on first launch
|
|
LaunchedEffect(Unit) {
|
|
if (!hasCheckedNotificationPermission) {
|
|
hasCheckedNotificationPermission = true
|
|
|
|
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
|
!NotificationPermissionUtils.isNotificationPermissionGranted(context)) {
|
|
showNotificationPermissionDialog = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure session tracking service is running when app resumes
|
|
LaunchedEffect(Unit) {
|
|
viewModel.ensureSessionTrackingServiceRunning(context)
|
|
}
|
|
|
|
// FAB configuration
|
|
var fabConfig by remember { mutableStateOf<FabConfig?>(null) }
|
|
|
|
Scaffold(
|
|
bottomBar = {
|
|
OpenClimbBottomNavigation(navController = navController)
|
|
},
|
|
floatingActionButton = {
|
|
fabConfig?.let { config ->
|
|
FloatingActionButton(
|
|
onClick = config.onClick,
|
|
containerColor = MaterialTheme.colorScheme.primary
|
|
) {
|
|
Icon(
|
|
imageVector = config.icon,
|
|
contentDescription = config.contentDescription
|
|
)
|
|
}
|
|
}
|
|
}
|
|
) { innerPadding ->
|
|
NavHost(
|
|
navController = navController,
|
|
startDestination = Screen.Sessions,
|
|
modifier = Modifier.padding(innerPadding)
|
|
) {
|
|
// Main screens
|
|
composable<Screen.Sessions> {
|
|
val gyms by viewModel.gyms.collectAsState()
|
|
val activeSession by viewModel.activeSession.collectAsState()
|
|
LaunchedEffect(gyms, activeSession) {
|
|
fabConfig = if (gyms.isNotEmpty() && activeSession == null) {
|
|
FabConfig(
|
|
icon = Icons.Default.PlayArrow,
|
|
contentDescription = "Start Session",
|
|
onClick = {
|
|
// Check notification permission before starting session
|
|
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
|
!NotificationPermissionUtils.isNotificationPermissionGranted(context)) {
|
|
showNotificationPermissionDialog = true
|
|
} else {
|
|
if (gyms.size == 1) {
|
|
viewModel.startSession(context, gyms.first().id)
|
|
} else {
|
|
navController.navigate(Screen.AddEditSession())
|
|
}
|
|
}
|
|
}
|
|
)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
SessionsScreen(
|
|
viewModel = viewModel,
|
|
onNavigateToSessionDetail = { sessionId ->
|
|
navController.navigate(Screen.SessionDetail(sessionId))
|
|
}
|
|
)
|
|
}
|
|
|
|
composable<Screen.Problems> {
|
|
val gyms by viewModel.gyms.collectAsState()
|
|
LaunchedEffect(gyms) {
|
|
fabConfig = if (gyms.isNotEmpty()) {
|
|
FabConfig(
|
|
icon = Icons.Default.Add,
|
|
contentDescription = "Add Problem",
|
|
onClick = {
|
|
navController.navigate(Screen.AddEditProblem())
|
|
}
|
|
)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
ProblemsScreen(
|
|
viewModel = viewModel,
|
|
onNavigateToProblemDetail = { problemId ->
|
|
navController.navigate(Screen.ProblemDetail(problemId))
|
|
}
|
|
)
|
|
}
|
|
|
|
composable<Screen.Analytics> {
|
|
LaunchedEffect(Unit) {
|
|
fabConfig = null // No FAB for analytics
|
|
}
|
|
AnalyticsScreen(viewModel = viewModel)
|
|
}
|
|
|
|
composable<Screen.Gyms> {
|
|
LaunchedEffect(Unit) {
|
|
fabConfig = FabConfig(
|
|
icon = Icons.Default.Add,
|
|
contentDescription = "Add Gym",
|
|
onClick = {
|
|
navController.navigate(Screen.AddEditGym())
|
|
}
|
|
)
|
|
}
|
|
GymsScreen(
|
|
viewModel = viewModel,
|
|
onNavigateToGymDetail = { gymId ->
|
|
navController.navigate(Screen.GymDetail(gymId))
|
|
}
|
|
)
|
|
}
|
|
|
|
composable<Screen.Settings> {
|
|
LaunchedEffect(Unit) {
|
|
fabConfig = null // No FAB for settings
|
|
}
|
|
SettingsScreen(viewModel = viewModel)
|
|
}
|
|
|
|
// Detail screens
|
|
composable<Screen.SessionDetail> { backStackEntry ->
|
|
val args = backStackEntry.toRoute<Screen.SessionDetail>()
|
|
LaunchedEffect(Unit) { fabConfig = null }
|
|
SessionDetailScreen(
|
|
sessionId = args.sessionId,
|
|
viewModel = viewModel,
|
|
onNavigateBack = { navController.popBackStack() },
|
|
onNavigateToProblemDetail = { problemId ->
|
|
navController.navigate(Screen.ProblemDetail(problemId))
|
|
}
|
|
)
|
|
}
|
|
|
|
composable<Screen.ProblemDetail> { backStackEntry ->
|
|
val args = backStackEntry.toRoute<Screen.ProblemDetail>()
|
|
LaunchedEffect(Unit) { fabConfig = null }
|
|
ProblemDetailScreen(
|
|
problemId = args.problemId,
|
|
viewModel = viewModel,
|
|
onNavigateBack = { navController.popBackStack() },
|
|
onNavigateToEdit = { problemId ->
|
|
navController.navigate(Screen.AddEditProblem(problemId = problemId))
|
|
}
|
|
)
|
|
}
|
|
|
|
composable<Screen.GymDetail> { backStackEntry ->
|
|
val args = backStackEntry.toRoute<Screen.GymDetail>()
|
|
LaunchedEffect(Unit) { fabConfig = null }
|
|
GymDetailScreen(
|
|
gymId = args.gymId,
|
|
viewModel = viewModel,
|
|
onNavigateBack = { navController.popBackStack() },
|
|
onNavigateToEdit = { gymId ->
|
|
navController.navigate(Screen.AddEditGym(gymId = gymId))
|
|
},
|
|
onNavigateToSessionDetail = { sessionId ->
|
|
navController.navigate(Screen.SessionDetail(sessionId))
|
|
},
|
|
onNavigateToProblemDetail = { problemId ->
|
|
navController.navigate(Screen.ProblemDetail(problemId))
|
|
}
|
|
)
|
|
}
|
|
|
|
|
|
composable<Screen.AddEditGym> { backStackEntry ->
|
|
val args = backStackEntry.toRoute<Screen.AddEditGym>()
|
|
LaunchedEffect(Unit) { fabConfig = null }
|
|
AddEditGymScreen(
|
|
gymId = args.gymId,
|
|
viewModel = viewModel,
|
|
onNavigateBack = { navController.popBackStack() }
|
|
)
|
|
}
|
|
|
|
composable<Screen.AddEditProblem> { backStackEntry ->
|
|
val args = backStackEntry.toRoute<Screen.AddEditProblem>()
|
|
LaunchedEffect(Unit) { fabConfig = null }
|
|
AddEditProblemScreen(
|
|
problemId = args.problemId,
|
|
gymId = args.gymId,
|
|
viewModel = viewModel,
|
|
onNavigateBack = { navController.popBackStack() }
|
|
)
|
|
}
|
|
|
|
composable<Screen.AddEditSession> { backStackEntry ->
|
|
val args = backStackEntry.toRoute<Screen.AddEditSession>()
|
|
LaunchedEffect(Unit) { fabConfig = null }
|
|
AddEditSessionScreen(
|
|
sessionId = args.sessionId,
|
|
gymId = args.gymId,
|
|
viewModel = viewModel,
|
|
onNavigateBack = { navController.popBackStack() }
|
|
)
|
|
}
|
|
}
|
|
|
|
// Notification permission dialog
|
|
if (showNotificationPermissionDialog) {
|
|
NotificationPermissionDialog(
|
|
onDismiss = { showNotificationPermissionDialog = false },
|
|
onRequestPermission = {
|
|
permissionLauncher.launch(NotificationPermissionUtils.getNotificationPermissionString())
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun OpenClimbBottomNavigation(navController: NavHostController) {
|
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
|
val currentRoute = navBackStackEntry?.destination?.route
|
|
|
|
NavigationBar {
|
|
bottomNavigationItems.forEach { item ->
|
|
val isSelected = when (item.screen) {
|
|
is Screen.Sessions -> currentRoute?.contains("Session") == true
|
|
is Screen.Problems -> currentRoute?.contains("Problem") == true
|
|
is Screen.Gyms -> currentRoute?.contains("Gym") == true
|
|
is Screen.Analytics -> currentRoute?.contains("Analytics") == true
|
|
is Screen.Settings -> currentRoute?.contains("Settings") == true
|
|
else -> currentRoute?.contains(item.screen::class.simpleName ?: "") == true
|
|
}
|
|
|
|
NavigationBarItem(
|
|
icon = { Icon(item.icon, contentDescription = item.label) },
|
|
label = { Text(item.label) },
|
|
selected = isSelected,
|
|
onClick = {
|
|
navController.navigate(item.screen) {
|
|
// Clear the entire back stack and go to the selected tab's root screen
|
|
popUpTo(0) {
|
|
inclusive = true
|
|
}
|
|
// Avoid multiple copies of the same destination when
|
|
// reselecting the same item
|
|
launchSingleTop = true
|
|
// Don't restore state - always start fresh when switching tabs
|
|
restoreState = false
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
data class FabConfig(
|
|
val icon: androidx.compose.ui.graphics.vector.ImageVector,
|
|
val contentDescription: String,
|
|
val onClick: () -> Unit
|
|
)
|
|
|
|
|