diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/Converters.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/Converters.kt index 3042cb2fedb..27934b2a26c 100644 --- a/app/src/main/java/ru/practicum/android/diploma/data/dto/Converters.kt +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/Converters.kt @@ -2,14 +2,20 @@ package ru.practicum.android.diploma.data.dto import ru.practicum.android.diploma.data.dto.components.Area import ru.practicum.android.diploma.data.dto.components.Contacts +import ru.practicum.android.diploma.data.dto.components.CountryDto import ru.practicum.android.diploma.data.dto.components.Employer import ru.practicum.android.diploma.data.dto.components.Employment import ru.practicum.android.diploma.data.dto.components.Experience import ru.practicum.android.diploma.data.dto.components.KeySkill import ru.practicum.android.diploma.data.dto.components.LogoUrls import ru.practicum.android.diploma.data.dto.components.Phone +import ru.practicum.android.diploma.data.dto.components.RegionDto import ru.practicum.android.diploma.data.dto.components.Salary import ru.practicum.android.diploma.data.dto.components.Schedule +import ru.practicum.android.diploma.data.dto.components.IndustriesDto +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.domain.models.Industries import ru.practicum.android.diploma.domain.models.VacanciesResponse import ru.practicum.android.diploma.domain.models.Vacancy import ru.practicum.android.diploma.domain.models.VacancyDetails @@ -75,3 +81,40 @@ fun Contacts.toModel(): ContactsModel { val phones = phones?.map { it.toModel() } return ContactsModel(email, name, phones) } + +fun CountryDto.toModel(): Country { + return Country( + id = this.id, + name = this.name + ) +} + +fun RegionDto.toModel(): Region { + return Region( + id = this.id, + name = this.name, + parentId = this.parentId + ) +} + +fun List.toSectorList(): List { + return this.map { + Industries( + id = it.id, + name = it.name, + isChecked = false + ) + } +} + +fun List.toCountryList(): List { + return this.map { it.toModel() } +} + +fun List.toRegionList(): List { + return this.map { it.toModel() } +} + +fun List.toAllRegions(): List { + return this.flatMap { it.areas.map { regionDto -> regionDto.toModel() } } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/CountriesRequest.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/CountriesRequest.kt new file mode 100644 index 00000000000..3aec722e5ef --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/CountriesRequest.kt @@ -0,0 +1,3 @@ +package ru.practicum.android.diploma.data.dto + +data object CountriesRequest diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/CountriesResponse.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/CountriesResponse.kt new file mode 100644 index 00000000000..7ec69bccc7e --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/CountriesResponse.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.data.dto + +import ru.practicum.android.diploma.data.dto.components.CountryDto + +data class CountriesResponse( + val countries: List +) : Response() diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/IndustriesRequest.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/IndustriesRequest.kt new file mode 100644 index 00000000000..2e66ac0003e --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/IndustriesRequest.kt @@ -0,0 +1,3 @@ +package ru.practicum.android.diploma.data.dto + +data object IndustriesRequest diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/IndustriesResponse.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/IndustriesResponse.kt new file mode 100644 index 00000000000..f2fa344372e --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/IndustriesResponse.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.data.dto + +import ru.practicum.android.diploma.data.dto.components.IndustriesDto + +data class IndustriesResponse( + val sectors: List +) : Response() diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/RegionsRequest.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/RegionsRequest.kt new file mode 100644 index 00000000000..34264496d35 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/RegionsRequest.kt @@ -0,0 +1,5 @@ +package ru.practicum.android.diploma.data.dto + +data class RegionsRequest( + val id: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/RegionsResponse.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/RegionsResponse.kt new file mode 100644 index 00000000000..f0b7f18d5c8 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/RegionsResponse.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.data.dto + +import ru.practicum.android.diploma.data.dto.components.RegionDto + +data class RegionsResponse( + val regions: List +) : Response() diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/SaveFiltersSharedPrefsDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/SaveFiltersSharedPrefsDto.kt new file mode 100644 index 00000000000..abafc7688e9 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/SaveFiltersSharedPrefsDto.kt @@ -0,0 +1,18 @@ +package ru.practicum.android.diploma.data.dto + +import ru.practicum.android.diploma.data.dto.components.CountryDto +import ru.practicum.android.diploma.data.dto.components.IndustriesDto +import ru.practicum.android.diploma.data.dto.components.RegionDto +import java.io.Serializable + +data class SaveFiltersSharedPrefsDto( + val industries: IndustriesDto?, + val country: CountryDto?, + val region: RegionDto?, + val currency: Int?, + val noCurrency: Boolean? +) : Serializable { + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/components/CountryDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/CountryDto.kt new file mode 100644 index 00000000000..8e160e7a29d --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/CountryDto.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.data.dto.components + +data class CountryDto( + val id: String, + val name: String, + val areas: List +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/components/IndustriesDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/IndustriesDto.kt new file mode 100644 index 00000000000..73508742a9f --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/IndustriesDto.kt @@ -0,0 +1,6 @@ +package ru.practicum.android.diploma.data.dto.components + +data class IndustriesDto( + val id: String, + val name: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/components/RegionDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/RegionDto.kt new file mode 100644 index 00000000000..19f2dcaedc4 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/RegionDto.kt @@ -0,0 +1,9 @@ +package ru.practicum.android.diploma.data.dto.components + +import com.google.gson.annotations.SerializedName + +data class RegionDto( + val id: String, + val name: String, + @SerializedName("parent_id") val parentId: Int? +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/dto/components/RegionListDto.kt b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/RegionListDto.kt new file mode 100644 index 00000000000..d75f3c73654 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/dto/components/RegionListDto.kt @@ -0,0 +1,5 @@ +package ru.practicum.android.diploma.data.dto.components + +data class RegionListDto( + val areas: List? = null +) diff --git a/app/src/main/java/ru/practicum/android/diploma/data/impl/FilterRepositoryImpl.kt b/app/src/main/java/ru/practicum/android/diploma/data/impl/FilterRepositoryImpl.kt new file mode 100644 index 00000000000..45d46ed72c1 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/impl/FilterRepositoryImpl.kt @@ -0,0 +1,81 @@ +package ru.practicum.android.diploma.data.impl + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import ru.practicum.android.diploma.data.dto.CountriesRequest +import ru.practicum.android.diploma.data.dto.CountriesResponse +import ru.practicum.android.diploma.data.dto.IndustriesRequest +import ru.practicum.android.diploma.data.dto.IndustriesResponse +import ru.practicum.android.diploma.data.dto.RESULT_CODE_BAD_REQUEST +import ru.practicum.android.diploma.data.dto.RESULT_CODE_NO_INTERNET +import ru.practicum.android.diploma.data.dto.RegionsRequest +import ru.practicum.android.diploma.data.dto.RegionsResponse +import ru.practicum.android.diploma.data.dto.Response +import ru.practicum.android.diploma.data.dto.toAllRegions +import ru.practicum.android.diploma.data.dto.toCountryList +import ru.practicum.android.diploma.data.dto.toRegionList +import ru.practicum.android.diploma.data.dto.toSectorList +import ru.practicum.android.diploma.data.network.NetworkClient +import ru.practicum.android.diploma.domain.api.FilterRepository +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Industries +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.util.ResponseData + +class FilterRepositoryImpl( + private val networkClient: NetworkClient +) : FilterRepository { + + override fun getCountries(): Flow>> = flow { + when (val response = networkClient.doRequest(CountriesRequest)) { + is CountriesResponse -> { + val countriesList = response.countries.toCountryList() + emit(ResponseData.Data(countriesList)) + } + + else -> emit(responseToError(response)) + } + } + + override fun getRegions(id: String): Flow>> = flow { + when (val response = networkClient.doRequest(RegionsRequest(id))) { + is RegionsResponse -> { + val regionsList = response.regions.toRegionList() + emit(ResponseData.Data(regionsList)) + } + + else -> emit(responseToError(response)) + } + } + + override fun getAllRegions(): Flow>> = flow { + when (val response = networkClient.doRequest(CountriesRequest)) { + is CountriesResponse -> { + val regionsList = response.countries.toAllRegions() + emit(ResponseData.Data(regionsList)) + } + + else -> emit(responseToError(response)) + } + } + + override fun getIndustries(): Flow>> = flow { + when (val response = networkClient.doRequest(IndustriesRequest)) { + is IndustriesResponse -> { + val sectorsList = response.sectors.toSectorList() + emit(ResponseData.Data(sectorsList)) + } + + else -> emit(responseToError(response)) + } + } + + private fun responseToError(response: Response): ResponseData = + ResponseData.Error( + when (response.resultCode) { + RESULT_CODE_NO_INTERNET -> ResponseData.ResponseError.NO_INTERNET + RESULT_CODE_BAD_REQUEST -> ResponseData.ResponseError.CLIENT_ERROR + else -> ResponseData.ResponseError.SERVER_ERROR + } + ) +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/impl/SharedPrefsRepositoryImpl.kt b/app/src/main/java/ru/practicum/android/diploma/data/impl/SharedPrefsRepositoryImpl.kt new file mode 100644 index 00000000000..b387a57eaad --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/data/impl/SharedPrefsRepositoryImpl.kt @@ -0,0 +1,43 @@ +package ru.practicum.android.diploma.data.impl + +import android.content.SharedPreferences +import com.google.gson.Gson +import ru.practicum.android.diploma.domain.SharedPrefsRepository +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs + +class SharedPrefsRepositoryImpl( + private val sharedPreferences: SharedPreferences, + private val gson: Gson +) : SharedPrefsRepository { + override suspend fun readSharedPrefs(): SaveFiltersSharedPrefs? { + val json = sharedPreferences.getString(HISTORY, null) ?: return null + /*SaveFiltersSharedPrefs( + Industries("", "", false), + Country("", ""), + Region("", "", null), + "", + false + )*/ + return gson.fromJson(json, SaveFiltersSharedPrefs::class.java) + } + + override suspend fun writeSharedPrefs(filters: SaveFiltersSharedPrefs) { + /* val oldShared = readSharedPrefs() + val newShared = oldShared.copy( + industries = filters.industries ?: oldShared.industries, + country = filters.country ?: oldShared.country, + region = filters.region ?: oldShared.region, + currency = filters.currency ?: oldShared.currency, + noCurrency = filters.noCurrency + )*/ + sharedPreferences.edit().putString(HISTORY, gson.toJson(filters)).apply() + } + + override suspend fun clearSharedPrefs() { + sharedPreferences.edit().remove(HISTORY).apply() + } + + companion object { + private const val HISTORY = "history" + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt index d236ec4b2df..ba5fa18491c 100644 --- a/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt +++ b/app/src/main/java/ru/practicum/android/diploma/data/network/HHApiService.kt @@ -1,10 +1,14 @@ package ru.practicum.android.diploma.data.network +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.QueryMap import ru.practicum.android.diploma.data.dto.SearchResponse import ru.practicum.android.diploma.data.dto.VacancyResponse +import ru.practicum.android.diploma.data.dto.components.CountryDto +import ru.practicum.android.diploma.data.dto.components.IndustriesDto +import ru.practicum.android.diploma.data.dto.components.RegionListDto interface HHApiService { @GET("vacancies/{vacancy_id}") @@ -12,4 +16,13 @@ interface HHApiService { @GET("vacancies") suspend fun searchVacancies(@QueryMap options: Map): SearchResponse + + @GET("areas") + suspend fun getCountries(): Response> + + @GET("areas/{area_id}") + suspend fun getRegions(@Path("area_id") areaId: String): Response + + @GET("industries") + suspend fun getIndustries(): Response> } diff --git a/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt b/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt index 3ddccf97473..ed7da7ddc74 100644 --- a/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt +++ b/app/src/main/java/ru/practicum/android/diploma/data/network/RetrofitNetworkClient.kt @@ -4,12 +4,18 @@ import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import retrofit2.HttpException +import ru.practicum.android.diploma.data.dto.CountriesRequest +import ru.practicum.android.diploma.data.dto.CountriesResponse import ru.practicum.android.diploma.data.dto.RESULT_CODE_BAD_REQUEST import ru.practicum.android.diploma.data.dto.RESULT_CODE_NO_INTERNET import ru.practicum.android.diploma.data.dto.RESULT_CODE_SERVER_ERROR import ru.practicum.android.diploma.data.dto.RESULT_CODE_SUCCESS +import ru.practicum.android.diploma.data.dto.RegionsRequest +import ru.practicum.android.diploma.data.dto.RegionsResponse import ru.practicum.android.diploma.data.dto.Response import ru.practicum.android.diploma.data.dto.SearchRequest +import ru.practicum.android.diploma.data.dto.IndustriesRequest +import ru.practicum.android.diploma.data.dto.IndustriesResponse import ru.practicum.android.diploma.data.dto.VacancyRequest import ru.practicum.android.diploma.util.isInternetAvailable @@ -21,24 +27,20 @@ class RetrofitNetworkClient( override suspend fun doRequest(dto: Any): Response { if (!isInternetAvailable(context)) { return Response().apply { resultCode = RESULT_CODE_NO_INTERNET } + } - return getResponse(dto = dto) + return getNetworkResponse(dto = dto) } - private suspend fun getResponse(dto: Any): Response { + private suspend fun getNetworkResponse(dto: Any): Response { return withContext(Dispatchers.IO) { try { when (dto) { - is VacancyRequest -> { - val response = hhApiService.getVacancy(dto.id) - response.apply { resultCode = RESULT_CODE_SUCCESS } - } - - is SearchRequest -> { - val response = hhApiService.searchVacancies(options = dto.options) - response.apply { resultCode = RESULT_CODE_SUCCESS } - } - + is VacancyRequest -> getVacancyResponse(dto) + is SearchRequest -> getSearchResponse(dto) + is CountriesRequest -> getCountriesResponse() + is RegionsRequest -> getRegionsResponse(dto) + is IndustriesRequest -> getIndustriesResponse() else -> { Response().apply { resultCode = RESULT_CODE_BAD_REQUEST } } @@ -51,4 +53,47 @@ class RetrofitNetworkClient( } } + private suspend fun getVacancyResponse(dto: VacancyRequest): Response { + val response = hhApiService.getVacancy(dto.id) + return response.apply { resultCode = RESULT_CODE_SUCCESS } + } + + private suspend fun getSearchResponse(dto: SearchRequest): Response { + val response = hhApiService.searchVacancies(options = dto.options) + return response.apply { resultCode = RESULT_CODE_SUCCESS } + } + + private suspend fun getCountriesResponse(): Response { + val networkResponse = hhApiService.getCountries() + if (networkResponse.isSuccessful) { + val countriesResponse = CountriesResponse(networkResponse.body() ?: emptyList()) + countriesResponse.resultCode = networkResponse.code() + return countriesResponse + } else { + return Response().apply { resultCode = networkResponse.code() } + } + } + + private suspend fun getRegionsResponse(dto: RegionsRequest): Response { + val networkResponse = hhApiService.getRegions(dto.id) + if (networkResponse.isSuccessful) { + val regionsResponse = RegionsResponse(networkResponse.body()?.areas ?: emptyList()) + regionsResponse.resultCode = networkResponse.code() + return regionsResponse + } else { + return Response().apply { resultCode = networkResponse.code() } + } + } + + private suspend fun getIndustriesResponse(): Response { + val networkResponse = hhApiService.getIndustries() + if (networkResponse.isSuccessful) { + val sectorsResponse = IndustriesResponse(networkResponse.body() ?: emptyList()) + sectorsResponse.resultCode = networkResponse.code() + return sectorsResponse + } else { + return Response().apply { resultCode = networkResponse.code() } + } + } + } diff --git a/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt index ba8cd87dd38..b61c19c4386 100644 --- a/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt +++ b/app/src/main/java/ru/practicum/android/diploma/di/DataModule.kt @@ -1,6 +1,8 @@ package ru.practicum.android.diploma.di +import android.content.Context import androidx.room.Room +import com.google.gson.Gson import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -15,6 +17,7 @@ import ru.practicum.android.diploma.data.network.HHApiService import ru.practicum.android.diploma.data.network.NetworkClient import ru.practicum.android.diploma.data.network.RetrofitNetworkClient +const val HISTORY_MAIN = "historyMain" const val BASE_URL = "https://api.hh.ru/" val dataModule = module { factory { @@ -51,4 +54,15 @@ val dataModule = module { ConverterIntoEntity() } + single { + Gson() + } + + single { + androidContext().getSharedPreferences( + HISTORY_MAIN, + Context.MODE_PRIVATE + ) + } + } diff --git a/app/src/main/java/ru/practicum/android/diploma/di/InteractorModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/InteractorModule.kt index 9707ef5cdda..afb21a09e38 100644 --- a/app/src/main/java/ru/practicum/android/diploma/di/InteractorModule.kt +++ b/app/src/main/java/ru/practicum/android/diploma/di/InteractorModule.kt @@ -2,10 +2,12 @@ package ru.practicum.android.diploma.di import org.koin.dsl.module import ru.practicum.android.diploma.domain.DetailsInteractor -import ru.practicum.android.diploma.domain.SearchInteractor import ru.practicum.android.diploma.domain.FavouriteVacanciesInteractor -import ru.practicum.android.diploma.domain.impl.FavouriteVacanciesInteractorImpl +import ru.practicum.android.diploma.domain.FilterInteractor +import ru.practicum.android.diploma.domain.SearchInteractor import ru.practicum.android.diploma.domain.impl.DetailsInteractorImpl +import ru.practicum.android.diploma.domain.impl.FavouriteVacanciesInteractorImpl +import ru.practicum.android.diploma.domain.impl.FilterInteractorImpl import ru.practicum.android.diploma.domain.impl.SearchInteractorImpl val interactorModule = module { @@ -20,4 +22,11 @@ val interactorModule = module { factory { DetailsInteractorImpl(repository = get()) } + + factory { + FilterInteractorImpl( + repository = get(), + repositorySharePrefs = get() + ) + } } diff --git a/app/src/main/java/ru/practicum/android/diploma/di/RepositoryModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/RepositoryModule.kt index 09ed2ec6559..cca031e9f95 100644 --- a/app/src/main/java/ru/practicum/android/diploma/di/RepositoryModule.kt +++ b/app/src/main/java/ru/practicum/android/diploma/di/RepositoryModule.kt @@ -2,10 +2,14 @@ package ru.practicum.android.diploma.di import org.koin.dsl.module import ru.practicum.android.diploma.data.impl.FavouriteVacanciesRepositoryImpl +import ru.practicum.android.diploma.data.impl.FilterRepositoryImpl import ru.practicum.android.diploma.data.impl.SearchRepositoryImpl +import ru.practicum.android.diploma.data.impl.SharedPrefsRepositoryImpl import ru.practicum.android.diploma.data.impl.VacancyRepositoryImpl -import ru.practicum.android.diploma.domain.api.SearchRepository import ru.practicum.android.diploma.domain.FavouriteVacanciesRepository +import ru.practicum.android.diploma.domain.SharedPrefsRepository +import ru.practicum.android.diploma.domain.api.FilterRepository +import ru.practicum.android.diploma.domain.api.SearchRepository import ru.practicum.android.diploma.domain.api.VacancyRepository val repositoryModule = module { @@ -23,4 +27,14 @@ val repositoryModule = module { single { VacancyRepositoryImpl(get(), get()) } + single { + FilterRepositoryImpl(networkClient = get()) + } + + single { + SharedPrefsRepositoryImpl( + gson = get(), + sharedPreferences = get() + ) + } } diff --git a/app/src/main/java/ru/practicum/android/diploma/di/ViewModelModule.kt b/app/src/main/java/ru/practicum/android/diploma/di/ViewModelModule.kt index dc3c1f0fd64..bacaa315f1e 100644 --- a/app/src/main/java/ru/practicum/android/diploma/di/ViewModelModule.kt +++ b/app/src/main/java/ru/practicum/android/diploma/di/ViewModelModule.kt @@ -2,6 +2,11 @@ package ru.practicum.android.diploma.di import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module +import ru.practicum.android.diploma.presentation.viewmodels.FilterCountryViewModel +import ru.practicum.android.diploma.presentation.viewmodels.FilterIndustryViewModel +import ru.practicum.android.diploma.presentation.viewmodels.FilterPlaceOfWorkViewModel +import ru.practicum.android.diploma.presentation.viewmodels.FilterRegionViewModel +import ru.practicum.android.diploma.presentation.viewmodels.FilterViewModel import ru.practicum.android.diploma.presentation.viewmodels.SearchViewModel import ru.practicum.android.diploma.presentation.viewmodels.VacancyViewModel import ru.practicum.android.diploma.presentation.viewmodels.favourites.FavouritesFragmentViewModel @@ -12,10 +17,26 @@ val viewModelModule = module { } viewModel { - SearchViewModel(get()) + SearchViewModel(get(), get()) } viewModel { VacancyViewModel(get(), get()) } + + viewModel { + FilterViewModel(get()) + } + viewModel { + FilterPlaceOfWorkViewModel(get()) + } + viewModel { + FilterCountryViewModel(get()) + } + viewModel { + FilterRegionViewModel(get()) + } + viewModel { + FilterIndustryViewModel(get()) + } } diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/FilterInteractor.kt b/app/src/main/java/ru/practicum/android/diploma/domain/FilterInteractor.kt new file mode 100644 index 00000000000..8ee7daa7bff --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/FilterInteractor.kt @@ -0,0 +1,18 @@ +package ru.practicum.android.diploma.domain + +import kotlinx.coroutines.flow.Flow +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Industries +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs +import ru.practicum.android.diploma.util.ResponseData + +interface FilterInteractor { + suspend fun getCountries(): Flow>> + suspend fun getRegions(id: String): Flow>> + suspend fun getAllRegions(): Flow>> + suspend fun getIndustries(): Flow>> + suspend fun readSharedPrefs(): SaveFiltersSharedPrefs? + suspend fun writeSharedPrefs(filters: SaveFiltersSharedPrefs) + suspend fun clearSharedPrefs() +} diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/SharedPrefsRepository.kt b/app/src/main/java/ru/practicum/android/diploma/domain/SharedPrefsRepository.kt new file mode 100644 index 00000000000..05f53525d5c --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/SharedPrefsRepository.kt @@ -0,0 +1,9 @@ +package ru.practicum.android.diploma.domain + +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs + +interface SharedPrefsRepository { + suspend fun readSharedPrefs(): SaveFiltersSharedPrefs? + suspend fun writeSharedPrefs(filters: SaveFiltersSharedPrefs) + suspend fun clearSharedPrefs() +} diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/api/FilterRepository.kt b/app/src/main/java/ru/practicum/android/diploma/domain/api/FilterRepository.kt new file mode 100644 index 00000000000..cd3ed68a36b --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/api/FilterRepository.kt @@ -0,0 +1,14 @@ +package ru.practicum.android.diploma.domain.api + +import kotlinx.coroutines.flow.Flow +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Industries +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.util.ResponseData + +interface FilterRepository { + fun getCountries(): Flow>> + fun getRegions(id: String): Flow>> + fun getAllRegions(): Flow>> + fun getIndustries(): Flow>> +} diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/impl/FilterInteractorImpl.kt b/app/src/main/java/ru/practicum/android/diploma/domain/impl/FilterInteractorImpl.kt new file mode 100644 index 00000000000..e00473d73d5 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/impl/FilterInteractorImpl.kt @@ -0,0 +1,44 @@ +package ru.practicum.android.diploma.domain.impl + +import kotlinx.coroutines.flow.Flow +import ru.practicum.android.diploma.domain.FilterInteractor +import ru.practicum.android.diploma.domain.SharedPrefsRepository +import ru.practicum.android.diploma.domain.api.FilterRepository +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Industries +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs +import ru.practicum.android.diploma.util.ResponseData + +class FilterInteractorImpl( + private val repository: FilterRepository, + private val repositorySharePrefs: SharedPrefsRepository +) : FilterInteractor { + override suspend fun getCountries(): Flow>> { + return repository.getCountries() + } + + override suspend fun getRegions(id: String): Flow>> { + return repository.getRegions(id) + } + + override suspend fun getAllRegions(): Flow>> { + return repository.getAllRegions() + } + + override suspend fun getIndustries(): Flow>> { + return repository.getIndustries() + } + + override suspend fun readSharedPrefs(): SaveFiltersSharedPrefs? { + return repositorySharePrefs.readSharedPrefs() + } + + override suspend fun writeSharedPrefs(filters: SaveFiltersSharedPrefs) { + repositorySharePrefs.writeSharedPrefs(filters) + } + + override suspend fun clearSharedPrefs() { + repositorySharePrefs.clearSharedPrefs() + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/Country.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/Country.kt new file mode 100644 index 00000000000..c108c31c404 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/Country.kt @@ -0,0 +1,6 @@ +package ru.practicum.android.diploma.domain.models + +data class Country( + val id: String, + val name: String +) diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/Industries.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/Industries.kt new file mode 100644 index 00000000000..ee9ab4bcb3e --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/Industries.kt @@ -0,0 +1,11 @@ +package ru.practicum.android.diploma.domain.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Industries( + val id: String, + val name: String, + val isChecked: Boolean +) : Parcelable diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/Region.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/Region.kt new file mode 100644 index 00000000000..da3ca4b0f9b --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/Region.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.domain.models + +data class Region( + val id: String, + val name: String, + val parentId: Int? +) diff --git a/app/src/main/java/ru/practicum/android/diploma/domain/models/SaveFiltersSharedPrefs.kt b/app/src/main/java/ru/practicum/android/diploma/domain/models/SaveFiltersSharedPrefs.kt new file mode 100644 index 00000000000..e1ef6b3fa23 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/domain/models/SaveFiltersSharedPrefs.kt @@ -0,0 +1,9 @@ +package ru.practicum.android.diploma.domain.models + +data class SaveFiltersSharedPrefs( + val industries: Industries?, + val country: Country?, + val region: Region?, + val currency: String?, + val noCurrency: Boolean +) diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterCountryViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterCountryViewModel.kt new file mode 100644 index 00000000000..b9fe4466dd9 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterCountryViewModel.kt @@ -0,0 +1,44 @@ +package ru.practicum.android.diploma.presentation.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import ru.practicum.android.diploma.domain.FilterInteractor +import ru.practicum.android.diploma.ui.state.CountriesScreenState +import ru.practicum.android.diploma.util.ResponseData + +class FilterCountryViewModel( + private val filterInteractor: FilterInteractor +) : ViewModel() { + private val countriesStateLiveData = MutableLiveData() + + fun render(): LiveData { + return countriesStateLiveData + } + + fun getCountries() { + viewModelScope.launch(Dispatchers.IO) { + setState(CountriesScreenState.Loading) + filterInteractor + .getCountries() + .collect { response -> + when (response) { + is ResponseData.Data -> { + val listOfCountries = response.value.sortedBy { it.id } + setState(CountriesScreenState.Success(listOfCountries)) + } + is ResponseData.Error -> { + setState(CountriesScreenState.Error(response.error)) + } + } + } + } + } + + private fun setState(state: CountriesScreenState) { + countriesStateLiveData.postValue(state) + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterIndustryViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterIndustryViewModel.kt new file mode 100644 index 00000000000..04dbee67d9d --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterIndustryViewModel.kt @@ -0,0 +1,111 @@ +package ru.practicum.android.diploma.presentation.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import ru.practicum.android.diploma.domain.FilterInteractor +import ru.practicum.android.diploma.domain.models.Industries +import ru.practicum.android.diploma.util.ResponseData +import ru.practicum.android.diploma.util.ResponseData.ResponseError + +class FilterIndustryViewModel( + private val interactor: FilterInteractor +) : ViewModel() { + private val _industries = MutableLiveData>() + private val listOfIndustries = mutableListOf() + private var selectedId = "" + val industries: LiveData> + get() = _industries + + private val _hasSelected = MutableLiveData(false) + val hasSelected: LiveData + get() = _hasSelected + + private val _selectedIndustry = MutableLiveData() + val selectedIndustry: LiveData + get() = _selectedIndustry + private val _error = MutableLiveData() + val error: LiveData + get() = _error + + fun itemChecked(industries: Industries) { + val newList = ArrayList() + _industries.value?.forEach { industry -> + if (industry.id == industries.id) { + newList.add(industry.copy(isChecked = !industry.isChecked)) + _hasSelected.postValue(!industry.isChecked) + if (!industry.isChecked) _selectedIndustry.postValue(industry) + } else { + newList.add(industry.copy(isChecked = false)) + } + } + _industries.postValue(newList) + } + + fun updateListIndustries() { + viewModelScope.launch(Dispatchers.IO) { + val industry = interactor.readSharedPrefs()?.industries + interactor.getIndustries().collect { list -> + when (list) { + is ResponseData.Data -> { + whenList(industry, list) + listOfIndustries.clear() + listOfIndustries.addAll(list.value) + } + + is ResponseData.Error -> { + _error.postValue(list.error) + } + } + } + } + } + + private fun whenList(industry: Industries?, list: ResponseData.Data>) { + if (industry != null) { + val newList = ArrayList() + list.value.forEach { industryItem -> + if (industryItem.id == industry.id) { + newList.add(industryItem.copy(isChecked = true)) + _selectedIndustry.postValue(industryItem) + _hasSelected.postValue(true) + selectedId = industryItem.id + } else { + newList.add(industryItem) + } + } + _industries.postValue(newList) + } else { + _industries.postValue(list.value) + } + } + + fun search(request: String) { + val sortedList = mutableListOf() + val newSortedList = mutableListOf() + sortedList.addAll(listOfIndustries) + sortedList.removeAll { + !it.name.contains(request, true) + } + sortedList.forEach { + if (it.id == selectedId) { + newSortedList.add(Industries(it.id, it.name, true)) + _selectedIndustry.postValue(it) + _hasSelected.postValue(true) + } else { + newSortedList.add(it) + } + } + if (sortedList.isNotEmpty()) { + _industries.postValue(newSortedList) + } else { + _industries.postValue(newSortedList) + _error.postValue(ResponseError.NOT_FOUND) + } + + } +} + diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterPlaceOfWorkViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterPlaceOfWorkViewModel.kt new file mode 100644 index 00000000000..dcac3e39979 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterPlaceOfWorkViewModel.kt @@ -0,0 +1,81 @@ +package ru.practicum.android.diploma.presentation.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import ru.practicum.android.diploma.domain.FilterInteractor +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.ui.state.PlaceOfWorkScreenState +import ru.practicum.android.diploma.util.ResponseData + +class FilterPlaceOfWorkViewModel( + private val filterInteractor: FilterInteractor +) : ViewModel() { + + private val screenStateLiveData = MutableLiveData() + + fun render(): LiveData { + return screenStateLiveData + } + + fun getCountryName(region: Region, isSaving: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + filterInteractor + .getCountries() + .collect { response -> + when (response) { + is ResponseData.Data -> { + response.value.forEach { + if (it.id == region.parentId.toString()) { + setData(isSaving, it, region) + } + } + } + + is ResponseData.Error -> {} + } + } + } + } + + fun saveFields(country: Country, region: Region) { + if (country.name.isEmpty()) { + getCountryName(region, true) + } else { + setState(PlaceOfWorkScreenState.Saved(country, region)) + } + } + + private fun setData(isSaving: Boolean, country: Country, region: Region) { + if (isSaving) { + setState(PlaceOfWorkScreenState.Saved(country, region)) + } else { + setState(PlaceOfWorkScreenState.CountryName(country)) + } + } + + fun setCountryName(country: Country) { + if (country.name.isNotEmpty()) { + setState(PlaceOfWorkScreenState.CountryName(country)) + } else { + setState(PlaceOfWorkScreenState.NoCountryName) + } + } + + fun setRegionName(region: Region) { + if (region.name.isNotEmpty()) { + setState(PlaceOfWorkScreenState.RegionName(region)) + } else { + setState(PlaceOfWorkScreenState.NoRegionName) + } + } + + private fun setState(state: PlaceOfWorkScreenState) { + screenStateLiveData.postValue(state) + } + +} diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterRegionViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterRegionViewModel.kt new file mode 100644 index 00000000000..285f3a3379b --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterRegionViewModel.kt @@ -0,0 +1,78 @@ +package ru.practicum.android.diploma.presentation.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import ru.practicum.android.diploma.domain.FilterInteractor +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.ui.state.RegionsScreenState +import ru.practicum.android.diploma.util.ResponseData + +class FilterRegionViewModel( + private val filterInteractor: FilterInteractor +) : ViewModel() { + + private val regionsStateLiveData = MutableLiveData() + private val listOfRegions = mutableListOf() + + fun render(): LiveData { + return regionsStateLiveData + } + + private fun setState(state: RegionsScreenState) { + regionsStateLiveData.postValue(state) + } + + fun search(query: String) { + val sortedList = mutableListOf() + sortedList.addAll(listOfRegions) + sortedList.removeAll { + !it.name.contains(query, true) + } + if (sortedList.isNotEmpty()) { + setState(RegionsScreenState.Success(sortedList)) + } else { + setState(RegionsScreenState.Error(ResponseData.ResponseError.NOT_FOUND)) + } + } + + fun getRegions(regionId: String) { + setState(RegionsScreenState.Loading) + viewModelScope.launch(Dispatchers.IO) { + if (regionId.isNotEmpty()) { + filterInteractor + .getRegions(regionId) + .collect { response -> + when (response) { + is ResponseData.Data -> { + listOfRegions.addAll(response.value.sortedBy { it.name }) + setState(RegionsScreenState.Success(listOfRegions)) + } + + is ResponseData.Error -> { + setState(RegionsScreenState.Error(response.error)) + } + } + } + } else { + filterInteractor + .getAllRegions() + .collect { response -> + when (response) { + is ResponseData.Data -> { + listOfRegions.addAll(response.value.sortedBy { it.name }) + setState(RegionsScreenState.Success(listOfRegions)) + } + + is ResponseData.Error -> { + setState(RegionsScreenState.Error(response.error)) + } + } + } + } + } + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterViewModel.kt new file mode 100644 index 00000000000..909556fa8f9 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/FilterViewModel.kt @@ -0,0 +1,144 @@ +package ru.practicum.android.diploma.presentation.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import ru.practicum.android.diploma.domain.FilterInteractor +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Industries +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs +import ru.practicum.android.diploma.ui.state.FilterScreenState + +class FilterViewModel( + private val filterInteractor: FilterInteractor +) : ViewModel() { + + private val screenStateLiveData = MutableLiveData() + + private val _noCurrency = MutableLiveData() + private val _industry = MutableLiveData() + private var region = Region("", "", null) + private var country = Country("", "") + private var industries = Industries("", "", false) + private var saveFiltersSharedPrefs = SaveFiltersSharedPrefs( + Industries("", "", false), + Country("", ""), + Region("", "", null), + "", + false + ) + + fun render(): LiveData { + return screenStateLiveData + } + + fun saveFilterAndClose(filter: SaveFiltersSharedPrefs) { + viewModelScope.launch(Dispatchers.IO) { + filterInteractor.writeSharedPrefs(filter) + setState(FilterScreenState.FiltersSaved(filter)) + } + } + + fun saveFilterSettings(filter: SaveFiltersSharedPrefs) { + viewModelScope.launch(Dispatchers.IO) { + filterInteractor.writeSharedPrefs(filter) + } + } + + fun getPrefs(): SaveFiltersSharedPrefs { + return saveFiltersSharedPrefs + } + + fun getRegion(): Region { + return region + } + + fun getCountry(): Country { + return country + } + + fun getIndustry(): Industries { + return industries + } + + fun setRegion(region: Region) { + this.region = region + } + + fun setCountry(country: Country) { + this.country = country + } + + fun setIndustry(industries: Industries) { + this.industries = industries + } + + fun getFilterSettings() { + viewModelScope.launch(Dispatchers.IO) { + val filters = filterInteractor.readSharedPrefs() + + if (filters != null) { + saveFiltersSharedPrefs = filters + if (filters.region != null && filters.region.id.isNotEmpty()) { + region = filters.region + } + if (filters.country != null && filters.country.id.isNotEmpty()) { + country = filters.country + } + if (filters.industries != null && filters.industries.id.isNotEmpty()) { + industries = filters.industries + } + setState(FilterScreenState.FiltersLoaded(filters)) + } + } + } + + fun clear() { + viewModelScope.launch(Dispatchers.IO) { + filterInteractor.clearSharedPrefs() + industries = Industries("", "", false) + country = Country("", "") + region = Region("", "", null) + saveFiltersSharedPrefs = SaveFiltersSharedPrefs( + industries, + country, + region, + "", + false + ) + setState(FilterScreenState.ClearState) + } + } + + fun setIndustryName(industry: String) { + if (industry.isNotEmpty()) { + setState(FilterScreenState.Industry(industry)) + } else { + setState(FilterScreenState.NoIndustry) + } + } + + private fun setState(state: FilterScreenState) { + screenStateLiveData.postValue(state) + } + + fun setPlaceOfWork(country: String) { + if (country.isNotEmpty()) { + setState(FilterScreenState.PlaceOfWork(country)) + } else { + setState(FilterScreenState.NoPlaceOfWork) + } + } + + fun setNoCurrencySelected(answer: Boolean) { + _noCurrency.postValue(answer) + } + + fun setIndustrySelected(industries: Industries?) { + _industry.postValue(industries) + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/SearchViewModel.kt b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/SearchViewModel.kt index a4973c8323d..6af0645f8e6 100644 --- a/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/SearchViewModel.kt +++ b/app/src/main/java/ru/practicum/android/diploma/presentation/viewmodels/SearchViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import ru.practicum.android.diploma.domain.FilterInteractor import ru.practicum.android.diploma.domain.SearchInteractor import ru.practicum.android.diploma.domain.models.Vacancy import ru.practicum.android.diploma.ui.state.SearchScreenState @@ -14,7 +15,8 @@ import ru.practicum.android.diploma.util.ResponseData import ru.practicum.android.diploma.util.debounce class SearchViewModel( - private val searchInteractor: SearchInteractor + private val searchInteractor: SearchInteractor, + private val filterInteractor: FilterInteractor ) : ViewModel() { companion object { const val ITEMS_PER_PAGE = 20 @@ -23,12 +25,21 @@ class SearchViewModel( const val TWO_SECONDS = 2000L } - private val screenStateLiveData = MutableLiveData(SearchScreenState.Default) + private val screenStateLiveData = MutableLiveData() private var currentPage = 0 private var maxPages = 1 private var isNextPageLoading = false private var mainRequest = "" private var requestNextPage = "" + private var options = Options( + requestNextPage, + ITEMS_PER_PAGE, + currentPage, + "", + "", + "", + false + ) private val _vacancyIsClickable = MutableLiveData(true) var vacancyIsClickable: LiveData = _vacancyIsClickable @@ -54,8 +65,8 @@ class SearchViewModel( this.mainRequest = request } - fun onStart() { - setScreenState(SearchScreenState.Default) + fun getMainRequest(): String { + return mainRequest } private fun searchVacancies(request: String) { @@ -64,15 +75,63 @@ class SearchViewModel( setScreenState(SearchScreenState.Loading) this.mainRequest = request requestNextPage = request + getOptions() search(true) } } - private fun search(isNewRequest: Boolean) { + fun setDefaultCurrentPage() { + currentPage = 0 + } + + fun getOptions() { + viewModelScope.launch(Dispatchers.IO) { + val filter = filterInteractor.readSharedPrefs() + if (filter != null) { + options = Options( + requestNextPage, + ITEMS_PER_PAGE, + 0, + if (filter.region?.id?.isNotEmpty() == true) { + filter.region.id + } else { + filter.country?.id.toString() + }, + filter.industries?.id.toString(), + filter.currency.toString(), + filter.noCurrency + ) + setScreenState(SearchScreenState.Default) + } else { + options = Options( + requestNextPage, + ITEMS_PER_PAGE, + currentPage, + "", + "", + "", + false + ) + setScreenState(SearchScreenState.Default) + } + } + } + + fun search(isNewRequest: Boolean) { viewModelScope.launch(Dispatchers.IO) { searchInteractor - .search(Options(requestNextPage, ITEMS_PER_PAGE, currentPage)) + .search( + Options( + requestNextPage, + ITEMS_PER_PAGE, + currentPage, + options.area, + options.industry, + options.salary, + options.withSalary + ) + ) .collect { response -> when (response) { is ResponseData.Data -> { @@ -101,6 +160,13 @@ class SearchViewModel( } } + fun isFilter(): Boolean { + return options.area?.isNotEmpty() == true || + options.industry?.isNotEmpty() == true || + options.salary?.isNotEmpty() == true || + options.withSalary == true + } + fun onVacancyClicked() { vacancyOnClickDebounce(true) } diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterCountryFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterCountryFragment.kt new file mode 100644 index 00000000000..2cdff140f71 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterCountryFragment.kt @@ -0,0 +1,108 @@ +package ru.practicum.android.diploma.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.CreateMethod +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.gson.Gson +import org.koin.androidx.viewmodel.ext.android.viewModel +import ru.practicum.android.diploma.databinding.FragmentSelectCountryBinding +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.presentation.viewmodels.FilterCountryViewModel +import ru.practicum.android.diploma.ui.state.CountriesScreenState +import ru.practicum.android.diploma.util.COUNTRY_BUNDLE_KEY +import ru.practicum.android.diploma.util.COUNTRY_REQUEST_KEY +import ru.practicum.android.diploma.util.ResponseData +import ru.practicum.android.diploma.util.adapter.country.CountryAdapter + +class FilterCountryFragment : Fragment() { + private val binding: FragmentSelectCountryBinding by viewBinding(CreateMethod.INFLATE) + private val viewModel by viewModel() + + private val adapter = CountryAdapter { + onItemClicked(it) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.selectCountryToolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + viewModel.render().observe(viewLifecycleOwner) { state -> + when (state) { + CountriesScreenState.Default -> {} + is CountriesScreenState.Error -> { + stopProgressBar() + when (state.error) { + ResponseData.ResponseError.NO_INTERNET, + ResponseData.ResponseError.CLIENT_ERROR, + ResponseData.ResponseError.SERVER_ERROR, + ResponseData.ResponseError.NOT_FOUND -> { + setNoListState() + } + } + } + + CountriesScreenState.Loading -> { + removePlaceholders() + startProgressBar() + } + + is CountriesScreenState.Success -> { + stopProgressBar() + adapter.setCountries(state.regions) + } + } + + } + val countriesRecyclerView = binding.countryRecycleView + countriesRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + countriesRecyclerView.adapter = adapter + viewModel.getCountries() + + } + + private fun onItemClicked(country: Country) { + val json = Gson().toJson(country) + setFragmentResult(COUNTRY_REQUEST_KEY, bundleOf(COUNTRY_BUNDLE_KEY to json)) + findNavController().navigateUp() + } + + private fun startProgressBar() { + binding.progressBar.isVisible = true + } + + private fun stopProgressBar() { + binding.progressBar.isVisible = false + } + + private fun removePlaceholders() { + binding.placeholderGroup.isVisible = false + binding.noConnectionPlaceholder.isVisible = false + binding.noConnectionText.isVisible = false + binding.serverError.isVisible = false + binding.serverErrorText.isVisible = false + } + + private fun setNoListState() { + binding.placeholderGroup.isVisible = true + } + +} diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterFragment.kt index fe65c5b1f17..b0025eddba7 100644 --- a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterFragment.kt +++ b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterFragment.kt @@ -1,18 +1,46 @@ package ru.practicum.android.diploma.ui.fragments +import android.content.res.ColorStateList +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.setFragmentResultListener import androidx.navigation.fragment.findNavController import by.kirich1409.viewbindingdelegate.CreateMethod import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.koin.androidx.viewmodel.ext.android.viewModel +import ru.practicum.android.diploma.R import ru.practicum.android.diploma.databinding.FragmentFilterBinding +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Industries +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs +import ru.practicum.android.diploma.presentation.viewmodels.FilterViewModel +import ru.practicum.android.diploma.ui.fragments.FilterIndustryFragment.Companion.INDUSTRY_ITEM_KEY +import ru.practicum.android.diploma.ui.fragments.FilterIndustryFragment.Companion.INDUSTRY_KEY +import ru.practicum.android.diploma.ui.state.FilterScreenState +import ru.practicum.android.diploma.util.FILTER_BUNDLE_KEY +import ru.practicum.android.diploma.util.FILTER_REQUEST_KEY +import ru.practicum.android.diploma.util.FILTER_TO_PLACE_OF_WORK_COUNTRY_KEY +import ru.practicum.android.diploma.util.FILTER_TO_PLACE_OF_WORK_KEY +import ru.practicum.android.diploma.util.FILTER_TO_PLACE_OF_WORK_REGION_KEY +import ru.practicum.android.diploma.util.PLACE_OF_WORK_COUNTRY_KEY +import ru.practicum.android.diploma.util.PLACE_OF_WORK_KEY +import ru.practicum.android.diploma.util.PLACE_OF_WORK_REGION_KEY class FilterFragment : Fragment() { - private val binding: FragmentFilterBinding by viewBinding(CreateMethod.INFLATE) + private val viewModel by viewModel() + override fun onCreateView( inflater: LayoutInflater, @@ -25,9 +53,303 @@ class FilterFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.buttonBackToSearchFromFilter.setOnClickListener { - findNavController().navigateUp() + initButtonListeners() // Инициализация слушателей для кнопок и других элементов интерфейса + initTextBehaviour() // Инициализация поведения текстовых полей + initResultListeners() + binding.salary.doOnTextChanged { _, _, _, _ -> + checkFields() + } + viewModel.render().observe(viewLifecycleOwner) { state -> + when (state) { + is FilterScreenState.PlaceOfWork -> { + binding.workTextInput.setText(state.countryName) + setCountryEndIcon() + checkFields() + } + + FilterScreenState.ClearState -> { + binding.workTextInput.text?.clear() + binding.industryTextInput.text?.clear() + binding.salary.text?.clear() + binding.salaryFlagCheckbox.isChecked = false + setNoIndustryEndIcon() + setNoCountryEndIcon() + checkFields() + setFragmentResult(FILTER_REQUEST_KEY, bundleOf()) + } + + is FilterScreenState.Industry -> { + binding.industryTextInput.setText(state.industry) + setIndustryEndIcon() + checkFields() + } + + FilterScreenState.NoIndustry -> { + binding.industryTextInput.text?.clear() + setNoIndustryEndIcon() + viewModel.setIndustry(Industries("", "", false)) + checkFields() + } + + FilterScreenState.NoPlaceOfWork -> { + binding.workTextInput.text?.clear() + setNoCountryEndIcon() + viewModel.setCountry(Country("", "")) + viewModel.setRegion(Region("", "", null)) + checkFields() + } + + is FilterScreenState.FiltersSaved -> { + val json = Gson().toJson(state.filters) + setFragmentResult(FILTER_REQUEST_KEY, bundleOf(FILTER_BUNDLE_KEY to json)) + findNavController().navigateUp() + } + + is FilterScreenState.FiltersLoaded -> { + binding.workTextInput.setText( + setPlaceOfWorkName( + state.filters.country?.name.toString(), + state.filters.region?.name.toString() + ) + ) + binding.industryTextInput.setText(state.filters.industries?.name) + binding.salary.setText(state.filters.currency) + binding.salaryFlagCheckbox.isChecked = state.filters.noCurrency + setCountryAndIndustriesFields() + } + } + } + viewModel.getFilterSettings() + } + + private fun checkFields() { + val newPrefs = makeFilterSettings() + val loadedPrefs = viewModel.getPrefs() + setResetButton() + if (checkPrefs(newPrefs, loadedPrefs)) { + setButtonsVisible() + } else { + setButtonsNotVisible() + } + } + + private fun setResetButton() { + binding.clearButton.isVisible = binding.workTextInput.text?.isNotEmpty() == true || + binding.industryTextInput.text?.isNotEmpty() == true || + binding.salary.text?.isNotEmpty() == true || + binding.salaryFlagCheckbox.isChecked + } + + private fun setCountryAndIndustriesFields() { + if (binding.workTextInput.text?.isNotEmpty() == true) { + setCountryEndIcon() + } + if (binding.industryTextInput.text?.isNotEmpty() == true) { + setIndustryEndIcon() + } + } + + private fun checkPrefs(newPrefs: SaveFiltersSharedPrefs, loadedPrefs: SaveFiltersSharedPrefs): Boolean { + return newPrefs.currency != loadedPrefs.currency || + newPrefs.noCurrency != loadedPrefs.noCurrency || + newPrefs.country?.id != loadedPrefs.country?.id || + newPrefs.region?.id != loadedPrefs.region?.id || + newPrefs.industries?.id != loadedPrefs.industries?.id + } + + private fun setButtonsVisible() { + binding.applyButton.isVisible = true + } + + private fun setButtonsNotVisible() { + binding.applyButton.isVisible = false + } + + private fun initResultListeners() { + setFragmentResultListener(PLACE_OF_WORK_KEY) { _, bundle -> + val countryJson = bundle.getString(PLACE_OF_WORK_COUNTRY_KEY).toString() + val type = object : TypeToken() {}.type + viewModel.setCountry(Gson().fromJson(countryJson, type)) + val regionJson = bundle.getString(PLACE_OF_WORK_REGION_KEY) + val typeRegion = object : TypeToken() {}.type + viewModel.setRegion(Gson().fromJson(regionJson, typeRegion)) + val country = viewModel.getCountry() + val region = viewModel.getRegion() + val countryName = setPlaceOfWorkName(country.name, region.name) + viewModel.setPlaceOfWork(countryName) + } + setFragmentResultListener(INDUSTRY_KEY) { _, bundle -> + val industryJson = bundle.getString(INDUSTRY_ITEM_KEY).toString() + val type = object : TypeToken() {}.type + val industry = Gson().fromJson(industryJson, type) + viewModel.setIndustry(industry) + viewModel.setIndustryName(industry.name) + viewModel.setIndustrySelected(industry) + } + } + + private fun setPlaceOfWorkName(countryName: String, regionName: String): String { + val placeOfWork = StringBuilder() + if (countryName.isNotEmpty()) { + placeOfWork.append(countryName) + if (regionName.isNotEmpty()) { + placeOfWork.append(", ").append(regionName) + } + } + return placeOfWork.toString() + } + + private fun initButtonListeners() { + binding.filterSettingsTitle.setNavigationOnClickListener { findNavController().navigateUp() } + binding.applyButton.setOnClickListener { saveFilterSettings() } + binding.clearButton.setOnClickListener { viewModel.clear() } + // Пример использования Checkbox, если включена опция показа только с зарплатой + binding.salaryFlagCheckbox.setOnCheckedChangeListener { _, isChecked -> // + viewModel.setNoCurrencySelected(isChecked) + checkFields() + } + binding.workTextInput.setOnClickListener { + navigateToPlaceOfWorkScreen() + } + binding.industryTextInput.setOnClickListener { + navigateToIndustryScreen() + } + } + + private fun initTextBehaviour() { + // initSalaryTextBehaviour() + initWorkAndIndustryTextBehaviour() + } + + private fun saveFilterSettings() { + viewModel.saveFilterAndClose(makeFilterSettings()) + } + + private fun makeFilterSettings(): SaveFiltersSharedPrefs { + val filter = SaveFiltersSharedPrefs( + viewModel.getIndustry(), + viewModel.getCountry(), + viewModel.getRegion(), + binding.salary.text.toString(), + binding.salaryFlagCheckbox.isChecked + ) + return filter + } + + override fun onStop() { + viewModel.saveFilterSettings(makeFilterSettings()) + super.onStop() + } + + // Настройка поведения полей workTextInput и industryTextInput при изменении текста + private fun initWorkAndIndustryTextBehaviour() { + // Настройка поведения поля workTextInput при изменении текста + binding.workTextInput.doOnTextChanged { text, _, _, _ -> + if (text.isNullOrEmpty()) { + updateWorkPlaceHintAppearance(false) // Если текст пустой, hint остаётся стандартным + } else { + updateWorkPlaceHintAppearance(true) // Если текст заполнен, hint уменьшается и меняет цвет + } + } + + // Настройка поведения поля industryTextInput при изменении текста + binding.industryTextInput.doOnTextChanged { text, _, _, _ -> + if (text.isNullOrEmpty()) { + updateIndustryHintAppearance(false) + } else { + updateIndustryHintAppearance(true) + } + } + } + + // Метод для обновления внешнего вида hint в зависимости от заполненности поля + private fun updateWorkPlaceHintAppearance(isFilled: Boolean) { + val isDarkTheme = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + if (isFilled) { + // Если поле заполнено, уменьшаем hint и меняем его цвет в зависимости от темы + binding.workPlaceLayout.setHintTextAppearance(R.style.text_12_regular_400) + binding.workPlaceLayout.defaultHintTextColor = ColorStateList.valueOf( + if (isDarkTheme) requireContext().getColor(R.color.white) else requireContext().getColor(R.color.black) + ) + } else { + // Если поле пустое, оставляем стандартный размер и серый цвет hint + binding.workPlaceLayout.setHintTextAppearance(R.style.text_16_regular_400) + binding.workPlaceLayout.defaultHintTextColor = + ColorStateList.valueOf(requireContext().getColor(R.color.gray)) + } + } + + // Метод для обновления внешнего вида hint в зависимости от заполненности поля industryTextInput + private fun updateIndustryHintAppearance(isFilled: Boolean) { + val isDarkTheme = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + if (isFilled) { + // Если поле заполнено, уменьшаем hint и меняем его цвет в зависимости от темы + binding.industryLayout.setHintTextAppearance(R.style.text_12_regular_400) + binding.industryLayout.defaultHintTextColor = ColorStateList.valueOf( + if (isDarkTheme) requireContext().getColor(R.color.white) else requireContext().getColor(R.color.black) + ) + } else { + // Если поле пустое, оставляем стандартный размер и серый цвет hint + binding.industryLayout.setHintTextAppearance(R.style.text_16_regular_400) + binding.industryLayout.defaultHintTextColor = + ColorStateList.valueOf(requireContext().getColor(R.color.gray)) + } + } + + private fun setNoCountryEndIcon() { + binding.workPlaceLayout.apply { + setEndIconDrawable(R.drawable.ic_arrow_forward_14px) + setEndIconOnClickListener { + navigateToPlaceOfWorkScreen() + } + } + } + + private fun setCountryEndIcon() { + binding.workPlaceLayout.apply { + setEndIconDrawable(R.drawable.ic_close_cross_14px) + setEndIconOnClickListener { + viewModel.setPlaceOfWork("") + } } } + private fun setNoIndustryEndIcon() { + binding.industryLayout.apply { + setEndIconDrawable(R.drawable.ic_arrow_forward_14px) + setEndIconOnClickListener { + navigateToIndustryScreen() + } + } + } + + private fun setIndustryEndIcon() { + binding.industryLayout.apply { + setEndIconDrawable(R.drawable.ic_close_cross_14px) + setEndIconOnClickListener { + viewModel.setIndustryName("") + } + } + } + + private fun navigateToPlaceOfWorkScreen() { + val jsonCountry = Gson().toJson(viewModel.getCountry()) + val jsonRegion = Gson().toJson(viewModel.getRegion()) + setFragmentResult( + FILTER_TO_PLACE_OF_WORK_KEY, + bundleOf( + FILTER_TO_PLACE_OF_WORK_COUNTRY_KEY to jsonCountry, + FILTER_TO_PLACE_OF_WORK_REGION_KEY to jsonRegion + ) + ) + findNavController().navigate(R.id.action_filterFragment_to_select_place_of_workFragment) + } + + private fun navigateToIndustryScreen() { + findNavController().navigate(R.id.action_filterFragment_to_select_industryFragment) + } } diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterIndustryFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterIndustryFragment.kt new file mode 100644 index 00000000000..a3c6e93ddab --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterIndustryFragment.kt @@ -0,0 +1,115 @@ +package ru.practicum.android.diploma.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.navigation.fragment.findNavController +import by.kirich1409.viewbindingdelegate.CreateMethod +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.gson.Gson +import org.koin.androidx.viewmodel.ext.android.viewModel +import ru.practicum.android.diploma.databinding.FragmentSelectIndustryBinding +import ru.practicum.android.diploma.presentation.viewmodels.FilterIndustryViewModel +import ru.practicum.android.diploma.util.ResponseData +import ru.practicum.android.diploma.util.adapter.industry.IndustryAdapter + +class FilterIndustryFragment : Fragment() { + + companion object { + const val INDUSTRY_KEY = "industryKey" + const val INDUSTRY_ITEM_KEY = "INDUSTRY_ITEM_KEY" + } + + private val binding: FragmentSelectIndustryBinding by viewBinding(CreateMethod.INFLATE) + private val viewModel: FilterIndustryViewModel by viewModel() + private val adapter by lazy { + IndustryAdapter( + onClick = { industryList -> + viewModel.itemChecked(industryList) + } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + observeViewModel() + setupSearchFunctionality() + setupToolbar() + setupApplyButton() + } + + private fun setupRecyclerView() { + viewModel.updateListIndustries() + binding.industryRecycleView.adapter = adapter + } + + private fun observeViewModel() { + viewModel.industries.observe(viewLifecycleOwner) { list -> + binding.placeholderGroup.isVisible = false + adapter.industry = list + } + + viewModel.error.observe(viewLifecycleOwner) { error -> + when (error) { + ResponseData.ResponseError.NO_INTERNET, + ResponseData.ResponseError.CLIENT_ERROR, + ResponseData.ResponseError.SERVER_ERROR, + ResponseData.ResponseError.NOT_FOUND -> { + binding.placeholderGroup.isVisible = true + } + + else -> {} + } + } + + viewModel.hasSelected.observe(viewLifecycleOwner) { + binding.applyButton.isVisible = it + } + } + + private fun setupSearchFunctionality() { + binding.industrySearchQuery.doOnTextChanged { text, _, _, _ -> + if (binding.industrySearchQuery.text.isNotEmpty()) { + binding.searchIconLoupe.isVisible = false + binding.clearCrossIc.isVisible = true + } else { + binding.searchIconLoupe.isVisible = true + binding.clearCrossIc.isVisible = false + } + viewModel.search(text.toString()) + } + + binding.clearCrossIc.setOnClickListener { + binding.industrySearchQuery.text.clear() + } + } + + private fun setupToolbar() { + binding.industryFilterToolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + } + + private fun setupApplyButton() { + binding.applyButton.setOnClickListener { + val json = Gson().toJson(viewModel.selectedIndustry.value) + setFragmentResult(INDUSTRY_KEY, bundleOf(INDUSTRY_ITEM_KEY to json)) + findNavController().popBackStack() + } + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterPlaceOfWorkFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterPlaceOfWorkFragment.kt new file mode 100644 index 00000000000..2e7cb82df48 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterPlaceOfWorkFragment.kt @@ -0,0 +1,268 @@ +package ru.practicum.android.diploma.ui.fragments + +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.setFragmentResultListener +import androidx.navigation.fragment.findNavController +import by.kirich1409.viewbindingdelegate.CreateMethod +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.koin.androidx.viewmodel.ext.android.viewModel +import ru.practicum.android.diploma.R +import ru.practicum.android.diploma.databinding.FragmentSelectPlaceOfWorkBinding +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.presentation.viewmodels.FilterPlaceOfWorkViewModel +import ru.practicum.android.diploma.ui.state.PlaceOfWorkScreenState +import ru.practicum.android.diploma.util.COUNTRY_BUNDLE_KEY +import ru.practicum.android.diploma.util.COUNTRY_REQUEST_KEY +import ru.practicum.android.diploma.util.FILTER_TO_PLACE_OF_WORK_COUNTRY_KEY +import ru.practicum.android.diploma.util.FILTER_TO_PLACE_OF_WORK_KEY +import ru.practicum.android.diploma.util.FILTER_TO_PLACE_OF_WORK_REGION_KEY +import ru.practicum.android.diploma.util.PLACE_OF_WORK_COUNTRY_KEY +import ru.practicum.android.diploma.util.PLACE_OF_WORK_KEY +import ru.practicum.android.diploma.util.PLACE_OF_WORK_REGION_KEY +import ru.practicum.android.diploma.util.REGION_BUNDLE_KEY +import ru.practicum.android.diploma.util.REGION_ID_KEY +import ru.practicum.android.diploma.util.REGION_REQUEST_KEY + +class FilterPlaceOfWorkFragment : Fragment() { + private val binding: FragmentSelectPlaceOfWorkBinding by viewBinding(CreateMethod.INFLATE) + private val viewModel by viewModel() + private var country = Country("", "") + private var region = Region("", "", null) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUI() + observeViewModel() + } + + private fun initUI() { + initButtonListeners() + initTextBehaviour() + initResultListeners() + } + + private fun observeViewModel() { + viewModel.render().observe(viewLifecycleOwner) { state -> + when (state) { + is PlaceOfWorkScreenState.CountryName -> handleCountryName(state) + PlaceOfWorkScreenState.NoCountryName -> handleNoCountryName() + PlaceOfWorkScreenState.NoRegionName -> handleNoRegionName() + is PlaceOfWorkScreenState.RegionName -> handleRegionName(state) + is PlaceOfWorkScreenState.Saved -> handleSavedState(state) + is PlaceOfWorkScreenState.Loaded -> handleLoadedState(state) + } + } + } + + private fun handleCountryName(state: PlaceOfWorkScreenState.CountryName) { + country = state.country + binding.countryTextInput.setText(state.country.name) + setCountryEndIcon() + setApplyButtonVisible() + } + + private fun handleNoCountryName() { + binding.countryTextInput.text?.clear() + country = Country("", "") + setNoCountryEndIcon() + checkFields() + } + + private fun handleNoRegionName() { + binding.regionTextInput.text?.clear() + region = Region("", "", null) + setNoRegionEndIcon() + checkFields() + } + + private fun handleRegionName(state: PlaceOfWorkScreenState.RegionName) { + region = state.region + binding.regionTextInput.setText(state.region.name) + setRegionEndIcon() + setApplyButtonVisible() + } + + private fun handleSavedState(state: PlaceOfWorkScreenState.Saved) { + val jsonCountry = Gson().toJson(state.country) + val jsonRegion = Gson().toJson(state.region) + setFragmentResult( + PLACE_OF_WORK_KEY, + bundleOf( + PLACE_OF_WORK_COUNTRY_KEY to jsonCountry, + PLACE_OF_WORK_REGION_KEY to jsonRegion, + ) + ) + findNavController().navigateUp() + } + + private fun handleLoadedState(state: PlaceOfWorkScreenState.Loaded) { + if (state.filters.country != null) { + country = state.filters.country + binding.countryTextInput.setText(state.filters.country.name) + setCountryEndIcon() + } + if (state.filters.region != null) { + region = state.filters.region + binding.regionTextInput.setText(state.filters.region.name) + setRegionEndIcon() + } + setApplyButtonVisible() + } + + private fun initResultListeners() { + setFragmentResultListener(COUNTRY_REQUEST_KEY) { _, bundle -> + val json = bundle.getString(COUNTRY_BUNDLE_KEY).toString() + val type = object : TypeToken() {}.type + country = Gson().fromJson(json, type) + viewModel.setCountryName(country) + setNoRegionEndIcon() + binding.regionTextInput.text?.clear() + } + + setFragmentResultListener(REGION_REQUEST_KEY) { _, bundle -> + val json = bundle.getString(REGION_BUNDLE_KEY).toString() + val type = object : TypeToken() {}.type + region = Gson().fromJson(json, type) + viewModel.getCountryName(region, false) + viewModel.setRegionName(region) + } + + setFragmentResultListener(FILTER_TO_PLACE_OF_WORK_KEY) { _, bundle -> + val json = bundle.getString(FILTER_TO_PLACE_OF_WORK_COUNTRY_KEY).toString() + val type = object : TypeToken() {}.type + country = Gson().fromJson(json, type) + val jsonRegion = bundle.getString(FILTER_TO_PLACE_OF_WORK_REGION_KEY).toString() + val typeRegion = object : TypeToken() {}.type + region = Gson().fromJson(jsonRegion, typeRegion) + handleLoadedFilters() + } + } + + private fun handleLoadedFilters() { + if (country.name.isNotEmpty()) { + binding.countryTextInput.setText(country.name) + setCountryEndIcon() + setApplyButtonVisible() + if (region.name.isNotEmpty()) { + binding.regionTextInput.setText(region.name) + setRegionEndIcon() + } + } + } + + private fun initButtonListeners() { + binding.selectPlaceOfWorkToolbar.setNavigationOnClickListener { findNavController().navigateUp() } + binding.applyButton.setOnClickListener { viewModel.saveFields(country, region) } + binding.countryTextInput.setOnClickListener { navigateToCountrySelection() } + binding.regionTextInput.setOnClickListener { navigateToRegionSelection() } + } + + private fun initTextBehaviour() { + binding.countryTextInput.doOnTextChanged { text, _, _, _ -> + updateCountryHintAppearance(text?.isNotEmpty() == true) + } + binding.regionTextInput.doOnTextChanged { text, _, _, _ -> + updateRegionHintAppearance(text?.isNotEmpty() == true) + } + } + + private fun setNoCountryEndIcon() { + binding.countryLayout.apply { + setEndIconDrawable(R.drawable.ic_arrow_forward_14px) + setEndIconOnClickListener { navigateToCountrySelection() } + } + } + + private fun setCountryEndIcon() { + binding.countryLayout.apply { + setEndIconDrawable(R.drawable.ic_close_cross_14px) + setEndIconOnClickListener { viewModel.setCountryName(Country("", "")) } + } + } + + private fun setNoRegionEndIcon() { + binding.regionLayout.apply { + setEndIconDrawable(R.drawable.ic_arrow_forward_14px) + setEndIconOnClickListener { navigateToRegionSelection() } + } + } + + private fun setRegionEndIcon() { + binding.regionLayout.apply { + setEndIconDrawable(R.drawable.ic_close_cross_14px) + setEndIconOnClickListener { viewModel.setRegionName(Region("", "", null)) } + } + } + + private fun checkFields() { + binding.applyButton.isVisible = binding.countryTextInput.text?.isNotEmpty() == true || + binding.regionTextInput.text?.isNotEmpty() == true + } + + private fun setApplyButtonVisible() { + binding.applyButton.isVisible = true + } + + private fun updateCountryHintAppearance(isFilled: Boolean) { + val isDarkTheme = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + if (isFilled) { + binding.countryLayout.setHintTextAppearance(R.style.text_12_regular_400) + binding.countryLayout.defaultHintTextColor = ColorStateList.valueOf( + if (isDarkTheme) requireContext().getColor(R.color.white) else requireContext().getColor(R.color.black) + ) + } else { + binding.countryLayout.setHintTextAppearance(R.style.text_16_regular_400) + binding.countryLayout.defaultHintTextColor = + ColorStateList.valueOf(requireContext().getColor(R.color.gray)) + } + } + + private fun updateRegionHintAppearance(isFilled: Boolean) { + val isDarkTheme = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + if (isFilled) { + binding.regionLayout.setHintTextAppearance(R.style.text_12_regular_400) + binding.regionLayout.defaultHintTextColor = ColorStateList.valueOf( + if (isDarkTheme) requireContext().getColor(R.color.white) else requireContext().getColor(R.color.black) + ) + } else { + binding.regionLayout.setHintTextAppearance(R.style.text_16_regular_400) + binding.regionLayout.defaultHintTextColor = + ColorStateList.valueOf(requireContext().getColor(R.color.gray)) + } + } + + private fun navigateToCountrySelection() { + findNavController().navigate(R.id.action_selectPlaceOfWorkFragment_to_filtersCountryFragment) + } + + private fun navigateToRegionSelection() { + setFragmentResult(REGION_ID_KEY, bundleOf(REGION_BUNDLE_KEY to country.id)) + findNavController().navigate( + R.id.action_selectPlaceOfWorkFragment_to_filterRegionFragment + ) + } +} + diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterRegionFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterRegionFragment.kt new file mode 100644 index 00000000000..5577653b918 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/FilterRegionFragment.kt @@ -0,0 +1,145 @@ +package ru.practicum.android.diploma.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.setFragmentResultListener +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.CreateMethod +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.gson.Gson +import org.koin.androidx.viewmodel.ext.android.viewModel +import ru.practicum.android.diploma.databinding.FragmentSelectRegionBinding +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.presentation.viewmodels.FilterRegionViewModel +import ru.practicum.android.diploma.ui.state.RegionsScreenState +import ru.practicum.android.diploma.util.App.Companion.REGION_ID_KEY +import ru.practicum.android.diploma.util.REGION_BUNDLE_KEY +import ru.practicum.android.diploma.util.REGION_REQUEST_KEY +import ru.practicum.android.diploma.util.ResponseData +import ru.practicum.android.diploma.util.adapter.region.RegionAdapter + +class FilterRegionFragment : Fragment() { + private val binding: FragmentSelectRegionBinding by viewBinding(CreateMethod.INFLATE) + private val viewModel by viewModel() + private val adapter = RegionAdapter { + onItemClicked(it) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setFragmentResultListener(REGION_ID_KEY) { _, bundle -> + val region = bundle.getString(REGION_BUNDLE_KEY).toString() + viewModel.getRegions(region) + } + binding.selectRegionToolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + binding.clearCrossIc.setOnClickListener { + binding.regionSearchQuery.text.clear() + } + binding.regionSearchQuery.doOnTextChanged { text, _, _, _ -> + if (text?.isNotEmpty() == true) { + binding.searchIconLoupe.isVisible = false + binding.clearCrossIc.isVisible = true + } else { + binding.searchIconLoupe.isVisible = true + binding.clearCrossIc.isVisible = false + } + search(text.toString()) + } + viewModel.render().observe(viewLifecycleOwner) { state -> + when (state) { + RegionsScreenState.Default -> {} + is RegionsScreenState.Error -> { + handleErrorState(state) + } + + RegionsScreenState.Loading -> { + removePlaceholders() + startProgressBar() + } + + is RegionsScreenState.Success -> { + handleSuccessState(state) + } + } + + } + setAdapter() + } + + private fun handleErrorState(state: RegionsScreenState.Error) { + stopProgressBar() + when (state.error) { + ResponseData.ResponseError.NO_INTERNET, + ResponseData.ResponseError.CLIENT_ERROR, + ResponseData.ResponseError.SERVER_ERROR, + ResponseData.ResponseError.NOT_FOUND -> { + setNoRegionsState() + } + } + } + + private fun handleSuccessState(state: RegionsScreenState.Success) { + stopProgressBar() + removePlaceholders() + adapter.setRegions(state.regions) + binding.regionRecycleView.isVisible = true + } + private fun setAdapter() { + val regionsRecyclerView = binding.regionRecycleView + regionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + regionsRecyclerView.adapter = adapter + } + + private fun onItemClicked(region: Region) { + val json = Gson().toJson(region) + setFragmentResult( + REGION_REQUEST_KEY, + bundleOf(REGION_BUNDLE_KEY to json) + ) + findNavController().navigateUp() + } + + private fun search(query: String) { + viewModel.search(query) + } + + private fun startProgressBar() { + binding.progressBar.isVisible = true + } + + private fun stopProgressBar() { + binding.progressBar.isVisible = false + } + + private fun removePlaceholders() { + binding.serverError.isVisible = false + binding.serverErrorText.isVisible = false + binding.noConnectionPlaceholder.isVisible = false + binding.noConnectionText.isVisible = false + binding.noListPlaceholderGroup.isVisible = false + binding.regionRecycleView.isVisible = false + } + + private fun setNoRegionsState() { + binding.regionRecycleView.isVisible = false + binding.noListPlaceholderGroup.isVisible = true + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/SearchFragment.kt b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/SearchFragment.kt index d0ae275783a..2bfb73d37e3 100644 --- a/app/src/main/java/ru/practicum/android/diploma/ui/fragments/SearchFragment.kt +++ b/app/src/main/java/ru/practicum/android/diploma/ui/fragments/SearchFragment.kt @@ -8,10 +8,12 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResultListener import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -25,6 +27,7 @@ import ru.practicum.android.diploma.databinding.FragmentSearchBinding import ru.practicum.android.diploma.domain.models.Vacancy import ru.practicum.android.diploma.presentation.viewmodels.SearchViewModel import ru.practicum.android.diploma.ui.state.SearchScreenState +import ru.practicum.android.diploma.util.FILTER_REQUEST_KEY import ru.practicum.android.diploma.util.ResponseData import ru.practicum.android.diploma.util.adapter.VacancyAdapter @@ -52,8 +55,6 @@ class SearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.onStart() - if (listOfVacancies.isNotEmpty()) { binding.searchDefaultPlaceholder.isVisible = false binding.textUnderSearch.text = getCorrectAmountText(amountVacancies) @@ -100,8 +101,13 @@ class SearchFragment : Fragment() { viewModel.render().observe(viewLifecycleOwner) { state -> when (state) { - SearchScreenState.Default -> {} + SearchScreenState.Default -> { + setFilterIcon() + } + is SearchScreenState.Error -> { + clearList() + removePlaceholders() when (state.error) { ResponseData.ResponseError.NO_INTERNET -> { showNoInternetState() @@ -133,6 +139,7 @@ class SearchFragment : Fragment() { } is SearchScreenState.NothingFound -> { + clearList() showNothingFoundState() } @@ -170,8 +177,8 @@ class SearchFragment : Fragment() { binding.searchQuery.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { - val view = activity?.currentFocus - if (view != null) { + val viewEdit = activity?.currentFocus + if (viewEdit != null) { val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager imm?.hideSoftInputFromWindow(view.windowToken, 0) @@ -179,11 +186,26 @@ class SearchFragment : Fragment() { } false } + viewModel.getOptions() val vacanciesRecyclerView = binding.searchRecycleView vacanciesRecyclerView.layoutManager = LinearLayoutManager(requireContext()) vacanciesRecyclerView.adapter = adapter + initResultListeners() + } + + private fun initResultListeners() { + setFragmentResultListener(FILTER_REQUEST_KEY) { _, _ -> + if (viewModel.getMainRequest().isNotEmpty()) { + viewModel.setDefaultCurrentPage() + viewModel.search(true) + } + } + } + private fun clearList() { + listOfVacancies.clear() + adapter.setVacancies(listOfVacancies) } private fun removePlaceholders() { @@ -196,6 +218,24 @@ class SearchFragment : Fragment() { binding.serverErrorText.isVisible = false } + private fun setFilterIcon() { + if (viewModel.isFilter()) { + binding.filterIc.setImageDrawable( + AppCompatResources.getDrawable( + requireContext(), + R.drawable.ic_filter_on_24px + ) + ) + } else { + binding.filterIc.setImageDrawable( + AppCompatResources.getDrawable( + requireContext(), + R.drawable.ic_filter_off_12px + ) + ) + } + } + private fun startProgressBar() { binding.progressBar.isVisible = true } diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/state/CountriesScreenState.kt b/app/src/main/java/ru/practicum/android/diploma/ui/state/CountriesScreenState.kt new file mode 100644 index 00000000000..de0ed05dfb6 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/state/CountriesScreenState.kt @@ -0,0 +1,16 @@ +package ru.practicum.android.diploma.ui.state + +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.util.ResponseData.ResponseError + +sealed interface CountriesScreenState { + data object Default : CountriesScreenState + data object Loading : CountriesScreenState + data class Success( + val regions: List + ) : CountriesScreenState + + data class Error( + val error: ResponseError + ) : CountriesScreenState +} diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/state/FilterScreenState.kt b/app/src/main/java/ru/practicum/android/diploma/ui/state/FilterScreenState.kt new file mode 100644 index 00000000000..604af127960 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/state/FilterScreenState.kt @@ -0,0 +1,24 @@ +package ru.practicum.android.diploma.ui.state + +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs + +sealed interface FilterScreenState { + data class PlaceOfWork( + val countryName: String + ) : FilterScreenState + + data object ClearState : FilterScreenState + data class Industry( + val industry: String + ) : FilterScreenState + + data object NoPlaceOfWork : FilterScreenState + data object NoIndustry : FilterScreenState + data class FiltersSaved( + val filters: SaveFiltersSharedPrefs + ) : FilterScreenState + + data class FiltersLoaded( + val filters: SaveFiltersSharedPrefs + ) : FilterScreenState +} diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/state/PlaceOfWorkScreenState.kt b/app/src/main/java/ru/practicum/android/diploma/ui/state/PlaceOfWorkScreenState.kt new file mode 100644 index 00000000000..235edf14fbf --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/state/PlaceOfWorkScreenState.kt @@ -0,0 +1,26 @@ +package ru.practicum.android.diploma.ui.state + +import ru.practicum.android.diploma.domain.models.Country +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.domain.models.SaveFiltersSharedPrefs + +sealed interface PlaceOfWorkScreenState { + data class CountryName( + val country: Country + ) : PlaceOfWorkScreenState + + data object NoCountryName : PlaceOfWorkScreenState + + data class RegionName( + val region: Region + ) : PlaceOfWorkScreenState + + data object NoRegionName : PlaceOfWorkScreenState + data class Saved( + val country: Country, + val region: Region + ) : PlaceOfWorkScreenState + data class Loaded( + val filters: SaveFiltersSharedPrefs + ) : PlaceOfWorkScreenState +} diff --git a/app/src/main/java/ru/practicum/android/diploma/ui/state/RegionsScreenState.kt b/app/src/main/java/ru/practicum/android/diploma/ui/state/RegionsScreenState.kt new file mode 100644 index 00000000000..6df393faae8 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/ui/state/RegionsScreenState.kt @@ -0,0 +1,16 @@ +package ru.practicum.android.diploma.ui.state + +import ru.practicum.android.diploma.domain.models.Region +import ru.practicum.android.diploma.util.ResponseData.ResponseError + +sealed interface RegionsScreenState { + data object Default : RegionsScreenState + data object Loading : RegionsScreenState + data class Error( + val error: ResponseError + ) : RegionsScreenState + + data class Success( + val regions: List + ) : RegionsScreenState +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/App.kt b/app/src/main/java/ru/practicum/android/diploma/util/App.kt index 04cb9829905..df34d1565ed 100644 --- a/app/src/main/java/ru/practicum/android/diploma/util/App.kt +++ b/app/src/main/java/ru/practicum/android/diploma/util/App.kt @@ -21,4 +21,10 @@ class App : Application() { ) } } + companion object { + const val REGION_ID_KEY = "REGION_ID_KEY" + const val PLACE_OF_WORK_KEY = "PLACE_OF_WORK_KEY" + const val PLACE_OF_WORK_COUNTRY_KEY = "PLACE_OF_WORK_COUNTRY_KEY" + const val PLACE_OF_WORK_REGION_KEY = "PLACE_OF_WORK_REGION_KEY" + } } diff --git a/app/src/main/java/ru/practicum/android/diploma/util/Constants.kt b/app/src/main/java/ru/practicum/android/diploma/util/Constants.kt new file mode 100644 index 00000000000..92dec3972fe --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/Constants.kt @@ -0,0 +1,16 @@ +package ru.practicum.android.diploma.util + +const val REGION_ID_KEY = "REGION_ID_KEY" +const val PLACE_OF_WORK_KEY = "PLACE_OF_WORK_KEY" +const val PLACE_OF_WORK_COUNTRY_KEY = "PLACE_OF_WORK_COUNTRY_KEY" +const val PLACE_OF_WORK_REGION_KEY = "PLACE_OF_WORK_REGION_KEY" +const val COUNTRY_REQUEST_KEY = "COUNTRY_REQUEST_KEY" +const val COUNTRY_BUNDLE_KEY = "COUNTRY_BUNDLE_KEY" +const val FILTER_REQUEST_KEY = "FILTER_REQUEST_KEY" +const val FILTER_BUNDLE_KEY = "FILTER_BUNDLE_KEY" +const val FILTER_TO_PLACE_OF_WORK_KEY = "FILTER_TO_PLACE_OF_WORK_KEY" +const val FILTER_TO_PLACE_OF_WORK_COUNTRY_KEY = "FILTER_TO_PLACE_OF_WORK_COUNTRY_KEY" +const val FILTER_TO_PLACE_OF_WORK_REGION_KEY = "FILTER_TO_PLACE_OF_WORK_REGION_KEY" +const val REGION_REQUEST_KEY = "REGION_REQUEST_KEY" +const val REGION_BUNDLE_KEY = "REGION_BUNDLE_KEY" + diff --git a/app/src/main/java/ru/practicum/android/diploma/util/Options.kt b/app/src/main/java/ru/practicum/android/diploma/util/Options.kt index e9c760ca56a..fb13d759089 100644 --- a/app/src/main/java/ru/practicum/android/diploma/util/Options.kt +++ b/app/src/main/java/ru/practicum/android/diploma/util/Options.kt @@ -4,14 +4,28 @@ data class Options( val searchText: String, val itemsPerPage: Int, val page: Int, + val area: String?, + val industry: String?, + val salary: String?, + val withSalary: Boolean? ) { companion object { fun toMap(options: Options): Map = with(options) { - mapOf( - "text" to searchText, - "per_page" to itemsPerPage.toString(), - "page" to page.toString(), - ) + buildMap { + put("text", searchText) + put("per_page", itemsPerPage.toString()) + put("page", page.toString()) + if (area?.isNotEmpty() == true) { + put("area", area.toString()) + } + if (industry?.isNotEmpty() == true) { + put("industry", industry.toString()) + } + if (salary?.isNotEmpty() == true) { + put("salary", salary.toString()) + } + put("only_with_salary", withSalary.toString()) + } } } } diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/CountryAdapter.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/CountryAdapter.kt new file mode 100644 index 00000000000..683785f9f54 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/CountryAdapter.kt @@ -0,0 +1,37 @@ +package ru.practicum.android.diploma.util.adapter.country + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ru.practicum.android.diploma.databinding.AreaItemViewBinding +import ru.practicum.android.diploma.domain.models.Country + +class CountryAdapter( + private val onClick: (Country) -> Unit +) : RecyclerView.Adapter() { + private val countries = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CountryViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + return CountryViewHolder(AreaItemViewBinding.inflate(layoutInflater, parent, false), onClick) + } + + override fun getItemCount(): Int { + return countries.size + } + + override fun onBindViewHolder(holder: CountryViewHolder, position: Int) { + holder.bind(countries[position]) + } + + fun setCountries(newList: List?) { + val compare = DiffCallbackCountries(countries, newList ?: emptyList()) + val diffResult = DiffUtil.calculateDiff(compare) + countries.clear() + if (!newList.isNullOrEmpty()) { + countries.addAll(newList) + } + diffResult.dispatchUpdatesTo(this) + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/CountryViewHolder.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/CountryViewHolder.kt new file mode 100644 index 00000000000..e46971f91fa --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/CountryViewHolder.kt @@ -0,0 +1,18 @@ +package ru.practicum.android.diploma.util.adapter.country + +import androidx.recyclerview.widget.RecyclerView +import ru.practicum.android.diploma.databinding.AreaItemViewBinding +import ru.practicum.android.diploma.domain.models.Country + +class CountryViewHolder( + private val binding: AreaItemViewBinding, + private val onClick: (Country) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: Country) { + binding.country.text = item.name + binding.root.setOnClickListener { + onClick(item) + } + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/DiffCallbackCountries.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/DiffCallbackCountries.kt new file mode 100644 index 00000000000..52044656fe5 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/country/DiffCallbackCountries.kt @@ -0,0 +1,30 @@ +package ru.practicum.android.diploma.util.adapter.country + +import androidx.recyclerview.widget.DiffUtil +import ru.practicum.android.diploma.domain.models.Country + +class DiffCallbackCountries( + private val oldList: List, + private val newList: List +) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldList.size + override fun getNewListSize(): Int = newList.size + + override fun areItemsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldCountry = oldList[oldItemPosition] + val newCountry = newList[newItemPosition] + return oldCountry == newCountry + } + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldCountry = oldList[oldItemPosition] + val newCountry = newList[newItemPosition] + return oldCountry == newCountry + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/industry/IndustryAdapter.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/industry/IndustryAdapter.kt new file mode 100644 index 00000000000..ac02b733b47 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/industry/IndustryAdapter.kt @@ -0,0 +1,32 @@ +package ru.practicum.android.diploma.util.adapter.industry + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import ru.practicum.android.diploma.databinding.IndustryItemViewBinding +import ru.practicum.android.diploma.domain.models.Industries + +class IndustryAdapter( + private val onClick: (Industries) -> Unit +) : RecyclerView.Adapter() { + val industries = mutableListOf() + var industry: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IndustryViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + return IndustryViewHolder(IndustryItemViewBinding.inflate(layoutInflater, parent, false), onClick) + } + + override fun getItemCount(): Int { + return industry.size + } + + override fun onBindViewHolder(holder: IndustryViewHolder, position: Int) { + holder.bind(industry[position]) + } + +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/industry/IndustryViewHolder.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/industry/IndustryViewHolder.kt new file mode 100644 index 00000000000..156b3c3d096 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/industry/IndustryViewHolder.kt @@ -0,0 +1,18 @@ +package ru.practicum.android.diploma.util.adapter.industry + +import androidx.recyclerview.widget.RecyclerView +import ru.practicum.android.diploma.databinding.IndustryItemViewBinding +import ru.practicum.android.diploma.domain.models.Industries + +class IndustryViewHolder( + private val binding: IndustryItemViewBinding, + private val onClick: (Industries) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: Industries) { + binding.industryUnit.text = item.name + binding.roundButton.isChecked = item.isChecked + binding.root.setOnClickListener { + onClick(item) + } + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/DiffCallbackRegions.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/DiffCallbackRegions.kt new file mode 100644 index 00000000000..9e2b31733de --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/DiffCallbackRegions.kt @@ -0,0 +1,30 @@ +package ru.practicum.android.diploma.util.adapter.region + +import androidx.recyclerview.widget.DiffUtil +import ru.practicum.android.diploma.domain.models.Region + +class DiffCallbackRegions( + private val oldList: List, + private val newList: List +) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldList.size + override fun getNewListSize(): Int = newList.size + + override fun areItemsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldRegion = oldList[oldItemPosition] + val newRegion = newList[newItemPosition] + return oldRegion == newRegion + } + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldRegion = oldList[oldItemPosition] + val newRegion = newList[newItemPosition] + return oldRegion == newRegion + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/RegionAdapter.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/RegionAdapter.kt new file mode 100644 index 00000000000..a84c5b4f3e4 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/RegionAdapter.kt @@ -0,0 +1,37 @@ +package ru.practicum.android.diploma.util.adapter.region + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ru.practicum.android.diploma.databinding.AreaItemViewBinding +import ru.practicum.android.diploma.domain.models.Region + +class RegionAdapter( + private val onClick: (Region) -> Unit +) : RecyclerView.Adapter() { + private val regions = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RegionViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + return RegionViewHolder(AreaItemViewBinding.inflate(layoutInflater, parent, false), onClick) + } + + override fun getItemCount(): Int { + return regions.size + } + + override fun onBindViewHolder(holder: RegionViewHolder, position: Int) { + holder.bind(regions[position]) + } + + fun setRegions(newList: List?) { + val compare = DiffCallbackRegions(regions, newList ?: emptyList()) + val diffResult = DiffUtil.calculateDiff(compare) + regions.clear() + if (!newList.isNullOrEmpty()) { + regions.addAll(newList) + } + diffResult.dispatchUpdatesTo(this) + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/RegionViewHolder.kt b/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/RegionViewHolder.kt new file mode 100644 index 00000000000..9aef2973a84 --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/adapter/region/RegionViewHolder.kt @@ -0,0 +1,18 @@ +package ru.practicum.android.diploma.util.adapter.region + +import androidx.recyclerview.widget.RecyclerView +import ru.practicum.android.diploma.databinding.AreaItemViewBinding +import ru.practicum.android.diploma.domain.models.Region + +class RegionViewHolder( + private val binding: AreaItemViewBinding, + private val onClick: (Region) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: Region) { + binding.country.text = item.name + binding.root.setOnClickListener { + onClick(item) + } + } +} diff --git a/app/src/main/java/ru/practicum/android/diploma/util/extension/BundleExt.kt b/app/src/main/java/ru/practicum/android/diploma/util/extension/BundleExt.kt new file mode 100644 index 00000000000..6b9927a5a0d --- /dev/null +++ b/app/src/main/java/ru/practicum/android/diploma/util/extension/BundleExt.kt @@ -0,0 +1,7 @@ +package ru.practicum.android.diploma.util.extension + +import android.os.Bundle +import androidx.core.os.BundleCompat + +inline fun Bundle.parcelable(key: String): T? = + BundleCompat.getParcelable(this, key, T::class.java) diff --git a/app/src/main/res/drawable/ic_arrow_forward_14px.xml b/app/src/main/res/drawable/ic_arrow_forward_14px.xml index 1c67cfceecc..03de6840c69 100644 --- a/app/src/main/res/drawable/ic_arrow_forward_14px.xml +++ b/app/src/main/res/drawable/ic_arrow_forward_14px.xml @@ -5,5 +5,5 @@ android:viewportHeight="14"> + android:fillColor="?attr/colorOnPrimary"/> diff --git a/app/src/main/res/layout/area_item_view.xml b/app/src/main/res/layout/area_item_view.xml new file mode 100644 index 00000000000..be9a6c87b97 --- /dev/null +++ b/app/src/main/res/layout/area_item_view.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_filter.xml b/app/src/main/res/layout/fragment_filter.xml index 82b7e408710..778c7d06ad6 100644 --- a/app/src/main/res/layout/fragment_filter.xml +++ b/app/src/main/res/layout/fragment_filter.xml @@ -1,4 +1,3 @@ - - + app:navigationIcon="@drawable/ic_arrow_back_16px" + app:title="@string/filter_settings_title" /> + + + + + + + + + + + + -