feat: clean code

This commit is contained in:
2026-06-12 23:39:17 +03:30
parent 0a4b339481
commit 1ed75fb823
19 changed files with 202 additions and 167 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
<bytecodeTargetLevel target="17" />
</component>
</project>
+2 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
@@ -3,12 +3,18 @@ package com.approagency.pharmacy
import android.app.Application
import com.approagency.pharmacy.data.local.database.LabDatabase
import com.approagency.pharmacy.di.appModule
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
class DrugApp : Application(){
class DrugApp : Application() {
// اسکوپ سطح‌اپ برای کارهای پس‌زمینه‌ی طول‌عمرِ برنامه (به‌جای GlobalScope).
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onCreate() {
super.onCreate()
startKoin {
@@ -17,9 +23,9 @@ class DrugApp : Application(){
printLogger()
}
// Preload database (optional, for faster access)
GlobalScope.launch {
// پیش‌بارگذاری دیتابیس برای دسترسی سریع‌تر
applicationScope.launch {
LabDatabase.getInstance(this@DrugApp)
}
}
}
}
@@ -20,6 +20,7 @@ import com.approagency.pharmacy.data.local.SessionManager
import com.approagency.pharmacy.domain.repository.AuthRepository
import com.approagency.pharmacy.navigation.AppNavGraph
import com.approagency.pharmacy.presentation.account.OtpAutoFillBus
import com.approagency.pharmacy.presentation.account.OtpAutofillController
import com.approagency.pharmacy.ui.theme.DrugTheme
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.common.api.CommonStatusCodes
@@ -27,7 +28,7 @@ import com.google.android.gms.common.api.Status
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class MainActivity : ComponentActivity() {
class MainActivity : ComponentActivity(), OtpAutofillController {
private val session: SessionManager by inject()
private val authRepository: AuthRepository by inject()
@@ -71,7 +72,7 @@ class MainActivity : ComponentActivity() {
// ---------- خودکارپُرکُنِ کد پیامک (SMS User Consent API) ----------
/** آغاز گوش‌دادن به پیامکِ کد؛ کدِ یافت‌شده از طریق [OtpAutoFillBus] تحویل می‌شود. */
fun startOtpAutofill() {
override fun startOtpAutofill() {
SmsRetriever.getClient(this).startSmsUserConsent(null)
if (otpSmsReceiver != null) return
val receiver = object : BroadcastReceiver() {
@@ -103,7 +104,7 @@ class MainActivity : ComponentActivity() {
)
}
fun stopOtpAutofill() {
override fun stopOtpAutofill() {
otpSmsReceiver?.let { runCatching { unregisterReceiver(it) } }
otpSmsReceiver = null
}
@@ -2,7 +2,7 @@ package com.approagency.pharmacy.data.local
import android.content.Context
import android.content.SharedPreferences
import com.approagency.pharmacy.ui.theme.ThemeMode
import com.approagency.pharmacy.domain.model.ThemeMode
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -37,10 +37,8 @@ class DrugRepositoryImpl(
override suspend fun drugDetail(cod: Int): Result<DrugModels> {
return try {
val response= apiService.getDrugDetail(cod = cod)
println(response.message)
return Result.success(response)
}catch (e: Exception){
println(e.message)
Result.failure(e)
}
}
@@ -48,7 +46,6 @@ class DrugRepositoryImpl(
override suspend fun getGorohDaroei(): Result<DarmanModel> {
return try {
val response = apiService.getGorohDaroei()
println(response)
return Result.success(response)
}catch (e: Exception){
Result.failure(e)
@@ -63,8 +60,6 @@ class DrugRepositoryImpl(
pageNumber = params.pageNumber
)
println("HTML Response length: ${htmlResponse.length}")
val searchResult = parser.parseSearchResultsWithPagination(htmlResponse)
Result.success(searchResult)
@@ -1,4 +1,4 @@
package com.approagency.pharmacy.ui.theme
package com.approagency.pharmacy.domain.model
/** حالت نمایش انتخابی کاربر؛ پیش‌فرض پیروی از سیستم است. */
enum class ThemeMode { SYSTEM, LIGHT, DARK }
@@ -0,0 +1,69 @@
package com.approagency.pharmacy.navigation
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.style.TextAlign
import com.approagency.pharmacy.data.local.AccountState
/**
* نوار بالای اپ: دکمهی منو، اطلاعات حساب (موبایل + وضعیت اشتراک) و کنشِ
* ورود/خرید اشتراک.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountAppBar(
account: AccountState,
onMenuClick: () -> Unit,
onAccountClick: () -> Unit
) {
TopAppBar(
navigationIcon = {
IconButton(onClick = onMenuClick) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "منو"
)
}
},
title = {
if (account.isLoggedIn) {
Column {
Text(
text = account.mobile ?: account.displayName.orEmpty(),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = account.subscriptionTitle ?: "بدون اشتراک",
style = MaterialTheme.typography.labelSmall,
color = if (account.isSubscribed)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
Text(text = "دارویاب", style = MaterialTheme.typography.titleMedium)
}
},
actions = {
if (!account.isLoggedIn) {
ElevatedButton(onClick = onAccountClick) {
Text("ورود", textAlign = TextAlign.Center)
}
} else if (!account.isSubscribed) {
ElevatedButton(onClick = onAccountClick) {
Text("خرید اشتراک", textAlign = TextAlign.Center)
}
}
}
)
}
@@ -1,22 +1,11 @@
package com.approagency.pharmacy.navigation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -27,9 +16,7 @@ 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
import androidx.navigation.NavHostController
import com.approagency.pharmacy.data.local.AccountState
import com.approagency.pharmacy.data.local.SessionManager
import com.approagency.pharmacy.domain.repository.AuthRepository
import com.approagency.pharmacy.presentation.account.AccountDrawer
@@ -112,53 +99,3 @@ fun MainContainer(
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AccountAppBar(
account: AccountState,
onMenuClick: () -> Unit,
onAccountClick: () -> Unit
) {
TopAppBar(
navigationIcon = {
IconButton(onClick = onMenuClick) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "منو"
)
}
},
title = {
if (account.isLoggedIn) {
Column {
Text(
text = account.mobile ?: account.displayName.orEmpty(),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = account.subscriptionTitle ?: "بدون اشتراک",
style = MaterialTheme.typography.labelSmall,
color = if (account.isSubscribed)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
Text(text = "دارویاب", style = MaterialTheme.typography.titleMedium)
}
},
actions = {
if (!account.isLoggedIn) {
ElevatedButton(onClick = onAccountClick) {
Text("ورود", textAlign = TextAlign.Center)
}
}else if (!account.isSubscribed){
ElevatedButton(onClick = onAccountClick) {
Text("خرید اشتراک", textAlign = TextAlign.Center)
}
}
}
)
}
@@ -43,7 +43,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.approagency.pharmacy.data.local.AccountState
import com.approagency.pharmacy.ui.theme.ThemeMode
import com.approagency.pharmacy.domain.model.ThemeMode
import com.vada.caller.ui.theme.dime
/**
@@ -12,9 +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
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -26,7 +24,6 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -35,12 +32,9 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.LayoutDirection
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.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
@@ -71,9 +65,9 @@ fun AccountSheet(
// در مرحله‌ی کد، گوش‌دادن به پیامک را آغاز کن و با خروج متوقفش کن.
val isOtpStep = ui.phase == AccountPhase.EnterOtp
DisposableEffect(isOtpStep) {
val mainActivity = activity as? MainActivity
if (isOtpStep) mainActivity?.startOtpAutofill()
onDispose { mainActivity?.stopOtpAutofill() }
val autofill = activity as? OtpAutofillController
if (isOtpStep) autofill?.startOtpAutofill()
onDispose { autofill?.stopOtpAutofill() }
}
// کدِ خوانده‌شده از پیامک را در فیلد بگذار و به‌صورت خودکار تأیید کن.
@@ -151,13 +145,13 @@ fun AccountSheet(
ui.productsLoading -> Column(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
) {
repeat(2) { ProductCardSkeleton() }
repeat(2) { SubscriptionProductCardSkeleton() }
}
else -> Column(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.md)
) {
ui.products.forEach { product ->
ProductCard(
SubscriptionProductCard(
product = product,
isPurchasing = ui.purchasingProductId == product.id,
onBuy = { activity?.let { viewModel.purchase(it, product) } }
@@ -205,74 +199,3 @@ private fun SheetTitle(text: String) {
Text(text = text, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(MaterialTheme.dime.xs))
}
@Composable
private fun ProductCard(
product: SubscriptionProduct,
isPurchasing: Boolean,
onBuy: () -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(MaterialTheme.dime.lg)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(product.title, style = MaterialTheme.typography.titleMedium)
Text(
"${formatPrice(product.price)} تومان",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
}
if (!product.description.isNullOrBlank()) {
Spacer(Modifier.height(MaterialTheme.dime.xs))
Text(
product.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(Modifier.height(MaterialTheme.dime.md))
PrimaryButton(text = "خرید", height = 44, isLoading = isPurchasing, onClick = onBuy)
}
}
}
/** جای‌گیرنده‌ی 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()
@@ -0,0 +1,12 @@
package com.approagency.pharmacy.presentation.account
/**
* قراردادِ شروع/توقفِ خودکارپُرکُنِ کدِ پیامک.
*
* اکتیویتی میزبان آن را پیادهسازی میکند؛ شیتِ ورود فقط به این اینترفیس وابسته
* است و از نوعِ مشخصِ اکتیویتی بیخبر میماند.
*/
interface OtpAutofillController {
fun startOtpAutofill()
fun stopOtpAutofill()
}
@@ -0,0 +1,94 @@
package com.approagency.pharmacy.presentation.account
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.approagency.pharmacy.domain.model.SubscriptionProduct
import com.approagency.pharmacy.presentation.common.PrimaryButton
import com.approagency.pharmacy.presentation.common.shimmer
import com.vada.caller.ui.theme.dime
/** کارت یک محصول اشتراک به‌همراه دکمه‌ی خرید. */
@Composable
fun SubscriptionProductCard(
product: SubscriptionProduct,
isPurchasing: Boolean,
onBuy: () -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(MaterialTheme.dime.lg)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(product.title, style = MaterialTheme.typography.titleMedium)
Text(
"${formatPrice(product.price)} تومان",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
}
if (!product.description.isNullOrBlank()) {
Spacer(Modifier.height(MaterialTheme.dime.xs))
Text(
product.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(Modifier.height(MaterialTheme.dime.md))
PrimaryButton(text = "خرید", height = 44, isLoading = isPurchasing, onClick = onBuy)
}
}
}
/** جای‌گیرنده‌ی shimmer برای کارت محصول هنگام بارگذاری. */
@Composable
fun SubscriptionProductCardSkeleton() {
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()
@@ -65,7 +65,6 @@ class HomeViewModel (
viewModelScope.launch {
try {
val result = getDrugSearchUseCase.invoke(drugSearchParams)
println(result)
_uiState.update { it.copy(
drugSearchState = it.drugSearchState.copy(
isLoading = false,
@@ -74,10 +73,8 @@ class HomeViewModel (
) }
} catch (e: Exception) {
println(e.message)
handleError(e.toUserMessage())
}catch (e: Exception) {
println(e.message)
handleError(e.toUserMessage())
}
}
@@ -103,7 +100,6 @@ class HomeViewModel (
)
)
}
println(result)
} catch (e: Exception) {
handleError(e.toUserMessage())
}catch (e: Exception) {
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.approagency.pharmacy.domain.model.ThemeMode
import com.fabirt.podcastapp.ui.theme.Shapes
import com.vada.caller.ui.theme.Dime
import com.vada.caller.ui.theme.LocalDime