231 lines
8.4 KiB
Kotlin
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)
|
|
}
|