Files
Clocked/app/src/main/java/net/tinsae/clocked/MainActivity.kt

231 lines
8.4 KiB
Kotlin

package net.tinsae.clocked
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.lifecycleScope
import io.github.jan.supabase.auth.status.SessionStatus
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.tinsae.clocked.biometric.GlobalAuthenticator
import net.tinsae.clocked.components.ShowLoadingScreen
import net.tinsae.clocked.dashboard.DashboardScreen
import net.tinsae.clocked.data.Locale
import net.tinsae.clocked.data.LogRepository
import net.tinsae.clocked.data.Theme
import net.tinsae.clocked.history.HistoryScreen
import net.tinsae.clocked.settings.SettingsScreen
import net.tinsae.clocked.settings.SettingsViewModel
import net.tinsae.clocked.ui.theme.ClockedTheme
import java.text.SimpleDateFormat
import java.util.Date
class MainActivity : AppCompatActivity() {
private val settingsViewModel: SettingsViewModel by viewModels()
private val authViewModel: AuthViewModel by viewModels()
private val createDocumentLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("text/csv")
) { uri: Uri? ->
uri?.let {
val content = settingsViewModel.uiState.value.pendingCsvContent
if (content != null) {
try {
contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.writer().use { writer ->
writer.write(content)
}
}
} catch (e: Exception) {
Log.e("MainActivity", "Error writing to file", e)
}
}
}
// Always reset the state
settingsViewModel.onExportHandled()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
settingsViewModel.uiState
.map { it.pendingCsvContent }
.distinctUntilChanged()
.collect { csvContent ->
if (csvContent != null) {
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US).format(Date())
createDocumentLauncher.launch("Clocked-Export-$timestamp.csv")
}
}
}
enableEdgeToEdge()
setContent {
// The single ViewModel instances from the Activity are passed down.
AppEntry(settingsViewModel, authViewModel)
GlobalAuthenticator()
}
}
}
@Composable
fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) {
val settingsState by settingsViewModel.uiState.collectAsState()
val sessionStatus by authViewModel.sessionStatus.collectAsState()
// Trigger fetching of logs when the session status changes.
LaunchedEffect(sessionStatus) {
if (sessionStatus is SessionStatus.Authenticated){
LogRepository.fetchLogs()
}
}
val useDarkTheme = when (settingsState.theme) {
Theme.LIGHT -> false
Theme.DARK -> true
Theme.SYSTEM -> isSystemInDarkTheme()
}
val context = LocalContext.current
updateLocale(context, settingsState.locale)
ClockedTheme(darkTheme = useDarkTheme) {
when (sessionStatus) {
is SessionStatus.Initializing -> {
// Only show loading for the initial session check.
ShowLoadingScreen()
}
is SessionStatus.Authenticated -> {
// Just show the app. The ViewModels inside will manage their own loading UI.
ClockedApp(settingsViewModel, authViewModel)
}
else -> {
LoginScreen(authViewModel)
}
}
}
}
fun updateLocale(context: Context, locale: Locale) {
val localeTag = locale.tag.ifEmpty { null }
val localeList = if (localeTag != null) {
LocaleListCompat.forLanguageTags(localeTag)
} else {
LocaleListCompat.getEmptyLocaleList()
}
AppCompatDelegate.setApplicationLocales(localeList)
}
@Composable
fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) {
var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) }
NavigationSuiteScaffold(
modifier = Modifier.fillMaxSize(),
navigationSuiteItems = {
AppDestinations.entries.forEach { destination ->
item(
icon = {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.label),
modifier = Modifier.padding(1.dp).size(24.dp)
)
},
alwaysShowLabel = false,
label = { Text(stringResource(destination.label)) },
selected = destination == currentDestination,
onClick = { currentDestination = destination }
)
}
}
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
/*topBar = {
TopBar(
appName = stringResource(id = R.string.app_name, currentDestination.label),
modifier = Modifier.padding(horizontal = 16.dp),
isForContainer = true,
actions = {
IconButton(
onClick = { authViewModel.logout() }, modifier = Modifier.padding(10.dp),
) {
Icon(imageVector = Icons.AutoMirrored.Filled.Logout, contentDescription = stringResource(R.string.logout))
}
}
)
}*/
) { innerPadding ->
val modifier = Modifier
.fillMaxWidth()
.padding(innerPadding)
when (currentDestination) {
AppDestinations.HOME -> DashboardScreen(modifier = modifier)
AppDestinations.HISTORY -> HistoryScreen(modifier = modifier)
AppDestinations.SETTING -> SettingsScreen(
modifier = modifier,
viewModel = settingsViewModel
)
AppDestinations.LOGOUT -> {
authViewModel.logout()
}
}
}
}
}
enum class AppDestinations(
val label: Int,
val icon: ImageVector,
) {
HOME(R.string.nav_home, Icons.Default.Home),
HISTORY(R.string.nav_history, Icons.AutoMirrored.Filled.List),
SETTING(R.string.nav_settings, Icons.Default.Settings),
LOGOUT(R.string.logout, Icons.AutoMirrored.Filled.Logout)
}