diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 1d70553e..bc07af92 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -44,7 +44,6 @@ import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.GridItemDetails import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.main.HomePageHeader -import com.github.damontecres.wholphin.ui.playback.scale import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType @@ -53,19 +52,18 @@ import kotlin.time.Duration @OptIn(ExperimentalFoundationApi::class) @Composable -fun CollectionFolderGrid( +fun CollectionFolderViewScope.CollectionFolderGrid( preferences: UserPreferences, - collectionType: CollectionType?, focusedItem: BaseItem?, items: List, sortAndDirection: SortAndDirection, onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, letterPosition: suspend (Char) -> Int, - viewOptions: ViewOptions, onClickPlay: (Int, BaseItem) -> Unit, initialPosition: Int, gridFocusRequester: FocusRequester, + cardContent: @Composable CollectionFolderViewScope.(GridItemDetails) -> Unit, modifier: Modifier = Modifier, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { @@ -99,18 +97,8 @@ fun CollectionFolderGrid( 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, - ) + cardContent = { details -> + cardContent.invoke(this@CollectionFolderGrid, details) }, columns = viewOptions.columns, spacing = viewOptions.spacing.dp, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt index a048ed94..1d951639 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -59,6 +60,7 @@ import com.github.damontecres.wholphin.ui.cards.FavoriteIndicator import com.github.damontecres.wholphin.ui.cards.WatchedIcon import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.detail.AlphabetButtons +import com.github.damontecres.wholphin.ui.detail.GridItemDetails import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.ifElse @@ -75,7 +77,7 @@ import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.extensions.ticks @Composable -fun CollectionFolderList( +fun CollectionFolderViewScope.CollectionFolderList( preferences: UserPreferences, collectionType: CollectionType?, focusedItem: BaseItem?, @@ -84,10 +86,10 @@ fun CollectionFolderList( onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, letterPosition: suspend (Char) -> Int, - viewOptions: ViewOptions, onClickPlay: (Int, BaseItem) -> Unit, initialPosition: Int, gridFocusRequester: FocusRequester, + itemContent: @Composable CollectionFolderViewScope.(GridItemDetails) -> Unit, modifier: Modifier = Modifier, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { @@ -153,10 +155,8 @@ fun CollectionFolderList( ).padding(8.dp), ) } - CollectionFolderContent( + this@CollectionFolderList.CollectionFolderListContent( items = items, - viewOptions = viewOptions, - collectionType = collectionType, listState = listState, position = position, positionFocusRequester = positionFocusRequester, @@ -167,6 +167,7 @@ fun CollectionFolderList( }, onClickItem = onClickItem, onLongClickItem = onLongClickItem, + itemContent = itemContent, modifier = Modifier .weight(1f) @@ -266,44 +267,43 @@ fun CollectionFolderListDetails( } @Composable -fun CollectionFolderContent( +fun CollectionFolderViewScope.CollectionFolderListContent( items: List, - viewOptions: ViewOptions, - collectionType: CollectionType?, listState: LazyListState, position: Int, positionFocusRequester: FocusRequester, onClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit, onPositionChange: (Int) -> Unit, + itemContent: @Composable CollectionFolderViewScope.(GridItemDetails) -> Unit, modifier: Modifier = Modifier, ) { + val currentPosition by rememberUpdatedState(position) LazyColumn( state = listState, verticalArrangement = Arrangement.spacedBy(viewOptions.spacing.dp), modifier = modifier.focusRestorer(positionFocusRequester), ) { itemsIndexed(items) { index, item -> - val onClick: () -> Unit = - remember(index, item) { - { item?.let { onClickItem.invoke(index, item) } } + val details = + remember(item, index, onClickItem, onLongClickItem, onPositionChange) { + GridItemDetails( + item = item, + index = index, + onClick = { item?.let { onClickItem.invoke(index, item) } }, + onLongClick = { item?.let { onLongClickItem.invoke(index, item) } }, + widthPx = -1, + mod = + Modifier + .onFocusChanged { + if (it.isFocused) onPositionChange.invoke(index) + }.ifElse( + index == currentPosition, + Modifier.focusRequester(positionFocusRequester), + ), + ) } - val onLongClick: () -> Unit = - remember(index, item) { - { item?.let { onLongClickItem.invoke(index, item) } } - } - CollectionFolderListItem( - item = item, - dense = viewOptions.type == ViewOptionsType.DENSE_LIST, - collectionType = collectionType, - onClick = onClick, - onLongClick = onLongClick, - modifier = - Modifier - .onFocusChanged { - if (it.isFocused) onPositionChange.invoke(index) - }.ifElse(index == position, Modifier.focusRequester(positionFocusRequester)), - ) + itemContent.invoke(this@CollectionFolderListContent, details) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt index e1e2b4ff..f383ed5e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt @@ -54,12 +54,15 @@ import com.github.damontecres.wholphin.services.ThemeSongPlayer import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.deleteItem import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.data.SortAndDirection +import com.github.damontecres.wholphin.ui.detail.GridItemDetails import com.github.damontecres.wholphin.ui.detail.music.addToQueue 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.playback.scale import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.toServerString @@ -843,6 +846,33 @@ fun CollectionFolderViewContent( positionCallback: ((columns: Int, position: Int) -> Unit)? = null, filterOptions: List> = DefaultFilterOptions, focusRequesterOnEmpty: FocusRequester? = null, + gridCardContent: @Composable CollectionFolderViewScope.( + GridItemDetails, + ) -> Unit = { (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, + ) + }, + listItemContent: @Composable CollectionFolderViewScope.( + GridItemDetails, + ) -> Unit = { (item, index, onClick, onLongClick, _, mod) -> + CollectionFolderListItem( + item = item, + dense = viewOptions.type == ViewOptionsType.DENSE_LIST, + collectionType = collectionType, + onClick = onClick, + onLongClick = onLongClick, + modifier = mod, + ) + }, ) { var position by rememberInt(savedPosition) @@ -1005,11 +1035,22 @@ fun CollectionFolderViewContent( focusRequester?.tryRequestFocus() clickedRandom = false } + val scope = + object : CollectionFolderViewScope { + override val viewOptions: ViewOptions + get() = state.viewOptions + override val collectionType: CollectionType? + get() = item?.data?.collectionType + override val sortAndDirection: SortAndDirection + get() = state.sortAndDirection + override val collectionSize: Int + get() = pager.data.size + } + Box(Modifier.fillMaxSize()) { if (state.viewOptions.type == ViewOptionsType.GRID) { - CollectionFolderGrid( + scope.CollectionFolderGrid( preferences = preferences, - collectionType = item?.data?.collectionType, initialPosition = savedPosition, items = pager.data, sortAndDirection = state.sortAndDirection, @@ -1023,12 +1064,12 @@ fun CollectionFolderViewContent( positionCallback?.invoke(columns, pos) }, letterPosition = { viewActions.positionOfLetter(it) ?: -1 }, - viewOptions = state.viewOptions, onClickPlay = gridActions.onClickPlayRemoteButton!!, focusedItem = focusedItem, + cardContent = gridCardContent, ) } else { - CollectionFolderList( + scope.CollectionFolderList( preferences = preferences, collectionType = item?.data?.collectionType, initialPosition = savedPosition, @@ -1044,9 +1085,9 @@ fun CollectionFolderViewContent( positionCallback?.invoke(columns, pos) }, letterPosition = { viewActions.positionOfLetter(it) ?: -1 }, - viewOptions = state.viewOptions, onClickPlay = gridActions.onClickPlayRemoteButton!!, focusedItem = focusedItem, + itemContent = listItemContent, ) } androidx.compose.animation.AnimatedVisibility( @@ -1085,3 +1126,13 @@ fun CollectionFolderViewContent( ) } } + +/** + * Scope with global data for [CollectionFolderView], [CollectionFolderGrid], [CollectionFolderList], and individual items + */ +interface CollectionFolderViewScope { + val viewOptions: ViewOptions + val collectionType: CollectionType? + val sortAndDirection: SortAndDirection + val collectionSize: Int +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index b7085db6..33960f83 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -1,49 +1,26 @@ package com.github.damontecres.wholphin.ui.detail import android.content.Context -import androidx.compose.foundation.background -import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf 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.FocusDirection -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.focusRestorer -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource @@ -51,60 +28,49 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import androidx.tv.material3.ListItem +import androidx.tv.material3.LocalTextStyle import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.data.filter.DefaultPlaylistItemsOptions -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.data.model.LibraryDisplayInfo +import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.services.BackdropService -import com.github.damontecres.wholphin.services.FavoriteWatchManager -import com.github.damontecres.wholphin.services.FilterOptionCache import com.github.damontecres.wholphin.services.MediaManagementService import com.github.damontecres.wholphin.services.MusicService -import com.github.damontecres.wholphin.services.MusicServiceState import com.github.damontecres.wholphin.services.NavigationManager -import com.github.damontecres.wholphin.services.PlaylistCreator -import com.github.damontecres.wholphin.services.ServerReportService import com.github.damontecres.wholphin.ui.FontAwesome -import com.github.damontecres.wholphin.ui.SlimItemFields -import com.github.damontecres.wholphin.ui.cards.ItemCardImage +import com.github.damontecres.wholphin.ui.cards.FavoriteIndicator +import com.github.damontecres.wholphin.ui.cards.WatchedIcon +import com.github.damontecres.wholphin.ui.collectLatestIn import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.Button +import com.github.damontecres.wholphin.ui.components.CollectionFolderState +import com.github.damontecres.wholphin.ui.components.CollectionFolderViewContent +import com.github.damontecres.wholphin.ui.components.CollectionFolderViewModel +import com.github.damontecres.wholphin.ui.components.CollectionFolderViewScope import com.github.damontecres.wholphin.ui.components.ContextMenu import com.github.damontecres.wholphin.ui.components.ContextMenuActions import com.github.damontecres.wholphin.ui.components.ContextMenuDialog -import com.github.damontecres.wholphin.ui.components.ErrorMessage -import com.github.damontecres.wholphin.ui.components.ExpandableFaButton -import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton -import com.github.damontecres.wholphin.ui.components.FilterByButton -import com.github.damontecres.wholphin.ui.components.GridTitle -import com.github.damontecres.wholphin.ui.components.LoadingPage +import com.github.damontecres.wholphin.ui.components.GridClickActions +import com.github.damontecres.wholphin.ui.components.ListItemWrapper import com.github.damontecres.wholphin.ui.components.MusicContextActions import com.github.damontecres.wholphin.ui.components.Optional -import com.github.damontecres.wholphin.ui.components.OverviewText -import com.github.damontecres.wholphin.ui.components.SortByButton import com.github.damontecres.wholphin.ui.components.TextButton +import com.github.damontecres.wholphin.ui.components.ViewOptions +import com.github.damontecres.wholphin.ui.components.ViewOptionsType import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel -import com.github.damontecres.wholphin.ui.data.BoxSetSortOptions +import com.github.damontecres.wholphin.ui.data.PlaylistSortOptions import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.detail.music.MusicQueueMarker import com.github.damontecres.wholphin.ui.detail.music.MusicViewModel import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.equalsNotNull -import com.github.damontecres.wholphin.ui.formatDateTime import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.formatTime -import com.github.damontecres.wholphin.ui.ifElse +import com.github.damontecres.wholphin.ui.gt import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.main.settings.MoveDirection @@ -113,13 +79,9 @@ import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundSeconds 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.LocalClock import com.github.damontecres.wholphin.util.ApiRequestPager -import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.GetItemsRequestHandler -import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.WholphinDispatchers +import com.github.damontecres.wholphin.util.successValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -127,62 +89,62 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.extensions.playlistsApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.SortOrder -import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.extensions.ticks import timber.log.Timber import java.util.UUID -import kotlin.time.Duration @HiltViewModel(assistedFactory = PlaylistViewModel.Factory::class) class PlaylistViewModel @AssistedInject constructor( + private val savedStateHandle: SavedStateHandle, @ApplicationContext context: Context, api: ApiClient, navigationManager: NavigationManager, musicService: MusicService, mediaManagementService: MediaManagementService, - private val backdropService: BackdropService, private val serverRepository: ServerRepository, - private val libraryDisplayInfoDao: LibraryDisplayInfoDao, - private val favoriteWatchManager: FavoriteWatchManager, - private val serverReportService: ServerReportService, - private val filterOptionCache: FilterOptionCache, - private val playlistCreator: PlaylistCreator, @Assisted itemId: UUID, + @Assisted private val collectionState: StateFlow, ) : MusicViewModel(itemId, context, api, musicService, navigationManager, mediaManagementService) { @AssistedFactory interface Factory { - fun create(itemId: UUID): PlaylistViewModel + fun create( + itemId: UUID, + collectionState: StateFlow, + ): PlaylistViewModel } - val state = MutableStateFlow(PlaylistDetailsState()) + private val _state = MutableStateFlow(PlaylistDetailsState()) + val state: StateFlow = _state + val musicState = musicService.state + var position: Int + get() = savedStateHandle.get("position") ?: 0 + set(value) { + savedStateHandle["position"] = value + } + init { init() } override fun init() { - state.update { it.copy(loading = LoadingState.Loading) } viewModelScope.launchDefault { try { - val playlist = - api.userLibraryApi - .getItem(itemId) - .content - .let { BaseItem(it, false) } val user = serverRepository.currentUser val canEdit = user?.let { user -> @@ -215,95 +177,12 @@ class PlaylistViewModel false } } ?: false - state.update { - it.copy( - playlist = playlist, - canEdit = canEdit, - ) - } - val libraryDisplayInfo = - user?.let { user -> - libraryDisplayInfoDao.getItem(user, itemId) - } - val filter = libraryDisplayInfo?.filter ?: GetItemsFilter() - val sortAndDirection = - libraryDisplayInfo?.sortAndDirection ?: SortAndDirection( - ItemSortBy.DEFAULT, - SortOrder.ASCENDING, - ) - loadItems(filter, sortAndDirection).join() + _state.update { it.copy(canEdit = canEdit) } determineMediaType() } catch (ex: Exception) { - Timber.e(ex, "Error fetching playlist %s", itemId) - state.update { it.copy(loading = LoadingState.Error(ex)) } - } - } - } - - fun loadItems( - filter: GetItemsFilter, - sortAndDirection: SortAndDirection, - ) = viewModelScope.launchIO { - backdropService.clearBackdrop() - state.update { - it.copy( - loading = LoadingState.Loading, - filterAndSort = FilterAndSort(filter, sortAndDirection), - ) - } - - serverRepository.currentUser?.let { user -> - viewModelScope.launchIO { - val libraryDisplayInfo = - libraryDisplayInfoDao.getItem(user, itemId)?.copy( - filter = filter, - sort = sortAndDirection.sort, - direction = sortAndDirection.direction, - ) - ?: LibraryDisplayInfo( - userId = user.rowId, - itemId = itemId.toServerString(), - sort = sortAndDirection.sort, - direction = sortAndDirection.direction, - filter = filter, - viewOptions = null, - ) - libraryDisplayInfoDao.saveItem(libraryDisplayInfo) - } - - val request = - filter.applyTo( - GetItemsRequest( - parentId = itemId, - userId = user.id, - fields = SlimItemFields, - sortBy = listOf(sortAndDirection.sort), - sortOrder = listOf(sortAndDirection.direction), - ), - ) - try { - val pager = - ApiRequestPager( - api, - request, - GetItemsRequestHandler, - viewModelScope, - ).init() - state.update { - it.copy( - items = pager, - loading = LoadingState.Success, - ) - } - } catch (ex: Exception) { - Timber.e(ex, "Error fetching playlist %s", itemId) - state.update { - it.copy( - items = emptyList(), - loading = LoadingState.Error(ex), - ) - } + Timber.e(ex, "Error fetching playlist info for %s", itemId) +// _state.update { it.copy(item = DataLoadingState.Error(ex)) } } } } @@ -313,77 +192,49 @@ class PlaylistViewModel * * In theory, the server will set the type, but sometimes it doesn't */ - private suspend fun determineMediaType() { - // Use the type the server says - var mediaType = - state.value.playlist - ?.data - ?.mediaType ?: MediaType.UNKNOWN - mediaType = - if (mediaType == MediaType.UNKNOWN) { - // Otherwise, if a most of the list is one type, we can assume that type - val pager = (state.value.items as? ApiRequestPager<*>) - if (pager != null && pager.size <= 50) { - val types = - (0..<50.coerceAtMost(pager.size)).groupBy { index -> - val pagerItem = pager.getBlocking(index) - when (pagerItem?.type) { - BaseItemKind.AUDIO -> MediaType.AUDIO + private fun determineMediaType() { + collectionState + .map { Pair(it.item, it.items) } + .distinctUntilChanged() + .catch { ex -> Timber.e(ex, "Error in determineMediaType") } + .collectLatestIn(viewModelScope) { (item, items) -> + var mediaType = item.successValue?.data?.mediaType ?: MediaType.UNKNOWN + mediaType = + if (mediaType == MediaType.UNKNOWN) { + // Otherwise, if a most of the list is one type, we can assume that type + val pager = (items.successValue as? ApiRequestPager<*>) + if (pager != null && pager.size <= 50) { + val types = + (0..<50.coerceAtMost(pager.size)).groupBy { index -> + val pagerItem = pager.getBlocking(index) + when (pagerItem?.type) { + BaseItemKind.AUDIO -> MediaType.AUDIO - BaseItemKind.VIDEO, - BaseItemKind.EPISODE, - BaseItemKind.MOVIE, - BaseItemKind.BOX_SET, - -> MediaType.VIDEO + BaseItemKind.VIDEO, + BaseItemKind.EPISODE, + BaseItemKind.MOVIE, + BaseItemKind.BOX_SET, + -> MediaType.VIDEO - else -> MediaType.UNKNOWN + else -> MediaType.UNKNOWN + } + } + if (types.keys.size == 1) { + types.keys.first() + } else { + MediaType.UNKNOWN } + } else { + MediaType.UNKNOWN } - if (types.keys.size == 1) { - types.keys.first() } else { - MediaType.UNKNOWN + mediaType } - } else { - MediaType.UNKNOWN + Timber.d("mediaType=%s", mediaType) + _state.update { + it.copy(mediaType = mediaType) } - } else { - mediaType } - Timber.d("mediaType=%s", mediaType) - state.update { - it.copy(mediaType = mediaType) - } - } - - suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List = - filterOptionCache.getFilterOptionValues( - itemId, - filterOption, - ) - - fun updateBackdrop(item: BaseItem) { - viewModelScope.launchIO { - backdropService.submit(item) - } - } - - fun setWatched( - itemId: UUID, - played: Boolean, - ) = viewModelScope.launch(ExceptionHandler() + WholphinDispatchers.IO) { - favoriteWatchManager.setWatched(itemId, played) - } - - fun setFavorite( - itemId: UUID, - favorite: Boolean, - ) = viewModelScope.launch(ExceptionHandler() + WholphinDispatchers.IO) { - favoriteWatchManager.setFavorite(itemId, favorite) - } - - fun sendMediaReport(itemId: UUID) { - viewModelScope.launchDefault { serverReportService.sendMediaReportFor(itemId) } } fun removeFromPlaylist( @@ -392,11 +243,12 @@ class PlaylistViewModel ) { viewModelScope.launchIO { try { - playlistCreator.removeFromServerPlaylist( - playlistId = this@PlaylistViewModel.itemId, - itemId = itemId, + api.playlistsApi.removeItemFromPlaylist( + this@PlaylistViewModel.itemId.toServerString(), + listOf(itemId.toServerString()), ) - (state.value.items as? ApiRequestPager<*>)?.refreshPagesAfter(index) + (collectionState.value.items.successValue as? ApiRequestPager<*>) + ?.refreshPagesAfter(index) } catch (ex: CancellationException) { throw ex } catch (ex: Exception) { @@ -423,31 +275,15 @@ class PlaylistViewModel itemId = itemId.toServerString(), newIndex = newIndex, ) - (state.value.items as? ApiRequestPager<*>)?.refreshPagesAfter(index - 1) + (collectionState.value.items.successValue as? ApiRequestPager<*>)?.refreshPagesAfter( + index - 1, + ) } } } -@Immutable -data class FilterAndSort( - val filter: GetItemsFilter, - val sortAndDirection: SortAndDirection, -) - data class PlaylistDetailsState( - val playlist: BaseItem? = null, val mediaType: MediaType = MediaType.UNKNOWN, - val items: List = emptyList(), - val filterAndSort: FilterAndSort = - FilterAndSort( - filter = GetItemsFilter(), - sortAndDirection = - SortAndDirection( - ItemSortBy.DEFAULT, - SortOrder.ASCENDING, - ), - ), - val loading: LoadingState = LoadingState.Pending, val canEdit: Boolean = false, ) @@ -456,14 +292,33 @@ fun PlaylistDetails( preferences: UserPreferences, destination: Destination.MediaItem, modifier: Modifier = Modifier, + collectionViewModel: CollectionFolderViewModel = + hiltViewModel( + creationCallback = { + it.create( + itemId = destination.itemId.toServerString(), + initialSortAndDirection = + SortAndDirection( + ItemSortBy.DEFAULT, + SortOrder.ASCENDING, + ), + recursive = false, + collectionFilter = CollectionFolderFilter(), + useSeriesForPrimary = false, + defaultViewOptions = playlistDefaultViewOptions, + ) + }, + ), viewModel: PlaylistViewModel = hiltViewModel( - creationCallback = { it.create(destination.itemId) }, + creationCallback = { it.create(destination.itemId, collectionViewModel.state) }, ), addToPlaylistViewModel: AddPlaylistViewModel = hiltViewModel(), ) { - val state by viewModel.state.collectAsState() + val state by collectionViewModel.state.collectAsState() + val playlistState by viewModel.state.collectAsState() val musicState by viewModel.musicState.collectAsState() + val focusManager = LocalFocusManager.current var showContextMenu by remember { mutableStateOf(null) } var showConfirmTypeDialog by remember { mutableStateOf?>(null) } @@ -476,15 +331,15 @@ fun PlaylistDetails( shuffle: Boolean, mediaTypeOverride: MediaType? = null, ) { - when (mediaTypeOverride ?: state.mediaType) { + when (mediaTypeOverride ?: playlistState.mediaType) { MediaType.VIDEO -> { - viewModel.navigationManager.navigateTo( + collectionViewModel.navigateTo( Destination.PlaybackList( itemId = destination.itemId, startIndex = index, shuffle = shuffle, - filter = state.filterAndSort.filter, - sortAndDirection = state.filterAndSort.sortAndDirection, + filter = state.filter, + sortAndDirection = state.sortAndDirection, ), ) } @@ -498,93 +353,152 @@ fun PlaylistDetails( } } } - val musicContextActions = - remember { - MusicContextActions( - navigateTo = { viewModel.navigationManager.navigateTo(it) }, - onClickPlay = { index, item -> play(index, item, false, MediaType.AUDIO) }, - onClickPlayNext = { index, item -> viewModel.playNext(item) }, - onClickAddToQueue = { item -> viewModel.addToQueue(item, Int.MAX_VALUE) }, - onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, - onClickAddPlaylist = { itemId -> - addToPlaylistViewModel.loadPlaylists() - showPlaylistDialog.makePresent(itemId) - }, - onClickRemoveFromQueue = { _, _ -> }, - onDeleteItem = viewModel::deleteItem, - onRemoveFromPlaylist = viewModel::removeFromPlaylist, - ) - } - val contextActions = - remember { - ContextMenuActions( - navigateTo = { viewModel.navigationManager.navigateTo(it) }, - onClickWatch = { id, watched -> viewModel.setWatched(id, watched) }, - onClickFavorite = { id, favorite -> viewModel.setFavorite(id, favorite) }, - onClickAddPlaylist = { itemId -> - addToPlaylistViewModel.loadPlaylists() - showPlaylistDialog.makePresent(itemId) - }, - onSendMediaInfo = viewModel::sendMediaReport, - onDeleteItem = viewModel::deleteItem, - onClickAddToQueue = { viewModel.addToQueue(it, 0) }, - onShowOverview = {}, - onChooseVersion = { _, _ -> }, - onChooseTracks = {}, - onClearChosenStreams = {}, - onClickRemoveFromNextUp = {}, - onRemoveFromPlaylist = viewModel::removeFromPlaylist, - ) - } - PlaylistDetailsContent( - loadingState = state.loading, - playlist = state.playlist, - items = state.items, - musicState = musicState, - onChangeBackdrop = viewModel::updateBackdrop, - onClickIndex = { index, item -> - play(index, item, false) - }, - onClickPlay = { shuffle -> - state.playlist?.let { - play(0, it, shuffle) + fun showContextMenu( + index: Int, + item: BaseItem, + fromLongClick: Boolean, + ) { + showContextMenu = + if (item.type == BaseItemKind.AUDIO) { + val musicContextActions = + MusicContextActions( + navigateTo = { collectionViewModel.navigateTo(it) }, + onClickPlay = { index, item -> + play( + index, + item, + false, + MediaType.AUDIO, + ) + }, + onClickPlayNext = { index, item -> viewModel.playNext(item) }, + onClickAddToQueue = { item -> + viewModel.addToQueue( + item, + Int.MAX_VALUE, + ) + }, + onClickFavorite = { id, favorite -> + collectionViewModel.setFavorite(index, id, favorite) + }, + onClickAddPlaylist = { itemId -> + addToPlaylistViewModel.loadPlaylists() + showPlaylistDialog.makePresent(itemId) + }, + onClickRemoveFromQueue = { _, _ -> }, + onDeleteItem = { collectionViewModel.deleteItem(index, item) }, + onRemoveFromPlaylist = viewModel::removeFromPlaylist, + ) + ContextMenu.ForMusic( + fromLongClick = fromLongClick, + item = item, + index = index, + canDelete = viewModel.canDelete(item, preferences.appPreferences), + canRemoveFromQueue = false, + actions = musicContextActions, + showRemoveFromPlaylist = playlistState.canEdit, + ) + } else { + val contextActions = + ContextMenuActions( + navigateTo = { collectionViewModel.navigateTo(it) }, + onClickWatch = { id, watched -> + collectionViewModel.setWatched( + index, + id, + watched, + ) + }, + onClickFavorite = { id, favorite -> + collectionViewModel.setFavorite(index, id, favorite) + }, + onClickAddPlaylist = { itemId -> + addToPlaylistViewModel.loadPlaylists() + showPlaylistDialog.makePresent(itemId) + }, + onSendMediaInfo = collectionViewModel::sendReportFor, + onDeleteItem = { collectionViewModel.deleteItem(index, item) }, + onClickAddToQueue = { viewModel.addToQueue(it, 0) }, + onShowOverview = {}, + onChooseVersion = { _, _ -> }, + onChooseTracks = {}, + onClearChosenStreams = {}, + onClickRemoveFromNextUp = {}, + onRemoveFromPlaylist = viewModel::removeFromPlaylist, + ) + ContextMenu.ForBaseItem( + fromLongClick = fromLongClick, + item = item, + index = index, + chosenStreams = null, + showGoTo = true, + showStreamChoices = false, + canDelete = viewModel.canDelete(item, preferences.appPreferences), + canRemoveContinueWatching = false, + canRemoveNextUp = false, + actions = contextActions, + showRemoveFromPlaylist = playlistState.canEdit, + ) } - }, - onShowContextMenu = { index, item, fromLongClick -> - showContextMenu = - if (item.type == BaseItemKind.AUDIO) { - ContextMenu.ForMusic( - fromLongClick = fromLongClick, - item = item, - index = index, - canDelete = viewModel.canDelete(item, preferences.appPreferences), - canRemoveFromQueue = false, - actions = musicContextActions, - showRemoveFromPlaylist = state.canEdit, - ) - } else { - ContextMenu.ForBaseItem( - fromLongClick = fromLongClick, - item = item, - index = index, - chosenStreams = null, - showGoTo = true, - showStreamChoices = false, - canDelete = viewModel.canDelete(item, preferences.appPreferences), - canRemoveContinueWatching = false, - canRemoveNextUp = false, - actions = contextActions, - showRemoveFromPlaylist = state.canEdit, - ) - } - }, - filterAndSort = state.filterAndSort, - onFilterAndSortChange = viewModel::loadItems, - getPossibleFilterValues = viewModel::getFilterOptionValues, - canEdit = state.canEdit, - onMoveItem = viewModel::onMoveItem, + } + + val actions = + remember { + GridClickActions( + onClickItem = { index, item -> collectionViewModel.navigateTo(item.destination(index)) }, + onLongClickItem = { index, item -> showContextMenu(index, item, true) }, + onClickPlayAll = { shuffle -> + state.item.successValue?.let { playlist -> play(0, playlist, shuffle) } + }, + onClickPlayRemoteButton = { index, item -> play(index, item, false) }, + ) + } + CollectionFolderViewContent( + preferences = preferences, + state = state, + savedPosition = viewModel.position, + itemId = destination.itemId.toServerString(), + initialFilter = CollectionFolderFilter(filter = state.filter), + recursive = false, + actions = actions, + sortOptions = PlaylistSortOptions, + playEnabled = true, + defaultViewOptions = playlistDefaultViewOptions, + viewActions = collectionViewModel, + provider = collectionViewModel, + positionCallback = { _, position -> viewModel.position = position }, modifier = modifier, + listItemContent = { (item, index, onClick, onLongClick, _, mod) -> + PlaylistItem( + item = item, + index = index, + onClick = onClick, + onLongClick = onLongClick, + canMove = playlistState.canEdit, + moveUpAllowed = index > 0, + moveDownAllowed = + state.items.successValue + ?.lastIndex + ?.gt(index) ?: false, + onClickMove = { direction -> + item?.let { viewModel.onMoveItem(index, item.id, direction) } + when (direction) { + MoveDirection.UP -> focusManager.moveFocus(FocusDirection.Up) + MoveDirection.DOWN -> focusManager.moveFocus(FocusDirection.Down) + } + }, + onClickMore = { item?.let { showContextMenu(index, item, false) } }, + modifier = mod, +// interactionSource = TODO(), + isPlaying = + equalsNotNull( + musicState.currentItemId, + item?.id, + ), + isQueued = remember(musicState.queueVersion) { item?.id in musicState.queuedIds }, + ) + }, ) showContextMenu?.let { contextMenu -> ContextMenuDialog( @@ -621,294 +535,7 @@ fun PlaylistDetails( } @Composable -fun PlaylistDetailsContent( - playlist: BaseItem?, - items: List, - musicState: MusicServiceState, - onClickIndex: (Int, BaseItem) -> Unit, - onShowContextMenu: (Int, BaseItem, Boolean) -> Unit, - onClickPlay: (shuffle: Boolean) -> Unit, - onChangeBackdrop: (BaseItem) -> Unit, - onMoveItem: (Int, UUID, MoveDirection) -> Unit, - filterAndSort: FilterAndSort, - onFilterAndSortChange: (GetItemsFilter, SortAndDirection) -> Unit, - getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, - loadingState: LoadingState, - canEdit: Boolean, - modifier: Modifier = Modifier, -) { - var savedIndex by rememberSaveable { mutableIntStateOf(0) } - var focusedIndex by remember { mutableIntStateOf(savedIndex) } - val focusedItem = items.getOrNull(focusedIndex) - LaunchedEffect(focusedItem) { - focusedItem?.let(onChangeBackdrop) - } - val focusRequester = remember { FocusRequester() } - LaunchedEffect(loadingState) { - if (loadingState is LoadingState.Success || loadingState is LoadingState.Error) { - focusRequester.tryRequestFocus() - } - } - - val playButtonFocusRequester = remember { FocusRequester() } - - Box( - modifier = modifier, - ) { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = - Modifier - .fillMaxSize(), - ) { - val title = - if (loadingState is LoadingState.Success) { - playlist?.name ?: stringResource(R.string.playlist) - } else { - "" - } - GridTitle(title) - Row( - horizontalArrangement = Arrangement.spacedBy(24.dp), - modifier = - Modifier - .fillMaxWidth(), - ) { - PlaylistDetailsHeader( - focusedItem = focusedItem, - onClickPlay = onClickPlay, - playButtonFocusRequester = playButtonFocusRequester, - focusRequester = if (items.isEmpty()) focusRequester else remember { FocusRequester() }, - filterAndSort = filterAndSort, - onFilterAndSortChange = onFilterAndSortChange, - getPossibleFilterValues = getPossibleFilterValues, - filterOptions = DefaultPlaylistItemsOptions, - modifier = - Modifier - .padding(top = 80.dp) - .fillMaxWidth(.25f), - ) - val filterCount = - remember(filterAndSort) { - filterAndSort.filter.countFilters(DefaultPlaylistItemsOptions) - } - PlaylistItems( - loadingState = loadingState, - items = items, - musicState = musicState, - playButtonFocusRequester = playButtonFocusRequester, - onFocusItem = { index, item -> - focusedIndex = index - }, - onClickItem = { index, item -> - savedIndex = index - item?.let { onClickIndex.invoke(index, item) } - }, - onShowContextMenu = { index, item, fromLongClick -> - savedIndex = index - item?.let { onShowContextMenu.invoke(index, item, fromLongClick) } - }, - canMove = canEdit && filterAndSort.sortAndDirection.sort == ItemSortBy.DEFAULT && filterCount == 0, - onMoveItem = onMoveItem, - modifier = - Modifier - .padding(start = 16.dp) - .weight(1f) - .focusRequester(focusRequester) - .focusProperties { - onExit = { - playButtonFocusRequester.tryRequestFocus() - } - }, - ) - } - } - } -} - -@Composable -fun PlaylistDetailsHeader( - focusedItem: BaseItem?, - filterOptions: List>, - onClickPlay: (shuffle: Boolean) -> Unit, - playButtonFocusRequester: FocusRequester, - focusRequester: FocusRequester, - filterAndSort: FilterAndSort, - onFilterAndSortChange: (GetItemsFilter, SortAndDirection) -> Unit, - getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, - modifier: Modifier = Modifier, -) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier, - ) { - ExpandablePlayButton( - title = R.string.play, - resume = Duration.ZERO, - icon = Icons.Default.PlayArrow, - onClick = { onClickPlay.invoke(false) }, - modifier = Modifier.focusRequester(playButtonFocusRequester), - ) - ExpandableFaButton( - title = R.string.shuffle, - iconStringRes = R.string.fa_shuffle, - onClick = { onClickPlay.invoke(true) }, - ) - } - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier, - ) { - FilterByButton( - filterOptions = filterOptions, - current = filterAndSort.filter, - onFilterChange = { - onFilterAndSortChange.invoke( - it, - filterAndSort.sortAndDirection, - ) - }, - getPossibleValues = getPossibleFilterValues, - modifier = Modifier.focusRequester(focusRequester), - ) - SortByButton( - sortOptions = BoxSetSortOptions, - current = filterAndSort.sortAndDirection, - onSortChange = { onFilterAndSortChange.invoke(filterAndSort.filter, it) }, - ) - } - Text( - text = focusedItem?.title ?: "", - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.headlineSmall, - ) - Text( - text = focusedItem?.subtitle ?: "", - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleMedium, - ) - if (focusedItem?.type == BaseItemKind.EPISODE && focusedItem.data.premiereDate != null) { - Text( - text = formatDateTime(focusedItem.data.premiereDate!!), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleSmall, - ) - } - OverviewText( - overview = focusedItem?.data?.overview ?: "", - maxLines = 10, - onClick = {}, - enabled = false, - ) - } -} - -@Composable -fun PlaylistItems( - loadingState: LoadingState, - items: List, - musicState: MusicServiceState, - playButtonFocusRequester: FocusRequester, - canMove: Boolean, - onMoveItem: (Int, UUID, MoveDirection) -> Unit, - onFocusItem: (Int, BaseItem?) -> Unit, - onClickItem: (Int, BaseItem?) -> Unit, - onShowContextMenu: (Int, BaseItem?, Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - val focusManager = LocalFocusManager.current - when (loadingState) { - is LoadingState.Error -> { - ErrorMessage(loadingState, modifier) - } - - LoadingState.Pending, LoadingState.Loading -> { - LoadingPage(modifier) - } - - LoadingState.Success -> { - if (items.isNotEmpty()) { - LazyColumn( - contentPadding = PaddingValues(8.dp), - modifier = - modifier - .padding(bottom = 32.dp) - .fillMaxHeight() - .background( - MaterialTheme.colorScheme - .surfaceColorAtElevation(1.dp) - .copy(alpha = .75f), - shape = RoundedCornerShape(16.dp), - ).focusGroup() - .focusRestorer(), - ) { - itemsIndexed(items) { index, item -> - PlaylistItem( - item = item, - index = index, - onClick = { - onClickItem.invoke(index, item) - }, - onLongClick = { - onShowContextMenu.invoke(index, item, true) - }, - isPlaying = - equalsNotNull( - musicState.currentItemId, - item?.id, - ), - isQueued = item?.id in musicState.queuedIds, - canMove = canMove, - moveUpAllowed = index > 0, - moveDownAllowed = index < items.lastIndex, - onClickMove = { direction -> - item?.let { onMoveItem.invoke(index, item.id, direction) } - when (direction) { - MoveDirection.UP -> focusManager.moveFocus(FocusDirection.Up) - MoveDirection.DOWN -> focusManager.moveFocus(FocusDirection.Down) - } - }, - onClickMore = { onShowContextMenu.invoke(index, item, false) }, - modifier = - Modifier - .animateItem() - .ifElse( - item?.type != BaseItemKind.AUDIO, - Modifier.height(80.dp), - ).onFocusChanged { - if (it.hasFocus) { - onFocusItem(index, item) - } - }, - ) - } - } - } else { - LaunchedEffect(Unit) { - playButtonFocusRequester.tryRequestFocus() - } - Box( - contentAlignment = Alignment.Center, - modifier = modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(R.string.no_results), - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center, - modifier = Modifier, - ) - } - } - } - } -} - -@Composable -fun PlaylistItem( +fun CollectionFolderViewScope.PlaylistItem( item: BaseItem?, index: Int, onClick: () -> Unit, @@ -923,19 +550,18 @@ fun PlaylistItem( isPlaying: Boolean = false, isQueued: Boolean = false, ) { + val dense = viewOptions.type == ViewOptionsType.DENSE_LIST val focused by interactionSource.collectIsFocusedAsState() - val imageWidth = 160.dp - val density = LocalDensity.current - val imageWidthPx = remember(imageWidth, density) { with(density) { imageWidth.roundToPx() } } Row( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier - .fillMaxWidth() - .heightIn(min = 40.dp, max = 88.dp), + .fillMaxWidth(), +// .heightIn(min = 40.dp, max = 88.dp), ) { - ListItem( + ListItemWrapper( + dense = dense, selected = false, onClick = onClick, onLongClick = onLongClick, @@ -954,19 +580,21 @@ fun PlaylistItem( }, trailingContent = { val duration = - when (item?.type) { - BaseItemKind.AUDIO -> { - item.data.runTimeTicks - ?.ticks - ?.roundSeconds - } + remember(item?.data?.runTimeTicks) { + when (item?.type) { + BaseItemKind.AUDIO -> { + item.data.runTimeTicks + ?.ticks + ?.roundSeconds + } - else -> { - item - ?.data - ?.runTimeTicks - ?.ticks - ?.roundMinutes + else -> { + item + ?.data + ?.runTimeTicks + ?.ticks + ?.roundMinutes + } } } duration?.let { duration -> @@ -980,15 +608,25 @@ fun PlaylistItem( val resources = LocalResources.current val durationText = remember(resources, duration) { resources.formatDuration(duration) } - Column { - Text( - text = durationText, - ) - if (item?.type != BaseItemKind.AUDIO) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column { Text( - text = stringResource(R.string.ends_at, endTimeStr), - style = MaterialTheme.typography.bodySmall, + text = durationText, ) + if (item?.type != BaseItemKind.AUDIO && !dense) { + val style = + LocalTextStyle.current.let { it.copy(fontSize = it.fontSize * .75) } + Text( + text = stringResource(R.string.ends_at, endTimeStr), + style = style, + ) + } + } + if (item?.played == true) { + WatchedIcon(padding = 0.dp) } } } @@ -996,41 +634,29 @@ fun PlaylistItem( leadingContent = { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Text( - text = "${index + 1}.", - style = MaterialTheme.typography.labelLarge, - ) + if (sortAndDirection.sort == ItemSortBy.DEFAULT) { +// val indexText = +// remember(index, collectionSize, sortAndDirection) { +// when (sortAndDirection.direction) { +// SortOrder.ASCENDING -> "${index + 1}." +// SortOrder.DESCENDING -> "${collectionSize - index}." +// } +// } + // TODO default descending doesn't work? + val indexText = "${index + 1}." + Text( + text = indexText, + ) + } if (item?.type == BaseItemKind.AUDIO) { MusicQueueMarker( isPlaying = isPlaying, isQueued = isQueued, ) - } else { - val imageType = - remember(item) { - if (item != null && ImageType.THUMB in item.data.imageTags.orEmpty()) { - ImageType.THUMB - } else { - ImageType.PRIMARY - } - } - ItemCardImage( - item = item, - name = item?.name, - imageType = imageType, - showOverlay = true, - favorite = item?.data?.userData?.isFavorite ?: false, - watched = item?.data?.userData?.played ?: false, - unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1, - watchedPercent = 0.0, - numberOfVersions = item?.data?.mediaSourceCount ?: 0, - modifier = Modifier.width(imageWidth), - useFallbackText = false, - fillWidth = imageWidthPx, - ) } + if (item?.favorite == true) FavoriteIndicator() } }, modifier = Modifier.weight(1f), @@ -1120,3 +746,10 @@ fun ConfirmMediaTypeDialog( } } } + +private val playlistDefaultViewOptions = + ViewOptions( + spacing = 0, + showDetails = true, + type = ViewOptionsType.LIST, + )