From a795d9c7ab0b191f3fc2591d7318c94b7083ccc9 Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Wed, 11 Dec 2024 18:08:56 +0300 Subject: [PATCH] Improve visibility on loading map data and sync UX (#3621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement LineSpinFadeLoaderProgressIndicator Signed-off-by: Elly Kitoto * Run spotless Signed-off-by: Elly Kitoto * Fix spacing Signed-off-by: Elly Kitoto * Show the loader dialog when syncing the location data (#3598) * Show the LoaderDialog when syncing the Location data Update the LoaderDialog: - Make box and progressBar size dynamic - Make the progressDialog message as optional - Add the ability to show either the ProgressBar or LineSpinFadeLoaderProgressIndicator * Add an option in the LoaderDialog to show a dialog with non blocking UI Show a non blocking dialog when applying location filtering * clean-up: rename variables * fix tests * 🚧 Increase the loading dialog size on the geowidget screen * Fix showing map snackbar messages Signed-off-by: Elly Kitoto * Formate code Signed-off-by: Elly Kitoto --------- Signed-off-by: Elly Kitoto Co-authored-by: qaziabubakar-vd <72507786+qaziabubakar-vd@users.noreply.github.com> Co-authored-by: Benjamin Mwalimu Co-authored-by: qaziabubakar-vd --- android/engine/build.gradle.kts | 3 +- .../LineSpinFadeLoaderProgressIndicator.kt | 149 +++++++++++++++++ .../ui/components/register/LoaderDialog.kt | 151 +++++++++++++----- .../ui/geowidget/GeoWidgetLauncherFragment.kt | 1 + .../ui/geowidget/GeoWidgetLauncherScreen.kt | 39 ++++- .../geowidget/GeoWidgetLauncherViewModel.kt | 53 +++--- .../ui/shared/components/SyncStatusView.kt | 130 ++++++++------- 7 files changed, 398 insertions(+), 128 deletions(-) create mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/LineSpinFadeLoaderProgressIndicator.kt diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index 753fbae6ce..9d90fcc3d3 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -147,6 +147,7 @@ dependencies { implementation(libs.slf4j.nop) implementation(libs.fhir.sdk.common) + // Shared dependencies api(libs.bundles.datastore.kt) api(libs.bundles.navigation) api(libs.bundles.materialicons) @@ -158,8 +159,6 @@ dependencies { api(libs.bundles.okhttp3) api(libs.bundles.paging) api(libs.ui) - - // Shared dependencies api(libs.glide) api(libs.knowledge) { exclude(group = "org.slf4j", module = "jcl-over-slf4j") } api(libs.p2p.lib) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/LineSpinFadeLoaderProgressIndicator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/LineSpinFadeLoaderProgressIndicator.kt new file mode 100644 index 0000000000..78d287df1d --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/LineSpinFadeLoaderProgressIndicator.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.ui.components + +import androidx.compose.animation.core.LinearEasing +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.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.dp +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated + +const val ANIMATION_LABEL = "LineSpinFadeLoaderProgressIndicator" + +/** + * A custom progress indicator that displays rotating lines in a circular pattern. Each line fades + * in and out as it rotates, creating a smooth loading animation effect. + * + * @param modifier Modifier to be applied to the Canvas composable + * @param color The color of the lines in the loading indicator + * @param lineCount The number of lines to be displayed in the circular pattern (default is 8) + * @param lineWidth The width/thickness of each line (default is 3f) + * @param lineLength The length of each line (default is 8f) + * @param innerRadius The radius of the circle on which the lines are positioned (default is 10f) + * + * Example usage: + * ``` + * LineSpinFadeLoaderProgressIndicator( + * modifier = Modifier.size(80.dp), + * color = Color.Blue, + * lineCount = 8, + * lineWidth = 3f, + * lineLength = 8f, + * innerRadius = 10f + * ) + * ``` + * + * The animation creates a rotating effect where: + * - All lines are visible simultaneously + * - Each line's opacity changes based on its current position in the rotation + * - Lines maintain fixed positions but fade in/out to create a rotation illusion + * - The animation continuously loops with a smooth transition + * + * @see Canvas + * @see rememberInfiniteTransition + */ +@Composable +fun LineSpinFadeLoaderProgressIndicator( + modifier: Modifier = Modifier, + color: Color = Color.Blue, + lineCount: Int = 12, + lineWidth: Float = 4f, + lineLength: Float = 20f, + innerRadius: Float = 20f, +) { + val infiniteTransition = rememberInfiniteTransition(ANIMATION_LABEL) + + val rotationAnimation by + infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = lineCount.toFloat(), + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1000, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + label = ANIMATION_LABEL, + ) + + Canvas(modifier = modifier.wrapContentSize()) { + val canvasWidth = size.width + val canvasHeight = size.height + val centerX = canvasWidth / 2 + val centerY = canvasHeight / 2 + + for (i in 0 until lineCount) { + val angle = 2 * PI * i / lineCount + val startX = centerX + cos(angle).toFloat() * innerRadius + val startY = centerY + sin(angle).toFloat() * innerRadius + val endX = centerX + cos(angle).toFloat() * (innerRadius + lineLength) + val endY = centerY + sin(angle).toFloat() * (innerRadius + lineLength) + + // Calculate alpha based on the current rotation + val distance = (i - rotationAnimation + lineCount) % lineCount + val alpha = + when { + distance < lineCount / 2f -> 1f - (distance / (lineCount / 2f)) + else -> (distance - (lineCount / 2f)) / (lineCount / 2f) + } + + drawLine( + color = color.copy(alpha = alpha), + start = androidx.compose.ui.geometry.Offset(startX, startY), + end = androidx.compose.ui.geometry.Offset(endX, endY), + strokeWidth = lineWidth, + cap = StrokeCap.Round, + ) + } + } +} + +@PreviewWithBackgroundExcludeGenerated +@Composable +fun LoadingScreen() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + LineSpinFadeLoaderProgressIndicator( + modifier = Modifier.padding(8.dp), + color = Color.Blue, + ) + + Spacer(modifier = Modifier.height(32.dp)) + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt index da98fa0606..2ceeda3d5a 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt @@ -19,10 +19,9 @@ package org.smartregister.fhircore.engine.ui.components.register import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Surface @@ -37,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog @@ -44,6 +44,7 @@ import androidx.compose.ui.window.DialogProperties import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.smartregister.fhircore.engine.R +import org.smartregister.fhircore.engine.ui.components.LineSpinFadeLoaderProgressIndicator import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated const val LOADER_DIALOG_PROGRESS_BAR_TAG = "loaderDialogProgressBarTag" @@ -52,58 +53,113 @@ const val LOADER_DIALOG_PROGRESS_MSG_TAG = "loaderDialogProgressMsgTag" @Composable fun LoaderDialog( modifier: Modifier = Modifier, - dialogMessage: String, + dialogMessage: String? = null, percentageProgressFlow: Flow = flowOf(0), showPercentageProgress: Boolean = false, + boxWidth: Dp = 240.dp, + boxHeight: Dp = 180.dp, + progressBarSize: Dp = 40.dp, + showBackground: Boolean = true, + showLineSpinIndicator: Boolean = false, + showOverlay: Boolean = true, + alignment: Alignment = Alignment.Center, ) { val currentPercentage = percentageProgressFlow.collectAsState(0).value + + if (showOverlay) { + Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) { + LoaderContent( + modifier = modifier, + dialogMessage = dialogMessage, + currentPercentage = currentPercentage, + showPercentageProgress = showPercentageProgress, + boxWidth = boxWidth, + boxHeight = boxHeight, + progressBarSize = progressBarSize, + showBackground = showBackground, + showLineSpinIndicator = showLineSpinIndicator, + ) + } + } else { + Box( + modifier = modifier.wrapContentSize(), + contentAlignment = alignment, + ) { + LoaderContent( + modifier = modifier, + dialogMessage = dialogMessage, + currentPercentage = currentPercentage, + showPercentageProgress = showPercentageProgress, + boxWidth = boxWidth, + boxHeight = boxHeight, + progressBarSize = progressBarSize, + showBackground = showBackground, + showLineSpinIndicator = showLineSpinIndicator, + ) + } + } +} + +@Composable +private fun LoaderContent( + modifier: Modifier, + dialogMessage: String?, + currentPercentage: Int, + showPercentageProgress: Boolean, + boxWidth: Dp, + boxHeight: Dp, + progressBarSize: Dp, + showBackground: Boolean, + showLineSpinIndicator: Boolean, +) { val openDialog = remember { mutableStateOf(true) } if (openDialog.value) { - Dialog( - onDismissRequest = { openDialog.value = true }, - properties = DialogProperties(dismissOnBackPress = true), - ) { - Box(modifier.size(240.dp, 180.dp)) { - Column( - modifier = modifier.padding(8.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + Box(modifier.size(boxWidth, boxHeight)) { + Column( + modifier = modifier.padding(8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Surface( + modifier = modifier.size(boxWidth, boxHeight), + shape = RoundedCornerShape(8.dp), + color = if (showBackground) Color.Black.copy(alpha = 0.56f) else Color.Transparent, ) { - Surface( - color = Color.Black.copy(alpha = 0.56f), - modifier = modifier.fillMaxSize(), - shape = RoundedCornerShape(8), + Column( + modifier = modifier.padding(8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { + if (showLineSpinIndicator) { + LineSpinFadeLoaderProgressIndicator( + color = Color.White, + lineLength = 12f, + innerRadius = 16f, + ) + } else { CircularProgressIndicator( color = Color.White, strokeWidth = 3.dp, - modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_BAR_TAG).size(40.dp), + modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_BAR_TAG).size(progressBarSize), ) - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - fontSize = 16.sp, - color = Color.White, - text = dialogMessage, - modifier = - modifier.testTag(LOADER_DIALOG_PROGRESS_MSG_TAG).padding(vertical = 16.dp), - ) + } - if (showPercentageProgress) { - Text( - fontSize = 15.sp, - color = Color.White, - text = stringResource(id = R.string.percentage_progress, currentPercentage), - modifier = modifier.padding(horizontal = 3.dp, vertical = 16.dp), - ) - } - } + dialogMessage?.let { + Text( + text = it, + color = Color.White, + fontSize = 14.sp, + modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_MSG_TAG).padding(top = 8.dp), + ) + } + + if (showPercentageProgress) { + Text( + fontSize = 15.sp, + color = Color.White, + text = "$currentPercentage%", + modifier = modifier.padding(top = 4.dp), + ) } } } @@ -122,3 +178,16 @@ fun LoaderDialog( fun LoaderDialogPreview() { LoaderDialog(dialogMessage = stringResource(id = R.string.syncing)) } + +@PreviewWithBackgroundExcludeGenerated +@Composable +fun LoaderDialogPreviewTest() { + LoaderDialog( + boxWidth = 50.dp, + boxHeight = 50.dp, + progressBarSize = 25.dp, + showBackground = false, + showLineSpinIndicator = true, + showOverlay = false, + ) +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 2eab074a82..18bb4efdf4 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -195,6 +195,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { launchQuestionnaire = geoWidgetLauncherViewModel::launchQuestionnaire, decodeImage = geoWidgetLauncherViewModel::getImageBitmap, onAppMainEvent = appMainViewModel::onEvent, + isSyncing = geoWidgetLauncherViewModel.isSyncing, ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt index 86dc61b6bc..516a7c460c 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt @@ -18,17 +18,25 @@ package org.smartregister.fhircore.quest.ui.geowidget import android.content.Context import android.graphics.Bitmap +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager import androidx.fragment.compose.AndroidFragment import androidx.fragment.compose.rememberFragmentState +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.navigation.NavController import org.hl7.fhir.r4.model.ResourceType @@ -36,6 +44,7 @@ import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation +import org.smartregister.fhircore.engine.ui.components.register.LoaderDialog import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment @@ -65,8 +74,11 @@ fun GeoWidgetLauncherScreen( launchQuestionnaire: (QuestionnaireConfig, GeoJsonFeature, Context) -> Unit, decodeImage: ((String) -> Bitmap?)?, onAppMainEvent: (AppMainEvent) -> Unit, + isSyncing: LiveData, ) { val context = LocalContext.current + val syncing by isSyncing.observeAsState() + Scaffold( topBar = { Column { @@ -118,14 +130,20 @@ fun GeoWidgetLauncherScreen( }, ) { innerPadding -> val fragmentState = rememberFragmentState() - Box(modifier = modifier.padding(innerPadding)) { + Box( + modifier = modifier.padding(innerPadding).fillMaxSize(), + ) { AndroidFragment(fragmentState = fragmentState) { fragment -> fragment .setUseGpsOnAddingLocation(false) .setAddLocationButtonVisibility(geoWidgetConfiguration.showAddLocation) .setOnAddLocationListener { feature: GeoJsonFeature -> if (feature.geometry?.coordinates == null) return@setOnAddLocationListener - launchQuestionnaire(geoWidgetConfiguration.registrationQuestionnaire, feature, context) + launchQuestionnaire( + geoWidgetConfiguration.registrationQuestionnaire, + feature, + context, + ) } .setOnCancelAddingLocationListener { context.showToast(context.getString(R.string.on_cancel_adding_location)) @@ -153,6 +171,23 @@ fun GeoWidgetLauncherScreen( observerGeoJsonFeatures(geoJsonFeatures) } } + if (syncing == true) { + Box( + modifier = + Modifier.fillMaxSize().padding(16.dp).pointerInput(Unit) { detectTapGestures {} }, + contentAlignment = Alignment.Center, + ) { + LoaderDialog( + boxWidth = 100.dp, + boxHeight = 100.dp, + progressBarSize = 130.dp, + showBackground = true, + showLineSpinIndicator = true, + showOverlay = false, + modifier = Modifier.align(Alignment.Center), + ) + } + } } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt index 8f7ae4b20e..e755cc748f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt @@ -75,6 +75,9 @@ constructor( private val _snackBarStateFlow = MutableSharedFlow() val snackBarStateFlow = _snackBarStateFlow.asSharedFlow() + private val _isSyncing = MutableLiveData(false) + val isSyncing: LiveData = _isSyncing + private val _noLocationFoundDialog = MutableLiveData() val noLocationFoundDialog: LiveData get() = _noLocationFoundDialog @@ -98,6 +101,7 @@ constructor( searchText: String?, ) { viewModelScope.launch { + _isSyncing.postValue(true) val (locationsWithCoordinates, locationsWithoutCoordinates) = defaultRepository .searchNestedResources( @@ -159,13 +163,15 @@ constructor( } else { registerData.filter { geoJsonFeature: GeoJsonFeature -> geoWidgetConfig.topScreenSection?.searchBar?.computedRules?.any { ruleName -> - // if ruleName not found in map return {-1}; check always return false hence no data + // if ruleName not found in map return {-1}; check always return false hence no + // data val value = geoJsonFeature.properties[ruleName]?.toString() ?: "{-1}" value.contains(other = searchText, ignoreCase = true) } == true } } + _isSyncing.postValue(false) geoJsonFeatures.postValue(features) Timber.w( @@ -197,30 +203,11 @@ constructor( ), ) } else { - val message = - if (searchText.isNullOrBlank()) { - context.getString(R.string.all_locations_rendered) - } else { - context.getString(R.string.all_matching_locations_rendered, locationsCount) - } - emitSnackBarState( - SnackBarMessageConfig( - message = message, - actionLabel = context.getString(org.smartregister.fhircore.engine.R.string.ok), - duration = SnackbarDuration.Short, - ), - ) - } - - // Account for missing locations - if (locationsCount == 0) { - if (!searchText.isNullOrBlank()) { + if (locationsCount == 0) { val message = - context.getString( - R.string.no_found_locations_matching_text, - searchText, - ) - Timber.w(message) + if (!searchText.isNullOrBlank()) { + context.getString(R.string.no_found_locations_matching_text, searchText) + } else context.getString(R.string.no_locations_to_render) emitSnackBarState( SnackBarMessageConfig( message = message, @@ -228,12 +215,22 @@ constructor( duration = SnackbarDuration.Long, ), ) + Timber.w(message) } else { - SnackBarMessageConfig( - message = context.getString(R.string.no_locations_to_render), - actionLabel = context.getString(org.smartregister.fhircore.engine.R.string.ok), - duration = SnackbarDuration.Long, + val message = + if (searchText.isNullOrBlank()) { + context.getString(R.string.all_locations_rendered) + } else { + context.getString(R.string.all_matching_locations_rendered, locationsCount) + } + emitSnackBarState( + SnackBarMessageConfig( + message = message, + actionLabel = context.getString(org.smartregister.fhircore.engine.R.string.ok), + duration = SnackbarDuration.Short, + ), ) + Timber.w(message) } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index 423323e660..cb371dfb20 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -19,6 +19,7 @@ package org.smartregister.fhircore.quest.ui.shared.components import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -63,6 +64,7 @@ import java.time.OffsetDateTime import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.smartregister.fhircore.engine.ui.components.LineSpinFadeLoaderProgressIndicator import org.smartregister.fhircore.engine.ui.theme.AppTheme import org.smartregister.fhircore.engine.ui.theme.DangerColor import org.smartregister.fhircore.engine.ui.theme.DefaultColor @@ -267,68 +269,86 @@ fun SyncStatusView( } if (currentSyncJobStatus is CurrentSyncJobStatus.Running) { - Column(modifier = Modifier.weight(1f)) { - if (!minimized) { - SyncStatusTitle( - text = - stringResource( - if (isSyncUpload == true) { - org.smartregister.fhircore.engine.R.string.sync_up_inprogress - } else { - org.smartregister.fhircore.engine.R.string.sync_down_inprogress - }, - progressPercentage ?: 0, - ), - minimized = false, - color = Color.White, - startPadding = 0, - ) - } - LinearProgressIndicator( - progress = (progressPercentage?.toFloat()?.div(100)) ?: 0f, - color = MaterialTheme.colors.primary, - backgroundColor = Color.White, - modifier = - Modifier.testTag(SYNC_PROGRESS_INDICATOR_TEST_TAG) - .padding(vertical = 4.dp) - .fillMaxWidth(), - ) - if (!minimized) { - Text( - text = stringResource(id = org.smartregister.fhircore.engine.R.string.please_wait), - color = SubtitleTextColor, - fontSize = 14.sp, - textAlign = TextAlign.Start, - modifier = Modifier.align(Alignment.Start), + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.weight(1f), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + if (!minimized) { + SyncStatusTitle( + text = + stringResource( + if (isSyncUpload == true) { + org.smartregister.fhircore.engine.R.string.sync_up_inprogress + } else { + org.smartregister.fhircore.engine.R.string.sync_down_inprogress + }, + progressPercentage ?: 0, + ), + minimized = false, + color = Color.White, + startPadding = 0, + ) + } + LinearProgressIndicator( + progress = (progressPercentage?.toFloat()?.div(100)) ?: 0f, + color = MaterialTheme.colors.primary, + backgroundColor = Color.White, + modifier = + Modifier.testTag(SYNC_PROGRESS_INDICATOR_TEST_TAG) + .padding(vertical = 4.dp) + .fillMaxWidth(), ) + if (!minimized) { + Text( + text = stringResource(id = org.smartregister.fhircore.engine.R.string.please_wait), + color = SubtitleTextColor, + fontSize = 14.sp, + textAlign = TextAlign.Start, + modifier = Modifier.align(Alignment.Start), + ) + } } } } - if ( - (currentSyncJobStatus is CurrentSyncJobStatus.Failed || - currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 16.dp), ) { - Text( - text = - stringResource( - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - org.smartregister.fhircore.engine.R.string.retry - } else { - org.smartregister.fhircore.engine.R.string.cancel + if (currentSyncJobStatus is CurrentSyncJobStatus.Running) { + LineSpinFadeLoaderProgressIndicator( + color = Color.White, + lineLength = 8f, + innerRadius = 12f, + ) + } + if ( + (currentSyncJobStatus is CurrentSyncJobStatus.Failed || + currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized + ) { + Text( + text = + stringResource( + if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { + org.smartregister.fhircore.engine.R.string.retry + } else { + org.smartregister.fhircore.engine.R.string.cancel + }, + ), + modifier = + Modifier.padding(start = 16.dp).clickable { + if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { + onRetry() + } else { + onCancel() + } }, - ), - modifier = - Modifier.padding(start = 16.dp).clickable { - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - onRetry() - } else { - onCancel() - } - }, - color = MaterialTheme.colors.primary, - fontWeight = FontWeight.SemiBold, - ) + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.SemiBold, + ) + } } } }