loading animation
This commit is contained in:
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -143,11 +143,16 @@ fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel)
|
|||||||
updateLocale(context, settingsState.locale)
|
updateLocale(context, settingsState.locale)
|
||||||
|
|
||||||
ClockedTheme(darkTheme = useDarkTheme) {
|
ClockedTheme(darkTheme = useDarkTheme) {
|
||||||
when (sessionStatus) {
|
if(sessionStatus is SessionStatus.NotAuthenticated){
|
||||||
is SessionStatus.Initializing -> {
|
LoginScreen(authViewModel)
|
||||||
|
}else{
|
||||||
|
ClockedApp(settingsViewModel, authViewModel)
|
||||||
|
}
|
||||||
|
/*when (sessionStatus) {
|
||||||
|
/*is SessionStatus.Initializing -> {
|
||||||
// Only show loading for the initial session check.
|
// Only show loading for the initial session check.
|
||||||
ShowLoadingScreen()
|
ShowLoadingScreen()
|
||||||
}
|
}*/
|
||||||
|
|
||||||
is SessionStatus.Authenticated -> {
|
is SessionStatus.Authenticated -> {
|
||||||
// Just show the app. The ViewModels inside will manage their own loading UI.
|
// Just show the app. The ViewModels inside will manage their own loading UI.
|
||||||
@@ -157,7 +162,7 @@ fun AppEntry(settingsViewModel: SettingsViewModel, authViewModel: AuthViewModel)
|
|||||||
else -> {
|
else -> {
|
||||||
LoginScreen(authViewModel)
|
LoginScreen(authViewModel)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
package net.tinsae.clocked.data
|
package net.tinsae.clocked.data
|
||||||
|
|
||||||
|
//import androidx.compose.ui.test.cancel
|
||||||
import io.github.jan.supabase.auth.auth
|
import io.github.jan.supabase.auth.auth
|
||||||
import io.github.jan.supabase.auth.status.SessionStatus
|
import io.github.jan.supabase.auth.status.SessionStatus
|
||||||
import io.github.jan.supabase.realtime.Realtime
|
import io.github.jan.supabase.realtime.PostgresAction
|
||||||
|
import io.github.jan.supabase.realtime.RealtimeChannel
|
||||||
|
import io.github.jan.supabase.realtime.channel
|
||||||
|
import io.github.jan.supabase.realtime.decodeRecord
|
||||||
|
import io.github.jan.supabase.realtime.postgresChangeFlow
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.tinsae.clocked.service.addLogToDB
|
import net.tinsae.clocked.service.addLogToDB
|
||||||
import net.tinsae.clocked.service.deleteLogFromDB
|
import net.tinsae.clocked.service.deleteLogFromDB
|
||||||
import net.tinsae.clocked.service.editLogFromDB
|
import net.tinsae.clocked.service.editLogFromDB
|
||||||
import net.tinsae.clocked.service.getAllLogsFromDB
|
import net.tinsae.clocked.service.getAllLogsFromDB
|
||||||
|
import net.tinsae.clocked.util.SupabaseClient
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import android.util.Log as LOG
|
import android.util.Log as LOG
|
||||||
import io.github.jan.supabase.realtime.PostgresAction
|
|
||||||
import io.github.jan.supabase.realtime.RealtimeChannel
|
|
||||||
import io.github.jan.supabase.realtime.channel
|
|
||||||
import io.github.jan.supabase.realtime.decodeRecord
|
|
||||||
import io.github.jan.supabase.realtime.postgresChangeFlow
|
|
||||||
import io.github.jan.supabase.realtime.realtime
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import net.tinsae.clocked.util.SupabaseClient
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -44,28 +43,57 @@ object LogRepository {
|
|||||||
|
|
||||||
private val repositoryScope = CoroutineScope(Dispatchers.IO)
|
private val repositoryScope = CoroutineScope(Dispatchers.IO)
|
||||||
private val realtimeChannel: RealtimeChannel = SupabaseClient.client.channel("logs-channel")
|
private val realtimeChannel: RealtimeChannel = SupabaseClient.client.channel("logs-channel")
|
||||||
private var realtimeJob: Job? = null
|
private var sessionJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
repositoryScope.launch {
|
repositoryScope.launch {
|
||||||
|
// Define the realtime listener flow once. This flow is cold and does not
|
||||||
|
// do anything until it's collected.
|
||||||
|
val changeFlow = realtimeChannel.postgresChangeFlow<PostgresAction>(schema = "public") {
|
||||||
|
table = "Logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch a long-lived collector for the realtime changes. This coroutine
|
||||||
|
// will run for the lifetime of the repositoryScope.
|
||||||
|
launch {
|
||||||
|
changeFlow
|
||||||
|
.catch { e ->
|
||||||
|
// This handles terminal errors in the flow itself.
|
||||||
|
// The Supabase client should automatically try to recover from
|
||||||
|
// transient network issues like SocketException.
|
||||||
|
LOG.e("LogRepository", "Realtime flow collection error", e)
|
||||||
|
}
|
||||||
|
.collect { action ->
|
||||||
|
// By placing the try-catch here, we ensure that a single
|
||||||
|
// malformed event doesn't stop us from processing subsequent events.
|
||||||
|
try {
|
||||||
|
handleRealtimeAction(action)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LOG.e("LogRepository", "Error handling realtime action", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect session status to manage the subscription lifecycle and data fetching.
|
||||||
SupabaseClient.client.auth.sessionStatus.collect { status ->
|
SupabaseClient.client.auth.sessionStatus.collect { status ->
|
||||||
|
// Cancel any existing data-fetching job to prevent race conditions on session changes.
|
||||||
|
sessionJob?.cancel()
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
is SessionStatus.Authenticated -> {
|
is SessionStatus.Authenticated -> {
|
||||||
LOG.d("LogRepository", "Auth: Authenticated. Fetching data and subscribing.")
|
LOG.d("LogRepository", "Auth: Authenticated. Subscribing to channel and fetching initial data.")
|
||||||
// Cancel any previous realtime listener to avoid duplicates
|
// Create a new job for the tasks associated with this authenticated session.
|
||||||
realtimeJob?.cancel()
|
sessionJob = launch {
|
||||||
// Fetch initial data
|
fetchLogs()
|
||||||
fetchLogs()
|
// The postgresChangeFlow listener is already set up.
|
||||||
// Start listening for new changes and subscribe
|
// subscribe() connects the websocket and starts receiving events.
|
||||||
listenForLogChanges()
|
realtimeChannel.subscribe()
|
||||||
realtimeChannel.subscribe()
|
}
|
||||||
}
|
}
|
||||||
is SessionStatus.NotAuthenticated -> {
|
is SessionStatus.NotAuthenticated -> {
|
||||||
LOG.d("LogRepository", "Auth: Not Authenticated. Clearing data.")
|
LOG.d("LogRepository", "Auth: Not Authenticated. Unsubscribing from channel and clearing data.")
|
||||||
// User is signed out, clear local data and unsubscribe
|
|
||||||
clearLogs()
|
|
||||||
realtimeJob?.cancel()
|
|
||||||
realtimeChannel.unsubscribe()
|
realtimeChannel.unsubscribe()
|
||||||
|
clearLogs()
|
||||||
}
|
}
|
||||||
else -> { /* Handle other states if necessary */ }
|
else -> { /* Handle other states if necessary */ }
|
||||||
}
|
}
|
||||||
@@ -73,54 +101,39 @@ object LogRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenForLogChanges() {
|
|
||||||
realtimeJob = repositoryScope.launch {
|
|
||||||
val changeFlow = realtimeChannel.postgresChangeFlow<PostgresAction>(schema = "public") {
|
|
||||||
table = "Logs" // The name of your table in Supabase
|
|
||||||
}
|
|
||||||
|
|
||||||
changeFlow
|
// Extracted the when block to a separate function for clarity
|
||||||
.catch { e -> LOG.e("LogRepository", "Realtime error", e) }
|
// Extracted the when block to a separate function for clarity
|
||||||
.collect { action ->
|
private fun handleRealtimeAction(action: PostgresAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is PostgresAction.Insert -> {
|
is PostgresAction.Insert -> {
|
||||||
LOG.d("LogRepository", "Realtime INSERT: ${action.record}")
|
LOG.d("LogRepository", "Realtime INSERT: ${action.record}")
|
||||||
val newLog = action.decodeRecord<Log>()
|
val newLog = action.decodeRecord<Log>()
|
||||||
_logs.update { currentLogs ->
|
_logs.update { currentLogs ->
|
||||||
(currentLogs + newLog).sortedByDescending { it.timestamp }
|
(currentLogs + newLog).sortedByDescending { it.timestamp }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is PostgresAction.Update -> {
|
is PostgresAction.Update -> {
|
||||||
LOG.d("LogRepository", "Realtime UPDATE: ${action.record}")
|
LOG.d("LogRepository", "Realtime UPDATE: ${action.record}")
|
||||||
val updatedLog = action.decodeRecord<Log>()
|
val updatedLog = action.decodeRecord<Log>()
|
||||||
_logs.update { currentLogs ->
|
_logs.update { currentLogs ->
|
||||||
currentLogs.map { if (it.id == updatedLog.id) updatedLog else it }
|
currentLogs.map { if (it.id == updatedLog.id) updatedLog else it }
|
||||||
.sortedByDescending { it.timestamp }
|
.sortedByDescending { it.timestamp }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is PostgresAction.Delete -> {
|
is PostgresAction.Delete -> {
|
||||||
LOG.d("LogRepository", "Realtime DELETE: ${action.oldRecord}")
|
LOG.d("LogRepository", "Realtime DELETE: ${action.oldRecord}")
|
||||||
val deletedId = action.oldRecord["id"]?.toString()?.toLongOrNull()
|
val deletedId = action.oldRecord["id"]?.toString()?.toLongOrNull()
|
||||||
if (deletedId != null) {
|
if (deletedId != null) {
|
||||||
_logs.update { currentLogs ->
|
_logs.update { currentLogs ->
|
||||||
currentLogs.filterNot { it.id == deletedId }
|
currentLogs.filterNot { it.id == deletedId }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> { /* Unhandled action */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else -> { /* Unhandled action */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the latest logs from the service and updates the flow.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the initial list of logs from the service.
|
|
||||||
* This is now a suspend function to ensure proper sequencing.
|
|
||||||
*/
|
|
||||||
private suspend fun fetchLogs() {
|
private suspend fun fetchLogs() {
|
||||||
if (_isLoading.value) return
|
if (_isLoading.value) return
|
||||||
_isLoading.update { true }
|
_isLoading.update { true }
|
||||||
@@ -128,7 +141,12 @@ object LogRepository {
|
|||||||
val latestLogs = getAllLogsFromDB()
|
val latestLogs = getAllLogsFromDB()
|
||||||
_logs.update { latestLogs }
|
_logs.update { latestLogs }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LOG.e("LogRepository", "Failed to fetch logs", e)
|
// The CancellationException from the log will be caught here
|
||||||
|
if (e is java.util.concurrent.CancellationException) {
|
||||||
|
LOG.w("LogRepository", "fetchLogs was cancelled, likely due to session change. This is expected.")
|
||||||
|
} else {
|
||||||
|
LOG.e("LogRepository", "Failed to fetch logs", e)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading.update { false }
|
_isLoading.update { false }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,87 @@
|
|||||||
package net.tinsae.clocked.ui.components
|
package net.tinsae.clocked.ui.components
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import net.tinsae.clocked.ui.theme.cyan
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadingAnimation(modifier: Modifier = Modifier) {
|
fun ShimmerBrush(showShimmer: Boolean = true, targetValue: Float = 1000f, color: Color = Color.LightGray): Brush {
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "ripple_transition")
|
if (!showShimmer) {
|
||||||
|
return Brush.horizontalGradient(listOf(Color.Transparent))
|
||||||
|
}
|
||||||
|
|
||||||
val scale by infiniteTransition.animateFloat(
|
val shimmerColors = listOf(
|
||||||
|
color.copy(alpha = 0.6f),
|
||||||
|
Color.LightGray.copy(alpha = 0.2f),
|
||||||
|
color.copy(alpha = 0.6f),
|
||||||
|
)
|
||||||
|
|
||||||
|
val transition = rememberInfiniteTransition(label = "shimmer_transition")
|
||||||
|
val translateAnimation by transition.animateFloat(
|
||||||
initialValue = 0f,
|
initialValue = 0f,
|
||||||
targetValue = 1f,
|
targetValue = targetValue,
|
||||||
animationSpec = infiniteRepeatable(
|
animationSpec = infiniteRepeatable(
|
||||||
animation = tween(500),
|
animation = tween(800),
|
||||||
repeatMode = RepeatMode.Restart
|
repeatMode = RepeatMode.Restart
|
||||||
),
|
),
|
||||||
label = "ripple_scale"
|
label = "shimmer_translate_animation"
|
||||||
)
|
)
|
||||||
|
|
||||||
val alpha by infiniteTransition.animateFloat(
|
return Brush.linearGradient(
|
||||||
initialValue = 1f,
|
colors = shimmerColors,
|
||||||
targetValue = 0f,
|
start = Offset.Zero,
|
||||||
animationSpec = infiniteRepeatable(
|
end = Offset(x = translateAnimation, y = translateAnimation)
|
||||||
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)
|
@Preview(showBackground = true)
|
||||||
fun ShowLoadingAnimation(){
|
@Composable
|
||||||
LoadingAnimation(modifier = Modifier.fillMaxSize())
|
fun ShimmerListItemPreview() {
|
||||||
|
val brush = ShimmerBrush()
|
||||||
|
Row(modifier = Modifier.padding(16.dp)) {
|
||||||
|
// Placeholder for a video thumbnail or user avatar
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.background(brush, shape = RoundedCornerShape(8.dp))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
// Placeholder for a title line
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(25.dp)
|
||||||
|
.background(brush, shape = RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
// Placeholder for a subtitle line
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.7f)
|
||||||
|
.height(25.dp)
|
||||||
|
.background(brush, shape = RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.tinsae.clocked.ui.screens.dashboard
|
package net.tinsae.clocked.ui.screens.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
|
||||||
@@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
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
|
||||||
@@ -39,8 +41,7 @@ import net.tinsae.clocked.data.Log
|
|||||||
import net.tinsae.clocked.ui.components.AddLogDialog
|
import net.tinsae.clocked.ui.components.AddLogDialog
|
||||||
import net.tinsae.clocked.ui.components.DetailsDialog
|
import net.tinsae.clocked.ui.components.DetailsDialog
|
||||||
import net.tinsae.clocked.ui.components.ListItem
|
import net.tinsae.clocked.ui.components.ListItem
|
||||||
import net.tinsae.clocked.ui.components.LoadingAnimation
|
import net.tinsae.clocked.ui.components.ShimmerBrush
|
||||||
import net.tinsae.clocked.ui.components.TopBar
|
|
||||||
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
|
||||||
import net.tinsae.clocked.ui.theme.red
|
import net.tinsae.clocked.ui.theme.red
|
||||||
@@ -98,7 +99,9 @@ fun DashboardScreen(
|
|||||||
value = uiState.netBalance+" ( "+uiState.balanceInDays+")",
|
value = uiState.netBalance+" ( "+uiState.balanceInDays+")",
|
||||||
//style = MaterialTheme.typography.titleLarge,
|
//style = MaterialTheme.typography.titleLarge,
|
||||||
color = cyan,
|
color = cyan,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
titleStyle = MaterialTheme.typography.titleMedium,
|
titleStyle = MaterialTheme.typography.titleMedium,
|
||||||
isLoading = uiState.isLoading
|
isLoading = uiState.isLoading
|
||||||
)
|
)
|
||||||
@@ -131,11 +134,17 @@ fun SummaryCard(title: String, titleStyle: TextStyle, value: String, color: Colo
|
|||||||
Text(text = title, style = titleStyle)
|
Text(text = title, style = titleStyle)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
if(!isLoading) {
|
if(!isLoading) {
|
||||||
Text(text = value, style = MaterialTheme.typography.headlineMedium, color = color, fontWeight = FontWeight.Bold)
|
Text(
|
||||||
|
text = value, style = MaterialTheme.typography.headlineMedium, color = color, fontWeight = FontWeight.Bold)
|
||||||
} else {
|
} else {
|
||||||
LoadingAnimation(modifier = Modifier
|
Spacer(modifier = Modifier
|
||||||
.fillMaxWidth()
|
.height(36.dp)
|
||||||
.height(36.dp))
|
.background(
|
||||||
|
ShimmerBrush(color = color),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
.fillMaxWidth())
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user