diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..7bedc08 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -5,6 +5,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0b0dfab..c81488a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -74,6 +74,7 @@ dependencies {
implementation(libs.androidx.compose.material3.window.size.class1)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.appcompat)
+ implementation("androidx.biometric:biometric:1.1.0")
// Supabase
implementation(libs.kotlinx.serialization.json)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0350e8e..3086a0a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,7 @@
-
+
+
@@ -17,7 +17,6 @@
diff --git a/app/src/main/java/net/tinsae/clocked/MainActivity.kt b/app/src/main/java/net/tinsae/clocked/MainActivity.kt
index ad16564..f6f79e5 100644
--- a/app/src/main/java/net/tinsae/clocked/MainActivity.kt
+++ b/app/src/main/java/net/tinsae/clocked/MainActivity.kt
@@ -4,39 +4,24 @@ import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.util.Log
-import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
-import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawing
-import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
-import androidx.compose.material3.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
@@ -48,20 +33,16 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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
-import androidx.compose.ui.text.font.FontWeight
-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.biometric.GlobalAuthenticator
import net.tinsae.clocked.components.ShowLoadingScreen
import net.tinsae.clocked.dashboard.DashboardScreen
import net.tinsae.clocked.data.Locale
@@ -71,11 +52,10 @@ 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
-class MainActivity : ComponentActivity() {
+class MainActivity : AppCompatActivity() {
private val settingsViewModel: SettingsViewModel by viewModels()
private val authViewModel: AuthViewModel by viewModels()
@@ -120,6 +100,7 @@ class MainActivity : ComponentActivity() {
setContent {
// The single ViewModel instances from the Activity are passed down.
AppEntry(settingsViewModel, authViewModel)
+ GlobalAuthenticator()
}
}
}
@@ -128,19 +109,7 @@ class MainActivity : ComponentActivity() {
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
- }
- }
- }*/
+ // Trigger fetching of logs when the session status changes.
LaunchedEffect(sessionStatus) {
if (sessionStatus is SessionStatus.Authenticated){
LogRepository.fetchLogs()
@@ -169,11 +138,9 @@ fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel)
ClockedApp(settingsViewModel, authViewModel)
}
- is SessionStatus.NotAuthenticated -> {
+ else -> {
LoginScreen(authViewModel)
}
-
- else -> {}
}
}
}
@@ -194,12 +161,16 @@ fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewMode
NavigationSuiteScaffold(
+ modifier = Modifier.fillMaxSize(),
navigationSuiteItems = {
AppDestinations.entries.forEach { destination ->
item(
- icon = { Icon(
- imageVector = destination.icon,
- contentDescription = stringResource(destination.label))
+ icon = {
+ Icon(
+ imageVector = destination.icon,
+ contentDescription = stringResource(destination.label),
+ modifier = Modifier.padding(1.dp).size(24.dp)
+ )
},
alwaysShowLabel = false,
label = { Text(stringResource(destination.label)) },
@@ -211,52 +182,40 @@ fun ClockedApp(settingsViewModel: SettingsViewModel, authViewModel: AuthViewMode
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
+ /*topBar = {
+ TopBar(
+ appName = stringResource(id = R.string.app_name, currentDestination.label),
+ modifier = Modifier.padding(horizontal = 16.dp),
+ isForContainer = true,
+ actions = {
+ IconButton(
+ onClick = { authViewModel.logout() }, modifier = Modifier.padding(10.dp),
- 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
- )
+ ) {
+ Icon(imageVector = Icons.AutoMirrored.Filled.Logout, contentDescription = stringResource(R.string.logout))
}
}
- }
- },
- ) { 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,
- authViewModel = authViewModel
)
+ }*/
+
+ ) { 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
+ )
+
+ AppDestinations.LOGOUT -> {
+ authViewModel.logout()
+ }
+ }
}
- }
}
}
@@ -267,12 +226,5 @@ enum class AppDestinations(
HOME(R.string.nav_home, Icons.Default.Home),
HISTORY(R.string.nav_history, Icons.AutoMirrored.Filled.List),
SETTING(R.string.nav_settings, Icons.Default.Settings),
-}
-
-@Preview(showBackground = true)
-@Composable
-fun AppEntryPreview() {
- ClockedTheme {
- AppEntry(SettingsViewModel(), AuthViewModel())
- }
+ LOGOUT(R.string.logout, Icons.AutoMirrored.Filled.Logout)
}
diff --git a/app/src/main/java/net/tinsae/clocked/biometric/AuthenticationDialog.kt b/app/src/main/java/net/tinsae/clocked/biometric/AuthenticationDialog.kt
new file mode 100644
index 0000000..040f01a
--- /dev/null
+++ b/app/src/main/java/net/tinsae/clocked/biometric/AuthenticationDialog.kt
@@ -0,0 +1,42 @@
+package net.tinsae.clocked.biometric
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.fragment.app.FragmentActivity
+
+@Composable
+fun AuthenticationDialog(
+ title: String,
+ action: String,
+ onSuccess: () -> Unit,
+ onFailure: () -> Unit,
+ onCancel: () -> Unit
+) {
+ val context = LocalContext.current
+ val activity = context as FragmentActivity
+
+ LaunchedEffect(Unit) {
+ val biometricAuthenticator = BiometricAuthenticator(activity)
+ biometricAuthenticator.authenticate(
+ action = action,
+ title = title,
+ onSuccess = onSuccess,
+ onFailure = onFailure,
+ onCancel = onCancel
+ )
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun BiometricAuthenticationDialogPreview() {
+ AuthenticationDialog(
+ title = "Edit Log",
+ action = "Edit",
+ onSuccess = {},
+ onFailure = {},
+ onCancel = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/tinsae/clocked/biometric/AuthenticationManager.kt b/app/src/main/java/net/tinsae/clocked/biometric/AuthenticationManager.kt
new file mode 100644
index 0000000..ea23a25
--- /dev/null
+++ b/app/src/main/java/net/tinsae/clocked/biometric/AuthenticationManager.kt
@@ -0,0 +1,31 @@
+package net.tinsae.clocked.biometric
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+// A data class to represent an authentication request.
+// The 'onSuccess' lambda is the protected action to run.
+data class AuthRequest(
+ val action: String,
+ val onSuccess: () -> Unit
+)
+
+// A singleton object to manage the authentication request state.
+object AuthenticationManager {
+
+ // A private MutableStateFlow to hold the current request.
+ // It's nullable, so 'null' means no request is active.
+ private val _request = MutableStateFlow(null)
+ val request = _request.asStateFlow() // Publicly expose as a read-only StateFlow
+
+ // Any ViewModel can call this to request authentication.
+ fun requestAuth(action: String, onAuthenticated: () -> Unit) {
+ _request.update { AuthRequest(action = action, onSuccess = onAuthenticated) }
+ }
+
+ // Call this to clear the request after it's been handled (success or fail).
+ fun clearRequest() {
+ _request.update { null }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/tinsae/clocked/biometric/BiometricAuthenticator.kt b/app/src/main/java/net/tinsae/clocked/biometric/BiometricAuthenticator.kt
new file mode 100644
index 0000000..b678e1c
--- /dev/null
+++ b/app/src/main/java/net/tinsae/clocked/biometric/BiometricAuthenticator.kt
@@ -0,0 +1,54 @@
+package net.tinsae.clocked.biometric
+
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+
+class BiometricAuthenticator(private val activity: FragmentActivity) {
+
+ private val executor = ContextCompat.getMainExecutor(activity)
+ private val biometricManager = BiometricManager.from(activity)
+
+ fun authenticate(
+ onSuccess: () -> Unit,
+ onFailure: () -> Unit,
+ onCancel: () -> Unit,
+ title: String,
+ action: String
+ ) {
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setSubtitle("Authentication required to $action")
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
+ .build()
+
+ val biometricPrompt = BiometricPrompt(
+ activity, executor,
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ // Distinguish between user cancellation and other errors
+ if (errorCode == BiometricPrompt.ERROR_USER_CANCELED || errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
+ onCancel()
+ } else {
+ onFailure()
+ }
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ onSuccess()
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ }
+ })
+
+ when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
+ BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo)
+ else -> onCancel()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/tinsae/clocked/biometric/GlobalAuthenticator.kt b/app/src/main/java/net/tinsae/clocked/biometric/GlobalAuthenticator.kt
new file mode 100644
index 0000000..1708dc4
--- /dev/null
+++ b/app/src/main/java/net/tinsae/clocked/biometric/GlobalAuthenticator.kt
@@ -0,0 +1,34 @@
+package net.tinsae.clocked.biometric
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.res.stringResource
+import net.tinsae.clocked.R
+
+@Composable
+fun GlobalAuthenticator() {
+ // Collect the current authentication request from the global manager.
+ val authRequest by AuthenticationManager.request.collectAsState()
+
+ // If there is a request, show the AuthenticationDialog.
+ authRequest?.let { request ->
+ AuthenticationDialog(
+ title = stringResource(R.string.app_name),
+ action = request.action,
+ onSuccess = {
+ // First, execute the protected action (e.g., viewModel.deleteLog)
+ request.onSuccess()
+ // Then, clear the request from the manager.
+ AuthenticationManager.clearRequest()
+ },
+ onFailure = {
+ // On failure or cancel, just clear the request.
+ AuthenticationManager.clearRequest()
+ },
+ onCancel = {
+ AuthenticationManager.clearRequest()
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/net/tinsae/clocked/components/AddLogDialog.kt b/app/src/main/java/net/tinsae/clocked/components/AddLogDialog.kt
index fe56b50..2f77a58 100644
--- a/app/src/main/java/net/tinsae/clocked/components/AddLogDialog.kt
+++ b/app/src/main/java/net/tinsae/clocked/components/AddLogDialog.kt
@@ -15,6 +15,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.PrimaryTabRow
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
@@ -43,24 +46,30 @@ import kotlin.time.Instant
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddLogDialog(
- type: EntryType,
onDismiss: () -> Unit,
onSave: (timestamp: Instant, duration: Duration, reason: String?) -> Unit
) {
- val sheetState = rememberModalBottomSheetState()
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var hours by remember { mutableStateOf("") }
var minutes by remember { mutableStateOf("") }
var reason by remember { mutableStateOf("") }
+ // error if submit button is clicked with mandatory values not filled (duration)
+ val error = remember { mutableStateOf(null) }
var selectedInstant by remember { mutableStateOf(Clock.System.now()) }
var showDatePicker by remember { mutableStateOf(false) }
+ // State for the new EntryType selection
+ var selectedType by remember { mutableStateOf(EntryType.OVERTIME) }
+ val entryTypes = listOf(EntryType.OVERTIME, EntryType.TIME_OFF)
+
// Custom date formatting logic using kotlinx-datetime
val localDateTime = selectedInstant.toLocalDateTime(TimeZone.currentSystemDefault())
val formattedDate = "${localDateTime.month.name.take(3).lowercase().replaceFirstChar { it.uppercase() }} ${localDateTime.day}, ${localDateTime.year}"
if (showDatePicker) {
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = selectedInstant.toEpochMilliseconds())
+
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
@@ -94,10 +103,27 @@ fun AddLogDialog(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
- text = if (type == EntryType.OVERTIME) stringResource(id = R.string.add_overtime_title) else stringResource(id = R.string.add_time_off_title),
+ text = stringResource(R.string.add_entry),
style = MaterialTheme.typography.titleLarge
)
+ // Entry Type Selector
+ PrimaryTabRow(selectedTabIndex = entryTypes.indexOf(selectedType)) {
+ entryTypes.forEachIndexed { index, type ->
+ Tab(
+ selected = index == entryTypes.indexOf(selectedType),
+ onClick = { selectedType = type },
+ text = {
+ when(type) {
+ EntryType.OVERTIME -> Text(stringResource(R.string.overtime))
+ EntryType.TIME_OFF -> Text(stringResource(R.string.time_off))
+ }
+ }
+ )
+ }
+ }
+
+
// Date Row
Row(
modifier = Modifier.fillMaxWidth(),
@@ -113,11 +139,24 @@ fun AddLogDialog(
}
}
- // Duration Row
- Text(
- text = stringResource(id = R.string.duration),
- style = MaterialTheme.typography.bodyLarge,
- )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Duration Row
+ Text(
+ text = stringResource(id = R.string.duration),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ if (error.value != null) {
+ Text(
+ text = error.value!!,
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp)
+ )
+ }
+ }
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
@@ -163,11 +202,18 @@ fun AddLogDialog(
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
+ if(hours.isEmpty() || minutes.isEmpty()) { // duration values cannot be empty
+ error.value = "Please enter a duration or cancel"
+ return@Button
+ }
+ // Convert hours and minutes to)
val h = hours.toLongOrNull() ?: 0L
val m = minutes.toLongOrNull() ?: 0L
- val totalMinutes = if (type == EntryType.OVERTIME) h * 60 + m else -(h * 60 + m)
+ // Use the selectedType state to determine the sign
+ val totalMinutes = if (selectedType == EntryType.OVERTIME) h * 60 + m else -(h * 60 + m)
val duration = totalMinutes.minutes
- onSave(selectedInstant, duration, reason.takeIf(String::isNotBlank))
+
+ onSave(selectedInstant, duration, reason)
}
) {
Text(stringResource(id = R.string.save))
@@ -181,7 +227,6 @@ fun AddLogDialog(
fun AddLogDialogPreview(){
ClockedTheme {
AddLogDialog(
- type = EntryType.OVERTIME,
onDismiss = {},
onSave = { _, _, _ -> }
)
diff --git a/app/src/main/java/net/tinsae/clocked/components/DetailsDialog.kt b/app/src/main/java/net/tinsae/clocked/components/DetailsDialog.kt
index c3f1023..01d46e9 100644
--- a/app/src/main/java/net/tinsae/clocked/components/DetailsDialog.kt
+++ b/app/src/main/java/net/tinsae/clocked/components/DetailsDialog.kt
@@ -56,12 +56,6 @@ fun DetailsDialog(log: Log, onDismiss: () -> Unit) {
}
-@Composable
-private fun getType(type: EntryType):String{
- return if(type==EntryType.OVERTIME) stringResource(R.string.overtime) else stringResource(R.string.time_off)
-}
-
-
@Composable
private fun DetailRow(label: String, value: String) {
// buildAnnotatedString allows mixing different styles in one Text composable.
diff --git a/app/src/main/java/net/tinsae/clocked/components/ListItem.kt b/app/src/main/java/net/tinsae/clocked/components/ListItem.kt
index 5d4482d..f8c42bf 100644
--- a/app/src/main/java/net/tinsae/clocked/components/ListItem.kt
+++ b/app/src/main/java/net/tinsae/clocked/components/ListItem.kt
@@ -20,6 +20,7 @@ import androidx.compose.material.icons.filled.Edit
// Import the new icon from the extended library
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -55,7 +56,7 @@ fun ListItem(
onDelete: () -> Unit,
onEdit: () -> Unit,
onClick: () -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier
) {
val coroutineScope = rememberCoroutineScope()
var offsetX by remember { mutableFloatStateOf(0f) }
@@ -129,11 +130,10 @@ fun ListItem(
.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),
+ contentDescription = stringResource(R.string.duration_entry_icon),
+ modifier = Modifier.size(30.dp),
tint = MaterialTheme.colorScheme.secondary // Changed to secondary for better contrast
)
@@ -155,20 +155,14 @@ fun ListItem(
}
// 4. Divider
- Divider(
+ HorizontalDivider(
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)
+ .padding(start = 56.dp) // 16dp (padding) + 40dp (icon) + 16dp (spacer)
)
}
}
}
}
-
-@Composable
-@Preview(showBackground = true)
-fun ListItemPreview() {
- ListItem(getRecentLogs().first(), {}, {}, {})
-}
diff --git a/app/src/main/java/net/tinsae/clocked/components/LoadingAnimation.kt b/app/src/main/java/net/tinsae/clocked/components/LoadingAnimation.kt
new file mode 100644
index 0000000..5bedea1
--- /dev/null
+++ b/app/src/main/java/net/tinsae/clocked/components/LoadingAnimation.kt
@@ -0,0 +1,57 @@
+package net.tinsae.clocked.components
+
+
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.tooling.preview.Preview
+import net.tinsae.clocked.ui.theme.cyan
+
+@Composable
+fun LoadingAnimation(modifier: Modifier = Modifier) {
+ val infiniteTransition = rememberInfiniteTransition(label = "ripple_transition")
+
+ val scale by infiniteTransition.animateFloat(
+ initialValue = 0f,
+ targetValue = 1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(500),
+ repeatMode = RepeatMode.Restart
+ ),
+ label = "ripple_scale"
+ )
+
+ val alpha by infiniteTransition.animateFloat(
+ initialValue = 1f,
+ targetValue = 0f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1200),
+ repeatMode = RepeatMode.Restart
+ ),
+ label = "ripple_alpha"
+ )
+
+ Box(
+ modifier = modifier
+ .scale(scale)
+ .clip(CircleShape)
+ .background(cyan.copy(alpha = 0.5f))
+ )
+}
+
+@Composable
+@Preview(showBackground = true)
+fun ShowLoadingAnimation(){
+ LoadingAnimation(modifier = Modifier.fillMaxSize())
+}
diff --git a/app/src/main/java/net/tinsae/clocked/components/TopBar.kt b/app/src/main/java/net/tinsae/clocked/components/TopBar.kt
new file mode 100644
index 0000000..e56e983
--- /dev/null
+++ b/app/src/main/java/net/tinsae/clocked/components/TopBar.kt
@@ -0,0 +1,67 @@
+package net.tinsae.clocked.components
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+/**
+ *
+ */
+@Composable
+fun TopBar(
+ modifier: Modifier = Modifier,
+ appName: String?,
+ actions: @Composable (RowScope.() -> Unit)? = null,
+ isForContainer: Boolean = false
+) {
+ Surface(
+ color = colorScheme.surfaceContainer,
+ shadowElevation = 2.dp, // Adds a subtle shadow to lift the bar
+ modifier = if (isForContainer) Modifier
+ .fillMaxWidth()
+ .padding(
+ WindowInsets
+ .safeDrawing.only(WindowInsetsSides.Top)
+ .asPaddingValues()
+ )
+ else Modifier.fillMaxWidth(),
+ // No padding needed here on the Surface itself
+ ) {
+ Row(
+ // Apply safe area padding to the Row to push content down
+ modifier = modifier,
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
+ ) {
+ if (appName != null) {
+ Text(
+ //modifier = Modifier.padding(start = 16.dp), // Vertical padding is handled by Row's alignment
+ text = appName,
+ style = typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ if(actions != null){
+ actions()
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt
index 98d2fc3..772a1a3 100644
--- a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt
+++ b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt
@@ -1,6 +1,5 @@
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
@@ -12,6 +11,7 @@ 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.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -30,22 +30,21 @@ 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 net.tinsae.clocked.R
-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.components.ListItem
+import net.tinsae.clocked.components.LoadingAnimation
+import net.tinsae.clocked.components.TopBar
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
import net.tinsae.clocked.ui.theme.red
+
@Composable
fun DashboardScreen(
modifier: Modifier = Modifier,
@@ -53,115 +52,101 @@ fun DashboardScreen(
) {
val uiState by viewModel.uiState.collectAsState()
- /*LaunchedEffect(Unit) {
- LogRepository.fetchLogs()
- }*/
-
if (uiState.showAddLogDialog) {
AddLogDialog(
- type = uiState.dialogType,
- onDismiss = viewModel::onDismissDialog,
+ onDismiss = viewModel::toggleAddLogDialog,
onSave = viewModel::onSaveLog
)
}
- if (uiState.isLoading) {
- ShowLoadingScreen()
- } else {
- Column(
- modifier = modifier
- .fillMaxSize()
- .padding(top=16.dp)
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ //BalanceBanner(uiState.netBalance+" ( "+uiState.balanceInDays+")", cyan, uiState.isLoading)
+ TopBar(
+ modifier = Modifier.padding(16.dp),
+ appName = stringResource(id = R.string.net_balance),
+ actions = {
+ Text(
+ text = uiState.netBalance+" ( "+uiState.balanceInDays+")",
+ style = MaterialTheme.typography.titleLarge,
+ color = cyan,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ )
+
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly
) {
- 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))
-
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))
-
- 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,
+ title = stringResource(id = R.string.overtime),
+ value = uiState.overtime,
+ color = green,
modifier = Modifier.weight(1f),
- onEditLog = viewModel::editLog,
- onDeleteLog = viewModel::deleteLog
+ titleStyle = MaterialTheme.typography.titleMedium,
+ isLoading = uiState.isLoading
+ )
+ 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,
+ isLoading = uiState.isLoading
)
}
+ ActivityList(
+ recentLogs = uiState.recentActivities,
+ modifier = Modifier.weight(1f),
+ onEditLog = viewModel::requestEditWithAuth,
+ onDeleteLog = viewModel::requestDeleteWithAuth,
+ viewModel = viewModel
+ )
}
}
@Composable
-fun SummaryCard(title: String, titleStyle: TextStyle, value: String, color: Color, modifier: Modifier = Modifier) {
+fun SummaryCard(title: String, titleStyle: TextStyle, value: String, color: Color, modifier: Modifier = Modifier, isLoading: Boolean) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
- modifier = Modifier.padding(16.dp),
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = title, style = titleStyle)
Spacer(modifier = Modifier.height(8.dp))
- Text(text = value, style = MaterialTheme.typography.headlineMedium, color = color, fontWeight = FontWeight.Bold)
+ if(!isLoading) {
+ Text(text = value, style = MaterialTheme.typography.headlineMedium, color = color, fontWeight = FontWeight.Bold)
+ } else {
+ LoadingAnimation(modifier = Modifier
+ .fillMaxWidth()
+ .height(36.dp))
+ }
}
}
}
@Composable
-fun ActionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
+fun ActionButton(text: String, onClick: () -> Unit) {
Button(
onClick = onClick,
- modifier = modifier.fillMaxWidth()
+ shape = CircleShape
) {
- Text(text = text, fontSize = 16.sp, modifier = Modifier.padding(6.dp))
+ Text(text = text, fontSize = 16.sp)
}
}
@@ -171,7 +156,8 @@ fun ActivityList(
modifier: Modifier = Modifier,
onEditLog: (Log) -> Unit = {},
onDeleteLog: (Log) -> Unit = {},
- recentLogs: List
+ recentLogs: List,
+ viewModel: DashboardViewModel
) {
//var mutableItems by remember { mutableStateOf(items) }
// State to hold the log that should be shown in the dialog. Null means no dialog.
@@ -183,12 +169,26 @@ fun ActivityList(
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
- Text(
- text = stringResource(R.string.recent_activity),
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(16.dp)
- )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ){
+ Text(
+ text = stringResource(R.string.recent_activity),
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(16.dp)
+ )
+
+ ActionButton(
+ text = "+ ${stringResource(id = R.string.new_entry)}",
+ onClick = { viewModel.toggleAddLogDialog() }
+ )
+ }
}
+
LazyColumn {
items(recentLogs, key = { it.id }) { log ->
ListItem(
@@ -201,6 +201,7 @@ fun ActivityList(
)
}
}
+
}
// When a log is selected, show the dialog.
@@ -211,3 +212,4 @@ fun ActivityList(
)
}
}
+
diff --git a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt
index 79c54e8..ca6e51d 100644
--- a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt
+++ b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import net.tinsae.clocked.biometric.AuthenticationManager
import net.tinsae.clocked.data.EntryType
import net.tinsae.clocked.data.Log
import net.tinsae.clocked.data.LogRepository
@@ -19,6 +20,8 @@ import net.tinsae.clocked.data.LogRepository.getTotalTimeOffDuration
import net.tinsae.clocked.util.Util.formatDuration
import kotlin.time.Duration
import kotlin.time.Instant
+import android.util.Log as LOG
+
data class DashboardUiState(
@@ -28,12 +31,12 @@ data class DashboardUiState(
val balanceInDays: String = "0d",
val recentActivities: List = emptyList(),
val showAddLogDialog: Boolean = false,
- val dialogType: EntryType = EntryType.OVERTIME, // Default, will be updated
- val isLoading: Boolean = true
+ val dialogType: EntryType = EntryType.OVERTIME,
+ val isLoading: Boolean = true,
)
class DashboardViewModel : ViewModel() {
- // This is the state holder for UI-driven events (e.g., showing a dialog)
+
private val _internalState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow = combine(
@@ -44,13 +47,11 @@ class DashboardViewModel : ViewModel() {
val timeOffDuration = getTotalTimeOffDuration()
-
- // Construct the final state using data from both sources.
val netBalanceDuration = overtimeDuration + timeOffDuration
val netBalanceInHours = netBalanceDuration.inWholeMinutes / 60.0
- // 2. Then, calculate the balance in days (assuming an 8-hour workday)
- // We format it to one decimal place for a clean look.
+ // calculate the balance in days (assuming an 8-hour workday)
+ // and format it to one decimal place for a clean look.
val balanceInDaysValue = netBalanceInHours / 8.0
val balanceInDaysString = "%.1f".format(balanceInDaysValue) + " d "
@@ -73,14 +74,8 @@ class DashboardViewModel : ViewModel() {
initialValue = DashboardUiState(isLoading = true)
)
-
- fun onAddLogClicked(type: EntryType) {
- // This correctly updates the internal state, which triggers the 'combine' to re-run.
- _internalState.update { it.copy(showAddLogDialog = true, dialogType = type) }
- }
-
- fun onDismissDialog() {
- _internalState.update { it.copy(showAddLogDialog = false) }
+ fun toggleAddLogDialog(){
+ _internalState.update { it.copy(showAddLogDialog = !it.showAddLogDialog) }
}
fun onSaveLog(timestamp: Instant, duration: Duration, reason: String?) {
@@ -94,15 +89,18 @@ class DashboardViewModel : ViewModel() {
reason = reason
)
// On success, hide the dialog. The repository will trigger the data refresh.
- onDismissDialog()
+ toggleAddLogDialog()
} catch (e: Exception) {
// Handle errors
- android.util.Log.e("DashboardViewModel", "Failed to save log", e)
+ LOG.e("DashboardViewModel", "Failed to save log", e)
}
}
}
+
+
+
fun editLog(log: Log) {
LogRepository.editLog(log)
}
@@ -110,4 +108,18 @@ class DashboardViewModel : ViewModel() {
fun deleteLog(log: Log) {
LogRepository.deleteLog(log)
}
+
+ fun requestDeleteWithAuth(log: Log) {
+ AuthenticationManager.requestAuth(
+ action = "delete this log",
+ onAuthenticated = { deleteLog(log) } // Pass the function to run on success
+ )
+ }
+
+ fun requestEditWithAuth(log: Log) {
+ AuthenticationManager.requestAuth(
+ action = "edit this log",
+ onAuthenticated = { editLog(log) }
+ )
+ }
}
diff --git a/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt b/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt
index 516579c..bbfd7b4 100644
--- a/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt
+++ b/app/src/main/java/net/tinsae/clocked/data/LogRepository.kt
@@ -91,7 +91,7 @@ object LogRepository {
fun getRecentLogs():List {
- return logs.value.take(5)
+ return logs.value.take(7)
}
fun getTotalOvertimeDuration(): Duration {
diff --git a/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt
index 1c69e84..313108e 100644
--- a/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt
+++ b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
@@ -53,11 +54,12 @@ fun HistoryScreen(
if (uiState.tabs.isNotEmpty()) {
PrimaryTabRow(selectedTabIndex = uiState.selectedTabIndex,
modifier = Modifier.fillMaxWidth()
- .background(color = MaterialTheme.colorScheme.surfaceContainer)
+ .background(color = MaterialTheme.colorScheme.surfaceContainer),
+ containerColor = MaterialTheme.colorScheme.surfaceContainer
) {
uiState.tabs.forEachIndexed { index, title ->
Tab(
- modifier = Modifier.padding( 6.dp),
+ modifier = Modifier.padding( 6.dp).alpha(1F),
selected = uiState.selectedTabIndex == index,
onClick = { viewModel.onTabSelected(index) },
text = { Text(title,style = MaterialTheme.typography.titleMedium) }
@@ -69,7 +71,7 @@ fun HistoryScreen(
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
- uiState.groupedEntries.forEach { (monthYear, entries) ->
+ uiState.groupedEntries?.forEach { (monthYear, entries) ->
stickyHeader {
MonthHeader(text = monthYear)
}
diff --git a/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt b/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt
index 70ca19f..8fb030d 100644
--- a/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt
+++ b/app/src/main/java/net/tinsae/clocked/history/HistoryViewModel.kt
@@ -8,18 +8,20 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
-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.util.Util.formatTimestampToMonthYear
+import android.util.Log as LOG
+
data class HistoryUiState(
val tabs: List = emptyList(),
val selectedTabIndex: Int = 0,
- val groupedEntries: Map> = emptyMap(),
+ val groupedEntries: Map>? = emptyMap(),
val selectedLogForDialog: Log? = null,
- val isLoading: Boolean = true // isLoading is true when the log list is empty
+ val isLoading: Boolean = true, // isLoading is true when the log list is empty
+ val showBiometricDialog: Boolean = false
)
class HistoryViewModel : ViewModel() {
@@ -90,4 +92,8 @@ class HistoryViewModel : ViewModel() {
// The old filterAndGroupEntries() function is no longer needed,
// as its logic is now inside the 'combine' block.
// private fun filterAndGroupEntries() { ... }
+ fun onToggleBiometricDialog() {
+ _internalState.update { it.copy(showBiometricDialog = !it.showBiometricDialog) }
+ LOG.d("HistoryViewModel", "Biometric dialog toggled")
+ }
}
diff --git a/app/src/main/java/net/tinsae/clocked/settings/SettingsScreen.kt b/app/src/main/java/net/tinsae/clocked/settings/SettingsScreen.kt
index 52b76d6..5a26bda 100644
--- a/app/src/main/java/net/tinsae/clocked/settings/SettingsScreen.kt
+++ b/app/src/main/java/net/tinsae/clocked/settings/SettingsScreen.kt
@@ -27,20 +27,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
-import net.tinsae.clocked.AuthViewModel
import net.tinsae.clocked.R
import net.tinsae.clocked.data.Locale
import net.tinsae.clocked.data.Theme
-import net.tinsae.clocked.ui.theme.ClockedTheme
@Composable
fun SettingsScreen(
modifier: Modifier = Modifier,
- viewModel: SettingsViewModel = viewModel(),
- authViewModel: AuthViewModel
+ viewModel: SettingsViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
@@ -61,8 +57,7 @@ fun SettingsScreen(
}
Column(
- modifier = modifier
- .padding(16.dp)
+ modifier = modifier.padding(16.dp),
) {
SettingsSection(title = stringResource(R.string.settings_section_data)) {
SettingsItem(
@@ -227,11 +222,3 @@ fun LocaleSelectionDialog(
)
}
-
-@Preview(showBackground = true)
-@Composable
-fun SettingsScreenPreview() {
- ClockedTheme {
- SettingsScreen(viewModel = SettingsViewModel(), authViewModel = AuthViewModel())
- }
-}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f457bb3..1a88e85 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,6 +25,7 @@
Add Overtime
Add Time Off
+ New
Hours
Minutes
Reason (Optional)
@@ -70,5 +71,8 @@
Don\'t have an account? Sign up
Already have an account? Log in
Sign-up successful! Please check your email for a confirmation link.
+ Add a new entry
+ Log out
+ loading
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 61e0319..fe841a3 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,6 @@
-
+
+
\ No newline at end of file