integration supabas

This commit is contained in:
2025-12-28 10:14:19 +01:00
parent 3a37eac107
commit 36d5fc5ce9
29 changed files with 1944 additions and 363 deletions

View File

@@ -1,15 +1,26 @@
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.Box
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
@@ -17,14 +28,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.AccountBox
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.MaterialTheme
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.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@@ -32,139 +47,216 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.tinsae.clocked.dashboard.DashboardScreen
import net.tinsae.clocked.data.Locale
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
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.LaunchedEffect
import io.github.jan.supabase.auth.status.SessionStatus
import net.tinsae.clocked.data.LogRepository
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 {
ClockedTheme {
// The root of the app now decides whether to show Login or Main content
AppEntry()
}
// The single ViewModel instances from the Activity are passed down.
AppEntry(settingsViewModel, authViewModel)
}
}
}
@Composable
fun AppEntry() {
// For now, we'll fake authentication. In a real app, this would come from a ViewModel or repository.
var isAuthenticated by rememberSaveable { mutableStateOf(true) } // Start as not authenticated
fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) {
val settingsState by settingsViewModel.uiState.collectAsState()
val sessionStatus by authViewModel.sessionStatus.collectAsState()
if (isAuthenticated) {
// If authenticated, show the main app UI
ClockedApp()
} else {
// If not, show the Login screen
LoginScreen(onLoginSuccess = { })
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) {
// Use a 'when' statement to handle all possible states.
when (sessionStatus) {
is SessionStatus.Authenticated -> {
// If we know the user is authenticated, show the main app.
ClockedApp(settingsViewModel, authViewModel,sessionStatus)
}
is SessionStatus.NotAuthenticated -> {
// Only if we are certain the user is not logged in, show the login screen.
LoginScreen(authViewModel)
}
is SessionStatus.Initializing -> {
// While Supabase is loading the session, show a loading indicator.
// This prevents the flicker.
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
else -> {}
}
}
}
@PreviewScreenSizes
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() {
fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel, sessionStatus: SessionStatus) {
var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) }
Scaffold(
modifier = Modifier.fillMaxSize(), // Removed background for clarity
modifier = Modifier.fillMaxSize(),
topBar = {
Box(
Row(
modifier = Modifier
.fillMaxWidth()
// Apply padding ONLY for the top safe area (status bar)
.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top).asPaddingValues())
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.CenterStart
.background(colorScheme.surface)
.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top).asPaddingValues()),
//contentAlignment = Alignment.CenterStart
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge,
style = typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.weight(1f))
// 2. Use IconButton to make the icon clickable with a ripple effect
IconButton(onClick = { authViewModel.logout() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Logout, // Replace with your logout icon
contentDescription = "Logout"
)
}
}
},
) { innerPadding -> // This padding from the parent Scaffold accounts for the TopBar.
//val layoutDirection = LocalLayoutDirection.current
) { innerPadding ->
val layoutDirection = LocalLayoutDirection.current
val contentPadding = PaddingValues(
// Respect the horizontal padding for gestures, etc.
//start = innerPadding.calculateStartPadding(layoutDirection),
//end = innerPadding.calculateEndPadding(layoutDirection),
// Respect the top padding to stay below the top bar.
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection),
top = innerPadding.calculateTopPadding(),
// CRITICAL: Ignore the bottom padding from the parent Scaffold.
bottom = 0.dp
)
NavigationSuiteScaffold(
// Apply our custom-calculated padding.
modifier = Modifier.padding(contentPadding),
navigationSuiteItems = {
AppDestinations.entries.forEach {
AppDestinations.entries.forEach { destination ->
item(
icon = { Icon(it.icon, contentDescription = it.label) },
label = { Text(it.label) },
selected = it == currentDestination,
onClick = { currentDestination = it }
icon = { Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.label))
},
label = { Text(stringResource(destination.label)) },
selected = destination == currentDestination,
onClick = { currentDestination = destination }
)
}
}
) {
// The router now shows the correct screen based on the destination.
when (currentDestination) {
AppDestinations.HOME -> DashboardScreen(modifier = Modifier.fillMaxSize())
AppDestinations.HOME -> DashboardScreen( modifier = Modifier.fillMaxSize())
AppDestinations.HISTORY -> HistoryScreen(modifier = Modifier.fillMaxSize())
AppDestinations.PROFILE -> Greeting(name = "Profile", modifier = Modifier.fillMaxSize())
AppDestinations.SETTING -> SettingsScreen(
modifier = Modifier.fillMaxSize(),
viewModel = settingsViewModel,
authViewModel = authViewModel
)
}
}
}
}
enum class AppDestinations(
val label: String,
val label: Int,
val icon: ImageVector,
) {
HOME("Home", Icons.Default.Home),
HISTORY("History", Icons.AutoMirrored.Filled.List),
PROFILE("Profile", Icons.Default.AccountBox),
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),
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Scaffold(modifier = modifier) { innerPadding ->
Text(
text = "Hello $name!",
modifier = Modifier.padding(innerPadding)
)
}
}
/*
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
ClockedTheme {
Greeting("Android")
}
}
*/
// --- PREVIEW THE ACTUAL APP ENTRY POINT ---
@Preview(showBackground = true)
@Composable
fun AppEntryPreview() {
ClockedTheme {
// This preview will correctly show the LoginScreen because isAuthenticated starts as false.
AppEntry()
AppEntry(SettingsViewModel(), AuthViewModel())
}
}