Fixes/improves showing delete button (#1353)

## Description
Fixes the delete button not showing on the movie detail page. Also fixes
some edge cases where button may not appear when toggling on the media
management setting.

Adds the delete button to album & artist pages. Previously, it was only
in the context menu.

### Related issues
Fixes #1343

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-05-06 11:49:27 -04:00 committed by GitHub
parent 461bad1ddd
commit 8107a82a6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 65 additions and 25 deletions

View file

@ -6,12 +6,15 @@ 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.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.combinePair
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
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collectLatest
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
@ -68,6 +71,16 @@ class MediaManagementService
} }
} }
suspend fun collectCanDelete(
itemFlow: Flow<BaseItem?>,
update: (Boolean) -> Unit,
) {
itemFlow.combinePair(userPreferencesService.flow).collectLatest { (item, prefs) ->
val canDelete = item?.let { canDelete(item, prefs.appPreferences) } ?: false
update.invoke(canDelete)
}
}
/** /**
* Delete the item. * Delete the item.
* *

View file

@ -207,7 +207,10 @@ fun CollectionDetails(
} }
}, },
canDelete = canDelete =
remember(state.collection) { remember(
state.collection,
preferences.appPreferences.interfacePreferences.enableMediaManagement,
) {
state.collection?.let { state.collection?.let {
viewModel.canDelete(it, preferences.appPreferences) viewModel.canDelete(it, preferences.appPreferences)
} ?: false } ?: false

View file

@ -20,7 +20,6 @@ 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.services.deleteItem
import com.github.damontecres.wholphin.ui.combinePair
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
@ -38,9 +37,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
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
@ -81,12 +77,8 @@ class EpisodeViewModel
init { init {
init() init()
viewModelScope.launchDefault { viewModelScope.launchDefault {
item mediaManagementService.collectCanDelete(item.asFlow()) { canDelete ->
.asFlow() this@EpisodeViewModel.canDelete.update { canDelete }
.filterNotNull()
.combinePair(userPreferencesService.flow.map { it.appPreferences })
.collectLatest { (item, preferences) ->
canDelete.update { mediaManagementService.canDelete(item, preferences) }
} }
} }
} }

View file

@ -42,7 +42,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi import org.jellyfin.sdk.api.client.extensions.libraryApi
@ -85,18 +85,9 @@ class MovieViewModel
init { init {
init() init()
viewModelScope.launchDefault { viewModelScope.launchDefault {
userPreferencesService.flow.collectLatest { preferences -> mediaManagementService.collectCanDelete(state.map { it.movie }) { canDelete ->
_state.update { _state.update {
val canDelete = it.copy(canDelete = canDelete)
it.movie?.let {
mediaManagementService.canDelete(
it,
preferences.appPreferences,
)
}
it.copy(
canDelete = canDelete ?: false,
)
} }
} }
} }

View file

@ -94,6 +94,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -136,6 +137,11 @@ class AlbumViewModel
init { init {
init() init()
viewModelScope.launchDefault {
mediaManagementService.collectCanDelete(state.map { it.album }) { canDelete ->
_state.update { it.copy(canDelete = canDelete) }
}
}
} }
override fun init() { override fun init() {
@ -275,6 +281,7 @@ data class AlbumState(
val similar: List<BaseItem>, val similar: List<BaseItem>,
val loading: LoadingState, val loading: LoadingState,
val musicVideos: List<BaseItem?> = emptyList(), val musicVideos: List<BaseItem?> = emptyList(),
val canDelete: Boolean = false,
) { ) {
companion object { companion object {
val EMPTY = AlbumState(null, false, null, emptyList(), emptyList(), LoadingState.Pending) val EMPTY = AlbumState(null, false, null, emptyList(), emptyList(), LoadingState.Pending)
@ -389,6 +396,7 @@ fun AlbumDetailsPage(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
MusicExpandableButtons( MusicExpandableButtons(
title = album.title ?: "",
actions = actions =
remember { remember {
MusicButtonActions( MusicButtonActions(
@ -415,9 +423,13 @@ fun AlbumDetailsPage(
actions = moreDialogActions, actions = moreDialogActions,
) )
}, },
onConfirmDelete = {
viewModel.deleteItem(album)
},
) )
}, },
favorite = album.favorite, favorite = album.favorite,
canDelete = state.canDelete,
buttonOnFocusChanged = { buttonOnFocusChanged = {
if (it.isFocused) { if (it.isFocused) {
position = RowColumn(HEADER_ROW, 0) position = RowColumn(HEADER_ROW, 0)

View file

@ -92,6 +92,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -135,6 +136,11 @@ class ArtistViewModel
init { init {
init() init()
viewModelScope.launchDefault {
mediaManagementService.collectCanDelete(state.map { it.artist }) { canDelete ->
_state.update { it.copy(canDelete = canDelete) }
}
}
} }
override fun init() { override fun init() {
@ -269,6 +275,7 @@ data class ArtistState(
val similar: List<BaseItem>, val similar: List<BaseItem>,
val loading: LoadingState, val loading: LoadingState,
val musicVideos: List<BaseItem?>, val musicVideos: List<BaseItem?>,
val canDelete: Boolean,
) { ) {
companion object { companion object {
val EMPTY = val EMPTY =
@ -280,6 +287,7 @@ data class ArtistState(
emptyList(), emptyList(),
LoadingState.Pending, LoadingState.Pending,
emptyList(), emptyList(),
false,
) )
} }
} }
@ -378,6 +386,7 @@ fun ArtistDetailsPage(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
MusicExpandableButtons( MusicExpandableButtons(
title = artist.title ?: "",
actions = actions =
remember { remember {
MusicButtonActions( MusicButtonActions(
@ -406,9 +415,11 @@ fun ArtistDetailsPage(
actions = moreDialogActions, actions = moreDialogActions,
) )
}, },
onConfirmDelete = { viewModel.deleteItem(artist) },
) )
}, },
favorite = artist.favorite, favorite = artist.favorite,
canDelete = state.canDelete,
buttonOnFocusChanged = { buttonOnFocusChanged = {
if (it.isFocused) { if (it.isFocused) {
position = RowColumn(HEADER_ROW, 0) position = RowColumn(HEADER_ROW, 0)

View file

@ -18,14 +18,17 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.DeleteButton
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
import kotlin.time.Duration import kotlin.time.Duration
@Composable @Composable
fun MusicExpandableButtons( fun MusicExpandableButtons(
title: String,
actions: MusicButtonActions, actions: MusicButtonActions,
favorite: Boolean, favorite: Boolean,
canDelete: Boolean,
buttonOnFocusChanged: (FocusState) -> Unit, buttonOnFocusChanged: (FocusState) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -75,6 +78,15 @@ fun MusicExpandableButtons(
modifier = Modifier.onFocusChanged(buttonOnFocusChanged), modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
) )
} }
if (canDelete) {
item("delete") {
DeleteButton(
title = title,
onConfirmDelete = actions.onConfirmDelete,
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
)
}
}
item("more") { item("more") {
ExpandablePlayButton( ExpandablePlayButton(
title = R.string.more, title = R.string.more,
@ -92,4 +104,5 @@ data class MusicButtonActions(
val onClickInstantMix: () -> Unit, val onClickInstantMix: () -> Unit,
val onClickFavorite: () -> Unit, val onClickFavorite: () -> Unit,
val onClickMore: () -> Unit, val onClickMore: () -> Unit,
val onConfirmDelete: () -> Unit,
) )

View file

@ -59,6 +59,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -138,7 +139,11 @@ class SeriesViewModel
Timber.v("Start") Timber.v("Start")
addCloseable { themeSongPlayer.stop() } addCloseable { themeSongPlayer.stop() }
val item = fetchItem(seriesId) val item = fetchItem(seriesId)
canDeleteSeries.update { mediaManagementService.canDelete(item) } viewModelScope.launchDefault {
mediaManagementService.collectCanDelete(flowOf(item)) { canDelete ->
canDeleteSeries.update { canDelete }
}
}
backdropService.submit(item) backdropService.submit(item)
val seasonsDeferred = getSeasons(item, seasonEpisodeIds?.seasonNumber) val seasonsDeferred = getSeasons(item, seasonEpisodeIds?.seasonNumber)