2022-12-26 16:10:38 +01:00
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 )
2023-07-21 17:22:45 +02:00
. data . firstOrNull ( ) ?. items ?. toItemMediaList ( )
2022-12-26 16:10:38 +01:00
?: 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 ) )
}
}
}