131 lines
3.9 KiB
Kotlin
131 lines
3.9 KiB
Kotlin
package org.mosad.teapod.ui.activity.main.viewmodel
|
|
|
|
import androidx.lifecycle.LifecycleCoroutineScope
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.update
|
|
import kotlinx.coroutines.launch
|
|
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
|
import org.mosad.teapod.util.ItemMedia
|
|
import org.mosad.teapod.util.toItemMediaList
|
|
|
|
class LibraryFragmentViewModel : ViewModel() {
|
|
|
|
val PAGESIZE = 50
|
|
|
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
|
private var oldSearchQuery = ""
|
|
private var searchJob: Job? = null
|
|
var isLazyLoading = false
|
|
internal set
|
|
|
|
sealed class UiState {
|
|
object Loading : UiState()
|
|
data class Browse(
|
|
val itemList: MutableList<ItemMedia>
|
|
) : UiState()
|
|
data class Search(
|
|
val itemList: List<ItemMedia>
|
|
) : UiState()
|
|
data class Error(val message: String?) : UiState()
|
|
}
|
|
|
|
init {
|
|
load()
|
|
}
|
|
|
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
|
scope.launch { uiState.collect { collector(it) } }
|
|
}
|
|
|
|
/**
|
|
* initially load the first n browsing items
|
|
*/
|
|
private fun load() {
|
|
viewModelScope.launch {
|
|
uiState.emit(UiState.Loading)
|
|
|
|
try {
|
|
initBrowse()
|
|
} catch (ex: Exception) {
|
|
uiState.emit(UiState.Error(ex.message))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search for a query string at Crunchyroll and emit the new ui state.
|
|
*/
|
|
fun search(query: String) {
|
|
// return if nothing has changed
|
|
if (query == oldSearchQuery) return
|
|
|
|
// update the old query since it has changed
|
|
oldSearchQuery = query
|
|
|
|
viewModelScope.launch {
|
|
|
|
// always cancel a running search job
|
|
if (searchJob?.isActive == true) searchJob?.cancel()
|
|
|
|
// handle state change: browse <-> search
|
|
if (query.isEmpty()) {
|
|
// if the query is empty change back to browse state
|
|
initBrowse()
|
|
} else {
|
|
// TODO handle errors
|
|
|
|
// if the current ui state is not search, clear the recyclerview
|
|
if (uiState.value !is UiState.Search) {
|
|
uiState.emit(UiState.Search(emptyList()))
|
|
}
|
|
|
|
// create a new search job
|
|
searchJob = viewModelScope.async {
|
|
// wait for a few ms: if the user is typing the task will get canceled
|
|
delay(250)
|
|
|
|
val results = Crunchyroll.search(query, 50)
|
|
.data.firstOrNull()?.items?.toItemMediaList()
|
|
?: listOf()
|
|
uiState.emit(UiState.Search(results))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onLazyLoad() = viewModelScope.launch {
|
|
isLazyLoading = true
|
|
|
|
try {
|
|
uiState.update { currentUiState ->
|
|
if (currentUiState is UiState.Browse) {
|
|
val newBrowseItems = Crunchyroll.browse(start = currentUiState.itemList.size, n = PAGESIZE)
|
|
.toItemMediaList()
|
|
currentUiState.itemList.addAll(newBrowseItems)
|
|
}
|
|
currentUiState
|
|
}
|
|
} catch (ex: Exception) {
|
|
uiState.emit(UiState.Error(ex.message))
|
|
}
|
|
|
|
isLazyLoading = false
|
|
}
|
|
|
|
private suspend fun initBrowse() {
|
|
try {
|
|
val initialBrowseItems = Crunchyroll.browse(n = PAGESIZE)
|
|
.toItemMediaList()
|
|
.toMutableList()
|
|
uiState.emit(UiState.Browse(initialBrowseItems))
|
|
} catch (ex: Exception) {
|
|
uiState.emit(UiState.Error(ex.message))
|
|
}
|
|
}
|
|
|
|
} |