Fixing a few UI issues (#61)

Gathering a bunch of smaller changes into one PR

### Changes
* Changes the behavior of the "combine continue watching & next up" to
apply only to TV Shows which basically makes the 'Continue Watching'
just movies and 'Next Up' just TV episodes
* Banner card (Series & Home pages) will stretch images to fit
* Close the "more" nav item if navigating to a non-more one (from #60)
* Fix episodes not updating/marked played when returning to series page
after playing multiple episodes
* Don't show the Intro/Outro skip button the Next Up dialog is showing
* Always should black background for playback, even when loading
This commit is contained in:
damontecres 2025-10-23 22:10:41 -04:00 committed by GitHub
parent fc4954b561
commit a35ecb6570
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 99 additions and 50 deletions

View file

@ -173,8 +173,7 @@ sealed interface AppPreference<T> {
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateHomePagePreferences { combineContinueNext = value } prefs.updateHomePagePreferences { combineContinueNext = value }
}, },
summaryOn = R.string.enabled, summary = R.string.combine_continue_next_summary,
summaryOff = R.string.disabled,
) )
val RewatchNextUp = val RewatchNextUp =

View file

@ -21,12 +21,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
@ -58,18 +60,22 @@ fun BannerCard(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
interactionSource = interactionSource, interactionSource = interactionSource,
colors =
CardDefaults.colors(
containerColor = Color.Transparent,
),
) { ) {
Box( Box(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize(),
.background(MaterialTheme.colorScheme.surfaceVariant), // .background(MaterialTheme.colorScheme.surfaceVariant),
) { ) {
if (!imageError && imageUrl.isNotNullOrBlank()) { if (!imageError && imageUrl.isNotNullOrBlank()) {
AsyncImage( AsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Fit, contentScale = ContentScale.FillBounds,
onError = { imageError = true }, onError = { imageError = true },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )

View file

@ -52,7 +52,7 @@ fun CollectionFolderMovie(
val rememberedTabIndex = val rememberedTabIndex =
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) } remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
val tabs = listOf("Recommended", "Library", "Genres") val tabs = listOf("Recommended", "Library", "Collections", "Genres")
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -170,6 +170,29 @@ fun CollectionFolderMovie(
) )
} }
2 -> { 2 -> {
CollectionFolderGrid(
preferences = preferences,
onClickItem = onClickItem,
itemId = destination.itemId,
item = destination.item,
initialFilter =
GetItemsFilter(
includeItemTypes = listOf(BaseItemKind.BOX_SET),
),
showTitle = false,
recursive = true,
modifier =
Modifier
.padding(start = 16.dp)
.fillMaxSize()
.focusRequester(focusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
)
}
3 -> {
GenreCardGrid( GenreCardGrid(
itemId = destination.itemId, itemId = destination.itemId,
modifier = modifier =

View file

@ -33,7 +33,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -219,7 +218,7 @@ class SeriesViewModel
fun loadEpisodes(season: Int) { fun loadEpisodes(season: Int) {
this@SeriesViewModel.episodes.value = EpisodeList.Loading this@SeriesViewModel.episodes.value = EpisodeList.Loading
viewModelScope.async(ExceptionHandler(true)) { viewModelScope.launch(ExceptionHandler(true)) {
val episodes = val episodes =
try { try {
loadEpisodesInternal(season) loadEpisodesInternal(season)

View file

@ -72,6 +72,7 @@ fun SeriesOverview(
val firstItemFocusRequester = remember { FocusRequester() } val firstItemFocusRequester = remember { FocusRequester() }
val episodeRowFocusRequester = remember { FocusRequester() } val episodeRowFocusRequester = remember { FocusRequester() }
var initialLoadDone by rememberSaveable { mutableStateOf(false) }
OneTimeLaunchedEffect { OneTimeLaunchedEffect {
Timber.v("SeriesDetailParent: itemId=${destination.itemId}, initialSeasonEpisode=$initialSeasonEpisode") Timber.v("SeriesDetailParent: itemId=${destination.itemId}, initialSeasonEpisode=$initialSeasonEpisode")
viewModel.init( viewModel.init(
@ -81,7 +82,9 @@ fun SeriesOverview(
initialSeasonEpisode?.season, initialSeasonEpisode?.season,
initialSeasonEpisode?.episode, initialSeasonEpisode?.episode,
) )
initialLoadDone = true
} }
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val series by viewModel.item.observeAsState(null) val series by viewModel.item.observeAsState(null)
@ -108,6 +111,13 @@ fun SeriesOverview(
), ),
) )
} }
if (initialLoadDone) {
LaunchedEffect(Unit) {
seasons.indexToNumber[position.seasonTabIndex]?.let {
viewModel.loadEpisodes(it)
}
}
}
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) }

View file

@ -225,21 +225,21 @@ fun HomePageContent(
} }
} }
} }
Box( when (loadingState) {
modifier = LoadingState.Pending,
Modifier LoadingState.Loading,
.padding(8.dp) ->
.size(24.dp) Box(
.align(Alignment.BottomEnd), modifier =
) { Modifier
when (loadingState) { .padding(16.dp)
LoadingState.Pending, .size(40.dp)
LoadingState.Loading, .align(Alignment.TopEnd),
-> ) {
CircularProgress(Modifier.fillMaxSize()) CircularProgress(Modifier.fillMaxSize())
}
else -> {} else -> {}
}
} }
} }
} }

