mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-06 18:54:49 +01:00
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 {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,6 +314,7 @@ fun DestinationContent(
|
|||
is Destination.ItemGrid<*> -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
ItemGrid(
|
||||
preferences,
|
||||
destination,
|
||||
modifier,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue