feat: crawl data
This commit is contained in:
@@ -92,4 +92,5 @@ dependencies {
|
||||
//crawl
|
||||
implementation(libs.jsoup)
|
||||
|
||||
implementation(libs.retrofit.scalars)
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.approagency.drug.data.remote
|
||||
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface DarooyabApiService {
|
||||
@GET("Search")
|
||||
@FormUrlEncoded
|
||||
@POST("Home/PartialNewSearch")
|
||||
@Headers(
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"Accept-Language: en-US,en;q=0.5"
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
||||
"X-Requested-With: XMLHttpRequest",
|
||||
"Accept: */*",
|
||||
"Accept-Language: en-US,en;q=0.9,fa;q=0.8"
|
||||
)
|
||||
suspend fun searchDrugs(
|
||||
@Query("SearchText") searchText: String
|
||||
): String // Returns raw HTML for the first page
|
||||
@Field("autocomplete") searchText: String,
|
||||
@Field("DrugName_pageNumber") pageNumber: Int = 1
|
||||
): String
|
||||
}
|
||||
@@ -1,58 +1,102 @@
|
||||
package com.approagency.drug.data.remote
|
||||
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import okio.IOException
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class DrugHtmlParser {
|
||||
|
||||
fun parseSearchResults(html: String): List<DrugSearchResult> {
|
||||
fun parseSearchResultsWithPagination(html: String): DaroYabSearchResult {
|
||||
val document: Document = Jsoup.parse(html)
|
||||
val results = mutableListOf<DrugSearchResult>()
|
||||
val drugs = parseDrugRows(document)
|
||||
val paginationInfo = extractPaginationInfo(document)
|
||||
|
||||
// Select the table body that contains the drug rows
|
||||
return DaroYabSearchResult(
|
||||
drugs = drugs,
|
||||
currentPage = paginationInfo.currentPage,
|
||||
totalPages = paginationInfo.totalPages,
|
||||
hasNextPage = paginationInfo.currentPage < paginationInfo.totalPages,
|
||||
hasPreviousPage = paginationInfo.currentPage > 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseDrugRows(document: Document): List<DrugSearchResult> {
|
||||
val results = mutableListOf<DrugSearchResult>()
|
||||
val rows = document.select("#tbody_DrugList tr")
|
||||
|
||||
println("Found ${rows.size} rows in the response")
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
// Handle case where no results are found
|
||||
println("No rows found. HTML snippet: ${document.html().take(500)}")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
for (row in rows) {
|
||||
try {
|
||||
// --- Extract Persian Name and Detail Page URL ---
|
||||
val nameCell = row.selectFirst("td:eq(0)") // First td
|
||||
val nameCell = row.selectFirst("td:eq(0)")
|
||||
val drugLink = nameCell?.selectFirst("a.ahref_Generic")
|
||||
val persianName = drugLink?.text()?.trim() ?: continue // Skip if no name found
|
||||
val persianName = drugLink?.text()?.trim() ?: continue
|
||||
val detailPageRelativeUrl = drugLink?.attr("href") ?: continue
|
||||
val detailPageUrl = "https://www.darooyab.ir$detailPageRelativeUrl"
|
||||
|
||||
// --- Extract Generic ID from URL ---
|
||||
// URL format: /G-2556/Casanthranol or /G-2556/
|
||||
val genericIdRegex = "/G-(\\d+)/?".toRegex()
|
||||
val genericId = genericIdRegex.find(detailPageRelativeUrl)?.groupValues?.get(1) ?: ""
|
||||
|
||||
// --- Extract English Name ---
|
||||
// The last cell contains the English name link
|
||||
val brandIdRegex = "/B-(\\d+)/?".toRegex()
|
||||
val brandId = brandIdRegex.find(detailPageRelativeUrl)?.groupValues?.get(1) ?: ""
|
||||
|
||||
val lastCell = row.select("td").last()
|
||||
val englishLink = lastCell?.selectFirst("a.ahref_Generic")
|
||||
val englishName = englishLink?.text()?.trim()
|
||||
|
||||
val drugResult = DrugSearchResult(
|
||||
genericId = genericId,
|
||||
genericId = if (genericId.isNotEmpty()) genericId else brandId,
|
||||
persianName = persianName,
|
||||
englishName = englishName,
|
||||
detailPageUrl = detailPageUrl
|
||||
)
|
||||
results.add(drugResult)
|
||||
println("Parsed: $persianName -> $englishName")
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Log error for a specific row but continue with others
|
||||
println("Error parsing row: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private fun extractPaginationInfo(document: Document): PaginationInfo {
|
||||
var currentPage = 1
|
||||
var totalPages = 1
|
||||
|
||||
try {
|
||||
// Get current page from hidden input
|
||||
val currentPageInput = document.select("#CurrentPager_Number")
|
||||
if (currentPageInput.isNotEmpty()) {
|
||||
currentPage = currentPageInput.`val`()?.toIntOrNull() ?: 1
|
||||
}
|
||||
|
||||
// Get total pages from pagination links
|
||||
val pageLinks = document.select(".pagination li .PagerBtn_DrugName")
|
||||
if (pageLinks.isNotEmpty()) {
|
||||
val pageNumbers = pageLinks.mapNotNull { it.text().toIntOrNull() }
|
||||
if (pageNumbers.isNotEmpty()) {
|
||||
totalPages = pageNumbers.maxOrNull() ?: 1
|
||||
}
|
||||
}
|
||||
|
||||
println("Pagination: Current page=$currentPage, Total pages=$totalPages")
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Error extracting pagination: ${e.message}")
|
||||
}
|
||||
|
||||
return PaginationInfo(currentPage, totalPages)
|
||||
}
|
||||
|
||||
private data class PaginationInfo(
|
||||
val currentPage: Int,
|
||||
val totalPages: Int
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.approagency.drug.data.remote
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class StringConverterFactory : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<ResponseBody, *>? {
|
||||
return if (type == String::class.java) {
|
||||
Converter<ResponseBody, String> { value -> value.string() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import com.approagency.drug.data.dto.DrugModels
|
||||
import com.approagency.drug.data.remote.DarooyabApiService
|
||||
import com.approagency.drug.data.remote.DrugApiService
|
||||
import com.approagency.drug.data.remote.DrugHtmlParser
|
||||
import com.approagency.drug.domain.model.DaroYabParams
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugSearchParams
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import com.approagency.drug.domain.repository.DrugRepository
|
||||
@@ -30,8 +32,10 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -45,25 +49,26 @@ class DrugRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun searchDrugs(params: DrugSearchParams): Result<List<DrugSearchResult>> {
|
||||
override suspend fun searchDrugs(params: DaroYabParams): Result<DaroYabSearchResult> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// 1. Fetch raw HTML from the website
|
||||
val htmlResponse = darooyabApiService.searchDrugs(params.query!!)
|
||||
val htmlResponse = darooyabApiService.searchDrugs(
|
||||
searchText = params.query ?: "",
|
||||
pageNumber = params.pageNumber
|
||||
)
|
||||
|
||||
// 2. Parse the HTML to extract drug data
|
||||
val drugList = parser.parseSearchResults(htmlResponse)
|
||||
println("HTML Response length: ${htmlResponse.length}")
|
||||
|
||||
if (drugList.isNotEmpty()) {
|
||||
Result.success(drugList)
|
||||
val searchResult = parser.parseSearchResultsWithPagination(htmlResponse)
|
||||
|
||||
if (searchResult.drugs.isNotEmpty()) {
|
||||
Result.success(searchResult)
|
||||
} else {
|
||||
Result.failure(Exception("No drugs found for query: '${params.query}'"))
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// Network error
|
||||
Result.failure(Exception("Network error: ${e.message}", e))
|
||||
} catch (e: Exception) {
|
||||
// Parsing or other error
|
||||
Result.failure(Exception("An error occurred: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.approagency.drug.domain.usecase.SearchTestsUseCase
|
||||
import com.approagency.drug.presentation.viewModel.HomeViewModel
|
||||
import com.approagency.drug.presentation.viewModel.LabViewModel
|
||||
import com.approagency.drug.utils.Config
|
||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
@@ -26,6 +27,9 @@ import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
val appModule= module {
|
||||
@@ -35,17 +39,23 @@ val appModule= module {
|
||||
single { get<LabDatabase>().testGroupDao() }
|
||||
single { get<LabDatabase>().testItemDao() }
|
||||
|
||||
// Shared OkHttpClient for both APIs
|
||||
single {
|
||||
val cookieManager = CookieManager()
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
level = HttpLoggingInterceptor.Level.BODY // Change to BODY for debugging
|
||||
})
|
||||
// Add cookie support
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
// ========== Retrofit for YOUR API ==========
|
||||
// ========== Retrofit for YOUR JSON API ==========
|
||||
single {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(Config.BASE_URL)
|
||||
@@ -55,19 +65,24 @@ val appModule= module {
|
||||
}
|
||||
|
||||
single<DrugApiService> {
|
||||
get<Retrofit>().create(DrugApiService::class.java)
|
||||
val retrofit: Retrofit = get()
|
||||
retrofit.create(DrugApiService::class.java)
|
||||
}
|
||||
|
||||
// ========== Retrofit for DAROOYAB Website API ==========
|
||||
// ========== Retrofit for DAROOYAB Website (HTML response) ==========
|
||||
// IMPORTANT: Use ScalarsConverterFactory for plain text/HTML, NOT Gson!
|
||||
single {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(Config.Darro_Url) // Darooyab base URL
|
||||
.baseUrl(Config.Darro_Url)
|
||||
.client(get<OkHttpClient>())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addConverterFactory(ScalarsConverterFactory.create()) // This handles String responses
|
||||
.build()
|
||||
.let { retrofit ->
|
||||
retrofit.create(DarooyabApiService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the API service from the Scalars-based Retrofit instance
|
||||
single<DarooyabApiService> {
|
||||
val retrofit: Retrofit = get() // Gets the Scalars Retrofit instance
|
||||
retrofit.create(DarooyabApiService::class.java)
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +129,10 @@ val appModule= module {
|
||||
HomeViewModel(get() , get() , get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
SearchViewModel(get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
LabViewModel(get() , get() , get())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.approagency.drug.domain.model
|
||||
|
||||
data class DaroYabParams(
|
||||
val query: String? = null,
|
||||
val pageNumber: Int = 1,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.approagency.drug.domain.model
|
||||
|
||||
data class DaroYabSearchResult(
|
||||
val drugs: List<DrugSearchResult>,
|
||||
val currentPage: Int,
|
||||
val totalPages: Int,
|
||||
val hasNextPage: Boolean,
|
||||
val hasPreviousPage: Boolean
|
||||
)
|
||||
@@ -6,4 +6,5 @@ data class DrugSearchParams(
|
||||
val withRelations: Boolean = true,
|
||||
val drugGroup: Int? = null,
|
||||
val healGroup: Int? = null,
|
||||
|
||||
)
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.approagency.drug.domain.repository
|
||||
import com.approagency.drug.data.dto.DarmanModel
|
||||
import com.approagency.drug.data.dto.DrugListResponse
|
||||
import com.approagency.drug.data.dto.DrugModels
|
||||
import com.approagency.drug.domain.model.DaroYabParams
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugSearchParams
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
|
||||
@@ -10,5 +12,5 @@ interface DrugRepository {
|
||||
suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse>
|
||||
suspend fun drugDetail(cod:Int): Result<DrugModels>
|
||||
suspend fun getGorohDaroei(): Result<DarmanModel>
|
||||
suspend fun searchDrugs(params:DrugSearchParams):Result<List<DrugSearchResult>>
|
||||
suspend fun searchDrugs(params:DaroYabParams):Result<DaroYabSearchResult>
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.approagency.drug.domain.usecase
|
||||
|
||||
import com.approagency.drug.data.repository.DrugRepositoryImpl
|
||||
import com.approagency.drug.domain.model.DaroYabParams
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugSearchParams
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import com.approagency.drug.domain.repository.DrugRepository
|
||||
@@ -8,8 +10,8 @@ import com.approagency.drug.domain.repository.DrugRepository
|
||||
class SearchDrugsYabUseCase (
|
||||
private val repository: DrugRepository
|
||||
) {
|
||||
suspend operator fun invoke(query: String): Result<List<DrugSearchResult>> {
|
||||
val params = DrugSearchParams(query = query)
|
||||
suspend operator fun invoke(query: String, pageNumber: Int = 1): Result<DaroYabSearchResult> {
|
||||
val params = DaroYabParams(query = query, pageNumber = pageNumber)
|
||||
return repository.searchDrugs(params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.approagency.drug.presentation.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
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 androidx.compose.ui.unit.sp
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
|
||||
@Composable
|
||||
fun EmptySearchState(
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "جستجوی داروها",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
Text(
|
||||
text = "نام دارو را وارد کنید",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(top = dime.sm)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorState(
|
||||
message: String,
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "خطا",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(top = dime.sm, bottom = dime.lg),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
Button(
|
||||
onClick = onRetry,
|
||||
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text("تلاش مجدد")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.approagency.drug.presentation.common
|
||||
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.vada.caller.ui.theme.LocalDime
|
||||
|
||||
@Composable
|
||||
fun LoadingMoreIndicator(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dime.lg),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(32.dp),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EndOfListIndicator(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dime.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "پایان نتایج",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.approagency.drug.presentation.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
|
||||
@Composable
|
||||
fun DaroYabSearchResult(
|
||||
drug: DrugSearchResult,
|
||||
onClick: (DrugSearchResult) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dime.xs)
|
||||
.clickable { onClick(drug) },
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.lg)
|
||||
) {
|
||||
Text(
|
||||
text = drug.persianName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
drug.englishName?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "کد: ${drug.genericId}",
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.approagency.drug.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun PaginationControls(
|
||||
currentPage: Int,
|
||||
totalPages: Int,
|
||||
onPageSelected: (Int) -> Unit,
|
||||
onNextPage: () -> Unit,
|
||||
onPreviousPage: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (totalPages <= 1) return
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Previous button
|
||||
PaginationButton(
|
||||
enabled = currentPage > 1,
|
||||
onClick = onPreviousPage,
|
||||
isPrevious = true
|
||||
)
|
||||
|
||||
// Page numbers
|
||||
val pageRange = getVisiblePageRange(currentPage, totalPages)
|
||||
|
||||
if (pageRange.first > 1) {
|
||||
PageNumberButton(page = 1, isSelected = false, onClick = onPageSelected)
|
||||
if (pageRange.first > 2) {
|
||||
Text("...", modifier = Modifier.padding(horizontal = 4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
pageRange.forEach { page ->
|
||||
PageNumberButton(
|
||||
page = page,
|
||||
isSelected = page == currentPage,
|
||||
onClick = onPageSelected
|
||||
)
|
||||
}
|
||||
|
||||
if (pageRange.last < totalPages) {
|
||||
if (pageRange.last < totalPages - 1) {
|
||||
Text("...", modifier = Modifier.padding(horizontal = 4.dp))
|
||||
}
|
||||
PageNumberButton(page = totalPages, isSelected = false, onClick = onPageSelected)
|
||||
}
|
||||
|
||||
// Next button
|
||||
PaginationButton(
|
||||
enabled = currentPage < totalPages,
|
||||
onClick = onNextPage,
|
||||
isPrevious = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageNumberButton(
|
||||
page: Int,
|
||||
isSelected: Boolean,
|
||||
onClick: (Int) -> Unit
|
||||
) {
|
||||
val backgroundColor = if (isSelected) Color(0xFF7D64BA) else Color.Transparent
|
||||
val textColor = if (isSelected) Color.White else Color(0xFF7D64BA)
|
||||
|
||||
Text(
|
||||
text = page.toString(),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(backgroundColor)
|
||||
.clickable { onClick(page) }
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
color = textColor,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaginationButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
isPrevious: Boolean
|
||||
) {
|
||||
val icon = if (isPrevious) Icons.Default.ArrowBack else Icons.Default.ArrowForward
|
||||
val color = if (enabled) Color(0xFF7D64BA) else Color.Gray
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = if (isPrevious) "Previous" else "Next",
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(if (enabled) Modifier.clickable(onClick = onClick) else Modifier)
|
||||
.padding(8.dp),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVisiblePageRange(currentPage: Int, totalPages: Int): IntRange {
|
||||
val maxVisible = 5
|
||||
val halfVisible = maxVisible / 2
|
||||
|
||||
var start = currentPage - halfVisible
|
||||
var end = currentPage + halfVisible
|
||||
|
||||
if (start < 1) {
|
||||
end += (1 - start)
|
||||
start = 1
|
||||
}
|
||||
if (end > totalPages) {
|
||||
start -= (end - totalPages)
|
||||
end = totalPages
|
||||
}
|
||||
if (start < 1) start = 1
|
||||
|
||||
return start..end
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -54,7 +55,8 @@ import androidx.navigation.NavHostController
|
||||
fun RootScreen(navHostController: NavHostController, modifier: Modifier){
|
||||
val pages = listOf(
|
||||
"دارو" to Icons.Default.Search,
|
||||
"آزمایش" to Icons.Default.Build
|
||||
"آزمایش" to Icons.Default.Build,
|
||||
"سرچ" to Icons.Rounded.Refresh
|
||||
)
|
||||
var seletedTab by remember { mutableStateOf(0) }
|
||||
val context = LocalContext.current
|
||||
@@ -121,6 +123,7 @@ fun RootScreen(navHostController: NavHostController, modifier: Modifier){
|
||||
padding -> when (seletedTab) {
|
||||
0 -> HomeScreen( navController = navHostController,Modifier.padding(padding).consumeWindowInsets(padding))
|
||||
1 -> LabScreen(Modifier.padding(padding))
|
||||
2 -> SearchScreen(navHostController,modifier = Modifier.padding(padding).consumeWindowInsets(padding))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.approagency.drug.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import com.approagency.drug.presentation.common.CustomTextFilled
|
||||
import com.approagency.drug.presentation.common.EmptySearchState
|
||||
import com.approagency.drug.presentation.common.EndOfListIndicator
|
||||
import com.approagency.drug.presentation.common.ErrorState
|
||||
import com.approagency.drug.presentation.common.LoadingMoreIndicator
|
||||
import com.approagency.drug.presentation.common.PrimaryButton
|
||||
import com.approagency.drug.presentation.components.DaroYabSearchResult
|
||||
import com.approagency.drug.presentation.components.PaginationControls
|
||||
import com.approgency.drug.presentation.viewModel.SearchState
|
||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
navController: NavController,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SearchViewModel = koinViewModel()
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
val state by viewModel.searchState.collectAsState()
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
// Auto-search for testing (remove in production)
|
||||
LaunchedEffect(Unit) {
|
||||
if (searchText.isEmpty()) {
|
||||
searchText = "انتی"
|
||||
viewModel.searchDrugs(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect when user scrolls to the bottom to load more
|
||||
LaunchedEffect(lazyListState) {
|
||||
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||
.collect { lastVisibleIndex ->
|
||||
if (lastVisibleIndex != null && state is SearchState.Success) {
|
||||
val successState = state as SearchState.Success
|
||||
val totalItems = successState.drugs.size
|
||||
// Load more when user is 3 items from the end
|
||||
if (lastVisibleIndex >= totalItems - 3 &&
|
||||
successState.hasMorePages &&
|
||||
!successState.isLoadingMore) {
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = modifier.fillMaxSize().padding(dime.lg)) {
|
||||
// Search input
|
||||
CustomTextFilled(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
onSearch = { query ->
|
||||
if (query.isNotBlank()) {
|
||||
keyboardController?.hide()
|
||||
viewModel.searchDrugs(searchText)
|
||||
}
|
||||
},
|
||||
placeholder = "جستجوی دارو",
|
||||
showClearButton = true,
|
||||
showSearchButton = true,
|
||||
autoSearch = true, // Set to true if you want search while typing
|
||||
height = 45
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
PrimaryButton(
|
||||
text = "جستجو",
|
||||
height = 40,
|
||||
isLoading = state is SearchState.Loading,
|
||||
onClick = {
|
||||
if (searchText.isNotBlank()) {
|
||||
viewModel.searchDrugs(searchText)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
// Results area
|
||||
when (val currentState = state) {
|
||||
is SearchState.Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "در حال جستجو...",
|
||||
modifier = Modifier.padding(top = dime.md),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SearchState.LoadingMore -> {
|
||||
// Show existing items with loading indicator at bottom
|
||||
LazyColumn(state = lazyListState) {
|
||||
items(currentState.currentItems) { drug ->
|
||||
DaroYabSearchResult(
|
||||
drug = drug,
|
||||
onClick = { selectedDrug ->
|
||||
// Navigate to drug detail
|
||||
// navController.navigate("drug_detail/${selectedDrug.genericId}")
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
LoadingMoreIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SearchState.Success -> {
|
||||
if (currentState.drugs.isEmpty()) {
|
||||
EmptySearchState(onRetry = { viewModel.retryLastSearch() })
|
||||
} else {
|
||||
LazyColumn(state = lazyListState) {
|
||||
items(currentState.drugs) { drug ->
|
||||
DaroYabSearchResult(
|
||||
drug = drug,
|
||||
onClick = { selectedDrug ->
|
||||
// Navigate to drug detail
|
||||
// navController.navigate("drug_detail/${selectedDrug.genericId}")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Show loading indicator at bottom when loading more
|
||||
if (currentState.isLoadingMore) {
|
||||
item {
|
||||
LoadingMoreIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
// Show end of list indicator
|
||||
if (!currentState.hasMorePages && currentState.drugs.isNotEmpty()) {
|
||||
item {
|
||||
EndOfListIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SearchState.Error -> {
|
||||
ErrorState(
|
||||
message = currentState.message,
|
||||
onRetry = { viewModel.retryLastSearch() }
|
||||
)
|
||||
}
|
||||
|
||||
SearchState.Idle -> {
|
||||
EmptySearchState(onRetry = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,37 +42,21 @@ class HomeViewModel (
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
retryWithBackoff(
|
||||
maxRetries = 3,
|
||||
onRetry = { attempt, delay ->
|
||||
// Optional: log retry attempt
|
||||
println("Retrying getDarmani, attempt $attempt, delay $delay ms")
|
||||
}
|
||||
) {
|
||||
getDarmanUseCase.invoke()
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(
|
||||
isLoading = false,
|
||||
getDarmani = result
|
||||
)
|
||||
try {
|
||||
val result = getDarmanUseCase.invoke()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(
|
||||
isLoading = false,
|
||||
getDarmani = result
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
handleError(error.message ?: "Unknown error")
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(
|
||||
isLoading = false,
|
||||
getDarmani = Result.failure(error)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}catch (e: Exception) {
|
||||
handleError(e.message)
|
||||
}catch (e: Exception) {
|
||||
handleError(e.localizedMessage ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +67,7 @@ class HomeViewModel (
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = getDrugSearchUseCase.invoke(drugSearchParams)
|
||||
println(result)
|
||||
_uiState.update { it.copy(
|
||||
drugSearchState = it.drugSearchState.copy(
|
||||
isLoading = false,
|
||||
@@ -90,9 +75,11 @@ class HomeViewModel (
|
||||
)
|
||||
|
||||
) }
|
||||
} catch (e: HttpException) {
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
handleError(e.message)
|
||||
}catch (e: Exception) {
|
||||
println(e.message)
|
||||
handleError(e.localizedMessage ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
@@ -119,7 +106,7 @@ class HomeViewModel (
|
||||
)
|
||||
}
|
||||
println(result)
|
||||
} catch (e: HttpException) {
|
||||
} catch (e: Exception) {
|
||||
handleError(e.message)
|
||||
}catch (e: Exception) {
|
||||
handleError(e.localizedMessage ?: "Unknown error")
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.approgency.drug.presentation.viewModel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import com.approagency.drug.domain.usecase.SearchDrugsYabUseCase
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SearchViewModel(
|
||||
private val searchDrugsUseCase: SearchDrugsYabUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _searchState = MutableStateFlow<SearchState>(SearchState.Idle)
|
||||
val searchState: StateFlow<SearchState> = _searchState.asStateFlow()
|
||||
|
||||
private var currentQuery = ""
|
||||
private var currentPage = 1
|
||||
private var totalPages = 1
|
||||
private val allDrugs = mutableListOf<DrugSearchResult>()
|
||||
private var isLoadingMore = false
|
||||
private var hasMorePages = true
|
||||
|
||||
fun searchDrugs(query: String, isNewSearch: Boolean = true) {
|
||||
if (query.length < 3) {
|
||||
_searchState.value = SearchState.Error("لطفاً حداقل ۳ حرف وارد کنید")
|
||||
return
|
||||
}
|
||||
|
||||
if (isNewSearch) {
|
||||
// Reset everything for new search
|
||||
currentQuery = query
|
||||
currentPage = 1
|
||||
allDrugs.clear()
|
||||
isLoadingMore = false
|
||||
hasMorePages = true
|
||||
_searchState.value = SearchState.Loading(isNew = true)
|
||||
} else {
|
||||
// Don't load if already loading or no more pages
|
||||
if (isLoadingMore || !hasMorePages) return
|
||||
isLoadingMore = true
|
||||
_searchState.value = SearchState.LoadingMore(
|
||||
currentItems = allDrugs.toList(),
|
||||
currentPage = currentPage
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = searchDrugsUseCase(currentQuery, currentPage)
|
||||
|
||||
_searchState.value = when {
|
||||
result.isSuccess -> {
|
||||
val searchResult = result.getOrNull()!!
|
||||
totalPages = searchResult.totalPages
|
||||
hasMorePages = currentPage < totalPages
|
||||
|
||||
allDrugs.addAll(searchResult.drugs)
|
||||
isLoadingMore = false
|
||||
|
||||
SearchState.Success(
|
||||
drugs = allDrugs.toList(),
|
||||
currentPage = searchResult.currentPage,
|
||||
totalPages = searchResult.totalPages,
|
||||
isLoadingMore = false,
|
||||
hasMorePages = hasMorePages
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
isLoadingMore = false
|
||||
SearchState.Error(result.exceptionOrNull()?.message ?: "خطا در جستجو")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
if (hasMorePages && !isLoadingMore && currentPage < totalPages) {
|
||||
currentPage++
|
||||
searchDrugs(currentQuery, isNewSearch = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSearch() {
|
||||
currentQuery = ""
|
||||
currentPage = 1
|
||||
totalPages = 1
|
||||
allDrugs.clear()
|
||||
isLoadingMore = false
|
||||
hasMorePages = true
|
||||
_searchState.value = SearchState.Idle
|
||||
}
|
||||
|
||||
fun retryLastSearch() {
|
||||
if (currentQuery.isNotEmpty()) {
|
||||
searchDrugs(currentQuery, isNewSearch = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SearchState {
|
||||
object Idle : SearchState()
|
||||
data class Loading(val isNew: Boolean) : SearchState()
|
||||
data class LoadingMore(
|
||||
val currentItems: List<DrugSearchResult>,
|
||||
val currentPage: Int
|
||||
) : SearchState()
|
||||
data class Success(
|
||||
val drugs: List<DrugSearchResult>,
|
||||
val currentPage: Int,
|
||||
val totalPages: Int,
|
||||
val isLoadingMore: Boolean = false,
|
||||
val hasMorePages: Boolean = true
|
||||
) : SearchState()
|
||||
data class Error(val message: String) : SearchState()
|
||||
}
|
||||
@@ -21,6 +21,7 @@ room = "2.6.1"
|
||||
navigationCompose = "2.8.5"
|
||||
material3 = "1.4.0"
|
||||
jsoup = "1.18.3"
|
||||
scalars = "2.11.0"
|
||||
[libraries]
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -55,7 +56,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3",
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
|
||||
retrofit-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "scalars" }
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user