Delete media follow up (#1040)

## Description
A follow up to #1014 to add the delete option in more context menus.
Also improves the "reaction" logic which updates previous pages from
deletions on later pages.

### Related issues
Addresses
https://github.com/damontecres/Wholphin/pull/1014#issuecomment-3987205355
Related to #104

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-03-04 19:30:08 -05:00 committed by GitHub
parent ce6461d23b
commit 354577e2e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 261 additions and 28 deletions

View file

@ -5,6 +5,7 @@ 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.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.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -40,11 +41,16 @@ class MediaManagementService
val deletedItemFlow: SharedFlow<DeletedItem> = _deletedItemFlow val deletedItemFlow: SharedFlow<DeletedItem> = _deletedItemFlow
suspend fun canDelete(item: BaseItem): Boolean { suspend fun canDelete(item: BaseItem): Boolean {
val appPreferences = userPreferencesService.getCurrent().appPreferences
return canDelete(item, appPreferences)
}
fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean {
Timber.v("canDelete %s: %s", item.id, item.canDelete) Timber.v("canDelete %s: %s", item.id, item.canDelete)
val enabled = val enabled = appPreferences.interfacePreferences.enableMediaManagement
userPreferencesService
.getCurrent()
.appPreferences.interfacePreferences.enableMediaManagement
return enabled && return enabled &&
item.canDelete && item.canDelete &&
if (item.type == BaseItemKind.RECORDING) { if (item.type == BaseItemKind.RECORDING) {

View file

@ -57,6 +57,7 @@ import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
@ -65,6 +66,7 @@ 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.ThemeSongPlayer 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.ui.AspectRatios import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
@ -77,6 +79,7 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.equalsNotNull
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.main.HomePageHeader import com.github.damontecres.wholphin.ui.main.HomePageHeader
@ -84,6 +87,7 @@ 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.rememberInt
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.FilterUtils import com.github.damontecres.wholphin.ui.util.FilterUtils
@ -100,6 +104,9 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -202,18 +209,34 @@ class CollectionFolderViewModel
loading.setValueOnMain(DataLoadingState.Error(ex)) loading.setValueOnMain(DataLoadingState.Error(ex))
} }
} }
viewModelScope.launchDefault { mediaManagementService.deletedItemFlow
mediaManagementService.deletedItemFlow.collect { deletedItem -> .onEach { deletedItem ->
val pager = refreshAfterDelete(position, deletedItem.item)
((loading.value as? DataLoadingState.Success)?.data as? ApiRequestPager<*>) }.catch { ex ->
position.let { Timber.e(ex, "Error refreshing after deleted item")
Timber.v("Item deleted: position=%s, id=%s", it, deletedItem.item.id) }.launchIn(viewModelScope)
val item = pager?.get(it) }
if (item?.id == deletedItem.item.id) {
pager.refreshPagesAfter(position) private suspend fun refreshAfterDelete(
} position: Int,
deletedItem: BaseItem,
) {
try {
val pager =
((loading.value as? DataLoadingState.Success)?.data as? ApiRequestPager<*>)
position.let {
Timber.v("Item deleted: position=%s, id=%s", it, itemId)
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", itemId)
showToast(context, "Error refreshing after item deleted")
} }
} }
@ -491,6 +514,22 @@ class CollectionFolderViewModel
} }
} }
} }
fun deleteItem(
index: Int,
item: BaseItem,
) {
deleteItem(context, mediaManagementService, item) {
viewModelScope.launchDefault {
refreshAfterDelete(index, item)
}
}
}
fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean = mediaManagementService.canDelete(item, appPreferences)
} }
/** /**
@ -578,6 +617,7 @@ fun CollectionFolderGrid(
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) } var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) } var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<PositionItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
when (val state = loading) { when (val state = loading) {
@ -713,6 +753,7 @@ fun CollectionFolderGrid(
playbackPosition = item.playbackPosition, playbackPosition = item.playbackPosition,
watched = item.played, watched = item.played,
favorite = item.favorite, favorite = item.favorite,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
actions = actions =
MoreDialogActions( MoreDialogActions(
navigateTo = { viewModel.navigateTo(it) }, navigateTo = { viewModel.navigateTo(it) },
@ -727,6 +768,9 @@ fun CollectionFolderGrid(
showPlaylistDialog.makePresent(it) showPlaylistDialog.makePresent(it)
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor, onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = {
showDeleteDialog = PositionItem(position, item)
},
), ),
), ),
onDismissRequest = { moreDialog.makeAbsent() }, onDismissRequest = { moreDialog.makeAbsent() },
@ -751,6 +795,16 @@ fun CollectionFolderGrid(
elevation = 3.dp, elevation = 3.dp,
) )
} }
showDeleteDialog?.let { (position, item) ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(position, item)
showDeleteDialog = null
},
)
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)

View file

@ -72,12 +72,14 @@ fun ExpandablePlayButtons(
resumePosition: Duration, resumePosition: Duration,
watched: Boolean, watched: Boolean,
favorite: Boolean, favorite: Boolean,
canDelete: Boolean,
trailers: List<Trailer>?, trailers: List<Trailer>?,
playOnClick: (position: Duration) -> Unit, playOnClick: (position: Duration) -> Unit,
watchOnClick: () -> Unit, watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
moreOnClick: () -> Unit, moreOnClick: () -> Unit,
trailerOnClick: (Trailer) -> Unit, trailerOnClick: (Trailer) -> Unit,
deleteOnClick: () -> Unit,
buttonOnFocusChanged: (FocusState) -> Unit, buttonOnFocusChanged: (FocusState) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -158,6 +160,16 @@ fun ExpandablePlayButtons(
) )
} }
} }
if (canDelete) {
item("delete") {
DeleteButton(
onClick = deleteOnClick,
modifier =
Modifier
.onFocusChanged(buttonOnFocusChanged),
)
}
}
// More button // More button
item("more") { item("more") {
@ -424,6 +436,8 @@ private fun ExpandablePlayButtonsPreview() {
buttonOnFocusChanged = {}, buttonOnFocusChanged = {},
trailers = listOf(), trailers = listOf(),
trailerOnClick = {}, trailerOnClick = {},
canDelete = true,
deleteOnClick = {},
modifier = Modifier, modifier = Modifier,
) )
} }

View file

@ -19,11 +19,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
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.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.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.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
@ -31,6 +34,7 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
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.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
@ -51,6 +55,7 @@ abstract class RecommendedViewModel(
val favoriteWatchManager: FavoriteWatchManager, val favoriteWatchManager: FavoriteWatchManager,
val mediaReportService: MediaReportService, val mediaReportService: MediaReportService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
private val mediaManagementService: MediaManagementService,
) : ViewModel() { ) : ViewModel() {
abstract fun init() abstract fun init()
@ -117,6 +122,25 @@ abstract class RecommendedViewModel(
} }
update(title, row) update(title, row)
} }
fun deleteItem(
position: RowColumn,
item: BaseItem,
) {
deleteItem(context, mediaManagementService, item) {
viewModelScope.launchDefault {
val row = rows.value.getOrNull(position.row)
if (row is HomeRowLoadingState.Success) {
(row.items as? ApiRequestPager<*>)?.refreshPagesAfter(position.column)
}
}
}
}
fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean = mediaManagementService.canDelete(item, appPreferences)
} }
@Composable @Composable
@ -130,6 +154,7 @@ fun RecommendedContent(
val context = LocalContext.current val context = LocalContext.current
var moreDialog by remember { mutableStateOf<Optional<RowColumnItem>>(Optional.absent()) } var moreDialog by remember { mutableStateOf<Optional<RowColumnItem>>(Optional.absent()) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) } var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<RowColumnItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
OneTimeLaunchedEffect { OneTimeLaunchedEffect {
@ -196,6 +221,7 @@ fun RecommendedContent(
playbackPosition = item.playbackPosition, playbackPosition = item.playbackPosition,
watched = item.played, watched = item.played,
favorite = item.favorite, favorite = item.favorite,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
actions = actions =
MoreDialogActions( MoreDialogActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) }, navigateTo = { viewModel.navigationManager.navigateTo(it) },
@ -210,6 +236,7 @@ fun RecommendedContent(
showPlaylistDialog.makePresent(it) showPlaylistDialog.makePresent(it)
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor, onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = { showDeleteDialog = RowColumnItem(position, item) },
), ),
), ),
onDismissRequest = { moreDialog.makeAbsent() }, onDismissRequest = { moreDialog.makeAbsent() },
@ -234,9 +261,19 @@ fun RecommendedContent(
elevation = 3.dp, elevation = 3.dp,
) )
} }
showDeleteDialog?.let { (position, item) ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(position, item)
showDeleteDialog = null
},
)
}
} }
private data class RowColumnItem( data class RowColumnItem(
val position: RowColumn, val position: RowColumn,
val item: BaseItem, val item: BaseItem,
) )

View file

@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SuggestionService import com.github.damontecres.wholphin.services.SuggestionService
@ -63,12 +64,14 @@ class RecommendedMovieViewModel
favoriteWatchManager: FavoriteWatchManager, favoriteWatchManager: FavoriteWatchManager,
mediaReportService: MediaReportService, mediaReportService: MediaReportService,
backdropService: BackdropService, backdropService: BackdropService,
mediaManagementService: MediaManagementService,
) : RecommendedViewModel( ) : RecommendedViewModel(
context, context,
navigationManager, navigationManager,
favoriteWatchManager, favoriteWatchManager,
mediaReportService, mediaReportService,
backdropService, backdropService,
mediaManagementService,
) { ) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {

View file

@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.LatestNextUpService import com.github.damontecres.wholphin.services.LatestNextUpService
import com.github.damontecres.wholphin.services.MediaManagementService
import com.github.damontecres.wholphin.services.MediaReportService 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.SuggestionService import com.github.damontecres.wholphin.services.SuggestionService
@ -67,12 +68,14 @@ class RecommendedTvShowViewModel
favoriteWatchManager: FavoriteWatchManager, favoriteWatchManager: FavoriteWatchManager,
mediaReportService: MediaReportService, mediaReportService: MediaReportService,
backdropService: BackdropService, backdropService: BackdropService,
mediaManagementService: MediaManagementService,
) : RecommendedViewModel( ) : RecommendedViewModel(
context, context,
navigationManager, navigationManager,
favoriteWatchManager, favoriteWatchManager,
mediaReportService, mediaReportService,
backdropService, backdropService,
mediaManagementService,
) { ) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {

View file

@ -29,7 +29,7 @@ data class MoreDialogActions(
val onClickFavorite: (UUID, Boolean) -> Unit, val onClickFavorite: (UUID, Boolean) -> Unit,
val onClickAddPlaylist: (UUID) -> Unit, val onClickAddPlaylist: (UUID) -> Unit,
val onSendMediaInfo: (UUID) -> Unit, val onSendMediaInfo: (UUID) -> Unit,
val onClickDelete: (BaseItem) -> Unit = {}, val onClickDelete: (BaseItem) -> Unit,
) )
enum class ClearChosenStreams { enum class ClearChosenStreams {
@ -63,7 +63,7 @@ fun buildMoreDialogItems(
watched: Boolean, watched: Boolean,
favorite: Boolean, favorite: Boolean,
canClearChosenStreams: Boolean, canClearChosenStreams: Boolean,
canDelete: Boolean = false, canDelete: Boolean,
actions: MoreDialogActions, actions: MoreDialogActions,
onChooseVersion: () -> Unit, onChooseVersion: () -> Unit,
onChooseTracks: (MediaStreamType) -> Unit, onChooseTracks: (MediaStreamType) -> Unit,
@ -236,7 +236,7 @@ fun buildMoreDialogItemsForHome(
playbackPosition: Duration, playbackPosition: Duration,
watched: Boolean, watched: Boolean,
favorite: Boolean, favorite: Boolean,
canDelete: Boolean = false, canDelete: Boolean,
actions: MoreDialogActions, actions: MoreDialogActions,
): List<DialogItem> = ): List<DialogItem> =
buildList { buildList {

View file

@ -60,7 +60,6 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
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.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -102,15 +101,6 @@ fun DiscoverMovieDetails(
val requestStr = stringResource(R.string.request) val requestStr = stringResource(R.string.request)
val request4kStr = stringResource(R.string.request_4k) val request4kStr = stringResource(R.string.request_4k)
val moreActions =
MoreDialogActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched -> },
onClickFavorite = { itemId, favorite -> },
onClickAddPlaylist = { itemId -> },
onSendMediaInfo = {},
)
when (val state = loading) { when (val state = loading) {
is LoadingState.Error -> { is LoadingState.Error -> {
ErrorMessage(state, modifier) ErrorMessage(state, modifier)

View file

@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -82,6 +83,7 @@ fun EpisodeDetails(
var moreDialog by remember { mutableStateOf<DialogParams?>(null) } var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) } var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) } var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
val moreActions = val moreActions =
@ -98,6 +100,7 @@ fun EpisodeDetails(
showPlaylistDialog.makePresent(itemId) showPlaylistDialog.makePresent(itemId)
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor, onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = { showDeleteDialog = it },
) )
when (val state = loading) { when (val state = loading) {
@ -215,6 +218,7 @@ fun EpisodeDetails(
onClearChosenStreams = { onClearChosenStreams = {
viewModel.clearChosenStreams(chosenStreams) viewModel.clearChosenStreams(chosenStreams)
}, },
canDelete = viewModel.canDelete,
), ),
) )
}, },
@ -224,6 +228,8 @@ fun EpisodeDetails(
favoriteOnClick = { favoriteOnClick = {
viewModel.setFavorite(ep.id, !ep.favorite) viewModel.setFavorite(ep.id, !ep.favorite)
}, },
canDelete = viewModel.canDelete,
deleteOnClick = { showDeleteDialog = ep },
modifier = modifier, modifier = modifier,
) )
} }
@ -276,6 +282,16 @@ fun EpisodeDetails(
elevation = 3.dp, elevation = 3.dp,
) )
} }
showDeleteDialog?.let { item ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(item)
showDeleteDialog = null
},
)
}
} }
private const val HEADER_ROW = 0 private const val HEADER_ROW = 0
@ -290,6 +306,8 @@ fun EpisodeDetailsContent(
watchOnClick: () -> Unit, watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
moreOnClick: () -> Unit, moreOnClick: () -> Unit,
canDelete: Boolean,
deleteOnClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -347,6 +365,8 @@ fun EpisodeDetailsContent(
}, },
trailers = null, trailers = null,
trailerOnClick = {}, trailerOnClick = {},
canDelete = canDelete,
deleteOnClick = deleteOnClick,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -12,11 +12,13 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager 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.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.StreamChoiceService import com.github.damontecres.wholphin.services.StreamChoiceService
import com.github.damontecres.wholphin.services.ThemeSongPlayer 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.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.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
@ -54,6 +56,7 @@ class EpisodeViewModel
private val favoriteWatchManager: FavoriteWatchManager, private val favoriteWatchManager: FavoriteWatchManager,
private val userPreferencesService: UserPreferencesService, private val userPreferencesService: UserPreferencesService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
private val mediaManagementService: MediaManagementService,
@Assisted val itemId: UUID, @Assisted val itemId: UUID,
) : ViewModel() { ) : ViewModel() {
@AssistedFactory @AssistedFactory
@ -65,6 +68,9 @@ class EpisodeViewModel
val item = MutableLiveData<BaseItem?>(null) val item = MutableLiveData<BaseItem?>(null)
val chosenStreams = MutableLiveData<ChosenStreams?>(null) val chosenStreams = MutableLiveData<ChosenStreams?>(null)
var canDelete: Boolean = false
private set
init { init {
init() init()
} }
@ -95,6 +101,7 @@ class EpisodeViewModel
) { ) {
val prefs = userPreferencesService.getCurrent() val prefs = userPreferencesService.getCurrent()
val item = fetchAndSetItem().await() val item = fetchAndSetItem().await()
canDelete = mediaManagementService.canDelete(item)
val result = itemPlaybackRepository.getSelectedTracks(item.id, item, prefs) val result = itemPlaybackRepository.getSelectedTracks(item.id, item, prefs)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@EpisodeViewModel.item.value = item this@EpisodeViewModel.item.value = item
@ -199,4 +206,10 @@ class EpisodeViewModel
} }
} }
} }
fun deleteItem(item: BaseItem) {
deleteItem(context, mediaManagementService, item) {
navigationManager.goBack()
}
}
} }

View file

@ -315,6 +315,8 @@ fun MovieDetails(
onClickDiscover = { index, item -> onClickDiscover = { index, item ->
viewModel.navigateTo(item.destination) viewModel.navigateTo(item.destination)
}, },
canDelete = viewModel.canDelete,
deleteOnClick = { showDeleteDialog = movie },
modifier = modifier, modifier = modifier,
) )
} }
@ -410,6 +412,8 @@ fun MovieDetailsContent(
onLongClickSimilar: (Int, BaseItem) -> Unit, onLongClickSimilar: (Int, BaseItem) -> Unit,
onClickExtra: (Int, ExtrasItem) -> Unit, onClickExtra: (Int, ExtrasItem) -> Unit,
onClickDiscover: (Int, DiscoverItem) -> Unit, onClickDiscover: (Int, DiscoverItem) -> Unit,
canDelete: Boolean,
deleteOnClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -472,6 +476,8 @@ fun MovieDetailsContent(
position = TRAILER_ROW position = TRAILER_ROW
trailerOnClick.invoke(it) trailerOnClick.invoke(it)
}, },
canDelete = canDelete,
deleteOnClick = deleteOnClick,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -25,6 +25,8 @@ fun FocusedEpisodeFooter(
moreOnClick: () -> Unit, moreOnClick: () -> Unit,
watchOnClick: () -> Unit, watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
canDelete: Boolean,
deleteOnClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
buttonOnFocusChanged: (FocusState) -> Unit = {}, buttonOnFocusChanged: (FocusState) -> Unit = {},
) { ) {
@ -47,6 +49,8 @@ fun FocusedEpisodeFooter(
buttonOnFocusChanged = buttonOnFocusChanged, buttonOnFocusChanged = buttonOnFocusChanged,
trailers = null, trailers = null,
trailerOnClick = {}, trailerOnClick = {},
canDelete = canDelete,
deleteOnClick = deleteOnClick,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }

View file

@ -584,6 +584,7 @@ fun SeriesDetailsContent(
watched = item.played, watched = item.played,
favorite = item.favorite, favorite = item.favorite,
actions = moreActions, actions = moreActions,
canDelete = false,
) )
moreDialog = moreDialog =
DialogParams( DialogParams(

View file

@ -383,6 +383,8 @@ fun SeriesOverview(
), ),
) )
}, },
canDelete = { viewModel.canDelete(it, preferences.appPreferences) },
deleteOnClick = { showDeleteDialog = it },
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -88,6 +88,8 @@ fun SeriesOverviewContent(
moreOnClick: () -> Unit, moreOnClick: () -> Unit,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
personOnClick: (Person) -> Unit, personOnClick: (Person) -> Unit,
canDelete: (BaseItem) -> Boolean,
deleteOnClick: (BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -294,6 +296,8 @@ fun SeriesOverviewContent(
} }
} }
}, },
canDelete = canDelete.invoke(ep),
deleteOnClick = { deleteOnClick.invoke(ep) },
modifier = modifier =
Modifier Modifier
.padding(top = 4.dp) .padding(top = 4.dp)

View file

@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.ExtrasService import com.github.damontecres.wholphin.services.ExtrasService
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
@ -56,6 +57,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -229,6 +233,20 @@ class SeriesViewModel
discovered.update { results } discovered.update { results }
} }
} }
mediaManagementService.deletedItemFlow
.onEach { deletedItem ->
if (deletedItem.item.data.seriesId == seriesId) {
Timber.d(
"Item %s deleted from series %s",
deletedItem.item.id,
seriesId,
)
val seasons = getSeasons(item, seasonEpisodeIds?.seasonNumber).await()
this@SeriesViewModel.seasons.setValueOnMain(seasons)
}
}.catch { ex ->
Timber.e(ex, "Error refreshing after deleted item")
}.launchIn(viewModelScope)
} }
} }
@ -614,6 +632,11 @@ class SeriesViewModel
} }
suspend fun canDelete(item: BaseItem): Boolean = mediaManagementService.canDelete(item) suspend fun canDelete(item: BaseItem): Boolean = mediaManagementService.canDelete(item)
fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean = mediaManagementService.canDelete(item, appPreferences)
} }
sealed interface EpisodeList { sealed interface EpisodeList {

View file

@ -55,6 +55,7 @@ import com.github.damontecres.wholphin.ui.cards.BannerCardWithTitle
import com.github.damontecres.wholphin.ui.cards.GenreCard import com.github.damontecres.wholphin.ui.cards.GenreCard
import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.EpisodeName import com.github.damontecres.wholphin.ui.components.EpisodeName
@ -62,6 +63,7 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.FocusableItemRow import com.github.damontecres.wholphin.ui.components.FocusableItemRow
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.components.QuickDetails
import com.github.damontecres.wholphin.ui.components.RowColumnItem
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
@ -116,6 +118,7 @@ fun HomePage(
LoadingState.Success -> { LoadingState.Success -> {
var dialog by remember { mutableStateOf<DialogParams?>(null) } var dialog by remember { mutableStateOf<DialogParams?>(null) }
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) } var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
var showDeleteDialog by remember { mutableStateOf<RowColumnItem?>(null) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var position by rememberPosition() var position by rememberPosition()
HomePageContent( HomePageContent(
@ -136,6 +139,7 @@ fun HomePage(
playbackPosition = item.playbackPosition, playbackPosition = item.playbackPosition,
watched = item.played, watched = item.played,
favorite = item.favorite, favorite = item.favorite,
canDelete = viewModel.canDelete(item, preferences.appPreferences),
actions = actions =
MoreDialogActions( MoreDialogActions(
navigateTo = viewModel.navigationManager::navigateTo, navigateTo = viewModel.navigationManager::navigateTo,
@ -150,6 +154,9 @@ fun HomePage(
showPlaylistDialog = itemId showPlaylistDialog = itemId
}, },
onSendMediaInfo = viewModel.mediaReportService::sendReportFor, onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
onClickDelete = {
showDeleteDialog = RowColumnItem(position, item)
},
), ),
) )
dialog = dialog =
@ -190,6 +197,16 @@ fun HomePage(
elevation = 3.dp, elevation = 3.dp,
) )
} }
showDeleteDialog?.let { (position, item) ->
ConfirmDeleteDialog(
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
onCancel = { showDeleteDialog = null },
onConfirm = {
viewModel.deleteItem(position, item)
showDeleteDialog = null
},
)
}
} }
} }
} }

View file

@ -6,16 +6,21 @@ import androidx.lifecycle.viewModelScope
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
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.DatePlayedService import com.github.damontecres.wholphin.services.DatePlayedService
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.HomePageResolvedSettings import com.github.damontecres.wholphin.services.HomePageResolvedSettings
import com.github.damontecres.wholphin.services.HomeSettingsService import com.github.damontecres.wholphin.services.HomeSettingsService
import com.github.damontecres.wholphin.services.MediaManagementService
import com.github.damontecres.wholphin.services.MediaReportService import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavDrawerService import com.github.damontecres.wholphin.services.NavDrawerService
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
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.tvAccess import com.github.damontecres.wholphin.services.tvAccess
import com.github.damontecres.wholphin.ui.data.RowColumn
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.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
@ -52,6 +57,7 @@ class HomeViewModel
private val datePlayedService: DatePlayedService, private val datePlayedService: DatePlayedService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
private val userPreferencesService: UserPreferencesService, private val userPreferencesService: UserPreferencesService,
private val mediaManagementService: MediaManagementService,
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(HomeState.EMPTY) private val _state = MutableStateFlow(HomeState.EMPTY)
val state: StateFlow<HomeState> = _state val state: StateFlow<HomeState> = _state
@ -189,6 +195,36 @@ class HomeViewModel
backdropService.submit(item) backdropService.submit(item)
} }
} }
fun deleteItem(
position: RowColumn,
item: BaseItem,
) {
deleteItem(context, mediaManagementService, item) {
viewModelScope.launchDefault {
val row = state.value.homeRows.getOrNull(position.row)
if (row is HomeRowLoadingState.Success) {
_state.update {
val newRow =
row.items.toMutableList().apply {
removeAt(position.column)
}
it.copy(
homeRows =
it.homeRows.toMutableList().apply {
set(position.row, row.copy(items = newRow))
},
)
}
}
}
}
}
fun canDelete(
item: BaseItem,
appPreferences: AppPreferences,
): Boolean = mediaManagementService.canDelete(item, appPreferences)
} }
data class HomeState( data class HomeState(