almost done

This commit is contained in:
2025-12-29 04:42:40 +01:00
parent 36d5fc5ce9
commit d07d369546
30 changed files with 380 additions and 318 deletions

View File

@@ -87,6 +87,14 @@ dependencies {
implementation(libs.supabase.auth) implementation(libs.supabase.auth)
implementation(libs.supabase.postgrest) implementation(libs.supabase.postgrest)
implementation(libs.ktor.client.android) implementation(libs.ktor.client.android)
implementation(libs.supabase.realtime)
//implementation(libs.supabase.gotrue.live)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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.auth.status.SessionStatus
import io.github.jan.supabase.exceptions.BadRequestRestException import io.github.jan.supabase.exceptions.BadRequestRestException
import io.github.jan.supabase.exceptions.RestException import io.github.jan.supabase.exceptions.RestException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import net.tinsae.clocked.data.FormType import net.tinsae.clocked.data.FormType
import net.tinsae.clocked.data.LogRepository
import net.tinsae.clocked.util.SupabaseClient import net.tinsae.clocked.util.SupabaseClient
// Add this data class here for UI state // Add this data class here for UI state
@@ -52,7 +55,6 @@ class AuthViewModel : ViewModel() {
this.email = email this.email = email
this.password = password this.password = password
} }
// No need to emit a success event. The isAuthenticated flow will automatically update.
} catch (e: Exception) { } catch (e: Exception) {
uiState = uiState.copy(error = parseError(e)) //e.message ?: "An unknown error occurred") uiState = uiState.copy(error = parseError(e)) //e.message ?: "An unknown error occurred")
} finally { } finally {

View File

@@ -12,7 +12,6 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Logout 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.colorScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource 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.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.lifecycle.lifecycleScope 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.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.tinsae.clocked.components.ShowLoadingScreen
import net.tinsae.clocked.dashboard.DashboardScreen import net.tinsae.clocked.dashboard.DashboardScreen
import net.tinsae.clocked.data.Locale import net.tinsae.clocked.data.Locale
import net.tinsae.clocked.data.LogRepository
import net.tinsae.clocked.data.Theme import net.tinsae.clocked.data.Theme
import net.tinsae.clocked.history.HistoryScreen import net.tinsae.clocked.history.HistoryScreen
import net.tinsae.clocked.settings.SettingsScreen import net.tinsae.clocked.settings.SettingsScreen
import net.tinsae.clocked.settings.SettingsViewModel import net.tinsae.clocked.settings.SettingsViewModel
import net.tinsae.clocked.ui.theme.ClockedTheme import net.tinsae.clocked.ui.theme.ClockedTheme
import net.tinsae.clocked.util.SupabaseClient
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date 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() { class MainActivity : ComponentActivity() {
@@ -126,6 +129,25 @@ fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel)
val settingsState by settingsViewModel.uiState.collectAsState() val settingsState by settingsViewModel.uiState.collectAsState()
val sessionStatus by authViewModel.sessionStatus.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) { val useDarkTheme = when (settingsState.theme) {
Theme.LIGHT -> false Theme.LIGHT -> false
Theme.DARK -> true Theme.DARK -> true
@@ -136,29 +158,21 @@ fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel)
updateLocale(context, settingsState.locale) updateLocale(context, settingsState.locale)
ClockedTheme(darkTheme = useDarkTheme) { ClockedTheme(darkTheme = useDarkTheme) {
// Use a 'when' statement to handle all possible states.
when (sessionStatus) { when (sessionStatus) {
is SessionStatus.Initializing -> {
// Only show loading for the initial session check.
ShowLoadingScreen()
}
is SessionStatus.Authenticated -> { is SessionStatus.Authenticated -> {
// If we know the user is authenticated, show the main app. // Just show the app. The ViewModels inside will manage their own loading UI.
ClockedApp(settingsViewModel, authViewModel,sessionStatus) ClockedApp(settingsViewModel, authViewModel)
} }
is SessionStatus.NotAuthenticated -> { is SessionStatus.NotAuthenticated -> {
// Only if we are certain the user is not logged in, show the login screen.
LoginScreen(authViewModel) 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 -> {} else -> {}
} }
} }
@@ -175,67 +189,69 @@ fun updateLocale(context: Context, locale: Locale) {
} }
@Composable @Composable
fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel, sessionStatus: SessionStatus) { fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel) {
var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) } var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) }
Scaffold(
modifier = Modifier.fillMaxSize(), NavigationSuiteScaffold(
topBar = { navigationSuiteItems = {
Row( AppDestinations.entries.forEach { destination ->
modifier = Modifier item(
.fillMaxWidth() icon = { Icon(
.background(colorScheme.surface) imageVector = destination.icon,
.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top).asPaddingValues()), contentDescription = stringResource(destination.label))
//contentAlignment = Alignment.CenterStart },
) { alwaysShowLabel = false,
Text( label = { Text(stringResource(destination.label)) },
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), selected = destination == currentDestination,
text = stringResource(R.string.app_name), onClick = { currentDestination = destination }
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 -> ) {
val layoutDirection = LocalLayoutDirection.current Scaffold(
val contentPadding = PaddingValues( modifier = Modifier.fillMaxSize(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection),
top = innerPadding.calculateTopPadding(),
bottom = 0.dp
)
NavigationSuiteScaffold( topBar = {
modifier = Modifier.padding(contentPadding), Surface(
navigationSuiteItems = { color = colorScheme.surfaceContainer,
AppDestinations.entries.forEach { destination -> shadowElevation = 2.dp, // Adds a subtle shadow to lift the bar
item( modifier = Modifier.fillMaxWidth()
icon = { Icon( // No padding needed here on the Surface itself
imageVector = destination.icon, ) {
contentDescription = stringResource(destination.label)) Row(
}, // Apply safe area padding to the Row to push content down
label = { Text(stringResource(destination.label)) }, modifier = Modifier
selected = destination == currentDestination, .fillMaxWidth()
onClick = { currentDestination = destination } .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) { when (currentDestination) {
AppDestinations.HOME -> DashboardScreen( modifier = Modifier.fillMaxSize()) AppDestinations.HOME -> DashboardScreen( modifier = modifier)
AppDestinations.HISTORY -> HistoryScreen(modifier = Modifier.fillMaxSize()) AppDestinations.HISTORY -> HistoryScreen(modifier = modifier)
AppDestinations.SETTING -> SettingsScreen( AppDestinations.SETTING -> SettingsScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier,
viewModel = settingsViewModel, viewModel = settingsViewModel,
authViewModel = authViewModel authViewModel = authViewModel
) )

View File

@@ -9,40 +9,40 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons 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.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Card // Import the new icon from the extended library
import androidx.compose.material3.CardDefaults import androidx.compose.material.icons.filled.History
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource 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.Preview
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.tinsae.clocked.R import net.tinsae.clocked.R
import net.tinsae.clocked.data.Log 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.cyan
import net.tinsae.clocked.ui.theme.red import net.tinsae.clocked.ui.theme.red
import net.tinsae.clocked.util.Util.formatDuration import net.tinsae.clocked.util.Util.formatDuration
@@ -50,74 +50,30 @@ import net.tinsae.clocked.util.Util.formatTimestampToLocalDateString
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
fun ActivityList( fun ListItem(
modifier: Modifier = Modifier,
onEditLog: (Log) -> Unit = {},
onDeleteLog: (Log) -> Unit = {},
recentLogs: List<Log>
) {
//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<Log?>(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(
log: Log, log: Log,
onDelete: () -> Unit, onDelete: () -> Unit,
onEdit: () -> Unit, onEdit: () -> Unit,
onClick: () -> Unit, // Add onClick callback onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var offsetX by remember { mutableFloatStateOf(0f) } var offsetX by remember { mutableFloatStateOf(0f) }
val animatedOffsetX by animateFloatAsState(targetValue = offsetX, label = "offset") val animatedOffsetX by animateFloatAsState(targetValue = offsetX, label = "offset")
// Use stable LocalConfiguration
val screenWidthPx = LocalWindowInfo.current.containerSize.width val screenWidthPx = LocalWindowInfo.current.containerSize.width
// Define thresholds for swiping
val editThreshold = screenWidthPx * 0.4f val editThreshold = screenWidthPx * 0.4f
val deleteThreshold = -screenWidthPx * 0.4f val deleteThreshold = -screenWidthPx * 0.4f
Box( Box(
modifier = modifier modifier = modifier
.padding(vertical = 4.dp)
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick() } // Make the whole area clickable
) { ) {
// Background content (icons) // --- Background content (icons for swipe) ---
Row( Row(
modifier = Modifier modifier = Modifier
.matchParentSize() .matchParentSize()
.clip(MaterialTheme.shapes.medium)
.background(if (animatedOffsetX > 0) cyan else red) .background(if (animatedOffsetX > 0) cyan else red)
.padding(horizontal = 20.dp), .padding(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -139,8 +95,8 @@ fun ActivityItem(
} }
} }
// --- Foreground content (the Card) --- // --- Foreground content (the new list item UI) ---
Card( Surface( // Use Surface for background color and elevation control
modifier = Modifier modifier = Modifier
.offset { IntOffset(animatedOffsetX.roundToInt(), 0) } .offset { IntOffset(animatedOffsetX.roundToInt(), 0) }
.pointerInput(Unit) { .pointerInput(Unit) {
@@ -152,6 +108,7 @@ fun ActivityItem(
onEdit() onEdit()
offsetX = 0f offsetX = 0f
} }
offsetX < deleteThreshold -> onDelete() offsetX < deleteThreshold -> onDelete()
else -> offsetX = 0f else -> offsetX = 0f
} }
@@ -162,29 +119,48 @@ fun ActivityItem(
offsetX += dragAmount offsetX += dragAmount
} }
} }
.fillMaxWidth() .fillMaxWidth(),
.clip(MaterialTheme.shapes.medium) color = MaterialTheme.colorScheme.surface // Ensures background is opaque
// Use the onClick callback here
.clickable { onClick() },
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column { Column {
Row( Row(
modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp), modifier = Modifier
verticalAlignment = Alignment.CenterVertically .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( Text(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
text = formatTimestampToLocalDateString(log.timestamp), text = formatTimestampToLocalDateString(log.timestamp),
style = MaterialTheme.typography.bodyLarge, 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 ?: "", // 4. Divider
style = MaterialTheme.typography.bodyMedium, Divider(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 4.dp) 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 @Composable
@Preview(showBackground = true) @Preview(showBackground = true)
fun ActivityItemPreview() { fun ListItemPreview() {
ActivityList(recentLogs = emptyList(), onEditLog = {_->{}}, onDeleteLog = {_->{}}) ListItem(getRecentLogs().first(), {}, {}, {})
} }

View File

@@ -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()
}
}

View File

@@ -1,5 +1,6 @@
package net.tinsae.clocked.dashboard package net.tinsae.clocked.dashboard
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width 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.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.jan.supabase.auth.status.SessionStatus
import net.tinsae.clocked.R 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.AddLogDialog
import net.tinsae.clocked.components.DetailsDialog
import net.tinsae.clocked.components.ShowLoadingScreen
import net.tinsae.clocked.data.EntryType 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.ClockedTheme
import net.tinsae.clocked.ui.theme.cyan import net.tinsae.clocked.ui.theme.cyan
import net.tinsae.clocked.ui.theme.green import net.tinsae.clocked.ui.theme.green
@@ -45,9 +53,9 @@ fun DashboardScreen(
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { /*LaunchedEffect(Unit) {
LogRepository.fetchLogs() LogRepository.fetchLogs()
} }*/
if (uiState.showAddLogDialog) { if (uiState.showAddLogDialog) {
AddLogDialog( AddLogDialog(
@@ -57,57 +65,80 @@ fun DashboardScreen(
) )
} }
Column( if (uiState.isLoading) {
modifier = modifier ShowLoadingScreen()
.fillMaxSize() } else {
.padding(16.dp, 0.dp) Column(
) { modifier = modifier
Row( .fillMaxSize()
modifier = Modifier.fillMaxWidth(), .padding(top=16.dp)
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
SummaryCard(title = stringResource(id = R.string.overtime), value = uiState.overtime, color = green, modifier = Modifier.weight(1f)) Row(
Spacer(modifier = Modifier.width(16.dp)) modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
SummaryCard(title = stringResource(id = R.string.time_off), value = uiState.timeOff, color = red, modifier = Modifier.weight(1f)) 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()) SummaryCard(
title = stringResource(id = R.string.net_balance),
Spacer(modifier = Modifier.height(16.dp)) value = uiState.netBalance+" ( "+uiState.balanceInDays+")",
color = cyan,
Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
modifier = Modifier.fillMaxWidth(), titleStyle = MaterialTheme.typography.titleLarge
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( Spacer(modifier = Modifier.height(16.dp))
text = stringResource(id = R.string.add_time_off),
onClick = { viewModel.onAddLogClicked(EntryType.TIME_OFF) }, Row(
modifier = Modifier.weight(1f) 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 @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( Card(
modifier = modifier, modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
@@ -117,31 +148,13 @@ fun SummaryCard(title: String, value: String, color: Color, modifier: Modifier =
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text(text = title, style = MaterialTheme.typography.titleMedium) Text(text = title, style = titleStyle)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = value, style = MaterialTheme.typography.headlineMedium, color = color, fontWeight = FontWeight.Bold) 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 @Composable
fun ActionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { fun ActionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Button( Button(
@@ -152,10 +165,49 @@ fun ActionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifie
} }
} }
@Preview(showBackground = true)
@Composable @Composable
fun DashboardScreenPreview() { fun ActivityList(
ClockedTheme { modifier: Modifier = Modifier,
//DashboardScreen(viewModel = DashboardViewModel()) onEditLog: (Log) -> Unit = {},
onDeleteLog: (Log) -> Unit = {},
recentLogs: List<Log>
) {
//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<Log?>(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 }
)
} }
} }

View File

@@ -13,6 +13,9 @@ import kotlinx.coroutines.launch
import net.tinsae.clocked.data.EntryType import net.tinsae.clocked.data.EntryType
import net.tinsae.clocked.data.Log import net.tinsae.clocked.data.Log
import net.tinsae.clocked.data.LogRepository 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 net.tinsae.clocked.util.Util.formatDuration
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Instant import kotlin.time.Instant
@@ -25,7 +28,8 @@ data class DashboardUiState(
val balanceInDays: String = "0d", val balanceInDays: String = "0d",
val recentActivities: List<Log> = emptyList(), val recentActivities: List<Log> = emptyList(),
val showAddLogDialog: Boolean = false, 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() { class DashboardViewModel : ViewModel() {
@@ -33,18 +37,13 @@ class DashboardViewModel : ViewModel() {
private val _internalState = MutableStateFlow(DashboardUiState()) private val _internalState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<DashboardUiState> = combine( val uiState: StateFlow<DashboardUiState> = combine(
LogRepository.logs, // Source 1: The List<Log> LogRepository.isLoading,
_internalState // Source 2: The internal UI state _internalState
) { logs, internalState -> ) { isLoading, internalState ->
// THE FIX: The 'logs' parameter is already the List<Log>. val overtimeDuration = getTotalOvertimeDuration()
// You do not need to call .first() on it. val timeOffDuration = getTotalTimeOffDuration()
val allLogs = logs
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. // Construct the final state using data from both sources.
val netBalanceDuration = overtimeDuration + timeOffDuration val netBalanceDuration = overtimeDuration + timeOffDuration
@@ -53,7 +52,7 @@ class DashboardViewModel : ViewModel() {
// 2. Then, calculate the balance in days (assuming an 8-hour workday) // 2. Then, calculate the balance in days (assuming an 8-hour workday)
// We format it to one decimal place for a clean look. // We format it to one decimal place for a clean look.
val balanceInDaysValue = netBalanceInHours / 8.0 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), overtime = formatDuration(overtimeDuration),
timeOff = formatDuration(timeOffDuration), timeOff = formatDuration(timeOffDuration),
netBalance = formatDuration(overtimeDuration + timeOffDuration), netBalance = formatDuration(overtimeDuration + timeOffDuration),
recentActivities = allLogs.take(7), recentActivities = getRecentLogs(),
balanceInDays = balanceInDaysString, balanceInDays = balanceInDaysString,
isLoading = isLoading,
// Pass through the dialog state from the internal state holder // Pass through the dialog state from the internal state holder
showAddLogDialog = internalState.showAddLogDialog, showAddLogDialog = internalState.showAddLogDialog,
dialogType = internalState.dialogType dialogType = internalState.dialogType
@@ -70,7 +70,7 @@ class DashboardViewModel : ViewModel() {
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), started = SharingStarted.WhileSubscribed(5000),
initialValue = DashboardUiState() initialValue = DashboardUiState(isLoading = true)
) )

View File

@@ -1,15 +1,18 @@
package net.tinsae.clocked.data package net.tinsae.clocked.data
import android.util.Log as LOG
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch 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.Duration
import kotlin.time.Instant import kotlin.time.Instant
import android.util.Log as LOG
/** /**
* A Singleton Repository that acts as the Single Source of Truth for Log data. * 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. // A public, read-only StateFlow that ViewModels can collect.
val logs = _logs.asStateFlow() 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 { private val repositoryScope = CoroutineScope(Dispatchers.IO)
// Fetch initial data when the repository is first created.
fetchLogs()
}
/** /**
* Fetches the latest logs from the service and updates the flow. * Fetches the latest logs from the service and updates the flow.
*/ */
fun fetchLogs() { fun fetchLogs() {
// TEST CODE HERE
repositoryScope.launch { repositoryScope.launch {
if (_isLoading.value) return@launch // Prevent concurrent fetches
_isLoading.update { true }
try { try {
val latestLogs = getAllLogsFromDB() val latestLogs = getAllLogsFromDB()
_logs.update { latestLogs } _logs.update { latestLogs }
LOG.d("LogRepository", "Successfully fetched ${latestLogs.size} logs.")
} catch (e: Exception) { } catch (e: Exception) {
LOG.e("LogRepository", "Failed to fetch logs", e) 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() fetchLogs()
} }
} }
fun getRecentLogs():List<Log> {
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)
}
} }

View File

@@ -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)
}
}
}

View File

@@ -25,7 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import net.tinsae.clocked.R 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.components.DetailsDialog
import net.tinsae.clocked.ui.theme.ClockedTheme import net.tinsae.clocked.ui.theme.ClockedTheme
@@ -51,12 +51,16 @@ fun HistoryScreen(
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) { ) {
if (uiState.tabs.isNotEmpty()) { 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 -> uiState.tabs.forEachIndexed { index, title ->
Tab( Tab(
modifier = Modifier.padding( 6.dp),
selected = uiState.selectedTabIndex == index, selected = uiState.selectedTabIndex == index,
onClick = { viewModel.onTabSelected(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 -> items(entries, key = { it.id }) { entry ->
ActivityItem( ListItem(
log = entry, log = entry,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
onDelete = { viewModel.deleteLog(entry) }, onDelete = { viewModel.deleteLog(entry) },
@@ -94,15 +98,16 @@ fun HistoryScreen(
@Composable @Composable
fun MonthHeader(text: String, modifier: Modifier = Modifier) { fun MonthHeader(text: String, modifier: Modifier = Modifier) {
Surface( Surface(
modifier = modifier modifier = modifier.fillMaxWidth(),
.fillMaxWidth() color = MaterialTheme.colorScheme.surfaceContainer,
.background(MaterialTheme.colorScheme.background) shadowElevation = 1.dp
) { ) {
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp,10.dp)
) )
} }
} }

View File

@@ -57,7 +57,7 @@ class HistoryViewModel : ViewModel() {
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), 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<String>) { fun setTabTitles(titles: List<String>) {

View File

@@ -1,13 +1,14 @@
package net.tinsae.clocked.service 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.postgrest
import io.github.jan.supabase.postgrest.query.Order import io.github.jan.supabase.postgrest.query.Order
import kotlinx.coroutines.delay
import net.tinsae.clocked.data.EntryType import net.tinsae.clocked.data.EntryType
import net.tinsae.clocked.data.Log import net.tinsae.clocked.data.Log
import android.util.Log as LOG import android.util.Log as LOG
import net.tinsae.clocked.data.LogEntry 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.Instant
import kotlin.time.Duration import kotlin.time.Duration
@@ -26,10 +27,6 @@ suspend fun getAllLogsFromDB(): List<Log> {
} }
} }
suspend fun getRecentLogsFromDB(): List<Log> {
return getAllLogsFromDB().take(5)
}
suspend fun addLogToDB(type: EntryType, timestamp: Instant, duration: Duration, reason: String?) { suspend fun addLogToDB(type: EntryType, timestamp: Instant, duration: Duration, reason: String?) {
val newLog = LogEntry( val newLog = LogEntry(
duration = duration, duration = duration,
@@ -78,13 +75,3 @@ suspend fun editLogFromDB(updatedLog: Log): Boolean {
false false
} }
} }
// --- Calculation functions that operate on a given list of logs ---
fun getTotalOvertimeDuration(logs: List<Log>): Duration {
return logs.filter { it.type == EntryType.OVERTIME }.fold(Duration.ZERO) { acc, log -> acc + log.duration }
}
fun getTotalTimeOffDuration(logs: List<Log>): Duration {
return logs.filter { it.type == EntryType.TIME_OFF }.fold(Duration.ZERO) { acc, log -> acc + log.duration }
}

View File

@@ -1,8 +1,10 @@
package net.tinsae.clocked.util package net.tinsae.clocked.util
import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.createSupabaseClient 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.postgrest.Postgrest
import io.github.jan.supabase.realtime.Realtime
import io.ktor.client.engine.android.Android
import net.tinsae.clocked.BuildConfig import net.tinsae.clocked.BuildConfig
object SupabaseClient { object SupabaseClient {
@@ -14,6 +16,8 @@ object SupabaseClient {
) { ) {
install(Auth) install(Auth)
install(Postgrest) install(Postgrest)
install(Realtime)
httpEngine = Android.create()
} }
} }
} }

View File

@@ -1,30 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="960"
android:viewportHeight="108"> android:viewportHeight="960"
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> android:tint="#1B6184">
<aapt:attr name="android:fillColor"> <group android:scaleX="0.464"
<gradient android:scaleY="0.464"
android:endX="85.84757" android:translateX="257.28"
android:endY="92.4963" android:translateY="257.28">
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path <path
android:fillColor="#FFFFFF" android:fillColor="@android:color/white"
android:fillType="nonZero" 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"/>
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" </group>
android:strokeWidth="1" </vector>
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -34,6 +34,9 @@
<string name="add_overtime">+ Overtime</string> <string name="add_overtime">+ Overtime</string>
<string name="add_time_off"> Time Off</string> <string name="add_time_off"> Time Off</string>
<string name="recent_activity">Recent Activity</string> <string name="recent_activity">Recent Activity</string>
<string name="or"> or </string>
<string name="duration_entry_icon">Duration entry icon</string>
<!-- Settings Screen --> <!-- Settings Screen -->
<string name="settings_section_data">Data</string> <string name="settings_section_data">Data</string>

View File

@@ -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-bom = { group = "io.github.jan-tennert.supabase", name = "bom", version.ref = "supabase" }
supabase-postgrest = { group = "io.github.jan-tennert.supabase", name = "postgrest-kt" } supabase-postgrest = { group = "io.github.jan-tennert.supabase", name = "postgrest-kt" }
supabase-auth = { group = "io.github.jan-tennert.supabase", name = "auth-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" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }