284 lines
11 KiB
Kotlin
284 lines
11 KiB
Kotlin
package awais.instagrabber.viewmodels
|
|
|
|
import android.app.Application
|
|
import android.util.Log
|
|
import androidx.lifecycle.LiveData
|
|
import androidx.lifecycle.MutableLiveData
|
|
import androidx.lifecycle.Transformations
|
|
import androidx.lifecycle.viewModelScope
|
|
import awais.instagrabber.db.datasources.RecentSearchDataSource
|
|
import awais.instagrabber.db.entities.Favorite
|
|
import awais.instagrabber.db.entities.RecentSearch
|
|
import awais.instagrabber.db.entities.RecentSearch.Companion.fromSearchItem
|
|
import awais.instagrabber.db.repositories.FavoriteRepository
|
|
import awais.instagrabber.db.repositories.RecentSearchRepository
|
|
import awais.instagrabber.models.Resource
|
|
import awais.instagrabber.models.Resource.Companion.error
|
|
import awais.instagrabber.models.Resource.Companion.loading
|
|
import awais.instagrabber.models.Resource.Companion.success
|
|
import awais.instagrabber.models.enums.FavoriteType
|
|
import awais.instagrabber.repositories.responses.search.SearchItem
|
|
import awais.instagrabber.repositories.responses.search.SearchResponse
|
|
import awais.instagrabber.utils.*
|
|
import awais.instagrabber.utils.AppExecutors.mainThread
|
|
import awais.instagrabber.utils.TextUtils.isEmpty
|
|
import awais.instagrabber.webservices.SearchRepository
|
|
import com.google.common.collect.ImmutableList
|
|
import com.google.common.util.concurrent.FutureCallback
|
|
import com.google.common.util.concurrent.Futures
|
|
import com.google.common.util.concurrent.SettableFuture
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
import kotlinx.coroutines.launch
|
|
import retrofit2.Call
|
|
import retrofit2.Callback
|
|
import retrofit2.Response
|
|
import java.util.*
|
|
import java.util.function.BiConsumer
|
|
import java.util.stream.Collectors
|
|
|
|
class SearchFragmentViewModel(application: Application) : AppStateViewModel(application) {
|
|
private val query = MutableLiveData<String>()
|
|
private val topResults = MutableLiveData<Resource<List<SearchItem>?>>()
|
|
private val userResults = MutableLiveData<Resource<List<SearchItem>?>>()
|
|
private val hashtagResults = MutableLiveData<Resource<List<SearchItem>?>>()
|
|
private val locationResults = MutableLiveData<Resource<List<SearchItem>?>>()
|
|
private val searchRepository: SearchRepository by lazy { SearchRepository.getInstance() }
|
|
private val searchCallback: Debouncer.Callback<String> = object : Debouncer.Callback<String> {
|
|
override fun call(key: String) {
|
|
if (tempQuery == null) return
|
|
query.postValue(tempQuery!!)
|
|
}
|
|
|
|
override fun onError(t: Throwable) {
|
|
Log.e(TAG, "onError: ", t)
|
|
}
|
|
}
|
|
private val searchDebouncer = Debouncer(searchCallback, 500)
|
|
private val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
|
|
private val isLoggedIn = !isEmpty(cookie) && getUserIdFromCookie(cookie) != 0L
|
|
private val distinctQuery = Transformations.distinctUntilChanged(query)
|
|
private val recentSearchRepository: RecentSearchRepository by lazy {
|
|
RecentSearchRepository.getInstance(RecentSearchDataSource.getInstance(application))
|
|
}
|
|
private val favoriteRepository: FavoriteRepository by lazy { FavoriteRepository.getInstance(application) }
|
|
private var tempQuery: String? = null
|
|
fun getQuery(): LiveData<String> {
|
|
return distinctQuery
|
|
}
|
|
|
|
fun getTopResults(): LiveData<Resource<List<SearchItem>?>> {
|
|
return topResults
|
|
}
|
|
|
|
fun getUserResults(): LiveData<Resource<List<SearchItem>?>> {
|
|
return userResults
|
|
}
|
|
|
|
fun getHashtagResults(): LiveData<Resource<List<SearchItem>?>> {
|
|
return hashtagResults
|
|
}
|
|
|
|
fun getLocationResults(): LiveData<Resource<List<SearchItem>?>> {
|
|
return locationResults
|
|
}
|
|
|
|
fun submitQuery(query: String?) {
|
|
var localQuery = query
|
|
if (query == null) {
|
|
localQuery = ""
|
|
}
|
|
if (tempQuery != null && localQuery!!.lowercase(Locale.getDefault()) == tempQuery!!.lowercase(Locale.getDefault())) return
|
|
tempQuery = query
|
|
if (isEmpty(query)) {
|
|
// If empty immediately post it
|
|
searchDebouncer.cancel(QUERY)
|
|
this.query.postValue("")
|
|
return
|
|
}
|
|
searchDebouncer.call(QUERY)
|
|
}
|
|
|
|
fun search(
|
|
query: String,
|
|
type: FavoriteType
|
|
) {
|
|
val liveData = getLiveDataByType(type) ?: return
|
|
if (isEmpty(query)) {
|
|
showRecentSearchesAndFavorites(type, liveData)
|
|
return
|
|
}
|
|
if (query == "@" || query == "#") return
|
|
val c: String
|
|
c = when (type) {
|
|
FavoriteType.TOP -> "blended"
|
|
FavoriteType.USER -> "user"
|
|
FavoriteType.HASHTAG -> "hashtag"
|
|
FavoriteType.LOCATION -> "place"
|
|
else -> return
|
|
}
|
|
liveData.postValue(loading<List<SearchItem>?>(null))
|
|
viewModelScope.launch(Dispatchers.IO) {
|
|
try {
|
|
val response = searchRepository.search(isLoggedIn, query, c)
|
|
parseResponse(response, type)
|
|
}
|
|
catch (e: Exception) {
|
|
sendErrorResponse(type)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun showRecentSearchesAndFavorites(
|
|
type: FavoriteType,
|
|
liveData: MutableLiveData<Resource<List<SearchItem>?>>
|
|
) {
|
|
val recentResultsFuture = SettableFuture.create<List<RecentSearch>>()
|
|
val favoritesFuture = SettableFuture.create<List<Favorite>>()
|
|
viewModelScope.launch(Dispatchers.IO) {
|
|
try {
|
|
val recentSearches = recentSearchRepository.getAllRecentSearches()
|
|
recentResultsFuture.set(
|
|
if (type == FavoriteType.TOP) recentSearches
|
|
else recentSearches.stream()
|
|
.filter { (_, _, _, _, _, type1) -> type1 === type }
|
|
.collect(Collectors.toList())
|
|
)
|
|
}
|
|
catch (e: Exception) {
|
|
recentResultsFuture.set(emptyList())
|
|
}
|
|
try {
|
|
val favorites = favoriteRepository.getAllFavorites()
|
|
favoritesFuture.set(
|
|
if (type == FavoriteType.TOP) favorites
|
|
else favorites
|
|
.stream()
|
|
.filter { (_, _, type1) -> type1 === type }
|
|
.collect(Collectors.toList())
|
|
)
|
|
}
|
|
catch (e: Exception) {
|
|
favoritesFuture.set(emptyList())
|
|
}
|
|
}
|
|
val listenableFuture = Futures.allAsList<List<*>>(recentResultsFuture, favoritesFuture)
|
|
Futures.addCallback(listenableFuture, object : FutureCallback<List<List<*>?>?> {
|
|
override fun onSuccess(result: List<List<*>?>?) {
|
|
if (!isEmpty(tempQuery)) return // Make sure user has not entered anything before updating results
|
|
if (result == null) {
|
|
liveData.postValue(success(emptyList()))
|
|
return
|
|
}
|
|
try {
|
|
liveData.postValue(
|
|
success(
|
|
ImmutableList.builder<SearchItem>()
|
|
.addAll(SearchItem.fromRecentSearch(result[0] as List<RecentSearch?>?))
|
|
.addAll(SearchItem.fromFavorite(result[1] as List<Favorite?>?))
|
|
.build()
|
|
)
|
|
)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "onSuccess: ", e)
|
|
liveData.postValue(success(emptyList()))
|
|
}
|
|
}
|
|
|
|
override fun onFailure(t: Throwable) {
|
|
if (!isEmpty(tempQuery)) return
|
|
liveData.postValue(success(emptyList()))
|
|
Log.e(TAG, "onFailure: ", t)
|
|
}
|
|
}, mainThread)
|
|
}
|
|
|
|
private fun sendErrorResponse(type: FavoriteType) {
|
|
val liveData = getLiveDataByType(type) ?: return
|
|
liveData.postValue(error(null, emptyList()))
|
|
}
|
|
|
|
private fun getLiveDataByType(type: FavoriteType): MutableLiveData<Resource<List<SearchItem>?>>? {
|
|
val liveData: MutableLiveData<Resource<List<SearchItem>?>>
|
|
liveData = when (type) {
|
|
FavoriteType.TOP -> topResults
|
|
FavoriteType.USER -> userResults
|
|
FavoriteType.HASHTAG -> hashtagResults
|
|
FavoriteType.LOCATION -> locationResults
|
|
else -> return null
|
|
}
|
|
return liveData
|
|
}
|
|
|
|
private fun parseResponse(
|
|
body: SearchResponse,
|
|
type: FavoriteType
|
|
) {
|
|
val liveData = getLiveDataByType(type) ?: return
|
|
if (isLoggedIn) {
|
|
if (body.list == null) {
|
|
liveData.postValue(success(emptyList()))
|
|
return
|
|
}
|
|
if (type === FavoriteType.HASHTAG || type === FavoriteType.LOCATION) {
|
|
liveData.postValue(success(body.list
|
|
.stream()
|
|
.filter { i: SearchItem -> i.user == null }
|
|
.collect(Collectors.toList())))
|
|
return
|
|
}
|
|
liveData.postValue(success(body.list))
|
|
return
|
|
}
|
|
|
|
// anonymous
|
|
val list: List<SearchItem>?
|
|
list = when (type) {
|
|
FavoriteType.TOP -> ImmutableList
|
|
.builder<SearchItem>()
|
|
.addAll(body.users ?: emptyList())
|
|
.addAll(body.hashtags ?: emptyList())
|
|
.addAll(body.places ?: emptyList())
|
|
.build()
|
|
FavoriteType.USER -> body.users
|
|
FavoriteType.HASHTAG -> body.hashtags
|
|
FavoriteType.LOCATION -> body.places
|
|
else -> return
|
|
}
|
|
liveData.postValue(success(list))
|
|
}
|
|
|
|
fun saveToRecentSearches(searchItem: SearchItem?) {
|
|
if (searchItem == null) return
|
|
viewModelScope.launch(Dispatchers.IO) {
|
|
try {
|
|
val recentSearch = fromSearchItem(searchItem)
|
|
recentSearchRepository.insertOrUpdateRecentSearch(recentSearch!!)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "saveToRecentSearches: ", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun deleteRecentSearch(searchItem: SearchItem?): LiveData<Resource<Any?>>? {
|
|
if (searchItem == null || !searchItem.isRecent) return null
|
|
val (_, igId, _, _, _, type) = fromSearchItem(searchItem) ?: return null
|
|
val data = MutableLiveData<Resource<Any?>>()
|
|
data.postValue(loading(null))
|
|
viewModelScope.launch(Dispatchers.IO) {
|
|
try {
|
|
recentSearchRepository.deleteRecentSearchByIgIdAndType(igId, type)
|
|
data.postValue(success(Any()))
|
|
}
|
|
catch (e: Exception) {
|
|
data.postValue(error(e.message, null))
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
companion object {
|
|
private val TAG = SearchFragmentViewModel::class.java.simpleName
|
|
private const val QUERY = "query"
|
|
}
|
|
} |