From ce8cab0d1fe0836dcc7801f3301756396efe8eb9 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:18:36 -0500 Subject: [PATCH 1/6] Home page improvements (#190) Separates the loading logic into two groups: 1. Continue Watching & Next Up 2. Latest media The home page displays once the first group is ready and shows placeholders while the latest media is fetched. This also speeds up refreshes when returning home after watching an episode. Also ensure grouped Movie/TV libraries are displayed as Movie/TV libraries instead of falling back to a generic grid display. Fixes #189 --- .../damontecres/wholphin/ui/main/HomePage.kt | 16 +- .../wholphin/ui/main/HomeViewModel.kt | 146 ++++++++++-------- .../wholphin/ui/nav/DestinationContent.kt | 14 ++ 3 files changed, 106 insertions(+), 70 deletions(-) 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 9a189973..812c3343 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 @@ -87,11 +87,12 @@ fun HomePage( val context = LocalContext.current var firstLoad by rememberSaveable { mutableStateOf(true) } LaunchedEffect(Unit) { - viewModel.init(preferences).join() + viewModel.init(firstLoad, preferences).join() firstLoad = false } val loading by viewModel.loadingState.observeAsState(LoadingState.Loading) - val homeRows by viewModel.homeRows.observeAsState(listOf()) + val watchingRows by viewModel.watchingRows.observeAsState(listOf()) + val latestRows by viewModel.latestRows.observeAsState(listOf()) LaunchedEffect(loading) { val state = loading if (!firstLoad && state is LoadingState.Error) { @@ -118,7 +119,7 @@ fun HomePage( var showPlaylistDialog by remember { mutableStateOf(null) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) HomePageContent( - homeRows, + watchingRows + latestRows, onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) }, @@ -277,7 +278,7 @@ fun HomePageContent( -> { Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier, + modifier = Modifier.animateItem(), ) { Text( text = r.title, @@ -295,7 +296,7 @@ fun HomePageContent( is HomeRowLoadingState.Error -> { Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier, + modifier = Modifier.animateItem(), ) { Text( text = r.title, @@ -323,7 +324,10 @@ fun HomePageContent( } }, onLongClickItem = onLongClickItem, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .animateItem(), cardContent = { index, item, cardModifier, onClick, onLongClick -> // TODO better aspect ration handling? BannerCard( 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 623a5bb2..cf8668a6 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 @@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem +import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingExceptionHandler @@ -51,12 +52,18 @@ class HomeViewModel val navDrawerItemRepository: NavDrawerItemRepository, ) : ViewModel() { val loadingState = MutableLiveData(LoadingState.Pending) - val homeRows = MutableLiveData>() + val watchingRows = MutableLiveData>(listOf()) + val latestRows = MutableLiveData>(listOf()) private lateinit var preferences: UserPreferences - fun init(preferences: UserPreferences): Job { - loadingState.value = LoadingState.Loading + fun init( + firstLoad: Boolean, + preferences: UserPreferences, + ): Job { + if (firstLoad) { + loadingState.value = LoadingState.Loading + } this.preferences = preferences val prefs = preferences.appPreferences.homePagePreferences val limit = prefs.maxItemsPerRow @@ -84,9 +91,7 @@ class HomeViewModel prefs.enableRewatchingNextUp, prefs.combineContinueNext, ) - val latest = getLatest(userDto, limit, includedIds) - - val homeRows = + val watching = buildList { if (resume.isNotEmpty()) { add( @@ -104,12 +109,19 @@ class HomeViewModel ), ) } - addAll(latest) } + + val latest = getLatest(userDto, limit, includedIds) + val pendingLatest = latest.map { HomeRowLoadingState.Loading(it.title) } + withContext(Dispatchers.Main) { - this@HomeViewModel.homeRows.value = homeRows + this@HomeViewModel.watchingRows.value = watching + if (firstLoad) { + this@HomeViewModel.latestRows.value = pendingLatest + } loadingState.value = LoadingState.Success } + loadLatest(latest) } } } @@ -140,7 +152,7 @@ class HomeViewModel .getResumeItems(request) .content .items - .map { BaseItem.Companion.from(it, api, true) } + .map { BaseItem.from(it, api, true) } return items } @@ -174,70 +186,71 @@ class HomeViewModel user: UserDto, limit: Int, includedIds: List, - ): List { - val latestMediaIncludes = - user.configuration - ?.orderedViews - .orEmpty() - .toMutableList() - .apply { - removeAll(user.configuration?.latestItemsExcludes.orEmpty()) - }.filter { includedIds.contains(it) } - + ): List { + val excluded = user.configuration?.latestItemsExcludes.orEmpty() val views by api.userViewsApi.getUserViews() - val rows = - latestMediaIncludes - .mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } } - .filter { it.collectionType in supportedLatestCollectionTypes } - .mapNotNull { view -> + val latestData = + views.items + .filter { + it.id in includedIds && it.id !in excluded && + it.collectionType in supportedLatestCollectionTypes + }.mapNotNull { view -> val title = if (view.collectionType == CollectionType.LIVETV) { context.getString(R.string.recently_recorded) } else { view.name?.let { context.getString(R.string.recently_added_in, it) } } ?: context.getString(R.string.recently_added) - try { - val viewId = - if (view.collectionType == CollectionType.LIVETV) { - api.liveTvApi - .getRecordingFolders( - userId = user.id, - ).content.items - .firstOrNull() - ?.id - } else { - view.id - } - viewId?.let { - val request = - GetLatestMediaRequest( - fields = SlimItemFields, - imageTypeLimit = 1, - parentId = viewId, - groupItems = true, - limit = limit, - isPlayed = null, // Server will handle user's preference - ) - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem.from(it, api, true) } - HomeRowLoadingState.Success( - title = title, - items = latest, - ) + val viewId = + if (view.collectionType == CollectionType.LIVETV) { + api.liveTvApi + .getRecordingFolders( + userId = user.id, + ).content.items + .firstOrNull() + ?.id + } else { + view.id } - } catch (ex: Exception) { - Timber.e(ex, "Exception fetching %s", title) - HomeRowLoadingState.Error( - title = title, - exception = ex, - ) + viewId?.let { + val request = + GetLatestMediaRequest( + fields = SlimItemFields, + imageTypeLimit = 1, + parentId = viewId, + groupItems = true, + limit = limit, + isPlayed = null, // Server will handle user's preference + ) + LatestData(title, request) } } - return rows + return latestData + } + + private suspend fun loadLatest(latestData: List) { + val rows = + latestData.map { (title, request) -> + try { + val latest = + api.userLibraryApi + .getLatestMedia(request) + .content + .map { BaseItem.from(it, api, true) } + HomeRowLoadingState.Success( + title = title, + items = latest, + ) + } catch (ex: Exception) { + Timber.e(ex, "Exception fetching %s", title) + HomeRowLoadingState.Error( + title = title, + exception = ex, + ) + } + } + latestRows.setValueOnMain(rows) } fun setWatched( @@ -250,7 +263,7 @@ class HomeViewModel api.playStateApi.markUnplayedItem(itemId) } withContext(Dispatchers.Main) { - init(preferences) + init(false, preferences) } } @@ -264,7 +277,7 @@ class HomeViewModel api.userLibraryApi.unmarkFavoriteItem(itemId) } withContext(Dispatchers.Main) { - init(preferences) + init(false, preferences) } } } @@ -275,3 +288,8 @@ val supportedLatestCollectionTypes = CollectionType.TVSHOWS, CollectionType.LIVETV, ) + +data class LatestData( + val title: String, + val request: GetLatestMediaRequest, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 72d3fdd0..099f4816 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -148,6 +148,20 @@ fun DestinationContent( BaseItemKind.USER_VIEW -> when (destination.item?.data?.collectionType) { + CollectionType.TVSHOWS -> + CollectionFolderTv( + preferences, + destination, + modifier, + ) + + CollectionType.MOVIES -> + CollectionFolderMovie( + preferences, + destination, + modifier, + ) + CollectionType.LIVETV -> CollectionFolderLiveTv( preferences, From fbf77b937677fd623e411e6dbbc18f214e286b3f Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:55:58 -0500 Subject: [PATCH 2/6] Better playback lifecycle handling (#191) Refactors lifecycle event observers When the app is "paused", aka not actively focused, but still visible (such as when opening Android TV OS menu), playback will pause. When the app is "stopped", aka not visible (such as pressing Home button), playback will stop, report playback progress, and return to the previous screen. I think this will fix #181, but I don't have a controller to test with --- .../damontecres/wholphin/MainActivity.kt | 14 ++-- .../wholphin/ui/playback/PlaybackViewModel.kt | 28 +------- .../util/PlaybackLifecycleObserver.kt | 46 ++++++++++++ .../wholphin/util/PlayerFactory.kt | 72 +++++++++++++++++++ .../util/TrackActivityPlaybackListener.kt | 6 +- 5 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt 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 596168f8..c285294c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -37,6 +37,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination 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.PlaybackLifecycleObserver import com.github.damontecres.wholphin.util.ServerEventListener import com.github.damontecres.wholphin.util.UpdateChecker import com.github.damontecres.wholphin.util.profile.createDeviceProfile @@ -70,10 +71,14 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var serverEventListener: ServerEventListener + @Inject + lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver + @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) Timber.i("MainActivity.onCreate") + super.onCreate(savedInstanceState) + lifecycle.addObserver(playbackLifecycleObserver) setContent { CoilConfig(okHttpClient, false) val appPreferences by userPreferencesDataStore.data.collectAsState(null) @@ -175,11 +180,4 @@ class MainActivity : AppCompatActivity() { appUpgradeHandler.run() } } - - override fun onPause() { - if (navigationManager.backStack.lastOrNull() is Destination.Playback) { - navigationManager.goBack() - } - super.onPause() - } } 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 cb80363d..3cb73b14 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 @@ -17,8 +17,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.DefaultRenderersFactory -import androidx.media3.exoplayer.ExoPlayer import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackRepository @@ -33,7 +31,6 @@ import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.data.model.chooseStream import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.MediaExtensionStatus import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior import com.github.damontecres.wholphin.preferences.UserPreferences @@ -49,6 +46,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 @@ -62,12 +60,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.mediaInfoApi @@ -118,29 +114,11 @@ class PlaybackViewModel val serverRepository: ServerRepository, val itemPlaybackRepository: ItemPlaybackRepository, val appPreferences: DataStore, + private val playerFactory: PlayerFactory, ) : ViewModel(), Player.Listener { val player by lazy { - val extensions = - runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled - Timber.v("extensions=$extensions") - val rendererMode = - when (extensions) { - MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON - MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER - MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF - else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON - } - ExoPlayer - .Builder(context) - .setRenderersFactory( - DefaultRenderersFactory(context) - .setEnableDecoderFallback(true) - .setExtensionRendererMode(rendererMode), - ).build() - .apply { - playWhenReady = true - } + playerFactory.createVideoPlayer() } val loading = MutableLiveData(LoadingState.Loading) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt b/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt new file mode 100644 index 00000000..fcc49917 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt @@ -0,0 +1,46 @@ +package com.github.damontecres.wholphin.util + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.nav.NavigationManager +import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject + +/** + * Observes the activity lifecycle in order to pause/resume/stop playback + */ +@ActivityRetainedScoped +class PlaybackLifecycleObserver + @Inject + constructor( + private val navigationManager: NavigationManager, + private val playerFactory: PlayerFactory, + ) : DefaultLifecycleObserver { + private var wasPlaying: Boolean? = null + + override fun onStart(owner: LifecycleOwner) { + wasPlaying = null + } + + override fun onResume(owner: LifecycleOwner) { + if (wasPlaying == true) { + playerFactory.currentPlayer?.let { + if (!it.isReleased) it.play() + } + } + } + + override fun onPause(owner: LifecycleOwner) { + playerFactory.currentPlayer?.let { + wasPlaying = it.isPlaying + it.pause() + } + } + + override fun onStop(owner: LifecycleOwner) { + if (navigationManager.backStack.lastOrNull() is Destination.Playback) { + navigationManager.goBack() + } + } + } 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..36efeefa --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt @@ -0,0 +1,72 @@ +@file:OptIn(markerClass = [UnstableApi::class]) + +package com.github.damontecres.wholphin.util + +import android.content.Context +import androidx.annotation.OptIn +import androidx.datastore.core.DataStore +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.preferences.MediaExtensionStatus +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Constructs a [Player] instance for video playback + */ +@Singleton +class PlayerFactory + @Inject + constructor( + @param:ApplicationContext private val context: Context, + private val appPreferences: DataStore, + ) { + @Volatile + var currentPlayer: Player? = null + private set + + fun createVideoPlayer(): Player { + if (currentPlayer?.isReleased == true) { + throw IllegalStateException("Player was not released before trying to create a new one!") + } + + val extensions = + runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled + Timber.v("extensions=$extensions") + val rendererMode = + when (extensions) { + MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON + MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF + else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON + } + val newPlayer = + ExoPlayer + .Builder(context) + .setRenderersFactory( + DefaultRenderersFactory(context) + .setEnableDecoderFallback(true) + .setExtensionRendererMode(rendererMode), + ).build() + .apply { + playWhenReady = true + } + currentPlayer = newPlayer + return newPlayer + } + } + +val Player.isReleased: Boolean + get() { + return when (this) { + is ExoPlayer -> isReleased + else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}") + } + } 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 f37dbb5f..1e50d4a0 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 @@ -76,13 +76,13 @@ class TrackActivityPlaybackListener( fun release() { task.cancel() TIMER.purge() - val position = player.currentPosition.milliseconds.inWholeTicks + val position = player.currentPosition.milliseconds coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) { - Timber.v("reportPlaybackStopped for ${itemPlayback.itemId}") + Timber.v("reportPlaybackStopped for ${itemPlayback.itemId} at $position") api.playStateApi.reportPlaybackStopped( PlaybackStopInfo( itemId = itemPlayback.itemId, - positionTicks = position, + positionTicks = position.inWholeTicks, failed = false, playSessionId = playback.playSessionId, liveStreamId = playback.liveStreamId, From d6c9b236ef653ecc9d7fce5a42f325cbc2460be0 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 10 Nov 2025 18:45:31 -0500 Subject: [PATCH 3/6] Fix reversed logic --- .../com/github/damontecres/wholphin/util/PlayerFactory.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 36efeefa..9501fc01 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt @@ -33,8 +33,9 @@ class PlayerFactory private set fun createVideoPlayer(): Player { - if (currentPlayer?.isReleased == true) { - throw IllegalStateException("Player was not released before trying to create a new one!") + if (currentPlayer?.isReleased == false) { + Timber.w("Player was not released before trying to create a new one!") + currentPlayer?.release() } val extensions = From f8a2bb154529897eb4e122bdf644fea2455eafab Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:32:01 -0500 Subject: [PATCH 4/6] Fix showing home page refreshes (#194) Fixes the UI not showing that a refresh is in progress --- .../damontecres/wholphin/ui/main/HomePage.kt | 7 ++++--- .../wholphin/ui/main/HomeViewModel.kt | 17 +++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) 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 812c3343..ae67b21c 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 @@ -87,10 +87,11 @@ fun HomePage( val context = LocalContext.current var firstLoad by rememberSaveable { mutableStateOf(true) } LaunchedEffect(Unit) { - viewModel.init(firstLoad, preferences).join() + viewModel.init(preferences).join() firstLoad = false } val loading by viewModel.loadingState.observeAsState(LoadingState.Loading) + val refreshing by viewModel.refreshState.observeAsState(LoadingState.Loading) val watchingRows by viewModel.watchingRows.observeAsState(listOf()) val latestRows by viewModel.latestRows.observeAsState(listOf()) LaunchedEffect(loading) { @@ -107,7 +108,7 @@ fun HomePage( } } - when (val state = if (firstLoad) loading else LoadingState.Success) { + when (val state = loading) { is LoadingState.Error -> ErrorMessage(state) LoadingState.Loading, @@ -155,7 +156,7 @@ fun HomePage( items = dialogItems, ) }, - loadingState = loading, + loadingState = refreshing, showClock = preferences.appPreferences.interfacePreferences.showClock, modifier = modifier, ) 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 cf8668a6..4292909e 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 @@ -52,18 +52,18 @@ class HomeViewModel val navDrawerItemRepository: NavDrawerItemRepository, ) : ViewModel() { val loadingState = MutableLiveData(LoadingState.Pending) + val refreshState = MutableLiveData(LoadingState.Pending) val watchingRows = MutableLiveData>(listOf()) val latestRows = MutableLiveData>(listOf()) private lateinit var preferences: UserPreferences - fun init( - firstLoad: Boolean, - preferences: UserPreferences, - ): Job { - if (firstLoad) { + fun init(preferences: UserPreferences): Job { + val reload = loadingState.value != LoadingState.Success + if (reload) { loadingState.value = LoadingState.Loading } + refreshState.value = LoadingState.Loading this.preferences = preferences val prefs = preferences.appPreferences.homePagePreferences val limit = prefs.maxItemsPerRow @@ -116,12 +116,13 @@ class HomeViewModel withContext(Dispatchers.Main) { this@HomeViewModel.watchingRows.value = watching - if (firstLoad) { + if (reload) { this@HomeViewModel.latestRows.value = pendingLatest } loadingState.value = LoadingState.Success } loadLatest(latest) + refreshState.setValueOnMain(LoadingState.Success) } } } @@ -263,7 +264,7 @@ class HomeViewModel api.playStateApi.markUnplayedItem(itemId) } withContext(Dispatchers.Main) { - init(false, preferences) + init(preferences) } } @@ -277,7 +278,7 @@ class HomeViewModel api.userLibraryApi.unmarkFavoriteItem(itemId) } withContext(Dispatchers.Main) { - init(false, preferences) + init(preferences) } } } From 5b1c46f0b7102b716c0c56297fccfc02f8f6ceda Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 11 Nov 2025 15:45:22 -0500 Subject: [PATCH 5/6] Fix focus on recommended --- .../damontecres/wholphin/ui/components/RecommendedMovie.kt | 2 +- .../damontecres/wholphin/ui/components/RecommendedTvShow.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt index df1f80bb..2f9e971e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt @@ -223,7 +223,7 @@ fun RecommendedMovie( viewModel.init() } val loading by viewModel.loading.observeAsState(LoadingState.Loading) - val rows by viewModel.rows.collectAsState(listOf()) + val rows by viewModel.rows.collectAsState() when (val state = loading) { is LoadingState.Error -> ErrorMessage(state) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index 1ce8cff2..d19baf0e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -253,7 +253,7 @@ fun RecommendedTvShow( } val loading by viewModel.loading.observeAsState(LoadingState.Loading) - val rows by viewModel.rows.collectAsState(listOf()) + val rows by viewModel.rows.collectAsState() when (val state = loading) { is LoadingState.Error -> ErrorMessage(state) From ed2ec2f97e577b5670edd3ba3b1f244520e0d73c Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 11 Nov 2025 15:57:35 -0500 Subject: [PATCH 6/6] Release v0.2.8