diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index 18337e1b..9d3fdfa1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -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) { 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 ffb9cf92..091edd21 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 @@ -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> = DefaultFilterOptions, focusRequesterOnEmpty: FocusRequester? = null, - playlistViewModel: AddPlaylistViewModel = hiltViewModel(), viewModel: CollectionFolderViewModel = hiltViewModel( 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(null) } - var overviewDialog by remember { mutableStateOf(null) } - var showPlaylistDialog by remember { mutableStateOf>(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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenuUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenuUtils.kt new file mode 100644 index 00000000..f588851f --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenuUtils.kt @@ -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?>(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(null) } + var showPlaylistDialog by remember { mutableStateOf>(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) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt index 65329026..d4609d79 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt @@ -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 = _state - + ) : ViewModel(), + ContextMenuProvider { @AssistedFactory interface Factory { fun create(destination: Destination.ItemGrid<*>): ItemGridViewModel } + private val _state = MutableStateFlow(ItemGridState()) + val state: StateFlow = _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( 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> -> { + 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() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt index 20351c57..51c496dc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt @@ -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( 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(null) } - var overviewDialog by remember { mutableStateOf(null) } - var showPlaylistDialog by remember { mutableStateOf>(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() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 0b10a1fd..3e5ab947 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -314,6 +314,7 @@ fun DestinationContent( is Destination.ItemGrid<*> -> { LaunchedEffect(Unit) { onClearBackdrop.invoke() } ItemGrid( + preferences, destination, modifier, )