View file

@ -24,6 +24,7 @@ import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.UserDto import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import org.jellyfin.sdk.model.api.request.GetNextUpRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest
@ -64,32 +65,28 @@ class HomeViewModel
.filter { it is ServerNavDrawerItem } .filter { it is ServerNavDrawerItem }
.map { (it as ServerNavDrawerItem).itemId } .map { (it as ServerNavDrawerItem).itemId }
// TODO data is fetched all together which may be slow for large servers // TODO data is fetched all together which may be slow for large servers
val resume = getResume(userDto.id, limit) val resume = getResume(userDto.id, limit, !prefs.combineContinueNext)
val nextUp = getNextUp(userDto.id, limit, prefs.enableRewatchingNextUp) val nextUp =
getNextUp(
userDto.id,
limit,
prefs.enableRewatchingNextUp,
prefs.combineContinueNext,
)
val latest = getLatest(userDto, limit, includedIds) val latest = getLatest(userDto, limit, includedIds)
val homeRows = val homeRows =
if (prefs.combineContinueNext) { listOf(
listOf( HomeRow(
HomeRow( section = HomeSection.RESUME,
section = HomeSection.NEXT_UP, items = resume,
items = resume + nextUp, ),
), HomeRow(
*latest.toTypedArray(), section = HomeSection.NEXT_UP,
) items = nextUp,
} else { ),
listOf( *latest.toTypedArray(),
HomeRow( )
section = HomeSection.RESUME,
items = resume,
),
HomeRow(
section = HomeSection.NEXT_UP,
items = nextUp,
),
*latest.toTypedArray(),
)
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@HomeViewModel.homeRows.value = homeRows this@HomeViewModel.homeRows.value = homeRows
loadingState.value = LoadingState.Success loadingState.value = LoadingState.Success
@ -101,13 +98,23 @@ class HomeViewModel
private suspend fun getResume( private suspend fun getResume(
userId: UUID, userId: UUID,
limit: Int, limit: Int,
includeEpisodes: Boolean,
): List<BaseItem> { ): List<BaseItem> {
val request = val request =
GetResumeItemsRequest( GetResumeItemsRequest(
userId = userId, userId = userId,
fields = SlimItemFields, fields = SlimItemFields,
limit = limit, limit = limit,
includeItemTypes = supportItemKinds, includeItemTypes =
if (includeEpisodes) {
supportItemKinds
} else {
supportItemKinds
.toMutableSet()
.apply {
remove(BaseItemKind.EPISODE)
}
},
) )
val items = val items =
api.itemsApi api.itemsApi
@ -122,6 +129,7 @@ class HomeViewModel
userId: UUID, userId: UUID,
limit: Int, limit: Int,
enableRewatching: Boolean, enableRewatching: Boolean,
enableResumable: Boolean,
): List<BaseItem> { ): List<BaseItem> {
val request = val request =
GetNextUpRequest( GetNextUpRequest(
@ -130,7 +138,7 @@ class HomeViewModel
imageTypeLimit = 1, imageTypeLimit = 1,
parentId = null, parentId = null,
limit = limit, limit = limit,
enableResumable = false, enableResumable = enableResumable,
enableUserData = true, enableUserData = true,
enableRewatching = enableRewatching, enableRewatching = enableRewatching,
) )
@ -139,7 +147,7 @@ class HomeViewModel
.getNextUp(request) .getNextUp(request)
.content .content
.items .items
.map { BaseItem.Companion.from(it, api, true) } .map { BaseItem.from(it, api, true) }
return nextUp return nextUp
} }

View file

@ -287,7 +287,10 @@ fun NavDrawer(
library = it, library = it,
selected = selectedIndex == index, selected = selectedIndex == index,
moreExpanded = showMore, moreExpanded = showMore,
onClick = { onClick.invoke(index, it) }, onClick = {
onClick.invoke(index, it)
if (it !is NavDrawerItem.More) setShowMore(false)
},
modifier = modifier =
Modifier Modifier
.ifElse( .ifElse(

View file

@ -100,7 +100,7 @@ fun PlaybackPage(
is LoadingState.Error -> ErrorMessage(st, modifier) is LoadingState.Error -> ErrorMessage(st, modifier)
LoadingState.Pending, LoadingState.Pending,
LoadingState.Loading, LoadingState.Loading,
-> LoadingPage(modifier) -> LoadingPage(modifier.background(Color.Black))
LoadingState.Success -> { LoadingState.Success -> {
val prefs = preferences.appPreferences.playbackPreferences val prefs = preferences.appPreferences.playbackPreferences
@ -201,7 +201,7 @@ fun PlaybackPage(
val showSegment = val showSegment =
!segmentCancelled && currentSegment != null && !segmentCancelled && currentSegment != null &&
!controllerViewState.controlsVisible && skipIndicatorDuration == 0L nextUp == null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L
BackHandler(showSegment) { BackHandler(showSegment) {
segmentCancelled = true segmentCancelled = true
} }

View file

@ -91,5 +91,6 @@
<string name="nav_drawer_pins">Customize Navigation Drawer Items</string> <string name="nav_drawer_pins">Customize Navigation Drawer Items</string>
<string name="profile_specific_settings">User Profile Settings</string> <string name="profile_specific_settings">User Profile Settings</string>
<string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string> <string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string>
<string name="combine_continue_next_summary">Applies to TV Series only</string>
</resources> </resources>