Fix click/long click issues on some view more grid pages (#1486)

## Description
On a library recommended tab, if you clicked View More, clicking a card
on the grid would directly play the item instead of the regular
behavior.

This PR fixes that and simplifies the code required to support simple
context menus.

### Related issues
Fixes #1480

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-05-30 17:41:27 -04:00 • committed by GitHub
parent 7660c53e3a
commit 68836bb834
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 334 additions and 229 deletions

View file

@ -198,6 +198,10 @@ data class BaseItem(
*/
fun destination(index: Int? = null): Destination {
if (destinationOverride != null) return destinationOverride
if (data.extraType != null || type == BaseItemKind.TRAILER) {
// Extras including trailers should always play directly
return Destination.Playback(id, 0)
}
val result =
// Redirect episodes & seasons to their series if possible
when (type) {

View file

@ -53,11 +53,7 @@ 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.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.music.addToQueue
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.launchDefault
@ -95,7 +91,6 @@ import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
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.api.request.GetPersonsRequest
@ -128,7 +123,8 @@ class CollectionFolderViewModel
@Assisted private val collectionFilter: CollectionFolderFilter,
@Assisted("useSeriesForPrimary") private val useSeriesForPrimary: Boolean,
@Assisted defaultViewOptions: ViewOptions,
) : ViewModel() {
) : ViewModel(),
ContextMenuProvider {
@AssistedFactory
interface Factory {
fun create(
@ -461,25 +457,29 @@ class CollectionFolderViewModel
result.totalRecordCount
}
fun setWatched(
override fun setWatched(
position: Int,
itemId: UUID,
played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played)
(state.value.items as? DataLoadingState.Success)?.let {
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
) {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played)
(state.value.items as? DataLoadingState.Success)?.let {
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
}
}
fun setFavorite(
override fun setFavorite(
position: Int,
itemId: UUID,
favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
(state.value.items as? DataLoadingState.Success)?.let {
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
) {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
(state.value.items as? DataLoadingState.Success)?.let {
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
}
}
@ -489,7 +489,7 @@ class CollectionFolderViewModel
}
}
fun navigateTo(destination: Destination) {
override fun navigateTo(destination: Destination) {
release()
navigationManager.navigateTo(destination)
}
@ -509,7 +509,7 @@ class CollectionFolderViewModel
}
}
fun deleteItem(
override fun deleteItem(
index: Int,
item: BaseItem,
) {
@ -520,7 +520,7 @@ class CollectionFolderViewModel
}
}
fun canDelete(
override fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean = mediaManagementService.canDelete(item, appPreferences)
@ -529,6 +529,10 @@ class CollectionFolderViewModel
item: BaseItem,
index: Int,
) = addToQueue(api, musicService, item, index)
override fun sendReportFor(itemId: UUID) = mediaReportService.sendReportFor(itemId)
override fun isAdministrator(): Boolean = serverRepository.currentUserDto?.policy?.isAdministrator == true
}
data class CollectionFolderState(
@ -637,7 +641,6 @@ fun CollectionFolderView(
useSeriesForPrimary: Boolean = true,
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
focusRequesterOnEmpty: FocusRequester? = null,
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
viewModel: CollectionFolderViewModel =
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
key = viewModelKey,
@ -655,41 +658,9 @@ fun CollectionFolderView(
val state by viewModel.state.collectAsState()
var position by rememberInt(viewModel.position)
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.collectAsState()
val contextMenu = rememberContextMenu(preferences, viewModel)
var showViewOptions by rememberSaveable { mutableStateOf(false) }
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(viewModel.position, itemId, watched)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(viewModel.position, itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onDeleteItem = { viewModel.deleteItem(viewModel.position, it) },
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onChooseVersion = { _, _ ->
// Not supported on this page
},
onChooseTracks = { result ->
// Not supported on this page
},
onClearChosenStreams = {
// Not supported on this page
},
)
}
val gridActions =
remember(actions) {
GridClickActions(
@ -702,18 +673,7 @@ fun CollectionFolderView(
if (actions.onLongClickItem != null) {
actions.onLongClickItem.invoke(index, item)
} else {
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
item = item,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
contextMenu.showContextMenu(index, item)
}
},
onClickPlayAll =
@ -912,41 +872,7 @@ fun CollectionFolderView(
}
}
}
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
state = playlistState,
onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = {
playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog.makeAbsent()
},
createEnabled = true,
onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
elevation = 3.dp,
)
}
contextMenu.Compose()
AnimatedVisibility(showViewOptions) {
ViewOptionsDialog(
viewOptions = state.viewOptions,

View file

@ -0,0 +1,163 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.nav.Destination
import org.jellyfin.sdk.model.api.MediaType
import java.util.UUID
/**
* Simplifies showing a basic context menu
*/
@Composable
fun rememberContextMenu(
preferences: UserPreferences,
provider: ContextMenuProvider,
): ContextMenuUtils = remember(preferences, provider) { ContextMenuUtils(preferences, provider) }
class ContextMenuUtils(
private val preferences: UserPreferences,
private val provider: ContextMenuProvider,
) {
private var showContextMenu by mutableStateOf<Pair<Int, BaseItem>?>(null)
/**
* Show the context menu, typically from a long click
*/
fun showContextMenu(
position: Int,
item: BaseItem,
) {
showContextMenu = Pair(position, item)
}
/**
* Composes the context menu when needed
*/
@Composable
fun Compose(playlistViewModel: AddPlaylistViewModel = hiltViewModel()) {
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.collectAsState()
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath = provider.isAdministrator(),
onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { (position, item) ->
val contextActions =
remember {
ContextMenuActions(
navigateTo = provider::navigateTo,
onClickWatch = { itemId, watched ->
provider.setWatched(position, itemId, watched)
},
onClickFavorite = { itemId, favorite ->
provider.setFavorite(position, itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = provider::sendReportFor,
onDeleteItem = { provider.deleteItem(position, it) },
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onChooseVersion = { _, _ ->
// Not supported on this page
},
onChooseTracks = { result ->
// Not supported on this page
},
onClearChosenStreams = {
// Not supported on this page
},
)
}
val contextMenu =
remember {
ContextMenu.ForBaseItem(
fromLongClick = true,
item = item,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete = provider.canDelete(item, preferences.appPreferences),
canRemoveContinueWatching = false,
canRemoveNextUp = false,
actions = contextActions,
)
}
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
state = playlistState,
onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = {
playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog.makeAbsent()
},
createEnabled = true,
onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
elevation = 3.dp,
)
}
}
}
interface ContextMenuProvider {
fun isAdministrator(): Boolean
fun navigateTo(destination: Destination)
fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean
fun deleteItem(
index: Int,
item: BaseItem,
)
fun setWatched(
position: Int,
itemId: UUID,
played: Boolean,
)
fun setFavorite(
position: Int,
itemId: UUID,
favorite: Boolean,
)
fun sendReportFor(itemId: UUID)
}

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.components
import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
@ -7,49 +8,73 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaManagementService
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.deleteItem
import com.github.damontecres.wholphin.ui.cards.GridCard
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.detail.CardGrid
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.tryRequestFocus
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.DataLoadingState
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.RequestHandler
import dagger.assisted.Assisted
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.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import timber.log.Timber
import java.util.UUID
@HiltViewModel(assistedFactory = ItemGridViewModel.Factory::class)
class ItemGridViewModel
@AssistedInject
constructor(
@param:ApplicationContext private val context: Context,
private val api: ApiClient,
private val navigationManager: NavigationManager,
private val favoriteWatchManager: FavoriteWatchManager,
private val mediaManagementService: MediaManagementService,
val serverRepository: ServerRepository,
val mediaReportService: MediaReportService,
@Assisted private val destination: Destination.ItemGrid<*>,
) : ViewModel() {
private val _state = MutableStateFlow(ItemGridState())
val state: StateFlow<ItemGridState> = _state
) : ViewModel(),
ContextMenuProvider {
@AssistedFactory
interface Factory {
fun create(destination: Destination.ItemGrid<*>): ItemGridViewModel
}
private val _state = MutableStateFlow(ItemGridState())
val state: StateFlow<ItemGridState> = _state
init {
viewModelScope.launchIO {
try {
@ -77,9 +102,78 @@ class ItemGridViewModel
}
}
fun navigateTo(destination: Destination) {
override fun navigateTo(destination: Destination) {
navigationManager.navigateTo(destination)
}
override fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean = mediaManagementService.canDelete(item, appPreferences)
override fun deleteItem(
index: Int,
item: BaseItem,
) {
deleteItem(context, mediaManagementService, item) {
viewModelScope.launchDefault {
refreshAfterDelete(index, item)
}
}
}
private suspend fun refreshAfterDelete(
position: Int,
deletedItem: BaseItem,
) {
try {
val pager =
((state.value.items as? DataLoadingState.Success)?.data as? ApiRequestPager<*>)
position.let {
Timber.v("Item deleted: position=%s", it)
val item = pager?.get(it)
// Exact item deleted (eg a movie) or deleted item was within the series
if (item?.id == deletedItem.id ||
equalsNotNull(item?.data?.id, deletedItem.data.seriesId)
) {
pager?.refreshPagesAfter(position)
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error refreshing after deleted item %s", deletedItem.id)
showToast(context, "Error refreshing after item deleted")
}
}
override fun setWatched(
position: Int,
itemId: UUID,
played: Boolean,
) {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played)
(state.value.items as? DataLoadingState.Success)?.let {
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
}
}
override fun setFavorite(
position: Int,
itemId: UUID,
favorite: Boolean,
) {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
(state.value.items as? DataLoadingState.Success)?.let {
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
}
}
override fun isAdministrator(): Boolean = serverRepository.currentUserDto?.policy?.isAdministrator == true
override fun sendReportFor(itemId: UUID) = mediaReportService.sendReportFor(itemId)
}
data class ItemGridState(
@ -91,13 +185,17 @@ data class ItemGridState(
*/
@Composable
fun ItemGrid(
preferences: UserPreferences,
destination: Destination.ItemGrid<*>,
modifier: Modifier = Modifier,
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
viewModel: ItemGridViewModel =
hiltViewModel<ItemGridViewModel, ItemGridViewModel.Factory>(
creationCallback = { it.create(destination) },
),
) {
val contextMenu = rememberContextMenu(preferences, viewModel)
val state by viewModel.state.collectAsState()
when (val st = state.items) {
is DataLoadingState.Error -> {
@ -111,20 +209,25 @@ fun ItemGrid(
}
is DataLoadingState.Success<List<BaseItem?>> -> {
var position by rememberInt(destination.initialPosition)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.tryRequestFocus()
}
Column(modifier = modifier) {
GridTitle(destination.title.getString())
CardGrid(
pager = st.data,
onClickItem = { index: Int, item: BaseItem ->
// TODO handle more types
viewModel.navigateTo(Destination.Playback(item.id, 0))
position = index
viewModel.navigateTo(item.destination(index))
},
onLongClickItem = { index: Int, item: BaseItem ->
position = index
contextMenu.showContextMenu(index, item)
},
onLongClickItem = { index: Int, item: BaseItem -> },
onClickPlay = { _, item -> viewModel.navigateTo(Destination.Playback(item)) },
letterPosition = { c: Char -> 0 },
gridFocusRequester = focusRequester,
@ -148,4 +251,5 @@ fun ItemGrid(
}
}
}
contextMenu.Compose()
}

View file

@ -10,17 +10,13 @@ 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
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.HomeRowConfig
@ -40,23 +36,17 @@ import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.deleteItem
import com.github.damontecres.wholphin.services.tvAccess
import com.github.damontecres.wholphin.ui.cards.GridCard
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.ContextMenuProvider
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
import com.github.damontecres.wholphin.ui.components.GridTitle
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.Optional
import com.github.damontecres.wholphin.ui.components.StudioCardGrid
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.components.rememberContextMenu
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.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.StringProvider
import com.github.damontecres.wholphin.ui.util.StringStringProvider
@ -74,7 +64,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber
import java.util.UUID
@ -97,7 +86,8 @@ class HomeRowGridViewModel
val mediaReportService: MediaReportService,
@Assisted private val title: StringProvider,
@Assisted private val rowConfig: HomeRowConfig,
) : ViewModel() {
) : ViewModel(),
ContextMenuProvider {
@AssistedFactory
interface Factory {
fun create(
@ -171,44 +161,52 @@ class HomeRowGridViewModel
}
}
fun setWatched(
override fun setWatched(
position: Int,
itemId: UUID,
played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played)
(state.value.loading as? HomeRowLoadingState.Success)?.let {
(it.items as? ApiRequestPager<*>)?.refreshItem(position, itemId)
) {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played)
(state.value.loading as? HomeRowLoadingState.Success)?.let {
(it.items as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
}
}
fun setFavorite(
override fun setFavorite(
position: Int,
itemId: UUID,
favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
(state.value.loading as? HomeRowLoadingState.Success)?.let {
(it.items as? ApiRequestPager<*>)?.refreshItem(position, itemId)
) {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
(state.value.loading as? HomeRowLoadingState.Success)?.let {
(it.items as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
}
}
override fun sendReportFor(itemId: UUID) = mediaReportService.sendReportFor(itemId)
fun updateBackdrop(item: BaseItem) {
viewModelScope.launchIO {
backdropService.submit(item)
}
}
fun navigateTo(destination: Destination) {
override fun isAdministrator(): Boolean = serverRepository.currentUserDto?.policy?.isAdministrator == true
override fun navigateTo(destination: Destination) {
navigationManager.navigateTo(destination)
}
fun canDelete(
override fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean = mediaManagementService.canDelete(item, appPreferences)
fun deleteItem(
override fun deleteItem(
index: Int,
item: BaseItem,
) {
@ -234,47 +232,12 @@ fun HomeRowGrid(
hiltViewModel<HomeRowGridViewModel, HomeRowGridViewModel.Factory>(
creationCallback = { it.create(destination.title, destination.config) },
),
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
) {
val state by viewModel.state.collectAsState()
var position by rememberInt(destination.initialPosition)
val contextMenu = rememberContextMenu(preferences, viewModel)
val gridFocusRequester = remember { FocusRequester() }
val viewOptions = destination.config.viewOptions
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.collectAsState()
val contextActions =
remember {
ContextMenuActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(position, itemId, watched)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(position, itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onDeleteItem = { viewModel.deleteItem(position, it) },
onShowOverview = { overviewDialog = ItemDetailsDialogInfo(it) },
onChooseVersion = { _, _ ->
// Not supported on this page
},
onChooseTracks = { result ->
// Not supported on this page
},
onClearChosenStreams = {
// Not supported on this page
},
)
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier,
@ -320,32 +283,11 @@ fun HomeRowGrid(
viewModel.navigateTo(item.destination(index))
}
}
val onLongClickItem =
remember {
{ index: Int, item: BaseItem ->
showContextMenu =
ContextMenu.ForBaseItem(
fromLongClick = true,
item = item,
chosenStreams = null,
showGoTo = true,
showStreamChoices = false,
canDelete =
viewModel.canDelete(
item,
preferences.appPreferences,
),
canRemoveContinueWatching = false,
canRemoveNextUp = false, // TODO
actions = contextActions,
)
}
}
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
CardGrid(
pager = st.items,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onLongClickItem = contextMenu::showContextMenu,
onClickPlay = { _, _ -> },
letterPosition = { -1 },
gridFocusRequester = gridFocusRequester,
@ -354,7 +296,6 @@ fun HomeRowGrid(
modifier = Modifier.fillMaxSize(),
initialPosition = destination.initialPosition,
positionCallback = { columns, newPosition ->
position = newPosition
},
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
GridCard(
@ -378,39 +319,5 @@ fun HomeRowGrid(
}
}
}
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },
)
}
showContextMenu?.let { contextMenu ->
ContextMenuDialog(
onDismissRequest = { showContextMenu = null },
getMediaSource = null,
contextMenu = contextMenu,
preferredSubtitleLanguage = null,
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
state = playlistState,
onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = {
playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog.makeAbsent()
},
createEnabled = true,
onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
elevation = 3.dp,
)
}
contextMenu.Compose()
}

View file

@ -314,6 +314,7 @@ fun DestinationContent(
is Destination.ItemGrid<*> -> {
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
ItemGrid(
preferences,
destination,
modifier,
)