diff --git a/app/src/main/java/com/approagency/pharmacy/navigation/MainContainer.kt b/app/src/main/java/com/approagency/pharmacy/navigation/MainContainer.kt index b329a8c..59a6a7d 100644 --- a/app/src/main/java/com/approagency/pharmacy/navigation/MainContainer.kt +++ b/app/src/main/java/com/approagency/pharmacy/navigation/MainContainer.kt @@ -21,8 +21,10 @@ import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign @@ -33,6 +35,7 @@ import com.approagency.pharmacy.domain.repository.AuthRepository import com.approagency.pharmacy.presentation.account.AccountDrawer import com.approagency.pharmacy.presentation.account.AccountSheet import com.approagency.pharmacy.presentation.account.AccountSheetController +import com.approagency.pharmacy.presentation.common.ConfirmBottomSheet import kotlinx.coroutines.launch import org.koin.compose.koinInject @@ -52,6 +55,7 @@ fun MainContainer( val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() + var showLogoutConfirm by remember { mutableStateOf(false) } val context = LocalContext.current val appVersion = remember { @@ -68,12 +72,7 @@ fun MainContainer( appVersion = appVersion, themeMode = themeMode, onThemeModeChange = { session.setThemeMode(it) }, - onLogout = { - scope.launch { - authRepository.logout() - drawerState.close() - } - } + onLogout = { showLogoutConfirm = true } ) } ) { @@ -96,6 +95,22 @@ fun MainContainer( if (sheetVisible) { AccountSheet(onDismiss = { sheetController.hide() }) } + + if (showLogoutConfirm) { + ConfirmBottomSheet( + title = "خروج از حساب", + message = "آیا می‌خواهید از حساب کاربری خود خارج شوید؟", + confirmText = "خروج", + onConfirm = { + showLogoutConfirm = false + scope.launch { + authRepository.logout() + drawerState.close() + } + }, + onDismiss = { showLogoutConfirm = false } + ) + } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountDrawer.kt b/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountDrawer.kt index aa3fe0a..fc3efe3 100644 --- a/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountDrawer.kt +++ b/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountDrawer.kt @@ -11,6 +11,7 @@ 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.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Logout @@ -61,7 +62,10 @@ fun AccountDrawer( onThemeModeChange: (ThemeMode) -> Unit, onLogout: () -> Unit ) { - ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.surface) { + ModalDrawerSheet( + modifier = Modifier.width(280.dp), + drawerContainerColor = MaterialTheme.colorScheme.surface + ) { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { Column( modifier = Modifier diff --git a/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountSheet.kt b/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountSheet.kt index 8588549..c51adeb 100644 --- a/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountSheet.kt +++ b/app/src/main/java/com/approagency/pharmacy/presentation/account/AccountSheet.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api @@ -37,9 +38,9 @@ import androidx.compose.ui.unit.dp import com.approagency.pharmacy.MainActivity import com.approagency.pharmacy.domain.model.SubscriptionProduct import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet -import com.approagency.pharmacy.presentation.common.Loading import com.approagency.pharmacy.presentation.common.OtpTextField import com.approagency.pharmacy.presentation.common.PrimaryButton +import com.approagency.pharmacy.presentation.common.shimmer import com.approagency.pharmacy.presentation.viewModel.AccountPhase import com.approagency.pharmacy.presentation.viewModel.AccountViewModel import com.vada.caller.ui.theme.dime @@ -147,11 +148,11 @@ fun AccountSheet( ) Spacer(Modifier.height(MaterialTheme.dime.lg)) when { - ui.productsLoading -> Loading( - modifier = Modifier - .fillMaxWidth() - .height(160.dp) - ) + ui.productsLoading -> Column( + verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md) + ) { + repeat(2) { ProductCardSkeleton() } + } else -> Column( verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md) ) { @@ -182,8 +183,6 @@ fun AccountSheet( style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(Modifier.height(MaterialTheme.dime.lg)) - TextButton(onClick = { viewModel.logout() }) { Text("خروج از حساب") } } } @@ -241,6 +240,39 @@ private fun ProductCard( } } +/** جای‌گیرنده‌ی shimmer برای کارت محصول هنگام بارگذاری. */ +@Composable +private fun ProductCardSkeleton() { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(MaterialTheme.dime.lg)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box( + Modifier + .width(120.dp) + .height(18.dp) + .shimmer() + ) + Box( + Modifier + .width(72.dp) + .height(18.dp) + .shimmer() + ) + } + Spacer(Modifier.height(MaterialTheme.dime.md)) + Box( + Modifier + .fillMaxWidth() + .height(44.dp) + .shimmer(shape = MaterialTheme.shapes.medium) + ) + } + } +} + /** قالب‌بندی قیمت با جداکننده‌ی هزارگان. */ private fun formatPrice(price: Long): String = price.toString().reversed().chunked(3).joinToString(",").reversed() diff --git a/app/src/main/java/com/approagency/pharmacy/presentation/common/ConfirmBottomSheet.kt b/app/src/main/java/com/approagency/pharmacy/presentation/common/ConfirmBottomSheet.kt new file mode 100644 index 0000000..5583df1 --- /dev/null +++ b/app/src/main/java/com/approagency/pharmacy/presentation/common/ConfirmBottomSheet.kt @@ -0,0 +1,81 @@ +package com.approagency.pharmacy.presentation.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.vada.caller.ui.theme.dime + +/** + * شیتِ تأییدِ عمومی برای کنش‌های حساس (مثل خروج از حساب). + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfirmBottomSheet( + title: String, + message: String, + confirmText: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit, + dismissText: String = "انصراف", + confirmColor: Color = MaterialTheme.colorScheme.error +) { + CustomModalBottomSheet(onDismiss = onDismiss) { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(MaterialTheme.dime.lg) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Spacer(Modifier.height(MaterialTheme.dime.sm)) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(MaterialTheme.dime.xl)) + Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)) { + OutlinedButton( + onClick = onDismiss, + modifier = Modifier + .weight(1f) + .height(48.dp) + ) { Text(dismissText) } + Button( + onClick = onConfirm, + modifier = Modifier + .weight(1f) + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = confirmColor, + contentColor = MaterialTheme.colorScheme.onError + ) + ) { Text(confirmText) } + } + Spacer(Modifier.height(MaterialTheme.dime.sm)) + } + } + } +} diff --git a/app/src/main/java/com/approagency/pharmacy/presentation/common/Shimmer.kt b/app/src/main/java/com/approagency/pharmacy/presentation/common/Shimmer.kt new file mode 100644 index 0000000..824ea67 --- /dev/null +++ b/app/src/main/java/com/approagency/pharmacy/presentation/common/Shimmer.kt @@ -0,0 +1,53 @@ +package com.approagency.pharmacy.presentation.common + +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.shape.RoundedCornerShape +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.material3.MaterialTheme + +/** + * افکت درخششِ متحرک (shimmer) برای جای‌گیرنده‌های لودینگ. + * رنگ‌ها از تم گرفته می‌شوند تا در حالت تاریک/روشن هماهنگ باشد. + */ +fun Modifier.shimmer(shape: Shape = RoundedCornerShape(8.dp)): Modifier = composed { + var size by remember { mutableStateOf(IntSize.Zero) } + val transition = rememberInfiniteTransition(label = "shimmer") + val startX by transition.animateFloat( + initialValue = -2f * size.width.coerceAtLeast(1), + targetValue = 2f * size.width.coerceAtLeast(1), + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1200), + repeatMode = RepeatMode.Restart + ), + label = "shimmerX" + ) + val base = MaterialTheme.colorScheme.surfaceVariant + val highlight = MaterialTheme.colorScheme.surfaceContainerHighest + + clip(shape) + .background( + brush = Brush.linearGradient( + colors = listOf(base, highlight, base), + start = Offset(startX, 0f), + end = Offset(startX + size.width.toFloat(), size.height.toFloat()) + ) + ) + .onGloballyPositioned { size = it.size } +}