mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into fea/live-tv
This commit is contained in:
commit
8558644c7c
10 changed files with 99 additions and 50 deletions
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
|
|
||||||
|
|
@ -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 -> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.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.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
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
|
||||||
|
|
@ -65,32 +66,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
|
||||||
|
|
@ -102,13 +99,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
|
||||||
|
|
@ -123,6 +130,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(
|
||||||
|
|
@ -131,7 +139,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,
|
||||||
)
|
)
|
||||||
|
|
@ -140,7 +148,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue