integration supabas
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user