diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 5483090c..67277279 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -37,7 +37,6 @@ import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.util.AppUpgradeHandler import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.ThemeSongPlayer import com.github.damontecres.wholphin.util.UpdateChecker import com.github.damontecres.wholphin.util.profile.createDeviceProfile import dagger.hilt.android.AndroidEntryPoint @@ -69,9 +68,6 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var appUpgradeHandler: AppUpgradeHandler - @Inject - lateinit var themeSongPlayer: ThemeSongPlayer - @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -168,9 +164,4 @@ class MainActivity : AppCompatActivity() { } } } - - override fun onStop() { - super.onStop() - themeSongPlayer.stop() - } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt index defbd166..49ab7d89 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.lifecycle.MutableLiveData import androidx.media3.common.Player import coil3.request.ErrorResult import com.github.damontecres.wholphin.ui.data.RowColumn @@ -360,3 +361,8 @@ fun CoroutineScope.launchIO( * */ fun UUID.toServerString() = this.toString().replace("-", "") + +suspend fun MutableLiveData.setValueOnMain(value: T) = + withContext(Dispatchers.Main) { + this@setValueOnMain.value = value + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt index e109c7db..ee341352 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.LifecycleStartEffect import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import coil3.compose.AsyncImage @@ -82,6 +83,15 @@ fun SeriesDetails( modifier: Modifier = Modifier, viewModel: SeriesViewModel = hiltViewModel(), ) { + LifecycleStartEffect(destination.itemId) { + viewModel.maybePlayThemeSong( + destination.itemId, + preferences.appPreferences.interfacePreferences.playThemeSongs, + ) + onStopOrDispose { + viewModel.release() + } + } LaunchedEffect(Unit) { viewModel.init(preferences, destination.itemId, destination.item, null, null) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt index e7cc3670..3a3d6e58 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt @@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavigationManager +import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler @@ -101,7 +102,6 @@ class SeriesViewModel people.map { Person.fromDto(it, api) } }.orEmpty() } - maybePlayThemeSong(prefs.appPreferences.interfacePreferences.playThemeSongs) } } @@ -109,7 +109,10 @@ class SeriesViewModel * If the series has a theme song & app settings allow, play it */ @OptIn(UnstableApi::class) - private fun maybePlayThemeSong(playThemeSongs: ThemeSongVolume) { + fun maybePlayThemeSong( + seriesId: UUID, + playThemeSongs: ThemeSongVolume, + ) { viewModelScope.launch(ExceptionHandler()) { val themeSongs = api.libraryApi.getThemeSongs(seriesId).content themeSongs.items.firstOrNull()?.let { theme -> @@ -141,10 +144,10 @@ class SeriesViewModel themeSongPlayer.stop() } - private suspend fun getSeasons(item: BaseItem): ItemListAndMapping { + private suspend fun getSeasons(series: BaseItem): ItemListAndMapping { val request = GetItemsRequest( - parentId = item.id, + parentId = series.id, recursive = false, includeItemTypes = listOf(BaseItemKind.SEASON), sortBy = listOf(ItemSortBy.INDEX_NUMBER), @@ -164,7 +167,7 @@ class SeriesViewModel viewModelScope, ) pager.init() - Timber.Forest.v("Loaded ${pager.size} seasons for series ${item.id}") + Timber.Forest.v("Loaded ${pager.size} seasons for series ${series.id}") val pairs = pager.mapIndexed { index, _ -> val season = pager.getBlocking(index) @@ -235,7 +238,9 @@ class SeriesViewModel } else { api.playStateApi.markUnplayedItem(seriesId) } - fetchItem(seriesId, null) + val series = fetchItem(seriesId, null) + val seasons = getSeasons(series) + this@SeriesViewModel.seasons.setValueOnMain(seasons) } fun refreshEpisode( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index ca936c3f..f1620b6f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.LifecycleStartEffect import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.preferences.UserPreferences @@ -59,6 +60,15 @@ fun SeriesOverview( viewModel: SeriesViewModel = hiltViewModel(), initialSeasonEpisode: SeasonEpisode? = null, ) { + LifecycleStartEffect(destination.itemId) { + viewModel.maybePlayThemeSong( + destination.itemId, + preferences.appPreferences.interfacePreferences.playThemeSongs, + ) + onStopOrDispose { + viewModel.release() + } + } val firstItemFocusRequester = remember { FocusRequester() } val episodeRowFocusRequester = remember { FocusRequester() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index fa89553e..8ee78bfe 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -289,7 +289,10 @@ fun SeriesOverviewContent( chosenStreams = chosenStreams, playOnClick = playOnClick, moreOnClick = moreOnClick, - watchOnClick = watchOnClick, + watchOnClick = { + watchOnClick.invoke() + episodeRowFocusRequester.tryRequestFocus() + }, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index e5c8975a..1179cb77 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable @@ -43,6 +44,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.ItemRow +import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage @@ -71,11 +73,14 @@ fun HomePage( modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), ) { + var firstLoad by rememberSaveable { mutableStateOf(true) } LaunchedEffect(Unit) { - viewModel.init(preferences) + viewModel.init(preferences).join() + firstLoad = false } val loading by viewModel.loadingState.observeAsState(LoadingState.Loading) - when (val state = loading) { + val homeRows by viewModel.homeRows.observeAsState(listOf()) + when (val state = if (firstLoad) loading else LoadingState.Success) { is LoadingState.Error -> ErrorMessage(state) LoadingState.Loading, @@ -83,13 +88,13 @@ fun HomePage( -> LoadingPage() LoadingState.Success -> { - val homeRows by viewModel.homeRows.observeAsState(listOf()) HomePageContent( homeRows, onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) }, - modifier, + loadingState = loading, + modifier = modifier, ) } } @@ -101,6 +106,7 @@ fun HomePageContent( onClickItem: (BaseItem) -> Unit, modifier: Modifier = Modifier, onFocusPosition: ((RowColumn) -> Unit)? = null, + loadingState: LoadingState? = null, ) { var position by rememberSaveable(stateSaver = RowColumnSaver) { mutableStateOf(RowColumn(0, 0)) @@ -219,6 +225,22 @@ fun HomePageContent( } } } + Box( + modifier = + Modifier + .padding(8.dp) + .size(24.dp) + .align(Alignment.BottomEnd), + ) { + when (loadingState) { + LoadingState.Pending, + LoadingState.Loading, + -> + CircularProgress(Modifier.fillMaxSize()) + + else -> {} + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index e962e165..733dca2c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.util.supportItemKinds import com.github.damontecres.wholphin.util.supportedCollectionTypes import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient @@ -36,13 +37,14 @@ class HomeViewModel val api: ApiClient, val navigationManager: NavigationManager, ) : ViewModel() { - val loadingState = MutableLiveData(LoadingState.Loading) + val loadingState = MutableLiveData(LoadingState.Pending) val homeRows = MutableLiveData>() - fun init(preferences: UserPreferences) { + fun init(preferences: UserPreferences): Job { + loadingState.value = LoadingState.Loading val prefs = preferences.appPreferences.homePagePreferences val limit = prefs.maxItemsPerRow - viewModelScope.launch( + return viewModelScope.launch( Dispatchers.IO + LoadingExceptionHandler( loadingState, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index e1c810cd..d7688d0a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.Player import androidx.media3.common.text.Cue import androidx.media3.common.text.CueGroup @@ -85,6 +86,11 @@ fun PlaybackPage( modifier: Modifier = Modifier, viewModel: PlaybackViewModel = hiltViewModel(), ) { + LifecycleStartEffect(destination.itemId) { + onStopOrDispose { + viewModel.release() + } + } LaunchedEffect(destination.itemId) { viewModel.init(destination, deviceProfile, preferences) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 24117daf..e185cd46 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -15,7 +15,6 @@ import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ServerRepository @@ -38,6 +37,7 @@ import com.github.damontecres.wholphin.util.EqualityMutableLiveData 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.PlayerFactory import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener import com.github.damontecres.wholphin.util.TrackSupport import com.github.damontecres.wholphin.util.checkForSupport @@ -97,6 +97,7 @@ class PlaybackViewModel constructor( @param:ApplicationContext val context: Context, val api: ApiClient, + val playerFactory: PlayerFactory, val playlistCreator: PlaylistCreator, val navigationManager: NavigationManager, val itemPlaybackDao: ItemPlaybackDao, @@ -104,13 +105,13 @@ class PlaybackViewModel val itemPlaybackRepository: ItemPlaybackRepository, ) : ViewModel(), Player.Listener { - val player: ExoPlayer = - ExoPlayer - .Builder(context) - .build() + val player by lazy { + playerFactory + .create(false) .apply { playWhenReady = true } + } val loading = MutableLiveData(LoadingState.Loading) @@ -711,6 +712,11 @@ class PlaybackViewModel loading.value = LoadingState.Error("Error during playback", error) } } + + fun release() { + playerFactory.release() + navigationManager.goBack() + } } data class CurrentPlayback( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt new file mode 100644 index 00000000..191deaa8 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt @@ -0,0 +1,49 @@ +package com.github.damontecres.wholphin.util + +import android.content.Context +import androidx.annotation.OptIn +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import com.github.damontecres.wholphin.hilt.AuthOkHttpClient +import com.github.damontecres.wholphin.hilt.StandardOkHttpClient +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.OkHttpClient +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PlayerFactory + @Inject + constructor( + @param:ApplicationContext private val context: Context, + @param:StandardOkHttpClient private val standardOkHttpClient: OkHttpClient, + @param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient, + ) { + private var player: Player? = null + + @OptIn(UnstableApi::class) + fun create(useAuth: Boolean): Player { + release() + val player = + ExoPlayer + .Builder(context) + .setMediaSourceFactory( + DefaultMediaSourceFactory( + OkHttpDataSource.Factory(if (useAuth) authOkHttpClient else standardOkHttpClient), + ), + ).build() + this.player = player + return player + } + + fun release() { + player?.let { + it.stop() + it.release() + } + player = null + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt index 8f5e7b9e..90fd84a8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt @@ -1,17 +1,9 @@ package com.github.damontecres.wholphin.util -import android.content.Context import androidx.annotation.OptIn import androidx.media3.common.MediaItem -import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.okhttp.OkHttpDataSource -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import com.github.damontecres.wholphin.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.preferences.ThemeSongVolume -import dagger.hilt.android.qualifiers.ApplicationContext -import okhttp3.OkHttpClient import javax.inject.Inject import javax.inject.Singleton @@ -22,11 +14,8 @@ import javax.inject.Singleton class ThemeSongPlayer @Inject constructor( - @param:ApplicationContext val context: Context, - @param:AuthOkHttpClient val okHttpClient: OkHttpClient, + val playerFactory: PlayerFactory, ) { - private var player: Player? = null - @OptIn(UnstableApi::class) fun play( volume: ThemeSongVolume, @@ -46,25 +35,17 @@ class ThemeSongPlayer ThemeSongVolume.HIGHEST -> 75f } val player = - ExoPlayer - .Builder(context) - .setMediaSourceFactory( - DefaultMediaSourceFactory( - OkHttpDataSource.Factory(okHttpClient), - ), - ).build() + playerFactory + .create(true) .apply { this.volume = volumeLevel playWhenReady = true } player.setMediaItem(MediaItem.fromUri(url)) player.prepare() - this.player = player } fun stop() { - player?.stop() - player?.release() - player = null + playerFactory.release() } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt b/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt index 946db132..90d870c3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt @@ -129,7 +129,7 @@ class TrackActivityPlaybackListener( withContext(Dispatchers.Main) { (if (position >= 0) position else player.currentPosition).milliseconds } -// Timber.v("saveActivity: itemId=$itemId, pos=$calcPosition") + Timber.v("saveActivity: itemId=${itemPlayback.itemId}, pos=$calcPosition") api.playStateApi.reportPlaybackProgress( PlaybackProgressInfo( itemId = itemPlayback.itemId,