feat: change drawer

This commit is contained in:
2026-06-12 21:16:51 +03:30
parent 675123309a
commit fd8eb0a373
5 changed files with 200 additions and 15 deletions
@@ -21,8 +21,10 @@ import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign 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.AccountDrawer
import com.approagency.pharmacy.presentation.account.AccountSheet import com.approagency.pharmacy.presentation.account.AccountSheet
import com.approagency.pharmacy.presentation.account.AccountSheetController import com.approagency.pharmacy.presentation.account.AccountSheetController
import com.approagency.pharmacy.presentation.common.ConfirmBottomSheet
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
@@ -52,6 +55,7 @@ fun MainContainer(
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showLogoutConfirm by remember { mutableStateOf(false) }
val context = LocalContext.current val context = LocalContext.current
val appVersion = remember { val appVersion = remember {
@@ -68,12 +72,7 @@ fun MainContainer(
appVersion = appVersion, appVersion = appVersion,
themeMode = themeMode, themeMode = themeMode,
onThemeModeChange = { session.setThemeMode(it) }, onThemeModeChange = { session.setThemeMode(it) },
onLogout = { onLogout = { showLogoutConfirm = true }
scope.launch {
authRepository.logout()
drawerState.close()
}
}
) )
} }
) { ) {
@@ -96,6 +95,22 @@ fun MainContainer(
if (sheetVisible) { if (sheetVisible) {
AccountSheet(onDismiss = { sheetController.hide() }) 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) @OptIn(ExperimentalMaterial3Api::class)
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material.icons.automirrored.filled.Logout
@@ -61,7 +62,10 @@ fun AccountDrawer(
onThemeModeChange: (ThemeMode) -> Unit, onThemeModeChange: (ThemeMode) -> Unit,
onLogout: () -> Unit onLogout: () -> Unit
) { ) {
ModalDrawerSheet(drawerContainerColor = MaterialTheme.colorScheme.surface) { ModalDrawerSheet(
modifier = Modifier.width(280.dp),
drawerContainerColor = MaterialTheme.colorScheme.surface
) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -37,9 +38,9 @@ import androidx.compose.ui.unit.dp
import com.approagency.pharmacy.MainActivity import com.approagency.pharmacy.MainActivity
import com.approagency.pharmacy.domain.model.SubscriptionProduct import com.approagency.pharmacy.domain.model.SubscriptionProduct
import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet 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.OtpTextField
import com.approagency.pharmacy.presentation.common.PrimaryButton 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.AccountPhase
import com.approagency.pharmacy.presentation.viewModel.AccountViewModel import com.approagency.pharmacy.presentation.viewModel.AccountViewModel
import com.vada.caller.ui.theme.dime import com.vada.caller.ui.theme.dime
@@ -147,11 +148,11 @@ fun AccountSheet(
) )
Spacer(Modifier.height(MaterialTheme.dime.lg)) Spacer(Modifier.height(MaterialTheme.dime.lg))
when { when {
ui.productsLoading -> Loading( ui.productsLoading -> Column(
modifier = Modifier verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
.fillMaxWidth() ) {
.height(160.dp) repeat(2) { ProductCardSkeleton() }
) }
else -> Column( else -> Column(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md) verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
) { ) {
@@ -182,8 +183,6 @@ fun AccountSheet(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant 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 = private fun formatPrice(price: Long): String =
price.toString().reversed().chunked(3).joinToString(",").reversed() price.toString().reversed().chunked(3).joinToString(",").reversed()
@@ -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))
}
}
}
}
@@ -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 }
}