almost done
@@ -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)
|
||||||
|
|||||||
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.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 {
|
||||||
|
|||||||
@@ -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,48 +189,11 @@ 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(),
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationSuiteScaffold(
|
NavigationSuiteScaffold(
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
navigationSuiteItems = {
|
navigationSuiteItems = {
|
||||||
AppDestinations.entries.forEach { destination ->
|
AppDestinations.entries.forEach { destination ->
|
||||||
item(
|
item(
|
||||||
@@ -224,6 +201,7 @@ fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewMode
|
|||||||
imageVector = destination.icon,
|
imageVector = destination.icon,
|
||||||
contentDescription = stringResource(destination.label))
|
contentDescription = stringResource(destination.label))
|
||||||
},
|
},
|
||||||
|
alwaysShowLabel = false,
|
||||||
label = { Text(stringResource(destination.label)) },
|
label = { Text(stringResource(destination.label)) },
|
||||||
selected = destination == currentDestination,
|
selected = destination == currentDestination,
|
||||||
onClick = { currentDestination = destination }
|
onClick = { currentDestination = destination }
|
||||||
@@ -231,11 +209,49 @@ fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
when (currentDestination) {
|
Scaffold(
|
||||||
AppDestinations.HOME -> DashboardScreen( modifier = Modifier.fillMaxSize())
|
|
||||||
AppDestinations.HISTORY -> HistoryScreen(modifier = Modifier.fillMaxSize())
|
|
||||||
AppDestinations.SETTING -> SettingsScreen(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
||||||
|
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)
|
||||||
|
AppDestinations.HISTORY -> HistoryScreen(modifier = modifier)
|
||||||
|
AppDestinations.SETTING -> SettingsScreen(
|
||||||
|
modifier = Modifier,
|
||||||
viewModel = settingsViewModel,
|
viewModel = settingsViewModel,
|
||||||
authViewModel = authViewModel
|
authViewModel = authViewModel
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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(
|
||||||
text = log.reason ?: "",
|
text = formatDuration(log.duration),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 4.dp)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
@Composable
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
fun ActivityItemPreview() {
|
fun ListItemPreview() {
|
||||||
ActivityList(recentLogs = emptyList(), onEditLog = {_->{}}, onDeleteLog = {_->{}})
|
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
|
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,28 +65,49 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
ShowLoadingScreen()
|
||||||
|
} else {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp, 0.dp)
|
.padding(top=16.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
SummaryCard(title = stringResource(id = R.string.overtime), value = uiState.overtime, color = green, modifier = Modifier.weight(1f))
|
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))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
SummaryCard(title = stringResource(id = R.string.time_off), value = uiState.timeOff, color = red, modifier = Modifier.weight(1f))
|
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),
|
||||||
|
value = uiState.netBalance+" ( "+uiState.balanceInDays+")",
|
||||||
|
color = cyan,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
|
titleStyle = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceAround
|
horizontalArrangement = Arrangement.SpaceAround
|
||||||
) {
|
) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
@@ -103,11 +132,13 @@ fun DashboardScreen(
|
|||||||
onDeleteLog = viewModel::deleteLog
|
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 }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
</vector>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
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_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>
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||