279 lines
10 KiB
Kotlin
279 lines
10 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.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.activity.viewModels
|
|
import androidx.appcompat.app.AppCompatDelegate
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.isSystemInDarkTheme
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.WindowInsets
|
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
import androidx.compose.foundation.layout.asPaddingValues
|
|
import androidx.compose.foundation.layout.calculateEndPadding
|
|
import androidx.compose.foundation.layout.calculateStartPadding
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.only
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.safeDrawing
|
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
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.IconButton
|
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
|
import androidx.compose.material3.MaterialTheme.typography
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Surface
|
|
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.modifier.modifierLocalConsumer
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.tooling.preview.Preview
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.core.os.LocaleListCompat
|
|
import androidx.lifecycle.lifecycleScope
|
|
import io.github.jan.supabase.auth.auth
|
|
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.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 net.tinsae.clocked.util.SupabaseClient
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) {
|
|
val settingsState by settingsViewModel.uiState.collectAsState()
|
|
val sessionStatus by authViewModel.sessionStatus.collectAsState()
|
|
|
|
|
|
/*LaunchedEffect(Unit) {
|
|
SupabaseClient.client.auth.sessionStatus.collect { status ->
|
|
if (status is SessionStatus.Authenticated &&
|
|
SupabaseClient.client.auth.currentSessionOrNull() != null
|
|
) {
|
|
LogRepository.fetchLogs()
|
|
} else {
|
|
return@collect
|
|
}
|
|
}
|
|
}*/
|
|
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)
|
|
}
|
|
|
|
is SessionStatus.NotAuthenticated -> {
|
|
LoginScreen(authViewModel)
|
|
}
|
|
|
|
else -> {}
|
|
}
|
|
}
|
|
}
|
|
|
|
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(
|
|
navigationSuiteItems = {
|
|
AppDestinations.entries.forEach { destination ->
|
|
item(
|
|
icon = { Icon(
|
|
imageVector = destination.icon,
|
|
contentDescription = stringResource(destination.label))
|
|
},
|
|
alwaysShowLabel = false,
|
|
label = { Text(stringResource(destination.label)) },
|
|
selected = destination == currentDestination,
|
|
onClick = { currentDestination = destination }
|
|
)
|
|
}
|
|
}
|
|
) {
|
|
Scaffold(
|
|
modifier = Modifier.fillMaxSize(),
|
|
|
|
topBar = {
|
|
Surface(
|
|
color = colorScheme.surfaceContainer,
|
|
shadowElevation = 2.dp, // Adds a subtle shadow to lift the bar
|
|
modifier = Modifier.fillMaxWidth()
|
|
// No padding needed here on the Surface itself
|
|
) {
|
|
Row(
|
|
// Apply safe area padding to the Row to push content down
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top).asPaddingValues()),
|
|
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
modifier = Modifier.padding(start = 16.dp), // Vertical padding is handled by Row's alignment
|
|
text = stringResource(R.string.app_name),
|
|
style = typography.titleLarge,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
|
|
Spacer(modifier = Modifier.weight(1f))
|
|
|
|
IconButton(onClick = { authViewModel.logout() }, modifier = Modifier.padding(10.dp)) {
|
|
Icon(
|
|
imageVector = Icons.AutoMirrored.Filled.Logout,
|
|
contentDescription = "Logout" // Use string resource
|
|
)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
) { 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,
|
|
authViewModel = authViewModel
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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),
|
|
}
|
|
|
|
@Preview(showBackground = true)
|
|
@Composable
|
|
fun AppEntryPreview() {
|
|
ClockedTheme {
|
|
AppEntry(SettingsViewModel(), AuthViewModel())
|
|
}
|
|
}
|