mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Fixes favorites page not loading (#1462)
## Description - Fixes a bug introduced in #1453 which broke the favorite page - Follow up to #1428 to refactor the collection folder code to hoist state up another level - Fixes resuming playback from series details play button ### Related issues Fixes #1313 Fixes #1461 Follow up to #1453 & #1428 ### Testing Emulator ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
510160aceb
commit
23856ffcf8
4 changed files with 294 additions and 396 deletions
|
|
@ -16,21 +16,13 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
|
@ -44,17 +36,13 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.cards.GridCard
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
||||
import com.github.damontecres.wholphin.ui.detail.GridItemDetails
|
||||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||
import com.github.damontecres.wholphin.ui.playback.scale
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
|
@ -64,76 +52,25 @@ import kotlin.time.Duration
|
|||
@Composable
|
||||
fun CollectionFolderGrid(
|
||||
preferences: UserPreferences,
|
||||
item: BaseItem?,
|
||||
title: String,
|
||||
items: DataLoadingState<List<BaseItem?>>,
|
||||
collectionType: CollectionType?,
|
||||
focusedItem: BaseItem?,
|
||||
items: List<BaseItem?>,
|
||||
sortAndDirection: SortAndDirection,
|
||||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||
onSortChange: (SortAndDirection) -> Unit,
|
||||
letterPosition: suspend (Char) -> Int,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
playEnabled: Boolean,
|
||||
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||
viewOptions: ViewOptions,
|
||||
onClickPlayAll: (shuffle: Boolean) -> Unit,
|
||||
onClickPlay: (Int, BaseItem) -> Unit,
|
||||
onChangeBackdrop: (BaseItem) -> Unit,
|
||||
onClickShowViewOptions: () -> Unit,
|
||||
initialPosition: Int,
|
||||
gridFocusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
currentFilter: GetItemsFilter = GetItemsFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
||||
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||
focusRequesterOnEmpty: FocusRequester? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val pager = (items as? DataLoadingState.Success)?.data
|
||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||
val headerRowFocusRequester = remember { FocusRequester() }
|
||||
|
||||
val gridFocusRequester = remember { FocusRequester() }
|
||||
if (pager?.isNotEmpty() == true) {
|
||||
RequestOrRestoreFocus(gridFocusRequester)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
(focusRequesterOnEmpty ?: headerRowFocusRequester).tryRequestFocus()
|
||||
}
|
||||
}
|
||||
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var position by rememberInt(initialPosition)
|
||||
val focusedItem = pager?.getOrNull(position)
|
||||
if (viewOptions.showBackdrop) {
|
||||
LaunchedEffect(focusedItem) {
|
||||
focusedItem?.let(onChangeBackdrop)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
CollectionFolderHeader(
|
||||
showHeader = showHeader || items !is DataLoadingState.Success,
|
||||
showTitle = showTitle,
|
||||
playEnabled = playEnabled && pager?.isNotEmpty() == true,
|
||||
title = title,
|
||||
sortAndDirection = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
sortOptions = sortOptions,
|
||||
getPossibleFilterValues = getPossibleFilterValues,
|
||||
onClickShowViewOptions = onClickShowViewOptions,
|
||||
onClickPlayAll = onClickPlayAll,
|
||||
currentFilter = currentFilter,
|
||||
filterOptions = filterOptions,
|
||||
onFilterChange = onFilterChange,
|
||||
modifier = Modifier.focusRequester(headerRowFocusRequester),
|
||||
)
|
||||
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||
val density = LocalDensity.current
|
||||
AnimatedVisibility(viewOptions.showDetails) {
|
||||
|
|
@ -147,62 +84,43 @@ fun CollectionFolderGrid(
|
|||
.padding(HeaderUtils.padding),
|
||||
)
|
||||
}
|
||||
when (val state = items) {
|
||||
DataLoadingState.Pending,
|
||||
DataLoadingState.Loading,
|
||||
-> {
|
||||
// This shouldn't happen, so just show placeholder
|
||||
Text(stringResource(R.string.loading))
|
||||
}
|
||||
|
||||
is DataLoadingState.Error -> {
|
||||
ErrorMessage(state.message, state.exception)
|
||||
}
|
||||
|
||||
is DataLoadingState.Success<List<BaseItem?>> -> {
|
||||
CardGrid(
|
||||
pager = state.data,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickPlay = onClickPlay,
|
||||
letterPosition = letterPosition,
|
||||
gridFocusRequester = gridFocusRequester,
|
||||
showJumpButtons = false, // TODO add preference
|
||||
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
initialPosition = initialPosition,
|
||||
positionCallback = { columns, newPosition ->
|
||||
showHeader = newPosition < columns
|
||||
position = newPosition
|
||||
positionCallback?.invoke(columns, newPosition)
|
||||
},
|
||||
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||
GridCard(
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
imageContentScale = viewOptions.contentScale.scale,
|
||||
imageAspectRatio = viewOptions.aspectRatio.ratio,
|
||||
imageType = viewOptions.imageType,
|
||||
showTitle = viewOptions.showTitles,
|
||||
fillWidth = widthPx,
|
||||
modifier = mod,
|
||||
)
|
||||
},
|
||||
columns = viewOptions.columns,
|
||||
spacing = viewOptions.spacing.dp,
|
||||
bringIntoViewSpec =
|
||||
remember(viewOptions) {
|
||||
val spacingPx = with(density) { viewOptions.spacing.dp.toPx() }
|
||||
if (viewOptions.showDetails) {
|
||||
ScrollToTopBringIntoViewSpec(spacingPx)
|
||||
} else {
|
||||
defaultBringIntoViewSpec
|
||||
}
|
||||
},
|
||||
CardGrid(
|
||||
pager = items,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickPlay = onClickPlay,
|
||||
letterPosition = letterPosition,
|
||||
gridFocusRequester = gridFocusRequester,
|
||||
showJumpButtons = false, // TODO add preference
|
||||
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
initialPosition = initialPosition,
|
||||
positionCallback = positionCallback,
|
||||
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||
GridCard(
|
||||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
imageContentScale = viewOptions.contentScale.scale,
|
||||
imageAspectRatio = viewOptions.aspectRatio.ratio,
|
||||
imageType = viewOptions.imageType,
|
||||
showTitle = viewOptions.showTitles,
|
||||
fillWidth = widthPx,
|
||||
modifier = mod,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
columns = viewOptions.columns,
|
||||
spacing = viewOptions.spacing.dp,
|
||||
bringIntoViewSpec =
|
||||
remember(viewOptions) {
|
||||
val spacingPx = with(density) { viewOptions.spacing.dp.toPx() }
|
||||
if (viewOptions.showDetails) {
|
||||
ScrollToTopBringIntoViewSpec(spacingPx)
|
||||
} else {
|
||||
defaultBringIntoViewSpec
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -51,10 +50,7 @@ import coil3.compose.useExistingImageAsPlaceholder
|
|||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.filter.FilterValueOption
|
||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.cards.FavoriteIndicator
|
||||
|
|
@ -67,7 +63,6 @@ import com.github.damontecres.wholphin.ui.launchIO
|
|||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
|
@ -79,60 +74,30 @@ import org.jellyfin.sdk.model.extensions.ticks
|
|||
@Composable
|
||||
fun CollectionFolderList(
|
||||
preferences: UserPreferences,
|
||||
item: BaseItem?,
|
||||
title: String,
|
||||
items: DataLoadingState<List<BaseItem?>>,
|
||||
collectionType: CollectionType?,
|
||||
focusedItem: BaseItem?,
|
||||
items: List<BaseItem?>,
|
||||
sortAndDirection: SortAndDirection,
|
||||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||
onSortChange: (SortAndDirection) -> Unit,
|
||||
letterPosition: suspend (Char) -> Int,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
playEnabled: Boolean,
|
||||
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||
viewOptions: ViewOptions,
|
||||
onClickPlayAll: (shuffle: Boolean) -> Unit,
|
||||
onClickPlay: (Int, BaseItem) -> Unit,
|
||||
onChangeBackdrop: (BaseItem) -> Unit,
|
||||
onClickShowViewOptions: () -> Unit,
|
||||
initialPosition: Int,
|
||||
gridFocusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
currentFilter: GetItemsFilter = GetItemsFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
||||
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||
focusRequesterOnEmpty: FocusRequester? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val pager = (items as? DataLoadingState.Success)?.data
|
||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||
val headerRowFocusRequester = remember { FocusRequester() }
|
||||
val showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME
|
||||
|
||||
val gridFocusRequester = remember { FocusRequester() }
|
||||
if (pager?.isNotEmpty() == true) {
|
||||
LaunchedEffect(viewOptions.type) {
|
||||
gridFocusRequester.tryRequestFocus()
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
(focusRequesterOnEmpty ?: headerRowFocusRequester).tryRequestFocus()
|
||||
}
|
||||
}
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
var contentHashFocus by remember { mutableStateOf(false) }
|
||||
var position by rememberInt(initialPosition)
|
||||
val positionFocusRequester = remember { FocusRequester() }
|
||||
val focusedItem = pager?.getOrNull(position)
|
||||
if (viewOptions.showBackdrop) {
|
||||
LaunchedEffect(focusedItem) {
|
||||
focusedItem?.let(onChangeBackdrop)
|
||||
}
|
||||
}
|
||||
val showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME
|
||||
|
||||
var showHeader by remember { mutableStateOf(true) }
|
||||
|
||||
fun jumpTo(newPosition: Int) {
|
||||
scope.launch {
|
||||
|
|
@ -147,125 +112,93 @@ fun CollectionFolderList(
|
|||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
CollectionFolderHeader(
|
||||
showHeader = showHeader || items !is DataLoadingState.Success,
|
||||
showTitle = showTitle,
|
||||
playEnabled = playEnabled && pager?.isNotEmpty() == true,
|
||||
title = title,
|
||||
sortAndDirection = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
sortOptions = sortOptions,
|
||||
getPossibleFilterValues = getPossibleFilterValues,
|
||||
onClickShowViewOptions = onClickShowViewOptions,
|
||||
onClickPlayAll = onClickPlayAll,
|
||||
currentFilter = currentFilter,
|
||||
filterOptions = filterOptions,
|
||||
onFilterChange = onFilterChange,
|
||||
modifier = Modifier.focusRequester(headerRowFocusRequester),
|
||||
val topPadding by animateDpAsState(
|
||||
targetValue =
|
||||
when {
|
||||
showHeader -> 8.dp
|
||||
|
||||
// showClock-> TODO()
|
||||
else -> 48.dp
|
||||
},
|
||||
)
|
||||
when (val state = items) {
|
||||
DataLoadingState.Pending,
|
||||
DataLoadingState.Loading,
|
||||
-> {
|
||||
// This shouldn't happen, so just show placeholder
|
||||
Text(stringResource(R.string.loading))
|
||||
}
|
||||
|
||||
is DataLoadingState.Error -> {
|
||||
ErrorMessage(state.message, state.exception)
|
||||
}
|
||||
|
||||
is DataLoadingState.Success<List<BaseItem?>> -> {
|
||||
val pager = state.data
|
||||
val topPadding by animateDpAsState(
|
||||
targetValue =
|
||||
when {
|
||||
showHeader -> 8.dp
|
||||
|
||||
// showClock-> TODO()
|
||||
else -> 48.dp
|
||||
},
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = topPadding, bottom = 16.dp)
|
||||
.onFocusChanged {
|
||||
contentHashFocus = it.hasFocus
|
||||
},
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = viewOptions.showDetails,
|
||||
enter = fadeIn() + expandHorizontally(expandFrom = Alignment.Start),
|
||||
exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.Start),
|
||||
) {
|
||||
CollectionFolderListDetails(
|
||||
item = focusedItem,
|
||||
showLogo = true,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = topPadding, bottom = 16.dp)
|
||||
.onFocusChanged {
|
||||
contentHashFocus = it.hasFocus
|
||||
},
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = viewOptions.showDetails,
|
||||
enter = fadeIn() + expandHorizontally(expandFrom = Alignment.Start),
|
||||
exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.Start),
|
||||
) {
|
||||
CollectionFolderListDetails(
|
||||
item = focusedItem,
|
||||
showLogo = true,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(.33f)
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
color = Color.Transparent, // MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
).padding(8.dp),
|
||||
)
|
||||
}
|
||||
CollectionFolderContent(
|
||||
items = state.data,
|
||||
viewOptions = viewOptions,
|
||||
collectionType = item?.data?.collectionType,
|
||||
listState = listState,
|
||||
position = position,
|
||||
positionFocusRequester = positionFocusRequester,
|
||||
onPositionChange = {
|
||||
showHeader = it < 1
|
||||
positionCallback?.invoke(1, it)
|
||||
position = it
|
||||
},
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(gridFocusRequester),
|
||||
)
|
||||
val letters = stringResource(R.string.jump_letters)
|
||||
// Letters
|
||||
val currentLetter =
|
||||
remember(focusedItem) {
|
||||
focusedItem
|
||||
?.sortName
|
||||
?.firstOrNull()
|
||||
?.uppercaseChar()
|
||||
?.let {
|
||||
when (it) {
|
||||
in '0'..'9' -> '#'
|
||||
in 'A'..'Z' -> it
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
?: letters[0]
|
||||
.fillMaxWidth(.33f)
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
color = Color.Transparent, // MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
).padding(8.dp),
|
||||
)
|
||||
}
|
||||
CollectionFolderContent(
|
||||
items = items,
|
||||
viewOptions = viewOptions,
|
||||
collectionType = collectionType,
|
||||
listState = listState,
|
||||
position = position,
|
||||
positionFocusRequester = positionFocusRequester,
|
||||
onPositionChange = {
|
||||
showHeader = it < 1
|
||||
positionCallback?.invoke(1, it)
|
||||
position = it
|
||||
},
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(gridFocusRequester),
|
||||
)
|
||||
val letters = stringResource(R.string.jump_letters)
|
||||
// Letters
|
||||
val currentLetter =
|
||||
remember(focusedItem) {
|
||||
focusedItem
|
||||
?.sortName
|
||||
?.firstOrNull()
|
||||
?.uppercaseChar()
|
||||
?.let {
|
||||
when (it) {
|
||||
in '0'..'9' -> '#'
|
||||
in 'A'..'Z' -> it
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (showLetterButtons && pager.isNotEmpty()) {
|
||||
AlphabetButtons(
|
||||
letters = letters,
|
||||
currentLetter = currentLetter,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 16.dp),
|
||||
letterClicked = {
|
||||
scope.launchIO {
|
||||
val position = letterPosition.invoke(it)
|
||||
jumpTo(position)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
?: letters[0]
|
||||
}
|
||||
if (showLetterButtons && items.isNotEmpty()) {
|
||||
AlphabetButtons(
|
||||
letters = letters,
|
||||
currentLetter = currentLetter,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 16.dp),
|
||||
letterClicked = {
|
||||
scope.launchIO {
|
||||
val position = letterPosition.invoke(it)
|
||||
jumpTo(position)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import android.content.Context
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -18,6 +20,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
|
|
@ -60,8 +63,10 @@ import com.github.damontecres.wholphin.ui.equalsNotNull
|
|||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.ui.util.FilterUtils
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
|
|
@ -149,10 +154,12 @@ class CollectionFolderViewModel
|
|||
viewModelScope.launchIO {
|
||||
try {
|
||||
val item =
|
||||
api.userLibraryApi
|
||||
.getItem(itemId.toUUID())
|
||||
.content
|
||||
.let(::BaseItem)
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
api.userLibraryApi
|
||||
.getItem(it)
|
||||
.content
|
||||
.let(::BaseItem)
|
||||
}
|
||||
|
||||
val libraryDisplayInfo =
|
||||
serverRepository.currentUser?.let { user ->
|
||||
|
|
@ -184,7 +191,11 @@ class CollectionFolderViewModel
|
|||
// onResumePage()
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error during init")
|
||||
_state.update { it.copy(item = DataLoadingState.Error(ex)) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
item = DataLoadingState.Error(ex),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaManagementService.deletedItemFlow
|
||||
|
|
@ -313,7 +324,12 @@ class CollectionFolderViewModel
|
|||
sortAndDirection,
|
||||
filter,
|
||||
)
|
||||
_state.update { it.copy(items = DataLoadingState.Error(ex)) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
items = DataLoadingState.Error(ex),
|
||||
backgroundLoading = LoadingState.Pending,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +532,7 @@ class CollectionFolderViewModel
|
|||
}
|
||||
|
||||
data class CollectionFolderState(
|
||||
val item: DataLoadingState<BaseItem> = DataLoadingState.Loading,
|
||||
val item: DataLoadingState<BaseItem?> = DataLoadingState.Loading,
|
||||
val items: DataLoadingState<List<BaseItem?>> = DataLoadingState.Loading,
|
||||
val backgroundLoading: LoadingState = LoadingState.Loading,
|
||||
val sortAndDirection: SortAndDirection = SortAndDirection.DEFAULT,
|
||||
|
|
@ -738,122 +754,153 @@ fun CollectionFolderView(
|
|||
},
|
||||
)
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
when (val st = state.item) {
|
||||
DataLoadingState.Loading,
|
||||
DataLoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage(Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
when (val st = state.item) {
|
||||
DataLoadingState.Loading,
|
||||
DataLoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage(modifier)
|
||||
}
|
||||
is DataLoadingState.Error -> {
|
||||
ErrorMessage(st, Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
is DataLoadingState.Error,
|
||||
is DataLoadingState.Success<*>,
|
||||
-> {
|
||||
val item = st.successValue
|
||||
val title =
|
||||
initialFilter.nameOverride
|
||||
?: item?.name
|
||||
?: item?.data?.collectionType?.name
|
||||
?: stringResource(R.string.collection)
|
||||
Box(modifier = modifier) {
|
||||
LifecycleResumeEffect(itemId) {
|
||||
viewModel.onResumePage()
|
||||
is DataLoadingState.Success<BaseItem?> -> {
|
||||
val item = st.successValue
|
||||
val title =
|
||||
initialFilter.nameOverride
|
||||
?: item?.name
|
||||
?: item?.data?.collectionType?.name
|
||||
?: stringResource(R.string.collection)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
val headerRowFocusRequester = remember { FocusRequester() }
|
||||
LifecycleResumeEffect(itemId) {
|
||||
viewModel.onResumePage()
|
||||
|
||||
onPauseOrDispose {
|
||||
viewModel.release()
|
||||
onPauseOrDispose {
|
||||
viewModel.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.viewOptions.type == ViewOptionsType.GRID) {
|
||||
CollectionFolderGrid(
|
||||
preferences = preferences,
|
||||
initialPosition = viewModel.position,
|
||||
item = item,
|
||||
title = title,
|
||||
items = state.items,
|
||||
sortAndDirection = state.sortAndDirection,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
focusRequesterOnEmpty = focusRequesterOnEmpty,
|
||||
onClickItem = gridActions.onClickItem,
|
||||
onLongClickItem = gridActions.onLongClickItem!!,
|
||||
onSortChange = {
|
||||
viewModel.onSortChange(it, recursive, state.filter)
|
||||
},
|
||||
filterOptions = filterOptions,
|
||||
currentFilter = state.filter,
|
||||
onFilterChange = {
|
||||
viewModel.onFilterChange(it, recursive)
|
||||
},
|
||||
getPossibleFilterValues = {
|
||||
viewModel.getFilterOptionValues(it)
|
||||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = { columns, position ->
|
||||
viewModel.position = position
|
||||
positionCallback?.invoke(columns, position)
|
||||
},
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
viewOptions = state.viewOptions,
|
||||
onChangeBackdrop = viewModel::updateBackdrop,
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = gridActions.onClickPlayRemoteButton!!,
|
||||
onClickPlayAll = gridActions.onClickPlayAll!!,
|
||||
onClickShowViewOptions = { showViewOptions = true },
|
||||
)
|
||||
} else {
|
||||
CollectionFolderList(
|
||||
preferences = preferences,
|
||||
initialPosition = viewModel.position,
|
||||
item = item,
|
||||
title = title,
|
||||
items = state.items,
|
||||
sortAndDirection = state.sortAndDirection,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
focusRequesterOnEmpty = focusRequesterOnEmpty,
|
||||
onClickItem = gridActions.onClickItem,
|
||||
onLongClickItem = gridActions.onLongClickItem!!,
|
||||
onSortChange = {
|
||||
viewModel.onSortChange(it, recursive, state.filter)
|
||||
},
|
||||
filterOptions = filterOptions,
|
||||
currentFilter = state.filter,
|
||||
onFilterChange = {
|
||||
viewModel.onFilterChange(it, recursive)
|
||||
},
|
||||
getPossibleFilterValues = {
|
||||
viewModel.getFilterOptionValues(it)
|
||||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = { columns, position ->
|
||||
viewModel.position = position
|
||||
positionCallback?.invoke(columns, position)
|
||||
},
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
viewOptions = state.viewOptions,
|
||||
onChangeBackdrop = viewModel::updateBackdrop,
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = gridActions.onClickPlayRemoteButton!!,
|
||||
onClickPlayAll = gridActions.onClickPlayAll!!,
|
||||
onClickShowViewOptions = { showViewOptions = true },
|
||||
)
|
||||
}
|
||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||
val gridFocusRequester = remember { FocusRequester() }
|
||||
val pager = remember(state.items) { state.items.successValue }
|
||||
var position by rememberInt(viewModel.position)
|
||||
|
||||
AnimatedVisibility(
|
||||
state.backgroundLoading == LoadingState.Loading,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
CircularProgress(
|
||||
Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.background.copy(alpha = .25f),
|
||||
shape = CircleShape,
|
||||
).size(64.dp)
|
||||
.padding(4.dp),
|
||||
val focusedItem = pager?.getOrNull(position)
|
||||
if (state.viewOptions.showBackdrop) {
|
||||
LaunchedEffect(focusedItem) {
|
||||
focusedItem?.let(viewModel::updateBackdrop)
|
||||
}
|
||||
}
|
||||
CollectionFolderHeader(
|
||||
showHeader = showHeader || state.items !is DataLoadingState.Success,
|
||||
showTitle = showTitle,
|
||||
playEnabled = playEnabled && state.items.successValue?.isNotEmpty() == true,
|
||||
title = title,
|
||||
sortAndDirection = state.sortAndDirection,
|
||||
onSortChange = {
|
||||
viewModel.onSortChange(it, recursive, state.filter)
|
||||
},
|
||||
sortOptions = sortOptions,
|
||||
currentFilter = state.filter,
|
||||
onFilterChange = {
|
||||
viewModel.onFilterChange(it, recursive)
|
||||
},
|
||||
getPossibleFilterValues = {
|
||||
viewModel.getFilterOptionValues(it)
|
||||
},
|
||||
filterOptions = filterOptions,
|
||||
onClickPlayAll = gridActions.onClickPlayAll!!,
|
||||
onClickShowViewOptions = { showViewOptions = true },
|
||||
modifier = Modifier.focusRequester(headerRowFocusRequester),
|
||||
)
|
||||
|
||||
when (val pager = state.items) {
|
||||
is DataLoadingState.Error -> {
|
||||
LaunchedEffect(Unit) {
|
||||
(focusRequesterOnEmpty ?: headerRowFocusRequester).tryRequestFocus()
|
||||
}
|
||||
ErrorMessage(pager, Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
DataLoadingState.Loading,
|
||||
DataLoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage(Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
is DataLoadingState.Success<List<BaseItem?>> -> {
|
||||
LaunchedEffect(Unit) {
|
||||
if (pager.data.isNotEmpty()) {
|
||||
gridFocusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
if (state.viewOptions.type == ViewOptionsType.GRID) {
|
||||
CollectionFolderGrid(
|
||||
preferences = preferences,
|
||||
collectionType = item?.data?.collectionType,
|
||||
initialPosition = viewModel.position,
|
||||
items = pager.data,
|
||||
sortAndDirection = state.sortAndDirection,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
gridFocusRequester = gridFocusRequester,
|
||||
onClickItem = gridActions.onClickItem,
|
||||
onLongClickItem = gridActions.onLongClickItem!!,
|
||||
positionCallback = { columns, pos ->
|
||||
viewModel.position = pos
|
||||
showHeader = pos < columns
|
||||
position = pos
|
||||
positionCallback?.invoke(columns, pos)
|
||||
},
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
viewOptions = state.viewOptions,
|
||||
onClickPlay = gridActions.onClickPlayRemoteButton!!,
|
||||
focusedItem = focusedItem,
|
||||
)
|
||||
} else {
|
||||
CollectionFolderList(
|
||||
preferences = preferences,
|
||||
collectionType = item?.data?.collectionType,
|
||||
initialPosition = viewModel.position,
|
||||
items = pager.data,
|
||||
sortAndDirection = state.sortAndDirection,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
gridFocusRequester = gridFocusRequester,
|
||||
onClickItem = gridActions.onClickItem,
|
||||
onLongClickItem = gridActions.onLongClickItem!!,
|
||||
positionCallback = { columns, pos ->
|
||||
viewModel.position = pos
|
||||
showHeader = pos < columns
|
||||
position = pos
|
||||
positionCallback?.invoke(columns, pos)
|
||||
},
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
viewOptions = state.viewOptions,
|
||||
onClickPlay = gridActions.onClickPlayRemoteButton!!,
|
||||
focusedItem = focusedItem,
|
||||
)
|
||||
}
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
state.backgroundLoading == LoadingState.Loading,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
CircularProgress(
|
||||
Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.background.copy(alpha = .25f),
|
||||
shape = CircleShape,
|
||||
).size(64.dp)
|
||||
.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ class SeriesViewModel
|
|||
.firstOrNull()
|
||||
if (nextUp != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
navigateTo(Destination.Playback(nextUp.id, 0L))
|
||||
navigateTo(Destination.Playback(BaseItem(nextUp)))
|
||||
}
|
||||
} else {
|
||||
showToast(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue