mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Refactor movie page to use use state flow (#1183)
## Description Refactors the movie page & view model to use a `StateFlow` instead of many `LiveData` objects. This is in keeping with best practices. There are no user facing changes. ### Related issues None ### Testing Emulator ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
feb8c31791
commit
44d1ac484f
3 changed files with 316 additions and 307 deletions
|
|
@ -14,6 +14,7 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||||
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
@ -98,3 +99,13 @@ fun ErrorMessage(
|
||||||
exception = error.exception,
|
exception = error.exception,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ErrorMessage(
|
||||||
|
error: DataLoadingState.Error,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) = ErrorMessage(
|
||||||
|
message = error.message,
|
||||||
|
exception = error.exception,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
|
@ -28,10 +29,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
|
||||||
import com.github.damontecres.wholphin.data.ExtrasItem
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.Chapter
|
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.data.model.Person
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
|
|
@ -67,11 +66,11 @@ import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||||
import com.github.damontecres.wholphin.ui.discover.DiscoverRow
|
import com.github.damontecres.wholphin.ui.discover.DiscoverRow
|
||||||
import com.github.damontecres.wholphin.ui.discover.DiscoverRowData
|
import com.github.damontecres.wholphin.ui.discover.DiscoverRowData
|
||||||
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
|
|
@ -98,16 +97,7 @@ fun MovieDetails(
|
||||||
viewModel.init()
|
viewModel.init()
|
||||||
onPauseOrDispose { }
|
onPauseOrDispose { }
|
||||||
}
|
}
|
||||||
val item by viewModel.item.observeAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val people by viewModel.people.observeAsState(listOf())
|
|
||||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
|
||||||
val trailers by viewModel.trailers.observeAsState(listOf())
|
|
||||||
val extras by viewModel.extras.observeAsState(listOf())
|
|
||||||
val similar by viewModel.similar.observeAsState(listOf())
|
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
|
||||||
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
|
|
||||||
val discovered by viewModel.discovered.collectAsState()
|
|
||||||
val canDelete by viewModel.canDelete.collectAsState()
|
|
||||||
|
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
|
@ -140,19 +130,21 @@ fun MovieDetails(
|
||||||
onClickDelete = { showDeleteDialog = it },
|
onClickDelete = { showDeleteDialog = it },
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val s = state.loading) {
|
||||||
is LoadingState.Error -> {
|
is DataLoadingState.Error -> {
|
||||||
ErrorMessage(state, modifier)
|
ErrorMessage(s, modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadingState.Loading,
|
DataLoadingState.Loading,
|
||||||
LoadingState.Pending,
|
DataLoadingState.Pending,
|
||||||
-> {
|
-> {
|
||||||
LoadingPage(modifier)
|
LoadingPage(modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadingState.Success -> {
|
is DataLoadingState.Success -> {
|
||||||
item?.let { movie ->
|
val unknownStr = stringResource(R.string.unknown)
|
||||||
|
val movie by rememberUpdatedState(s.data)
|
||||||
|
val chosenStreams by rememberUpdatedState(state.chosenStreams)
|
||||||
LifecycleResumeEffect(destination.itemId) {
|
LifecycleResumeEffect(destination.itemId) {
|
||||||
viewModel.maybePlayThemeSong(
|
viewModel.maybePlayThemeSong(
|
||||||
destination.itemId,
|
destination.itemId,
|
||||||
|
|
@ -165,12 +157,7 @@ fun MovieDetails(
|
||||||
MovieDetailsContent(
|
MovieDetailsContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
movie = movie,
|
movie = movie,
|
||||||
chosenStreams = chosenStreams,
|
state = state,
|
||||||
people = people,
|
|
||||||
chapters = chapters,
|
|
||||||
extras = extras,
|
|
||||||
trailers = trailers,
|
|
||||||
similar = similar,
|
|
||||||
onClickItem = { index, item ->
|
onClickItem = { index, item ->
|
||||||
viewModel.navigateTo(item.destination())
|
viewModel.navigateTo(item.destination())
|
||||||
},
|
},
|
||||||
|
|
@ -193,7 +180,7 @@ fun MovieDetails(
|
||||||
overviewOnClick = {
|
overviewOnClick = {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = movie.name ?: context.getString(R.string.unknown),
|
title = movie.name ?: unknownStr,
|
||||||
overview = movie.data.overview,
|
overview = movie.data.overview,
|
||||||
genres = movie.data.genres.orEmpty(),
|
genres = movie.data.genres.orEmpty(),
|
||||||
files = movie.data.mediaSources.orEmpty(),
|
files = movie.data.mediaSources.orEmpty(),
|
||||||
|
|
@ -212,8 +199,8 @@ fun MovieDetails(
|
||||||
favorite = movie.data.userData?.isFavorite ?: false,
|
favorite = movie.data.userData?.isFavorite ?: false,
|
||||||
seriesId = null,
|
seriesId = null,
|
||||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
|
canClearChosenStreams = chosenStreams.let { it?.itemPlayback != null || it?.plc != null },
|
||||||
canDelete = canDelete,
|
canDelete = state.canDelete,
|
||||||
actions = moreActions,
|
actions = moreActions,
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
|
|
@ -230,7 +217,6 @@ fun MovieDetails(
|
||||||
moreDialog = null
|
moreDialog = null
|
||||||
},
|
},
|
||||||
onChooseTracks = { type ->
|
onChooseTracks = { type ->
|
||||||
|
|
||||||
viewModel.streamChoiceService
|
viewModel.streamChoiceService
|
||||||
.chooseSource(
|
.chooseSource(
|
||||||
movie.data,
|
movie.data,
|
||||||
|
|
@ -262,9 +248,7 @@ fun MovieDetails(
|
||||||
onShowOverview = {
|
onShowOverview = {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title =
|
title = movie.name ?: unknownStr,
|
||||||
movie.name
|
|
||||||
?: context.getString(R.string.unknown),
|
|
||||||
overview = movie.data.overview,
|
overview = movie.data.overview,
|
||||||
genres = movie.data.genres.orEmpty(),
|
genres = movie.data.genres.orEmpty(),
|
||||||
files = movie.data.mediaSources.orEmpty(),
|
files = movie.data.mediaSources.orEmpty(),
|
||||||
|
|
@ -321,17 +305,15 @@ fun MovieDetails(
|
||||||
onClickExtra = { index, extra ->
|
onClickExtra = { index, extra ->
|
||||||
viewModel.navigateTo(extra.destination)
|
viewModel.navigateTo(extra.destination)
|
||||||
},
|
},
|
||||||
discovered = discovered,
|
|
||||||
onClickDiscover = { index, item ->
|
onClickDiscover = { index, item ->
|
||||||
viewModel.navigateTo(item.destination)
|
viewModel.navigateTo(item.destination)
|
||||||
},
|
},
|
||||||
canDelete = canDelete,
|
canDelete = state.canDelete,
|
||||||
deleteOnClick = { showDeleteDialog = movie },
|
deleteOnClick = { showDeleteDialog = state.movie },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
overviewDialog?.let { info ->
|
overviewDialog?.let { info ->
|
||||||
ItemDetailsDialog(
|
ItemDetailsDialog(
|
||||||
info = info,
|
info = info,
|
||||||
|
|
@ -403,13 +385,7 @@ private const val DISCOVER_ROW = SIMILAR_ROW + 1
|
||||||
fun MovieDetailsContent(
|
fun MovieDetailsContent(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
movie: BaseItem,
|
movie: BaseItem,
|
||||||
chosenStreams: ChosenStreams?,
|
state: MovieState,
|
||||||
people: List<Person>,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
trailers: List<Trailer>,
|
|
||||||
extras: List<ExtrasItem>,
|
|
||||||
similar: List<BaseItem>,
|
|
||||||
discovered: List<DiscoverItem>,
|
|
||||||
playOnClick: (Duration) -> Unit,
|
playOnClick: (Duration) -> Unit,
|
||||||
trailerOnClick: (Trailer) -> Unit,
|
trailerOnClick: (Trailer) -> Unit,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
|
|
@ -454,7 +430,7 @@ fun MovieDetailsContent(
|
||||||
MovieDetailsHeader(
|
MovieDetailsHeader(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
movie = movie,
|
movie = movie,
|
||||||
chosenStreams = chosenStreams,
|
chosenStreams = state.chosenStreams,
|
||||||
bringIntoViewRequester = bringIntoViewRequester,
|
bringIntoViewRequester = bringIntoViewRequester,
|
||||||
overviewOnClick = overviewOnClick,
|
overviewOnClick = overviewOnClick,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -481,7 +457,7 @@ fun MovieDetailsContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailers = trailers,
|
trailers = state.trailers,
|
||||||
trailerOnClick = {
|
trailerOnClick = {
|
||||||
position = TRAILER_ROW
|
position = TRAILER_ROW
|
||||||
trailerOnClick.invoke(it)
|
trailerOnClick.invoke(it)
|
||||||
|
|
@ -496,7 +472,7 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (people.isNotEmpty()) {
|
state.people.letNotEmpty { people ->
|
||||||
item {
|
item {
|
||||||
PersonRow(
|
PersonRow(
|
||||||
people = people,
|
people = people,
|
||||||
|
|
@ -515,7 +491,7 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chapters.isNotEmpty()) {
|
state.chapters.letNotEmpty { chapters ->
|
||||||
item {
|
item {
|
||||||
ChapterRow(
|
ChapterRow(
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
|
|
@ -532,7 +508,7 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (extras.isNotEmpty()) {
|
state.extras.letNotEmpty { extras ->
|
||||||
item {
|
item {
|
||||||
ExtrasRow(
|
ExtrasRow(
|
||||||
extras = extras,
|
extras = extras,
|
||||||
|
|
@ -548,7 +524,7 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (similar.isNotEmpty()) {
|
state.similar.letNotEmpty { similar ->
|
||||||
item {
|
item {
|
||||||
val imageHeight =
|
val imageHeight =
|
||||||
remember(movie.type) {
|
remember(movie.type) {
|
||||||
|
|
@ -587,7 +563,7 @@ fun MovieDetailsContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (discovered.isNotEmpty()) {
|
state.discovered.letNotEmpty { discovered ->
|
||||||
item {
|
item {
|
||||||
DiscoverRow(
|
DiscoverRow(
|
||||||
row =
|
row =
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.movie
|
package com.github.damontecres.wholphin.ui.detail.movie
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.ExtrasItem
|
import com.github.damontecres.wholphin.data.ExtrasItem
|
||||||
|
|
@ -30,36 +28,28 @@ import com.github.damontecres.wholphin.services.TrailerService
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.services.deleteItem
|
import com.github.damontecres.wholphin.services.deleteItem
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.combinePair
|
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = MovieViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = MovieViewModel.Factory::class)
|
||||||
|
|
@ -89,94 +79,99 @@ class MovieViewModel
|
||||||
fun create(itemId: UUID): MovieViewModel
|
fun create(itemId: UUID): MovieViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
private val _state = MutableStateFlow(MovieState())
|
||||||
val item = MutableLiveData<BaseItem?>(null)
|
val state: StateFlow<MovieState> = _state
|
||||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
|
||||||
val people = MutableLiveData<List<Person>>(listOf())
|
|
||||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
|
||||||
val extras = MutableLiveData<List<ExtrasItem>>(listOf())
|
|
||||||
val similar = MutableLiveData<List<BaseItem>>()
|
|
||||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
|
||||||
val discovered = MutableStateFlow<List<DiscoverItem>>(listOf())
|
|
||||||
|
|
||||||
val canDelete = MutableStateFlow(false)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
init()
|
init()
|
||||||
viewModelScope.launchDefault {
|
viewModelScope.launchDefault {
|
||||||
item
|
userPreferencesService.flow.collectLatest { preferences ->
|
||||||
.asFlow()
|
_state.update {
|
||||||
.filterNotNull()
|
val canDelete =
|
||||||
.combinePair(userPreferencesService.flow.map { it.appPreferences })
|
it.movie?.let {
|
||||||
.collectLatest { (item, preferences) ->
|
mediaManagementService.canDelete(
|
||||||
canDelete.update { mediaManagementService.canDelete(item, preferences) }
|
it,
|
||||||
|
preferences.appPreferences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
it.copy(
|
||||||
|
canDelete = canDelete ?: false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAndSetItem(): Deferred<BaseItem> =
|
private suspend fun getMovie(): BaseItem {
|
||||||
viewModelScope.async(
|
|
||||||
Dispatchers.IO +
|
|
||||||
LoadingExceptionHandler(
|
|
||||||
loading,
|
|
||||||
"Error fetching movie",
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
val item =
|
val item =
|
||||||
api.userLibraryApi.getItem(itemId).content.let {
|
api.userLibraryApi.getItem(itemId).content.let {
|
||||||
BaseItem.from(it, api)
|
BaseItem(it)
|
||||||
}
|
}
|
||||||
this@MovieViewModel.item.setValueOnMain(item)
|
return item
|
||||||
item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(): Job =
|
fun init(): Job =
|
||||||
viewModelScope.launch(
|
viewModelScope.launchDefault {
|
||||||
Dispatchers.IO +
|
val movie =
|
||||||
LoadingExceptionHandler(
|
try {
|
||||||
loading,
|
getMovie()
|
||||||
"Error fetching movie",
|
} catch (ex: Exception) {
|
||||||
),
|
Timber.e(ex, "Failed to fetch movie %s", itemId)
|
||||||
) {
|
_state.update { it.copy(loading = DataLoadingState.Error(ex)) }
|
||||||
val item = fetchAndSetItem().await()
|
return@launchDefault
|
||||||
val result =
|
}
|
||||||
|
val chosenStreams =
|
||||||
itemPlaybackRepository.getSelectedTracks(
|
itemPlaybackRepository.getSelectedTracks(
|
||||||
item.id,
|
itemId,
|
||||||
item,
|
movie,
|
||||||
userPreferencesService.getCurrent(),
|
userPreferencesService.getCurrent(),
|
||||||
)
|
)
|
||||||
val remoteTrailers = trailerService.getRemoteTrailers(item)
|
val remoteTrailers = trailerService.getRemoteTrailers(movie)
|
||||||
withContext(Dispatchers.Main) {
|
val chapters = Chapter.fromDto(movie.data, api)
|
||||||
this@MovieViewModel.item.value = item
|
_state.update {
|
||||||
chosenStreams.value = result
|
it.copy(
|
||||||
this@MovieViewModel.trailers.value = remoteTrailers
|
loading = DataLoadingState.Success(movie),
|
||||||
loading.value = LoadingState.Success
|
chosenStreams = chosenStreams,
|
||||||
backdropService.submit(item)
|
trailers = remoteTrailers,
|
||||||
|
chapters = chapters,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
backdropService.submit(movie)
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
trailerService.getLocalTrailers(item).letNotEmpty { localTrailers ->
|
trailerService.getLocalTrailers(movie).letNotEmpty { localTrailers ->
|
||||||
withContext(Dispatchers.Main) {
|
_state.update {
|
||||||
this@MovieViewModel.trailers.value = localTrailers + remoteTrailers
|
it.copy(
|
||||||
|
trailers = localTrailers + remoteTrailers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val people = peopleFavorites.getPeopleFor(item)
|
val people = peopleFavorites.getPeopleFor(movie)
|
||||||
this@MovieViewModel.people.setValueOnMain(people)
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
people = people,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val extras = extrasService.getExtras(item.id)
|
val extras = extrasService.getExtras(itemId)
|
||||||
this@MovieViewModel.extras.setValueOnMain(extras)
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
extras = extras,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val results = seerrService.similar(item).orEmpty()
|
val results = seerrService.similar(movie).orEmpty()
|
||||||
discovered.update { results }
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
discovered = results,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
if (state.value.similar.isEmpty()) {
|
||||||
chapters.value = Chapter.fromDto(item.data, api)
|
|
||||||
}
|
|
||||||
if (!similar.isInitialized) {
|
|
||||||
val similar =
|
val similar =
|
||||||
api.libraryApi
|
api.libraryApi
|
||||||
.getSimilarItems(
|
.getSimilarItems(
|
||||||
|
|
@ -187,31 +182,48 @@ class MovieViewModel
|
||||||
limit = 25,
|
limit = 25,
|
||||||
),
|
),
|
||||||
).content.items
|
).content.items
|
||||||
.map { BaseItem.Companion.from(it, api) }
|
.map { BaseItem(it) }
|
||||||
this@MovieViewModel.similar.setValueOnMain(similar)
|
|
||||||
|
_state.update { it.copy(similar = similar) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWatched(
|
fun setWatched(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) = viewModelScope.launchDefault {
|
||||||
|
try {
|
||||||
favoriteWatchManager.setWatched(itemId, played)
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
fetchAndSetItem()
|
getMovie().let { movie ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(loading = DataLoadingState.Success(movie))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error updating watch status for movie %s", itemId)
|
||||||
|
showToast(context, "Something went wrong...")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFavorite(
|
fun setFavorite(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) = viewModelScope.launchDefault {
|
||||||
|
try {
|
||||||
favoriteWatchManager.setFavorite(itemId, favorite)
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
val item = item.value
|
val movie = getMovie()
|
||||||
fetchAndSetItem()
|
_state.update {
|
||||||
if (item != null && itemId != item.id) {
|
it.copy(loading = DataLoadingState.Success(movie))
|
||||||
viewModelScope.launchIO {
|
|
||||||
val people = peopleFavorites.getPeopleFor(item)
|
|
||||||
this@MovieViewModel.people.setValueOnMain(people)
|
|
||||||
}
|
}
|
||||||
|
if (itemId != movie.id) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val people = peopleFavorites.getPeopleFor(movie)
|
||||||
|
_state.update { it.copy(people = people) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error updating favorite %s", itemId)
|
||||||
|
showToast(context, "Something went wrong...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,9 +239,7 @@ class MovieViewModel
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
_state.update { it.copy(chosenStreams = chosen) }
|
||||||
chosenStreams.value = chosen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,9 +263,7 @@ class MovieViewModel
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
_state.update { it.copy(chosenStreams = chosen) }
|
||||||
chosenStreams.value = chosen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,14 +291,14 @@ class MovieViewModel
|
||||||
fun clearChosenStreams(chosenStreams: ChosenStreams?) {
|
fun clearChosenStreams(chosenStreams: ChosenStreams?) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
itemPlaybackRepository.deleteChosenStreams(chosenStreams)
|
itemPlaybackRepository.deleteChosenStreams(chosenStreams)
|
||||||
item.value?.let { item ->
|
state.value.movie?.let { item ->
|
||||||
val result =
|
val result =
|
||||||
itemPlaybackRepository.getSelectedTracks(
|
itemPlaybackRepository.getSelectedTracks(
|
||||||
itemId,
|
itemId,
|
||||||
item,
|
item,
|
||||||
userPreferencesService.getCurrent(),
|
userPreferencesService.getCurrent(),
|
||||||
)
|
)
|
||||||
this@MovieViewModel.chosenStreams.setValueOnMain(result)
|
_state.update { it.copy(chosenStreams = result) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,3 +309,17 @@ class MovieViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class MovieState(
|
||||||
|
val loading: DataLoadingState<BaseItem> = DataLoadingState.Pending,
|
||||||
|
val trailers: List<Trailer> = emptyList(),
|
||||||
|
val people: List<Person> = emptyList(),
|
||||||
|
val chapters: List<Chapter> = emptyList(),
|
||||||
|
val extras: List<ExtrasItem> = emptyList(),
|
||||||
|
val similar: List<BaseItem> = emptyList(),
|
||||||
|
val discovered: List<DiscoverItem> = emptyList(),
|
||||||
|
val chosenStreams: ChosenStreams? = null,
|
||||||
|
val canDelete: Boolean = false,
|
||||||
|
) {
|
||||||
|
val movie: BaseItem? = (loading as? DataLoadingState.Success<BaseItem>)?.data
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue