diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 790fe5f..0b0dfab 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -87,6 +87,14 @@ dependencies { implementation(libs.supabase.auth) implementation(libs.supabase.postgrest) implementation(libs.ktor.client.android) + implementation(libs.supabase.realtime) + + + + + + + //implementation(libs.supabase.gotrue.live) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..03808ed Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/net/tinsae/clocked/AuthViewModel.kt b/app/src/main/java/net/tinsae/clocked/AuthViewModel.kt index 5249ca1..80df16e 100644 --- a/app/src/main/java/net/tinsae/clocked/AuthViewModel.kt +++ b/app/src/main/java/net/tinsae/clocked/AuthViewModel.kt @@ -12,14 +12,17 @@ import io.github.jan.supabase.auth.providers.builtin.Email import io.github.jan.supabase.auth.status.SessionStatus import io.github.jan.supabase.exceptions.BadRequestRestException import io.github.jan.supabase.exceptions.RestException +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import net.tinsae.clocked.data.FormType +import net.tinsae.clocked.data.LogRepository import net.tinsae.clocked.util.SupabaseClient // Add this data class here for UI state @@ -52,7 +55,6 @@ class AuthViewModel : ViewModel() { this.email = email this.password = password } - // No need to emit a success event. The isAuthenticated flow will automatically update. } catch (e: Exception) { uiState = uiState.copy(error = parseError(e)) //e.message ?: "An unknown error occurred") } finally { diff --git a/app/src/main/java/net/tinsae/clocked/MainActivity.kt b/app/src/main/java/net/tinsae/clocked/MainActivity.kt index e71f3b2..ad16564 100644 --- a/app/src/main/java/net/tinsae/clocked/MainActivity.kt +++ b/app/src/main/java/net/tinsae/clocked/MainActivity.kt @@ -12,7 +12,6 @@ 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 @@ -26,6 +25,7 @@ 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 @@ -36,17 +36,19 @@ 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.Alignment 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 @@ -55,22 +57,23 @@ 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 -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() { @@ -126,6 +129,25 @@ 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 @@ -136,29 +158,21 @@ fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) updateLocale(context, settingsState.locale) ClockedTheme(darkTheme = useDarkTheme) { - // Use a 'when' statement to handle all possible states. when (sessionStatus) { + is SessionStatus.Initializing -> { + // Only show loading for the initial session check. + ShowLoadingScreen() + } + is SessionStatus.Authenticated -> { - // If we know the user is authenticated, show the main app. - ClockedApp(settingsViewModel, authViewModel,sessionStatus) + // Just show the app. The ViewModels inside will manage their own loading UI. + ClockedApp(settingsViewModel, authViewModel) } 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 -> {} } } @@ -175,67 +189,69 @@ fun updateLocale(context: Context, locale: Locale) { } @Composable -fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel, sessionStatus: SessionStatus) { +fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) { var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) } - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .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 = typography.titleLarge, - fontWeight = FontWeight.Bold + + 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 } ) - - 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 -> - val layoutDirection = LocalLayoutDirection.current - val contentPadding = PaddingValues( - start = innerPadding.calculateStartPadding(layoutDirection), - end = innerPadding.calculateEndPadding(layoutDirection), - top = innerPadding.calculateTopPadding(), - bottom = 0.dp - ) + } + ) { + Scaffold( + modifier = Modifier.fillMaxSize(), - NavigationSuiteScaffold( - modifier = Modifier.padding(contentPadding), - navigationSuiteItems = { - AppDestinations.entries.forEach { destination -> - item( - icon = { Icon( - imageVector = destination.icon, - contentDescription = stringResource(destination.label)) - }, - label = { Text(stringResource(destination.label)) }, - selected = destination == currentDestination, - onClick = { currentDestination = destination } - ) + 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.fillMaxSize()) - AppDestinations.HISTORY -> HistoryScreen(modifier = Modifier.fillMaxSize()) + AppDestinations.HOME -> DashboardScreen( modifier = modifier) + AppDestinations.HISTORY -> HistoryScreen(modifier = modifier) AppDestinations.SETTING -> SettingsScreen( - modifier = Modifier.fillMaxSize(), + modifier = Modifier, viewModel = settingsViewModel, authViewModel = authViewModel ) diff --git a/app/src/main/java/net/tinsae/clocked/components/ActivityList.kt b/app/src/main/java/net/tinsae/clocked/components/ListItem.kt similarity index 62% rename from app/src/main/java/net/tinsae/clocked/components/ActivityList.kt rename to app/src/main/java/net/tinsae/clocked/components/ListItem.kt index 1faca95..5d4482d 100644 --- a/app/src/main/java/net/tinsae/clocked/components/ActivityList.kt +++ b/app/src/main/java/net/tinsae/clocked/components/ListItem.kt @@ -9,40 +9,40 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults +// Import the new icon from the extended library +import androidx.compose.material.icons.filled.History +import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalWindowInfo 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.IntOffset import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import net.tinsae.clocked.R import net.tinsae.clocked.data.Log +import net.tinsae.clocked.data.LogRepository.getRecentLogs import net.tinsae.clocked.ui.theme.cyan import net.tinsae.clocked.ui.theme.red import net.tinsae.clocked.util.Util.formatDuration @@ -50,74 +50,30 @@ import net.tinsae.clocked.util.Util.formatTimestampToLocalDateString import kotlin.math.roundToInt @Composable -fun ActivityList( - modifier: Modifier = Modifier, - onEditLog: (Log) -> Unit = {}, - onDeleteLog: (Log) -> Unit = {}, - recentLogs: List -) { - //var mutableItems by remember { mutableStateOf(items) } - // State to hold the log that should be shown in the dialog. Null means no dialog. - var selectedLogForDialog by remember { mutableStateOf(null) } - - Column(modifier = modifier.fillMaxWidth()) { - Text( - text = stringResource(R.string.recent_activity), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(horizontal = 16.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - LazyColumn { - items(recentLogs, key = { it.id }) { log -> - ActivityItem( - log = log, - onEdit = { onEditLog(log) }, - onDelete = {onDeleteLog(log) }, - // When the item is clicked, set it as the selected log for the dialog - onClick = { selectedLogForDialog = log } - ) - } - } - } - - // When a log is selected, show the dialog. - selectedLogForDialog?.let { log -> - DetailsDialog( - log = log, - onDismiss = { selectedLogForDialog = null } - ) - } -} - -@Composable -fun ActivityItem( +fun ListItem( log: Log, onDelete: () -> Unit, onEdit: () -> Unit, - onClick: () -> Unit, // Add onClick callback + onClick: () -> Unit, modifier: Modifier = Modifier ) { val coroutineScope = rememberCoroutineScope() var offsetX by remember { mutableFloatStateOf(0f) } val animatedOffsetX by animateFloatAsState(targetValue = offsetX, label = "offset") - // Use stable LocalConfiguration val screenWidthPx = LocalWindowInfo.current.containerSize.width - - // Define thresholds for swiping val editThreshold = screenWidthPx * 0.4f val deleteThreshold = -screenWidthPx * 0.4f Box( modifier = modifier - .padding(vertical = 4.dp) .fillMaxWidth() + .clickable { onClick() } // Make the whole area clickable ) { - // Background content (icons) + // --- Background content (icons for swipe) --- Row( modifier = Modifier .matchParentSize() - .clip(MaterialTheme.shapes.medium) .background(if (animatedOffsetX > 0) cyan else red) .padding(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically @@ -139,8 +95,8 @@ fun ActivityItem( } } - // --- Foreground content (the Card) --- - Card( + // --- Foreground content (the new list item UI) --- + Surface( // Use Surface for background color and elevation control modifier = Modifier .offset { IntOffset(animatedOffsetX.roundToInt(), 0) } .pointerInput(Unit) { @@ -152,6 +108,7 @@ fun ActivityItem( onEdit() offsetX = 0f } + offsetX < deleteThreshold -> onDelete() else -> offsetX = 0f } @@ -162,29 +119,48 @@ fun ActivityItem( offsetX += dragAmount } } - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - // Use the onClick callback here - .clickable { onClick() }, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + .fillMaxWidth(), + color = MaterialTheme.colorScheme.surface // Ensures background is opaque ) { Column { Row( - modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier + .fillMaxWidth() + .padding( 12.dp), + verticalAlignment = Alignment.CenterVertically, ) { + // 1. Leading Icon - CHANGED + Icon( + imageVector = Icons.Default.AccessTime, // Changed to a more relevant round icon + contentDescription = stringResource(R.string.duration_entry_icon), // Added content description + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.secondary // Changed to secondary for better contrast + ) + + Spacer(modifier = Modifier.width(16.dp)) + + // 2. Primary Text (using your existing log data) Text( modifier = Modifier.weight(1f), text = formatTimestampToLocalDateString(log.timestamp), style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold ) - Text(text = formatDuration(log.duration), style = MaterialTheme.typography.bodyLarge) + + // 3. Trailing Text (using your existing log data) + Text( + text = formatDuration(log.duration), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } - Text( - text = log.reason ?: "", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 4.dp) + + // 4. Divider + Divider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + modifier = Modifier + .fillMaxWidth() + // Indent the divider to align with text + .padding(start = 72.dp) // 16dp (padding) + 40dp (icon) + 16dp (spacer) ) } } @@ -193,6 +169,6 @@ fun ActivityItem( @Composable @Preview(showBackground = true) -fun ActivityItemPreview() { - ActivityList(recentLogs = emptyList(), onEditLog = {_->{}}, onDeleteLog = {_->{}}) +fun ListItemPreview() { + ListItem(getRecentLogs().first(), {}, {}, {}) } diff --git a/app/src/main/java/net/tinsae/clocked/components/LoadingScreen.kt b/app/src/main/java/net/tinsae/clocked/components/LoadingScreen.kt new file mode 100644 index 0000000..b47b4ef --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/components/LoadingScreen.kt @@ -0,0 +1,18 @@ +package net.tinsae.clocked.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun ShowLoadingScreen(){ + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt index 60e85a3..98d2fc3 100644 --- a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt +++ b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt @@ -1,5 +1,6 @@ package net.tinsae.clocked.dashboard +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,30 +10,37 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text 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.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel -import io.github.jan.supabase.auth.status.SessionStatus import net.tinsae.clocked.R -import net.tinsae.clocked.components.ActivityList +import net.tinsae.clocked.components.ListItem import net.tinsae.clocked.components.AddLogDialog +import net.tinsae.clocked.components.DetailsDialog +import net.tinsae.clocked.components.ShowLoadingScreen import net.tinsae.clocked.data.EntryType -import net.tinsae.clocked.data.LogRepository +import net.tinsae.clocked.data.Log import net.tinsae.clocked.ui.theme.ClockedTheme import net.tinsae.clocked.ui.theme.cyan import net.tinsae.clocked.ui.theme.green @@ -45,9 +53,9 @@ fun DashboardScreen( ) { val uiState by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { + /*LaunchedEffect(Unit) { LogRepository.fetchLogs() - } + }*/ if (uiState.showAddLogDialog) { AddLogDialog( @@ -57,57 +65,80 @@ fun DashboardScreen( ) } - Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp, 0.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + if (uiState.isLoading) { + ShowLoadingScreen() + } else { + Column( + modifier = modifier + .fillMaxSize() + .padding(top=16.dp) ) { - SummaryCard(title = stringResource(id = R.string.overtime), value = uiState.overtime, color = green, modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.width(16.dp)) - SummaryCard(title = stringResource(id = R.string.time_off), value = uiState.timeOff, color = red, modifier = Modifier.weight(1f)) - } + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + SummaryCard( + title = stringResource(id = R.string.overtime), + value = uiState.overtime, + color = green, + modifier = Modifier.weight(1f), + titleStyle = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.width(16.dp)) + SummaryCard( + title = stringResource(id = R.string.time_off), + value = uiState.timeOff, + color = red, + modifier = Modifier.weight(1f), + titleStyle = MaterialTheme.typography.titleMedium + ) + } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - NetBalanceCard(value = uiState.netBalance+" / "+uiState.balanceInDays, modifier = Modifier.fillMaxWidth()) - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround - ) { - ActionButton( - text = stringResource(id = R.string.add_overtime), - onClick = { viewModel.onAddLogClicked(EntryType.OVERTIME) }, - modifier = Modifier.weight(1f) + SummaryCard( + title = stringResource(id = R.string.net_balance), + value = uiState.netBalance+" ( "+uiState.balanceInDays+")", + color = cyan, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + titleStyle = MaterialTheme.typography.titleLarge ) - Spacer(modifier = Modifier.width(16.dp)) - ActionButton( - text = stringResource(id = R.string.add_time_off), - onClick = { viewModel.onAddLogClicked(EntryType.TIME_OFF) }, - modifier = Modifier.weight(1f) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceAround + ) { + ActionButton( + text = stringResource(id = R.string.add_overtime), + onClick = { viewModel.onAddLogClicked(EntryType.OVERTIME) }, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(16.dp)) + ActionButton( + text = stringResource(id = R.string.add_time_off), + onClick = { viewModel.onAddLogClicked(EntryType.TIME_OFF) }, + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + ActivityList( + recentLogs = uiState.recentActivities, + modifier = Modifier.weight(1f), + onEditLog = viewModel::editLog, + onDeleteLog = viewModel::deleteLog ) } - Spacer(modifier = Modifier.height(16.dp)) - - ActivityList( - recentLogs = uiState.recentActivities, - modifier = Modifier.weight(1f), - onEditLog = viewModel::editLog, - onDeleteLog = viewModel::deleteLog - ) } } @Composable -fun SummaryCard(title: String, value: String, color: Color, modifier: Modifier = Modifier) { +fun SummaryCard(title: String, titleStyle: TextStyle, value: String, color: Color, modifier: Modifier = Modifier) { Card( modifier = modifier, elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) @@ -117,31 +148,13 @@ fun SummaryCard(title: String, value: String, color: Color, modifier: Modifier = horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Text(text = title, style = MaterialTheme.typography.titleMedium) + Text(text = title, style = titleStyle) Spacer(modifier = Modifier.height(8.dp)) Text(text = value, style = MaterialTheme.typography.headlineMedium, color = color, fontWeight = FontWeight.Bold) } } } -@Composable -fun NetBalanceCard(value: String, modifier: Modifier = Modifier) { - Card( - modifier = modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text(text = stringResource(id = R.string.net_balance), style = MaterialTheme.typography.titleLarge) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = value, style = MaterialTheme.typography.headlineLarge, color = cyan, fontWeight = FontWeight.Bold) - } - } -} - @Composable fun ActionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { Button( @@ -152,10 +165,49 @@ fun ActionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifie } } -@Preview(showBackground = true) + @Composable -fun DashboardScreenPreview() { - ClockedTheme { - //DashboardScreen(viewModel = DashboardViewModel()) +fun ActivityList( + modifier: Modifier = Modifier, + onEditLog: (Log) -> Unit = {}, + onDeleteLog: (Log) -> Unit = {}, + recentLogs: List +) { + //var mutableItems by remember { mutableStateOf(items) } + // State to hold the log that should be shown in the dialog. Null means no dialog. + var selectedLogForDialog by remember { mutableStateOf(null) } + + Column(modifier = modifier.fillMaxWidth()) { + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(R.string.recent_activity), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(16.dp) + ) + } + LazyColumn { + items(recentLogs, key = { it.id }) { log -> + ListItem( + modifier = Modifier.padding(horizontal = 16.dp), + log = log, + onEdit = { onEditLog(log) }, + onDelete = {onDeleteLog(log) }, + // When the item is clicked, set it as the selected log for the dialog + onClick = { selectedLogForDialog = log } + ) + } + } + } + + // When a log is selected, show the dialog. + selectedLogForDialog?.let { log -> + DetailsDialog( + log = log, + onDismiss = { selectedLogForDialog = null } + ) } } diff --git a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt index 4624e52..79c54e8 100644 --- a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt @@ -13,6 +13,9 @@ import kotlinx.coroutines.launch import net.tinsae.clocked.data.EntryType import net.tinsae.clocked.data.Log import net.tinsae.clocked.data.LogRepository +import net.tinsae.clocked.data.LogRepository.getRecentLogs +import net.tinsae.clocked.data.LogRepository.getTotalOvertimeDuration +import net.tinsae.clocked.data.LogRepository.getTotalTimeOffDuration import net.tinsae.clocked.util.Util.formatDuration import kotlin.time.Duration import kotlin.time.Instant @@ -25,7 +28,8 @@ data class DashboardUiState( val balanceInDays: String = "0d", val recentActivities: List = emptyList(), val showAddLogDialog: Boolean = false, - val dialogType: EntryType = EntryType.OVERTIME // Default, will be updated + val dialogType: EntryType = EntryType.OVERTIME, // Default, will be updated + val isLoading: Boolean = true ) class DashboardViewModel : ViewModel() { @@ -33,18 +37,13 @@ class DashboardViewModel : ViewModel() { private val _internalState = MutableStateFlow(DashboardUiState()) val uiState: StateFlow = combine( - LogRepository.logs, // Source 1: The List - _internalState // Source 2: The internal UI state - ) { logs, internalState -> - // THE FIX: The 'logs' parameter is already the List. - // You do not need to call .first() on it. - val allLogs = logs + LogRepository.isLoading, + _internalState + ) { isLoading, internalState -> + val overtimeDuration = getTotalOvertimeDuration() + val timeOffDuration = getTotalTimeOffDuration() - val overtimeDuration = allLogs.filter{ it.type == EntryType.OVERTIME } - .map { it.duration }.fold(Duration.ZERO, Duration::plus) - val timeOffDuration = allLogs.filter { it.type == EntryType.TIME_OFF } - .map { it.duration }.fold(Duration.ZERO, Duration::plus) // Construct the final state using data from both sources. val netBalanceDuration = overtimeDuration + timeOffDuration @@ -53,7 +52,7 @@ class DashboardViewModel : ViewModel() { // 2. Then, calculate the balance in days (assuming an 8-hour workday) // We format it to one decimal place for a clean look. val balanceInDaysValue = netBalanceInHours / 8.0 - val balanceInDaysString = "%.1f".format(balanceInDaysValue) + " days" + val balanceInDaysString = "%.1f".format(balanceInDaysValue) + " d " @@ -61,8 +60,9 @@ class DashboardViewModel : ViewModel() { overtime = formatDuration(overtimeDuration), timeOff = formatDuration(timeOffDuration), netBalance = formatDuration(overtimeDuration + timeOffDuration), - recentActivities = allLogs.take(7), + recentActivities = getRecentLogs(), balanceInDays = balanceInDaysString, + isLoading = isLoading, // Pass through the dialog state from the internal state holder showAddLogDialog = internalState.showAddLogDialog, dialogType = internalState.dialogType @@ -70,7 +70,7 @@ class DashboardViewModel : ViewModel() { }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), - initialValue = DashboardUiState() + initialValue = DashboardUiState(isLoading = true) ) diff --git a/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt b/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt index ccc41b0..516579c 100644 --- a/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt +++ b/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt @@ -1,15 +1,18 @@ package net.tinsae.clocked.data -import android.util.Log as LOG import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import net.tinsae.clocked.service.* +import net.tinsae.clocked.service.addLogToDB +import net.tinsae.clocked.service.deleteLogFromDB +import net.tinsae.clocked.service.editLogFromDB +import net.tinsae.clocked.service.getAllLogsFromDB import kotlin.time.Duration import kotlin.time.Instant +import android.util.Log as LOG /** * A Singleton Repository that acts as the Single Source of Truth for Log data. @@ -22,25 +25,30 @@ object LogRepository { // A public, read-only StateFlow that ViewModels can collect. val logs = _logs.asStateFlow() - private val repositoryScope = CoroutineScope(Dispatchers.IO) + // Testing code + private val _isLoading = MutableStateFlow(false) // Start as true + val isLoading = _isLoading.asStateFlow() + // testing end - init { - // Fetch initial data when the repository is first created. - fetchLogs() - } + private val repositoryScope = CoroutineScope(Dispatchers.IO) /** * Fetches the latest logs from the service and updates the flow. */ fun fetchLogs() { + // TEST CODE HERE repositoryScope.launch { + + if (_isLoading.value) return@launch // Prevent concurrent fetches + + _isLoading.update { true } try { val latestLogs = getAllLogsFromDB() _logs.update { latestLogs } - LOG.d("LogRepository", "Successfully fetched ${latestLogs.size} logs.") } catch (e: Exception) { LOG.e("LogRepository", "Failed to fetch logs", e) - // Optionally, you could expose an error state here. + } finally { + _isLoading.update { false } } } } @@ -80,4 +88,19 @@ object LogRepository { fetchLogs() } } + + + fun getRecentLogs():List { + return logs.value.take(5) + } + + fun getTotalOvertimeDuration(): Duration { + return logs.value.filter { it.type == EntryType.OVERTIME } + .map { it.duration }.fold(Duration.ZERO, Duration::plus) + } + + fun getTotalTimeOffDuration(): Duration { + return logs.value.filter { it.type == EntryType.TIME_OFF } + .map { it.duration }.fold(Duration.ZERO, Duration::plus) + } } diff --git a/app/src/main/java/net/tinsae/clocked/data/SupabaseClient.kt b/app/src/main/java/net/tinsae/clocked/data/SupabaseClient.kt deleted file mode 100644 index 073bc73..0000000 --- a/app/src/main/java/net/tinsae/clocked/data/SupabaseClient.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.tinsae.clocked.data - -import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.createSupabaseClient -import io.github.jan.supabase.postgrest.Postgrest -import net.tinsae.clocked.BuildConfig - -object SupabaseClient { - - val client by lazy { - createSupabaseClient( - supabaseUrl = BuildConfig.SUPABASE_URL, - supabaseKey = BuildConfig.SUPABASE_ANON_KEY - ) { - install(Auth) - install(Postgrest) - } - } - -} diff --git a/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt index 75e3109..1c69e84 100644 --- a/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt +++ b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import net.tinsae.clocked.R -import net.tinsae.clocked.components.ActivityItem +import net.tinsae.clocked.components.ListItem import net.tinsae.clocked.components.DetailsDialog import net.tinsae.clocked.ui.theme.ClockedTheme @@ -51,12 +51,16 @@ fun HistoryScreen( modifier = modifier.fillMaxSize() ) { if (uiState.tabs.isNotEmpty()) { - PrimaryTabRow(selectedTabIndex = uiState.selectedTabIndex) { + PrimaryTabRow(selectedTabIndex = uiState.selectedTabIndex, + modifier = Modifier.fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surfaceContainer) + ) { uiState.tabs.forEachIndexed { index, title -> Tab( + modifier = Modifier.padding( 6.dp), selected = uiState.selectedTabIndex == index, onClick = { viewModel.onTabSelected(index) }, - text = { Text(title) } + text = { Text(title,style = MaterialTheme.typography.titleMedium) } ) } } @@ -71,7 +75,7 @@ fun HistoryScreen( } items(entries, key = { it.id }) { entry -> - ActivityItem( + ListItem( log = entry, modifier = Modifier.padding(horizontal = 16.dp), onDelete = { viewModel.deleteLog(entry) }, @@ -94,15 +98,16 @@ fun HistoryScreen( @Composable fun MonthHeader(text: String, modifier: Modifier = Modifier) { Surface( - modifier = modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) + modifier = modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainer, + shadowElevation = 1.dp + ) { Text( text = text, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(16.dp,10.dp) ) } } diff --git a/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt b/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt index 938e6a6..70ca19f 100644 --- a/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt +++ b/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt @@ -57,7 +57,7 @@ class HistoryViewModel : ViewModel() { }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), - initialValue = HistoryUiState() // Start with a default empty state + initialValue = HistoryUiState(isLoading = true) // Start with a default empty state ) fun setTabTitles(titles: List) { diff --git a/app/src/main/java/net/tinsae/clocked/service/LogService.kt b/app/src/main/java/net/tinsae/clocked/service/LogService.kt index 6bb87f5..4800611 100644 --- a/app/src/main/java/net/tinsae/clocked/service/LogService.kt +++ b/app/src/main/java/net/tinsae/clocked/service/LogService.kt @@ -1,13 +1,14 @@ package net.tinsae.clocked.service -import androidx.compose.runtime.retain.retain +import io.github.jan.supabase.auth.auth import io.github.jan.supabase.postgrest.postgrest import io.github.jan.supabase.postgrest.query.Order +import kotlinx.coroutines.delay import net.tinsae.clocked.data.EntryType import net.tinsae.clocked.data.Log import android.util.Log as LOG import net.tinsae.clocked.data.LogEntry -import net.tinsae.clocked.data.SupabaseClient +import net.tinsae.clocked.util.SupabaseClient import kotlin.time.Instant import kotlin.time.Duration @@ -26,10 +27,6 @@ suspend fun getAllLogsFromDB(): List { } } -suspend fun getRecentLogsFromDB(): List { - return getAllLogsFromDB().take(5) -} - suspend fun addLogToDB(type: EntryType, timestamp: Instant, duration: Duration, reason: String?) { val newLog = LogEntry( duration = duration, @@ -78,13 +75,3 @@ suspend fun editLogFromDB(updatedLog: Log): Boolean { false } } - -// --- Calculation functions that operate on a given list of logs --- - -fun getTotalOvertimeDuration(logs: List): Duration { - return logs.filter { it.type == EntryType.OVERTIME }.fold(Duration.ZERO) { acc, log -> acc + log.duration } -} - -fun getTotalTimeOffDuration(logs: List): Duration { - return logs.filter { it.type == EntryType.TIME_OFF }.fold(Duration.ZERO) { acc, log -> acc + log.duration } -} diff --git a/app/src/main/java/net/tinsae/clocked/util/SupabaseClient.kt b/app/src/main/java/net/tinsae/clocked/util/SupabaseClient.kt index df6d9e0..8c201f0 100644 --- a/app/src/main/java/net/tinsae/clocked/util/SupabaseClient.kt +++ b/app/src/main/java/net/tinsae/clocked/util/SupabaseClient.kt @@ -1,8 +1,10 @@ package net.tinsae.clocked.util -import io.github.jan.supabase.auth.Auth import io.github.jan.supabase.createSupabaseClient +import io.github.jan.supabase.auth.Auth import io.github.jan.supabase.postgrest.Postgrest +import io.github.jan.supabase.realtime.Realtime +import io.ktor.client.engine.android.Android import net.tinsae.clocked.BuildConfig object SupabaseClient { @@ -14,6 +16,8 @@ object SupabaseClient { ) { install(Auth) install(Postgrest) + install(Realtime) + httpEngine = Android.create() } } } diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 2b068d1..7db116e 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,30 +1,15 @@ - - - - - - - - + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="#1B6184"> + - \ No newline at end of file + android:fillColor="@android:color/white" + android:pathData="M452,800L452,800Q452,800 452,800Q452,800 452,800Q452,800 452,800Q452,800 452,800Q452,800 452,800Q452,800 452,800Q452,800 452,800Q452,800 452,800ZM160,880L160,800L240,800L240,680Q240,619 268.5,565.5Q297,512 348,480Q297,448 268.5,394.5Q240,341 240,280L240,160L160,160L160,80L800,80L800,160L720,160L720,280Q720,328 702,372Q684,416 651,449Q651,449 651,449Q651,449 651,449Q613,459 580,478Q547,497 520,525Q510,523 500.5,521.5Q491,520 480,520Q414,520 367,567Q320,614 320,680L320,800L452,800Q459,822 468.5,842Q478,862 491,880L160,880ZM480,440Q546,440 593,393Q640,346 640,280L640,160L320,160L320,280Q320,346 367,393Q414,440 480,440ZM720,920Q637,920 578.5,861.5Q520,803 520,720Q520,637 578.5,578.5Q637,520 720,520Q803,520 861.5,578.5Q920,637 920,720Q920,803 861.5,861.5Q803,920 720,920ZM480,160Q480,160 480,160Q480,160 480,160L480,160L480,160L480,160Q480,160 480,160Q480,160 480,160ZM692,810L834,668L804,638L692,750L636,694L606,724L692,810Z"/> + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..c38da9d 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..ed79b23 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..fa4a69f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..4afa3bf 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..c30c4ac 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..71ac08b 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..84c229d 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..b2558a0 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..71045da 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..3886315 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ca3105..f457bb3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,9 @@ + Overtime − Time Off Recent Activity + or + Duration entry icon + Data diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 384407a..8c778f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,8 +47,9 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa supabase-bom = { group = "io.github.jan-tennert.supabase", name = "bom", version.ref = "supabase" } supabase-postgrest = { group = "io.github.jan-tennert.supabase", name = "postgrest-kt" } supabase-auth = { group = "io.github.jan-tennert.supabase", name = "auth-kt" } +supabase-realtime = { group = "io.github.jan-tennert.supabase", name = "realtime-kt" } ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } - +# supabase-gotrue-live = { group = "io.github.jan-tennert.supabase", name = "gotrue-live-kt" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }