diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index b88607e4..fe784adb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleStartEffect import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -89,12 +90,16 @@ fun MovieDetails( preferences: UserPreferences, destination: Destination.MediaItem, modifier: Modifier = Modifier, - viewModel: MovieViewModel = hiltViewModel(), + viewModel: MovieViewModel = + hiltViewModel( + creationCallback = { it.create(destination.itemId) }, + ), playlistViewModel: AddPlaylistViewModel = hiltViewModel(), ) { val context = LocalContext.current - LaunchedEffect(Unit) { - viewModel.init(destination.itemId) + LifecycleResumeEffect(Unit) { + viewModel.init() + onPauseOrDispose { } } val item by viewModel.item.observeAsState() val people by viewModel.people.observeAsState(listOf()) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index a384e016..8f7c2cf8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.detail.movie import android.content.Context import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams @@ -16,7 +17,6 @@ import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.ui.SlimItemFields -import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination @@ -26,10 +26,15 @@ import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.ThemeSongPlayer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient @@ -39,26 +44,53 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest import java.util.UUID -import javax.inject.Inject -@HiltViewModel +@HiltViewModel(assistedFactory = MovieViewModel.Factory::class) class MovieViewModel - @Inject + @AssistedInject constructor( - api: ApiClient, + private val api: ApiClient, @param:ApplicationContext private val context: Context, private val navigationManager: NavigationManager, val serverRepository: ServerRepository, val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, - ) : LoadingItemViewModel(api) { + @Assisted val itemId: UUID, + ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create(itemId2: UUID): MovieViewModel + } + + val loading = MutableLiveData(LoadingState.Pending) + val item = MutableLiveData(null) val trailers = MutableLiveData>(listOf()) val people = MutableLiveData>(listOf()) val chapters = MutableLiveData>(listOf()) val similar = MutableLiveData>() val chosenStreams = MutableLiveData(null) - fun init(itemId: UUID): Job? = + init { + init() + } + + private fun fetchAndSetItem(): Deferred = + viewModelScope.async( + Dispatchers.IO + + LoadingExceptionHandler( + loading, + "Error fetching movie", + ), + ) { + val item = + api.userLibraryApi.getItem(itemId).content.let { + BaseItem.from(it, api) + } + this@MovieViewModel.item.setValueOnMain(item) + item + } + + fun init(): Job = viewModelScope.launch( Dispatchers.IO + LoadingExceptionHandler( @@ -66,84 +98,83 @@ class MovieViewModel "Error fetching movie", ), ) { - fetchAndSetItem(itemId) - item.value?.let { item -> - val result = itemPlaybackRepository.getSelectedTracks(item.id, item) - withContext(Dispatchers.Main) { - chosenStreams.value = result - loading.value = LoadingState.Success - } - viewModelScope.launchIO { - val remoteTrailers = - item.data.remoteTrailers - ?.mapNotNull { t -> - t.url?.let { url -> - val name = - t.name - // TODO would be nice to clean up the trailer name + val item = fetchAndSetItem().await() + val result = itemPlaybackRepository.getSelectedTracks(item.id, item) + withContext(Dispatchers.Main) { + this@MovieViewModel.item.value = item + chosenStreams.value = result + loading.value = LoadingState.Success + } + viewModelScope.launchIO { + val remoteTrailers = + item.data.remoteTrailers + ?.mapNotNull { t -> + t.url?.let { url -> + val name = + t.name + // TODO would be nice to clean up the trailer name // ?.replace(item.name ?: "", "") // ?.removePrefix(" - ") - ?: context.getString(R.string.trailer) - RemoteTrailer(name, url) - } - }.orEmpty() - .sortedBy { it.name } - val localTrailerCount = item.data.localTrailerCount ?: 0 - val localTrailers = - if (localTrailerCount > 0) { - api.userLibraryApi.getLocalTrailers(itemId).content.map { - LocalTrailer(BaseItem.Companion.from(it, api)) + ?: context.getString(R.string.trailer) + RemoteTrailer(name, url) } - } else { - listOf() + }.orEmpty() + .sortedBy { it.name } + val localTrailerCount = item.data.localTrailerCount ?: 0 + val localTrailers = + if (localTrailerCount > 0) { + api.userLibraryApi.getLocalTrailers(itemId).content.map { + LocalTrailer(BaseItem.Companion.from(it, api)) } - withContext(Dispatchers.Main) { - this@MovieViewModel.trailers.value = localTrailers + remoteTrailers + } else { + listOf() } - } withContext(Dispatchers.Main) { - people.value = - item.data.people - ?.letNotEmpty { people -> - people.map { Person.fromDto(it, api) } - }.orEmpty() - chapters.value = Chapter.fromDto(item.data, api) - } - if (!similar.isInitialized) { - val similar = - api.libraryApi - .getSimilarItems( - GetSimilarItemsRequest( - userId = serverRepository.currentUser?.id, - itemId = itemId, - fields = SlimItemFields, - limit = 25, - ), - ).content.items - .map { BaseItem.Companion.from(it, api) } - this@MovieViewModel.similar.setValueOnMain(similar) + this@MovieViewModel.trailers.value = localTrailers + remoteTrailers } } + withContext(Dispatchers.Main) { + people.value = + item.data.people + ?.letNotEmpty { people -> + people.map { Person.fromDto(it, api) } + }.orEmpty() + chapters.value = Chapter.fromDto(item.data, api) + } + if (!similar.isInitialized) { + val similar = + api.libraryApi + .getSimilarItems( + GetSimilarItemsRequest( + userId = serverRepository.currentUser?.id, + itemId = itemId, + fields = SlimItemFields, + limit = 25, + ), + ).content.items + .map { BaseItem.Companion.from(it, api) } + this@MovieViewModel.similar.setValueOnMain(similar) + } } fun setWatched(played: Boolean) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { if (played) { - api.playStateApi.markPlayedItem(itemUuid) + api.playStateApi.markPlayedItem(itemId) } else { - api.playStateApi.markUnplayedItem(itemUuid) + api.playStateApi.markUnplayedItem(itemId) } - fetchAndSetItem(itemUuid) + fetchAndSetItem() } fun setFavorite(favorite: Boolean) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { if (favorite) { - api.userLibraryApi.markFavoriteItem(itemUuid) + api.userLibraryApi.markFavoriteItem(itemId) } else { - api.userLibraryApi.unmarkFavoriteItem(itemUuid) + api.userLibraryApi.unmarkFavoriteItem(itemId) } - fetchAndSetItem(itemUuid) + fetchAndSetItem() } fun savePlayVersion( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c00502d..d6143504 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,9 +10,9 @@ kotlin = "2.2.21" ksp = "2.3.0" coreKtx = "1.17.0" appcompat = "1.7.1" -composeBom = "2025.10.01" +composeBom = "2025.11.00" compose-runtime = "1.9.4" -multiplatformMarkdownRenderer = "0.37.0" +multiplatformMarkdownRenderer = "0.38.1" programguide = "1.6.0" slf4j2Timber = "1.2" timber = "5.0.1" @@ -23,8 +23,8 @@ activityCompose = "1.11.0" androidx-media3 = "1.8.0" coil = "3.3.0" jellyfin-sdk = "1.7.1" -nav3Core = "1.0.0-beta01" -lifecycleViewmodelNav3 = "2.10.0-beta01" +nav3Core = "1.0.0-rc01" +lifecycleViewmodelNav3 = "2.10.0-rc01" material3AdaptiveNav3 = "1.0.0-alpha03" protobuf = "0.9.5" datastore = "1.1.7"