diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt index cf7b32d6..2f39f238 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt @@ -41,6 +41,7 @@ sealed interface ContextMenu { data class ForBaseItem( val fromLongClick: Boolean, val item: BaseItem, + val index: Int = UNSPECIFIED_INDEX, val chosenStreams: ChosenStreams?, val showGoTo: Boolean, val showStreamChoices: Boolean, @@ -48,6 +49,7 @@ sealed interface ContextMenu { val canRemoveContinueWatching: Boolean, val canRemoveNextUp: Boolean, val actions: ContextMenuActions, + val showRemoveFromPlaylist: Boolean = false, ) : ContextMenu data class ForPerson( @@ -63,6 +65,7 @@ sealed interface ContextMenu { val canDelete: Boolean, val canRemoveFromQueue: Boolean, val actions: MusicContextActions, + val showRemoveFromPlaylist: Boolean = false, ) : ContextMenu data class ForQueue( @@ -71,6 +74,10 @@ sealed interface ContextMenu { val index: Int, val actions: QueueContextActions, ) : ContextMenu + + companion object { + const val UNSPECIFIED_INDEX = -12 + } } data class ContextMenuActions( @@ -87,6 +94,7 @@ data class ContextMenuActions( val onClickGoTo: (BaseItem) -> Unit = { navigateTo(it.destination()) }, val onClickRemoveFromNextUp: (BaseItem) -> Unit = {}, val onClickAddToQueue: (BaseItem) -> Unit = {}, + val onRemoveFromPlaylist: (Int, UUID) -> Unit = { _, _ -> }, ) data class PersonContextActions( @@ -109,6 +117,7 @@ data class MusicContextActions( val onClickGoToArtist: (UUID) -> Unit = { navigateTo.invoke(Destination.MediaItem(itemId = it, type = BaseItemKind.MUSIC_ARTIST)) }, + val onRemoveFromPlaylist: (Int, UUID) -> Unit = { _, _ -> }, ) data class QueueContextActions( @@ -200,11 +209,7 @@ fun ContextMenu( seriesId = item.data.seriesId, sourceId = chosenStreams?.source?.id?.toUUIDOrNull(), canClearChosenStreams = chosenStreams.let { it?.itemPlayback != null || it?.plc != null }, - showGoTo = contextMenu.showGoTo, - showStreamChoices = contextMenu.showStreamChoices, - canDelete = contextMenu.canDelete, - canRemoveContinueWatching = contextMenu.canRemoveContinueWatching, - canRemoveNextUp = contextMenu.canRemoveNextUp, + contextMenu = contextMenu, actions = actions, onChooseVersion = { chooseVersion = @@ -319,17 +324,13 @@ fun ContextMenu( private fun buildContextMenuItems( resources: Resources, + contextMenu: ContextMenu.ForBaseItem, item: BaseItem, seriesId: UUID?, sourceId: UUID?, watched: Boolean, favorite: Boolean, canClearChosenStreams: Boolean, - showGoTo: Boolean, - showStreamChoices: Boolean, - canDelete: Boolean, - canRemoveContinueWatching: Boolean, - canRemoveNextUp: Boolean, actions: ContextMenuActions, onChooseVersion: () -> Unit, onChooseTracks: (MediaStreamType) -> Unit, @@ -340,7 +341,7 @@ private fun buildContextMenuItems( ): List = buildList { // Songs should not show Go to - if (showGoTo && item.type != BaseItemKind.AUDIO) { + if (contextMenu.showGoTo && item.type != BaseItemKind.AUDIO) { add( DialogItem( resources.getString(R.string.go_to), @@ -414,7 +415,7 @@ private fun buildContextMenuItems( }, ) } - if (showStreamChoices) { + if (contextMenu.showStreamChoices) { item.data.mediaSources?.letNotEmpty { sources -> val source = sourceId?.let { sources.firstOrNull { it.id?.toUUIDOrNull() == sourceId } } @@ -478,6 +479,17 @@ private fun buildContextMenuItems( }, ) } + if (contextMenu.showRemoveFromPlaylist) { + add( + DialogItem( + text = R.string.remove_from_playlist, + iconStringRes = R.string.fa_circle_xmark, + dismissOnClick = true, + ) { + actions.onRemoveFromPlaylist.invoke(contextMenu.index, item.id) + }, + ) + } add( DialogItem( text = R.string.add_to_playlist, @@ -487,7 +499,7 @@ private fun buildContextMenuItems( actions.onClickAddPlaylist.invoke(item.id) }, ) - if (canDelete) { + if (contextMenu.canDelete) { add( DialogItem( resources.getString(R.string.delete), @@ -499,7 +511,7 @@ private fun buildContextMenuItems( }, ) } - if (canRemoveContinueWatching && !watched && item.playbackPosition > Duration.ZERO) { + if (contextMenu.canRemoveContinueWatching && !watched && item.playbackPosition > Duration.ZERO) { add( DialogItem( text = R.string.remove_continue_watching, @@ -510,7 +522,7 @@ private fun buildContextMenuItems( }, ) } - if (canRemoveNextUp && item.type == BaseItemKind.EPISODE && item.data.seriesId != null) { + if (contextMenu.canRemoveNextUp && item.type == BaseItemKind.EPISODE && item.data.seriesId != null) { add( DialogItem( text = R.string.remove_next_up, @@ -602,7 +614,7 @@ private fun buildContextMenuItems( }, ) } - if (showStreamChoices && canClearChosenStreams) { + if (contextMenu.showStreamChoices && canClearChosenStreams) { add( DialogItem( resources.getString(R.string.clear_track_choices), @@ -785,6 +797,17 @@ fun buildContextForMusic( }, ) } + if (music.showRemoveFromPlaylist) { + add( + DialogItem( + text = R.string.remove_from_playlist, + iconStringRes = R.string.fa_circle_xmark, + dismissOnClick = true, + ) { + actions.onRemoveFromPlaylist.invoke(index, item.id) + }, + ) + } add( DialogItem( text = R.string.add_to_playlist, 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 fa3443e3..b7085db6 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 @@ -14,9 +14,11 @@ 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 @@ -34,6 +36,7 @@ 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 @@ -41,6 +44,7 @@ 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 import androidx.compose.ui.text.style.TextAlign @@ -69,10 +73,13 @@ 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.components.BasicDialog +import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.components.ContextMenu import com.github.damontecres.wholphin.ui.components.ContextMenuActions import com.github.damontecres.wholphin.ui.components.ContextMenuDialog @@ -100,9 +107,11 @@ import com.github.damontecres.wholphin.ui.formatTime import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.main.settings.MoveDirection import com.github.damontecres.wholphin.ui.nav.Destination 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 @@ -116,12 +125,16 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject 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.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 @@ -146,6 +159,7 @@ class PlaylistViewModel private val favoriteWatchManager: FavoriteWatchManager, private val serverReportService: ServerReportService, private val filterOptionCache: FilterOptionCache, + private val playlistCreator: PlaylistCreator, @Assisted itemId: UUID, ) : MusicViewModel(itemId, context, api, musicService, navigationManager, mediaManagementService) { @AssistedFactory @@ -169,9 +183,47 @@ class PlaylistViewModel .getItem(itemId) .content .let { BaseItem(it, false) } - state.update { it.copy(playlist = playlist) } + val user = serverRepository.currentUser + val canEdit = + user?.let { user -> + try { + val permission by api.playlistsApi.getPlaylistUser(itemId, user.id) + permission.canEdit + } catch (ex: CancellationException) { + throw ex + } catch (ex: InvalidStatusException) { + if (ex.status == 404) { + // Server will return this if no permission exists + Timber.w( + "User doesn't have permission to edit playlist %s", + itemId, + ) + } else { + Timber.e( + ex, + "Error checking user permission for playlist %s", + itemId, + ) + } + false + } catch (ex: Exception) { + Timber.e( + ex, + "Error checking user permission for playlist %s", + itemId, + ) + false + } + } ?: false + state.update { + it.copy( + playlist = playlist, + canEdit = canEdit, + ) + } + val libraryDisplayInfo = - serverRepository.currentUser?.let { user -> + user?.let { user -> libraryDisplayInfoDao.getItem(user, itemId) } val filter = libraryDisplayInfo?.filter ?: GetItemsFilter() @@ -333,6 +385,47 @@ class PlaylistViewModel fun sendMediaReport(itemId: UUID) { viewModelScope.launchDefault { serverReportService.sendMediaReportFor(itemId) } } + + fun removeFromPlaylist( + index: Int, + itemId: UUID, + ) { + viewModelScope.launchIO { + try { + playlistCreator.removeFromServerPlaylist( + playlistId = this@PlaylistViewModel.itemId, + itemId = itemId, + ) + (state.value.items as? ApiRequestPager<*>)?.refreshPagesAfter(index) + } catch (ex: CancellationException) { + throw ex + } catch (ex: Exception) { + Timber.e( + ex, + "Error removing %s from playlist %s", + itemId, + this@PlaylistViewModel.itemId, + ) + showToast(context, "Error: ${ex.localizedMessage}") + } + } + } + + fun onMoveItem( + index: Int, + itemId: UUID, + direction: MoveDirection, + ) { + viewModelScope.launchIO { + val newIndex = index + if (direction == MoveDirection.UP) -1 else 1 + api.playlistsApi.moveItem( + playlistId = this@PlaylistViewModel.itemId.toServerString(), + itemId = itemId.toServerString(), + newIndex = newIndex, + ) + (state.value.items as? ApiRequestPager<*>)?.refreshPagesAfter(index - 1) + } + } } @Immutable @@ -355,6 +448,7 @@ data class PlaylistDetailsState( ), ), val loading: LoadingState = LoadingState.Pending, + val canEdit: Boolean = false, ) @Composable @@ -418,6 +512,7 @@ fun PlaylistDetails( }, onClickRemoveFromQueue = { _, _ -> }, onDeleteItem = viewModel::deleteItem, + onRemoveFromPlaylist = viewModel::removeFromPlaylist, ) } val contextActions = @@ -438,6 +533,7 @@ fun PlaylistDetails( onChooseTracks = {}, onClearChosenStreams = {}, onClickRemoveFromNextUp = {}, + onRemoveFromPlaylist = viewModel::removeFromPlaylist, ) } @@ -455,21 +551,23 @@ fun PlaylistDetails( play(0, it, shuffle) } }, - onLongClickIndex = { index, item -> + onShowContextMenu = { index, item, fromLongClick -> showContextMenu = if (item.type == BaseItemKind.AUDIO) { ContextMenu.ForMusic( - fromLongClick = true, + fromLongClick = fromLongClick, item = item, index = index, canDelete = viewModel.canDelete(item, preferences.appPreferences), canRemoveFromQueue = false, actions = musicContextActions, + showRemoveFromPlaylist = state.canEdit, ) } else { ContextMenu.ForBaseItem( - fromLongClick = true, + fromLongClick = fromLongClick, item = item, + index = index, chosenStreams = null, showGoTo = true, showStreamChoices = false, @@ -477,12 +575,15 @@ fun PlaylistDetails( canRemoveContinueWatching = false, canRemoveNextUp = false, actions = contextActions, + showRemoveFromPlaylist = state.canEdit, ) } }, filterAndSort = state.filterAndSort, onFilterAndSortChange = viewModel::loadItems, getPossibleFilterValues = viewModel::getFilterOptionValues, + canEdit = state.canEdit, + onMoveItem = viewModel::onMoveItem, modifier = modifier, ) showContextMenu?.let { contextMenu -> @@ -525,13 +626,15 @@ fun PlaylistDetailsContent( items: List, musicState: MusicServiceState, onClickIndex: (Int, BaseItem) -> Unit, - onLongClickIndex: (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) } @@ -579,11 +682,16 @@ fun PlaylistDetailsContent( 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, @@ -596,15 +704,22 @@ fun PlaylistDetailsContent( savedIndex = index item?.let { onClickIndex.invoke(index, item) } }, - onLongClickItem = { index, item -> + onShowContextMenu = { index, item, fromLongClick -> savedIndex = index - item?.let { onLongClickIndex.invoke(index, item) } + item?.let { onShowContextMenu.invoke(index, item, fromLongClick) } }, + canMove = canEdit && filterAndSort.sortAndDirection.sort == ItemSortBy.DEFAULT && filterCount == 0, + onMoveItem = onMoveItem, modifier = Modifier - .padding(horizontal = 16.dp) + .padding(start = 16.dp) .weight(1f) - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .focusProperties { + onExit = { + playButtonFocusRequester.tryRequestFocus() + } + }, ) } } @@ -614,6 +729,7 @@ fun PlaylistDetailsContent( @Composable fun PlaylistDetailsHeader( focusedItem: BaseItem?, + filterOptions: List>, onClickPlay: (shuffle: Boolean) -> Unit, playButtonFocusRequester: FocusRequester, focusRequester: FocusRequester, @@ -648,7 +764,7 @@ fun PlaylistDetailsHeader( modifier = Modifier, ) { FilterByButton( - filterOptions = DefaultPlaylistItemsOptions, + filterOptions = filterOptions, current = filterAndSort.filter, onFilterChange = { onFilterAndSortChange.invoke( @@ -697,11 +813,14 @@ fun PlaylistItems( items: List, musicState: MusicServiceState, playButtonFocusRequester: FocusRequester, + canMove: Boolean, + onMoveItem: (Int, UUID, MoveDirection) -> Unit, onFocusItem: (Int, BaseItem?) -> Unit, onClickItem: (Int, BaseItem?) -> Unit, - onLongClickItem: (Int, BaseItem?) -> Unit, + onShowContextMenu: (Int, BaseItem?, Boolean) -> Unit, modifier: Modifier = Modifier, ) { + val focusManager = LocalFocusManager.current when (loadingState) { is LoadingState.Error -> { ErrorMessage(loadingState, modifier) @@ -724,10 +843,7 @@ fun PlaylistItems( .surfaceColorAtElevation(1.dp) .copy(alpha = .75f), shape = RoundedCornerShape(16.dp), - ).focusProperties { - left = playButtonFocusRequester - previous = playButtonFocusRequester - }.focusGroup() + ).focusGroup() .focusRestorer(), ) { itemsIndexed(items) { index, item -> @@ -738,7 +854,7 @@ fun PlaylistItems( onClickItem.invoke(index, item) }, onLongClick = { - onLongClickItem.invoke(index, item) + onShowContextMenu.invoke(index, item, true) }, isPlaying = equalsNotNull( @@ -746,18 +862,27 @@ fun PlaylistItems( 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.isFocused) { + if (it.hasFocus) { onFocusItem(index, item) } - }.focusProperties { - left = playButtonFocusRequester - previous = playButtonFocusRequester }, ) } @@ -788,6 +913,11 @@ fun PlaylistItem( index: Int, onClick: () -> Unit, onLongClick: () -> Unit, + canMove: Boolean, + moveUpAllowed: Boolean, + moveDownAllowed: Boolean, + onClickMove: (MoveDirection) -> Unit, + onClickMore: () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, isPlaying: Boolean = false, @@ -797,97 +927,154 @@ fun PlaylistItem( val imageWidth = 160.dp val density = LocalDensity.current val imageWidthPx = remember(imageWidth, density) { with(density) { imageWidth.roundToPx() } } - ListItem( - selected = false, - onClick = onClick, - onLongClick = onLongClick, - interactionSource = interactionSource, - headlineContent = { - Text( - text = item?.title ?: "", - modifier = Modifier.enableMarquee(focused), - ) - }, - supportingContent = { - Text( - text = item?.subtitle ?: "", - modifier = Modifier.enableMarquee(focused), - ) - }, - trailingContent = { - val duration = - when (item?.type) { - BaseItemKind.AUDIO -> { - item.data.runTimeTicks - ?.ticks - ?.roundSeconds - } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = + modifier + .fillMaxWidth() + .heightIn(min = 40.dp, max = 88.dp), + ) { + ListItem( + selected = false, + onClick = onClick, + onLongClick = onLongClick, + interactionSource = interactionSource, + headlineContent = { + Text( + text = item?.title ?: "", + modifier = Modifier.enableMarquee(focused), + ) + }, + supportingContent = { + Text( + text = item?.subtitle ?: "", + modifier = Modifier.enableMarquee(focused), + ) + }, + trailingContent = { + val duration = + 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 -> + val now by LocalClock.current.now + val context = LocalContext.current + val endTimeStr = + remember(item, now, context) { + val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds) + formatTime(context, endTime) + } + val resources = LocalResources.current + val durationText = + remember(resources, duration) { resources.formatDuration(duration) } + Column { + Text( + text = durationText, + ) + if (item?.type != BaseItemKind.AUDIO) { + Text( + text = stringResource(R.string.ends_at, endTimeStr), + style = MaterialTheme.typography.bodySmall, + ) + } } } - duration?.let { duration -> - val now by LocalClock.current.now - val context = LocalContext.current - val endTimeStr = - remember(item, now, context) { - val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds) - formatTime(context, endTime) - } - val resources = LocalResources.current - val durationText = - remember(resources, duration) { resources.formatDuration(duration) } - Column { + }, + leadingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { Text( - text = durationText, + text = "${index + 1}.", + style = MaterialTheme.typography.labelLarge, ) - if (item?.type != BaseItemKind.AUDIO) { - Text( - text = stringResource(R.string.ends_at, endTimeStr), - style = MaterialTheme.typography.bodySmall, + 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, ) } } - } - }, - leadingContent = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text( - text = "${index + 1}.", - style = MaterialTheme.typography.labelLarge, - ) - if (item?.type == BaseItemKind.AUDIO) { - MusicQueueMarker( - isPlaying = isPlaying, - isQueued = isQueued, + }, + modifier = Modifier.weight(1f), + ) + val contentHeight = 24.dp + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.wrapContentWidth(), + ) { + if (canMove) { + Button( + onClick = { onClickMove.invoke(MoveDirection.UP) }, + enabled = moveUpAllowed, + contentHeight = contentHeight, + ) { + Text( + text = stringResource(R.string.fa_caret_up), + fontFamily = FontAwesome, ) - } else { - ItemCardImage( - item = item, - name = item?.name, - 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, + } + Button( + onClick = { onClickMove.invoke(MoveDirection.DOWN) }, + enabled = moveDownAllowed, + contentHeight = contentHeight, + ) { + Text( + text = stringResource(R.string.fa_caret_down), + fontFamily = FontAwesome, ) } } - }, - modifier = modifier, - ) + Button( + onClick = onClickMore, + enabled = true, + contentHeight = contentHeight, + ) { + Text( + text = stringResource(R.string.fa_ellipsis_vertical), + fontFamily = FontAwesome, + ) + } + } + } } @Composable diff --git a/app/src/main/res/values/fa_strings.xml b/app/src/main/res/values/fa_strings.xml index 61c7af06..45a6f7b2 100644 --- a/app/src/main/res/values/fa_strings.xml +++ b/app/src/main/res/values/fa_strings.xml @@ -61,4 +61,5 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e8ae0187..a233096f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -922,4 +922,5 @@ Dim screen Photo albums No favorites found! + Remove from playlist