almost done
@@ -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)
|
||||
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<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(
|
||||
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(), {}, {}, {})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Log> = 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<DashboardUiState> = combine(
|
||||
LogRepository.logs, // Source 1: The List<Log>
|
||||
_internalState // Source 2: The internal UI state
|
||||
) { logs, internalState ->
|
||||
// THE FIX: The 'logs' parameter is already the List<Log>.
|
||||
// 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)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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<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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>) {
|
||||
|
||||
@@ -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<Log> {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getRecentLogsFromDB(): List<Log> {
|
||||
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<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 }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<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">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
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>
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="#1B6184">
|
||||
<group android:scaleX="0.464"
|
||||
android:scaleY="0.464"
|
||||
android:translateX="257.28"
|
||||
android:translateY="257.28">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
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"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
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"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 834 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -34,6 +34,9 @@
|
||||
<string name="add_overtime">+ Overtime</string>
|
||||
<string name="add_time_off">− Time Off</string>
|
||||
<string name="recent_activity">Recent Activity</string>
|
||||
<string name="or"> or </string>
|
||||
<string name="duration_entry_icon">Duration entry icon</string>
|
||||
|
||||
|
||||
<!-- Settings Screen -->
|
||||
<string name="settings_section_data">Data</string>
|
||||
|
||||
@@ -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" }
|
||||
|
||||