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:
parent
7660c53e3a
commit
68836bb834
6 changed files with 334 additions and 229 deletions
|
|
@ -198,6 +198,10 @@ data class BaseItem(
|
||||||
*/
|
*/
|
||||||
fun destination(index: Int? = null): Destination {
|
fun destination(index: Int? = null): Destination {
|
||||||
if (destinationOverride != null) return destinationOverride
|
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 =
|
val result =
|
||||||
// Redirect episodes & seasons to their series if possible
|
// Redirect episodes & seasons to their series if possible
|
||||||
when (type) {
|
when (type) {
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,7 @@ import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.services.deleteItem
|
import com.github.damontecres.wholphin.services.deleteItem
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
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.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.detail.music.addToQueue
|
||||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
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.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
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.SortOrder
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
||||||
|
|
@ -128,7 +123,8 @@ class CollectionFolderViewModel
|
||||||
@Assisted private val collectionFilter: CollectionFolderFilter,
|
@Assisted private val collectionFilter: CollectionFolderFilter,
|
||||||
@Assisted("useSeriesForPrimary") private val useSeriesForPrimary: Boolean,
|
@Assisted("useSeriesForPrimary") private val useSeriesForPrimary: Boolean,
|
||||||
@Assisted defaultViewOptions: ViewOptions,
|
@Assisted defaultViewOptions: ViewOptions,
|
||||||
) : ViewModel() {
|
) : ViewModel(),
|
||||||
|
ContextMenuProvider {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
@ -461,25 +457,29 @@ class CollectionFolderViewModel
|
||||||
result.totalRecordCount
|
result.totalRecordCount
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWatched(
|
override fun setWatched(
|
||||||
position: Int,
|
position: Int,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) {
|
||||||
favoriteWatchManager.setWatched(itemId, played)
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
(state.value.items as? DataLoadingState.Success)?.let {
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
(state.value.items as? DataLoadingState.Success)?.let {
|
||||||
|
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFavorite(
|
override fun setFavorite(
|
||||||
position: Int,
|
position: Int,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) {
|
||||||
favoriteWatchManager.setFavorite(itemId, favorite)
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
(state.value.items as? DataLoadingState.Success)?.let {
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
(it.data as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
(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()
|
release()
|
||||||
navigationManager.navigateTo(destination)
|
navigationManager.navigateTo(destination)
|
||||||
}
|
}
|
||||||
|
|
@ -509,7 +509,7 @@ class CollectionFolderViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteItem(
|
override fun deleteItem(
|
||||||
index: Int,
|
index: Int,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
) {
|
) {
|
||||||
|
|
@ -520,7 +520,7 @@ class CollectionFolderViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canDelete(
|
override fun canDelete(
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
appPreferences: AppPreferences,
|
appPreferences: AppPreferences,
|
||||||
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
||||||
|
|
@ -529,6 +529,10 @@ class CollectionFolderViewModel
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
index: Int,
|
index: Int,
|
||||||
) = addToQueue(api, musicService, item, index)
|
) = 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(
|
data class CollectionFolderState(
|
||||||
|
|
@ -637,7 +641,6 @@ fun CollectionFolderView(
|
||||||
useSeriesForPrimary: Boolean = true,
|
useSeriesForPrimary: Boolean = true,
|
||||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||||
focusRequesterOnEmpty: FocusRequester? = null,
|
focusRequesterOnEmpty: FocusRequester? = null,
|
||||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
|
||||||
viewModel: CollectionFolderViewModel =
|
viewModel: CollectionFolderViewModel =
|
||||||
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
|
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
|
||||||
key = viewModelKey,
|
key = viewModelKey,
|
||||||
|
|
@ -655,41 +658,9 @@ fun CollectionFolderView(
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
var position by rememberInt(viewModel.position)
|
var position by rememberInt(viewModel.position)
|
||||||
|
|
||||||
var showContextMenu by remember { mutableStateOf<ContextMenu?>(null) }
|
val contextMenu = rememberContextMenu(preferences, viewModel)
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
|
||||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
|
||||||
val playlistState by playlistViewModel.playlistState.collectAsState()
|
|
||||||
var showViewOptions by rememberSaveable { mutableStateOf(false) }
|
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 =
|
val gridActions =
|
||||||
remember(actions) {
|
remember(actions) {
|
||||||
GridClickActions(
|
GridClickActions(
|
||||||
|
|
@ -702,18 +673,7 @@ fun CollectionFolderView(
|
||||||
if (actions.onLongClickItem != null) {
|
if (actions.onLongClickItem != null) {
|
||||||
actions.onLongClickItem.invoke(index, item)
|
actions.onLongClickItem.invoke(index, item)
|
||||||
} else {
|
} else {
|
||||||
showContextMenu =
|
contextMenu.showContextMenu(index, item)
|
||||||
ContextMenu.ForBaseItem(
|
|
||||||
fromLongClick = true,
|
|
||||||
item = item,
|
|
||||||
chosenStreams = null,
|
|
||||||
showGoTo = true,
|
|
||||||
showStreamChoices = false,
|
|
||||||
canDelete = viewModel.canDelete(item, preferences.appPreferences),
|
|
||||||
canRemoveContinueWatching = false,
|
|
||||||
canRemoveNextUp = false,
|
|
||||||
actions = contextActions,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickPlayAll =
|
onClickPlayAll =
|
||||||
|
|
@ -912,41 +872,7 @@ fun CollectionFolderView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
overviewDialog?.let { info ->
|
contextMenu.Compose()
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AnimatedVisibility(showViewOptions) {
|
AnimatedVisibility(showViewOptions) {
|
||||||
ViewOptionsDialog(
|
ViewOptionsDialog(
|
||||||
viewOptions = state.viewOptions,
|
viewOptions = state.viewOptions,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -7,49 +8,73 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.services.deleteItem
|
||||||
import com.github.damontecres.wholphin.ui.cards.GridCard
|
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.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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
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.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.RequestHandler
|
import com.github.damontecres.wholphin.util.RequestHandler
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = ItemGridViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = ItemGridViewModel.Factory::class)
|
||||||
class ItemGridViewModel
|
class ItemGridViewModel
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
private val mediaManagementService: MediaManagementService,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
val mediaReportService: MediaReportService,
|
||||||
@Assisted private val destination: Destination.ItemGrid<*>,
|
@Assisted private val destination: Destination.ItemGrid<*>,
|
||||||
) : ViewModel() {
|
) : ViewModel(),
|
||||||
private val _state = MutableStateFlow(ItemGridState())
|
ContextMenuProvider {
|
||||||
val state: StateFlow<ItemGridState> = _state
|
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(destination: Destination.ItemGrid<*>): ItemGridViewModel
|
fun create(destination: Destination.ItemGrid<*>): ItemGridViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(ItemGridState())
|
||||||
|
val state: StateFlow<ItemGridState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
try {
|
try {
|
||||||
|
|
@ -77,9 +102,78 @@ class ItemGridViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateTo(destination: Destination) {
|
override fun navigateTo(destination: Destination) {
|
||||||
navigationManager.navigateTo(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(
|
data class ItemGridState(
|
||||||
|
|
@ -91,13 +185,17 @@ data class ItemGridState(
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemGrid(
|
fun ItemGrid(
|
||||||
|
preferences: UserPreferences,
|
||||||
destination: Destination.ItemGrid<*>,
|
destination: Destination.ItemGrid<*>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
viewModel: ItemGridViewModel =
|
viewModel: ItemGridViewModel =
|
||||||
hiltViewModel<ItemGridViewModel, ItemGridViewModel.Factory>(
|
hiltViewModel<ItemGridViewModel, ItemGridViewModel.Factory>(
|
||||||
creationCallback = { it.create(destination) },
|
creationCallback = { it.create(destination) },
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
val contextMenu = rememberContextMenu(preferences, viewModel)
|
||||||
|
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
when (val st = state.items) {
|
when (val st = state.items) {
|
||||||
is DataLoadingState.Error -> {
|
is DataLoadingState.Error -> {
|
||||||
|
|
@ -111,20 +209,25 @@ fun ItemGrid(
|
||||||
}
|
}
|
||||||
|
|
||||||
is DataLoadingState.Success<List<BaseItem?>> -> {
|
is DataLoadingState.Success<List<BaseItem?>> -> {
|
||||||
|
var position by rememberInt(destination.initialPosition)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.tryRequestFocus()
|
focusRequester.tryRequestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
GridTitle(destination.title.getString())
|
GridTitle(destination.title.getString())
|
||||||
|
|
||||||
CardGrid(
|
CardGrid(
|
||||||
pager = st.data,
|
pager = st.data,
|
||||||
onClickItem = { index: Int, item: BaseItem ->
|
onClickItem = { index: Int, item: BaseItem ->
|
||||||
// TODO handle more types
|
position = index
|
||||||
viewModel.navigateTo(Destination.Playback(item.id, 0))
|
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)) },
|
onClickPlay = { _, item -> viewModel.navigateTo(Destination.Playback(item)) },
|
||||||
letterPosition = { c: Char -> 0 },
|
letterPosition = { c: Char -> 0 },
|
||||||
gridFocusRequester = focusRequester,
|
gridFocusRequester = focusRequester,
|
||||||
|
|
@ -148,4 +251,5 @@ fun ItemGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contextMenu.Compose()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,13 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.R
|
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.HomeRowConfig
|
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.deleteItem
|
||||||
import com.github.damontecres.wholphin.services.tvAccess
|
import com.github.damontecres.wholphin.services.tvAccess
|
||||||
import com.github.damontecres.wholphin.ui.cards.GridCard
|
import com.github.damontecres.wholphin.ui.cards.GridCard
|
||||||
import com.github.damontecres.wholphin.ui.components.ContextMenu
|
import com.github.damontecres.wholphin.ui.components.ContextMenuProvider
|
||||||
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.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.GridTitle
|
import com.github.damontecres.wholphin.ui.components.GridTitle
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
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.components.StudioCardGrid
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.components.rememberContextMenu
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playback.scale
|
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.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.ui.util.StringProvider
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
import com.github.damontecres.wholphin.ui.util.StringStringProvider
|
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.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
@ -97,7 +86,8 @@ class HomeRowGridViewModel
|
||||||
val mediaReportService: MediaReportService,
|
val mediaReportService: MediaReportService,
|
||||||
@Assisted private val title: StringProvider,
|
@Assisted private val title: StringProvider,
|
||||||
@Assisted private val rowConfig: HomeRowConfig,
|
@Assisted private val rowConfig: HomeRowConfig,
|
||||||
) : ViewModel() {
|
) : ViewModel(),
|
||||||
|
ContextMenuProvider {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
@ -171,44 +161,52 @@ class HomeRowGridViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWatched(
|
override fun setWatched(
|
||||||
position: Int,
|
position: Int,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) {
|
||||||
favoriteWatchManager.setWatched(itemId, played)
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
(state.value.loading as? HomeRowLoadingState.Success)?.let {
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
(it.items as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
(state.value.loading as? HomeRowLoadingState.Success)?.let {
|
||||||
|
(it.items as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFavorite(
|
override fun setFavorite(
|
||||||
position: Int,
|
position: Int,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) {
|
||||||
favoriteWatchManager.setFavorite(itemId, favorite)
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
(state.value.loading as? HomeRowLoadingState.Success)?.let {
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
(it.items as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
(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) {
|
fun updateBackdrop(item: BaseItem) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
backdropService.submit(item)
|
backdropService.submit(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateTo(destination: Destination) {
|
override fun isAdministrator(): Boolean = serverRepository.currentUserDto?.policy?.isAdministrator == true
|
||||||
|
|
||||||
|
override fun navigateTo(destination: Destination) {
|
||||||
navigationManager.navigateTo(destination)
|
navigationManager.navigateTo(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canDelete(
|
override fun canDelete(
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
appPreferences: AppPreferences,
|
appPreferences: AppPreferences,
|
||||||
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
||||||
|
|
||||||
fun deleteItem(
|
override fun deleteItem(
|
||||||
index: Int,
|
index: Int,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
) {
|
) {
|
||||||
|
|
@ -234,47 +232,12 @@ fun HomeRowGrid(
|
||||||
hiltViewModel<HomeRowGridViewModel, HomeRowGridViewModel.Factory>(
|
hiltViewModel<HomeRowGridViewModel, HomeRowGridViewModel.Factory>(
|
||||||
creationCallback = { it.create(destination.title, destination.config) },
|
creationCallback = { it.create(destination.title, destination.config) },
|
||||||
),
|
),
|
||||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
var position by rememberInt(destination.initialPosition)
|
val contextMenu = rememberContextMenu(preferences, viewModel)
|
||||||
val gridFocusRequester = remember { FocusRequester() }
|
val gridFocusRequester = remember { FocusRequester() }
|
||||||
val viewOptions = destination.config.viewOptions
|
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(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -320,32 +283,11 @@ fun HomeRowGrid(
|
||||||
viewModel.navigateTo(item.destination(index))
|
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() }
|
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
|
||||||
CardGrid(
|
CardGrid(
|
||||||
pager = st.items,
|
pager = st.items,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = contextMenu::showContextMenu,
|
||||||
onClickPlay = { _, _ -> },
|
onClickPlay = { _, _ -> },
|
||||||
letterPosition = { -1 },
|
letterPosition = { -1 },
|
||||||
gridFocusRequester = gridFocusRequester,
|
gridFocusRequester = gridFocusRequester,
|
||||||
|
|
@ -354,7 +296,6 @@ fun HomeRowGrid(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
initialPosition = destination.initialPosition,
|
initialPosition = destination.initialPosition,
|
||||||
positionCallback = { columns, newPosition ->
|
positionCallback = { columns, newPosition ->
|
||||||
position = newPosition
|
|
||||||
},
|
},
|
||||||
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GridCard(
|
GridCard(
|
||||||
|
|
@ -378,39 +319,5 @@ fun HomeRowGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
overviewDialog?.let { info ->
|
contextMenu.Compose()
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,7 @@ fun DestinationContent(
|
||||||
is Destination.ItemGrid<*> -> {
|
is Destination.ItemGrid<*> -> {
|
||||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
ItemGrid(
|
ItemGrid(
|
||||||
|
preferences,
|
||||||
destination,
|
destination,
|
||||||
modifier,
|
modifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue