Merge branch 'main' into fea/live-tv

This commit is contained in:
Damontecres 2025-10-23 22:12:27 -04:00
commit 8558644c7c
No known key found for this signature in database
10 changed files with 99 additions and 50 deletions

View file

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

View file

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

View file

@ -52,7 +52,7 @@ fun CollectionFolderMovie(
val rememberedTabIndex =
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 selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
@ -170,6 +170,29 @@ fun CollectionFolderMovie(
)
}
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(
itemId = destination.itemId,
modifier =

View file

@ -33,7 +33,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
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
@ -219,7 +218,7 @@ class SeriesViewModel
fun loadEpisodes(season: Int) {
this@SeriesViewModel.episodes.value = EpisodeList.Loading
viewModelScope.async(ExceptionHandler(true)) {
viewModelScope.launch(ExceptionHandler(true)) {
val episodes =
try {
loadEpisodesInternal(season)

View file

@ -72,6 +72,7 @@ fun SeriesOverview(
val firstItemFocusRequester = remember { FocusRequester() }
val episodeRowFocusRequester = remember { FocusRequester() }
var initialLoadDone by rememberSaveable { mutableStateOf(false) }
OneTimeLaunchedEffect {
Timber.v("SeriesDetailParent: itemId=${destination.itemId}, initialSeasonEpisode=$initialSeasonEpisode")
viewModel.init(
@ -81,7 +82,9 @@ fun SeriesOverview(
initialSeasonEpisode?.season,
initialSeasonEpisode?.episode,
)
initialLoadDone = true
}
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
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 moreDialog by remember { mutableStateOf<DialogParams?>(null) }

View file

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

View file

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

View file

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

View file

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

View file

@ -91,5 +91,6 @@
<string name="nav_drawer_pins">Customize Navigation Drawer Items</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="combine_continue_next_summary">Applies to TV Series only</string>
</resources>