mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add option to delete media (#1014)
## Description Adds a setting to enable "Media management" which adds a context menu item to delete media. This is accessible using either the More button or long clicking. You can delete episodes, seasons, series and movies/videos. If order for the option to appear the current user must have server-side settings for "Allow media deletion from" enabled for the library and the in-app setting under Settings->Advanced Settings->"Show media management options". ### Related issues Closes #104 ### Testing Emulator ## Screenshots ### Context menu  ### Confirmation  ## AI or LLM usage None
This commit is contained in:
parent
0b73b4cf64
commit
8935067c5a
19 changed files with 597 additions and 53 deletions
|
|
@ -62,13 +62,13 @@ class WholphinApplication :
|
|||
.penaltyDeathOnNetwork()
|
||||
.build(),
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy
|
||||
.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
// StrictMode.setVmPolicy(
|
||||
// StrictMode.VmPolicy
|
||||
// .Builder()
|
||||
// .detectAll()
|
||||
// .penaltyLog()
|
||||
// .build(),
|
||||
// )
|
||||
}
|
||||
OkHttp.initialize(this)
|
||||
initAcra {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ data class BaseItem(
|
|||
}
|
||||
}
|
||||
|
||||
val canDelete: Boolean get() = data.canDelete == true
|
||||
|
||||
@Transient
|
||||
val aspectRatio: Float? = data.primaryImageAspectRatio?.toFloat()?.takeIf { it > 0 }
|
||||
|
||||
|
|
|
|||
|
|
@ -741,6 +741,18 @@ sealed interface AppPreference<Pref, T> {
|
|||
valueToIndex = { it.number },
|
||||
)
|
||||
|
||||
val ManageMedia =
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.show_media_management,
|
||||
defaultValue = false,
|
||||
getter = { it.interfacePreferences.enableMediaManagement },
|
||||
setter = { prefs, value ->
|
||||
prefs.updateInterfacePreferences { enableMediaManagement = value }
|
||||
},
|
||||
summaryOn = R.string.enabled,
|
||||
summaryOff = R.string.disabled,
|
||||
)
|
||||
|
||||
val OneClickPause =
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.one_click_pause,
|
||||
|
|
@ -1110,6 +1122,7 @@ val advancedPreferences =
|
|||
preferences =
|
||||
listOf(
|
||||
AppPreference.ShowClock,
|
||||
AppPreference.ManageMedia,
|
||||
AppPreference.CombineContinueNext,
|
||||
// Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
|
||||
// AppPreference.NavDrawerSwitchOnFocus,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Context
|
||||
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.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MediaManagementService
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
) {
|
||||
private val _deletedItemFlow =
|
||||
MutableSharedFlow<DeletedItem>(
|
||||
replay = 1,
|
||||
extraBufferCapacity = 0,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
/**
|
||||
* Listen for recently deleted items. Useful for ViewModels to react and refresh data
|
||||
*/
|
||||
val deletedItemFlow: SharedFlow<DeletedItem> = _deletedItemFlow
|
||||
|
||||
suspend fun canDelete(item: BaseItem): Boolean {
|
||||
Timber.v("canDelete %s: %s", item.id, item.canDelete)
|
||||
val enabled =
|
||||
userPreferencesService
|
||||
.getCurrent()
|
||||
.appPreferences.interfacePreferences.enableMediaManagement
|
||||
return enabled &&
|
||||
item.canDelete &&
|
||||
if (item.type == BaseItemKind.RECORDING) {
|
||||
serverRepository.currentUserDto.value
|
||||
?.policy
|
||||
?.enableLiveTvManagement == true
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteItem(item: BaseItem): DeleteResult {
|
||||
try {
|
||||
Timber.i("Deleting %s", item.id)
|
||||
// TODO enable
|
||||
api.libraryApi.deleteItem(item.id)
|
||||
_deletedItemFlow.emit(DeletedItem(item))
|
||||
return DeleteResult.Success
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error deleting %s", item.id)
|
||||
return DeleteResult.Error(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DeletedItem(
|
||||
val item: BaseItem,
|
||||
)
|
||||
|
||||
sealed interface DeleteResult {
|
||||
data object Success : DeleteResult
|
||||
|
||||
data class Error(
|
||||
val ex: Exception,
|
||||
) : DeleteResult
|
||||
}
|
||||
|
||||
fun ViewModel.deleteItem(
|
||||
context: Context,
|
||||
mediaManagementService: MediaManagementService,
|
||||
item: BaseItem,
|
||||
onSuccess: () -> Unit = {},
|
||||
) = viewModelScope.launchIO {
|
||||
when (val r = mediaManagementService.deleteItem(item)) {
|
||||
is DeleteResult.Error -> {
|
||||
showToast(
|
||||
context,
|
||||
"Error deleting item: ${r.ex.localizedMessage}",
|
||||
)
|
||||
}
|
||||
|
||||
DeleteResult.Success -> {
|
||||
showToast(context, "Deleted")
|
||||
onSuccess.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.ui.showToast
|
|||
import com.github.damontecres.wholphin.util.supportedCollectionTypes
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
|
|
@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
|
|
@ -149,7 +151,9 @@ class NavDrawerService
|
|||
val allItems = builtins + libraries
|
||||
|
||||
val navDrawerPins =
|
||||
serverPreferencesDao.getNavDrawerPinnedItems(user).associateBy { it.itemId }
|
||||
withContext(Dispatchers.IO) {
|
||||
serverPreferencesDao.getNavDrawerPinnedItems(user).associateBy { it.itemId }
|
||||
}
|
||||
|
||||
val items = mutableListOf<NavDrawerItem>()
|
||||
val moreItems = mutableListOf<NavDrawerItem>()
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ object AppModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
@DefaultCoroutineScope
|
||||
fun defaultCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
fun defaultCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ val DefaultItemFields =
|
|||
ItemFields.MEDIA_SOURCES,
|
||||
ItemFields.MEDIA_SOURCE_COUNT,
|
||||
ItemFields.PARENT_ID,
|
||||
ItemFields.CAN_DELETE,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -72,6 +73,7 @@ val SlimItemFields =
|
|||
ItemFields.SORT_NAME,
|
||||
ItemFields.MEDIA_SOURCE_COUNT,
|
||||
ItemFields.PARENT_ID,
|
||||
ItemFields.CAN_DELETE,
|
||||
)
|
||||
|
||||
val PhotoItemFields =
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
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.ThemeSongPlayer
|
||||
|
|
@ -76,6 +77,7 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
|||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
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.main.HomePageHeader
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -128,6 +130,7 @@ class CollectionFolderViewModel
|
|||
private val navigationManager: NavigationManager,
|
||||
private val themeSongPlayer: ThemeSongPlayer,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val mediaManagementService: MediaManagementService,
|
||||
val mediaReportService: MediaReportService,
|
||||
@Assisted itemId: String,
|
||||
@Assisted initialSortAndDirection: SortAndDirection?,
|
||||
|
|
@ -199,6 +202,19 @@ class CollectionFolderViewModel
|
|||
loading.setValueOnMain(DataLoadingState.Error(ex))
|
||||
}
|
||||
}
|
||||
viewModelScope.launchDefault {
|
||||
mediaManagementService.deletedItemFlow.collect { deletedItem ->
|
||||
val pager =
|
||||
((loading.value as? DataLoadingState.Success)?.data as? ApiRequestPager<*>)
|
||||
position.let {
|
||||
Timber.v("Item deleted: position=%s, id=%s", it, deletedItem.item.id)
|
||||
val item = pager?.get(it)
|
||||
if (item?.id == deletedItem.item.id) {
|
||||
pager.refreshPagesAfter(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLibraryDisplayInfo(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import androidx.annotation.StringRes
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
|
|
@ -17,11 +19,14 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -30,9 +35,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
|
@ -56,8 +64,12 @@ import androidx.tv.material3.surfaceColorAtElevation
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.playback.SimpleMediaStream
|
||||
import com.github.damontecres.wholphin.ui.playback.isDown
|
||||
import com.github.damontecres.wholphin.ui.playback.isUp
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -259,31 +271,65 @@ fun DialogPopupContent(
|
|||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
val focusRequesters = remember { List(dialogItems.size) { FocusRequester() } }
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
items(dialogItems) {
|
||||
when (it) {
|
||||
itemsIndexed(dialogItems) { index, item ->
|
||||
when (item) {
|
||||
is DialogItemDivider -> {
|
||||
HorizontalDivider(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
is DialogItem -> {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
ListItem(
|
||||
selected = it.selected,
|
||||
enabled = !waiting && it.enabled,
|
||||
selected = item.selected,
|
||||
enabled = !waiting && item.enabled,
|
||||
onClick = {
|
||||
if (dismissOnClick) {
|
||||
onDismissRequest.invoke()
|
||||
}
|
||||
it.onClick.invoke()
|
||||
item.onClick.invoke()
|
||||
},
|
||||
headlineContent = it.headlineContent,
|
||||
overlineContent = it.overlineContent,
|
||||
supportingContent = it.supportingContent,
|
||||
leadingContent = it.leadingContent,
|
||||
trailingContent = it.trailingContent,
|
||||
modifier = Modifier,
|
||||
headlineContent = item.headlineContent,
|
||||
overlineContent = item.overlineContent,
|
||||
supportingContent = item.supportingContent,
|
||||
leadingContent = item.leadingContent,
|
||||
trailingContent = item.trailingContent,
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(focusRequesters[index])
|
||||
.ifElse(
|
||||
index == 0,
|
||||
Modifier.onKeyEvent {
|
||||
if (focused && isUp(it) && it.type == KeyEventType.KeyDown) {
|
||||
scope.launch {
|
||||
listState.animateScrollToItem(dialogItems.lastIndex)
|
||||
focusRequesters[dialogItems.lastIndex].tryRequestFocus()
|
||||
}
|
||||
return@onKeyEvent true
|
||||
}
|
||||
false
|
||||
},
|
||||
).ifElse(
|
||||
index == dialogItems.lastIndex,
|
||||
Modifier.onKeyEvent {
|
||||
if (focused && isDown(it) && it.type == KeyEventType.KeyDown) {
|
||||
scope.launch {
|
||||
listState.animateScrollToItem(0)
|
||||
focusRequesters[0].tryRequestFocus()
|
||||
}
|
||||
return@onKeyEvent true
|
||||
}
|
||||
false
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -471,6 +517,80 @@ fun ConfirmDialogContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConfirmDeleteDialog(
|
||||
itemTitle: String,
|
||||
onCancel: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
BasicDialog(
|
||||
onDismissRequest = onCancel,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.delete_item),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
text = itemTitle,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
Button(
|
||||
onClick = onCancel,
|
||||
modifier = Modifier.width(72.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.cancel),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = Color.Red.copy(alpha = .8f),
|
||||
modifier = Modifier,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.confirm),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun chooseVersionParams(
|
||||
context: Context,
|
||||
sources: List<MediaSourceInfo>,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ package com.github.damontecres.wholphin.ui.components
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -17,6 +21,7 @@ import androidx.compose.foundation.layout.requiredSizeIn
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
|
|
@ -34,6 +39,7 @@ import androidx.compose.ui.focus.focusRestorer
|
|||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -42,6 +48,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.LocalContentColor
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
|
|
@ -234,7 +241,7 @@ fun ExpandablePlayButton(
|
|||
fun ExpandablePlayButton(
|
||||
@StringRes title: Int,
|
||||
resume: Duration,
|
||||
icon: @Composable () -> Unit,
|
||||
icon: @Composable BoxScope.() -> Unit,
|
||||
onClick: (position: Duration) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
|
|
@ -254,12 +261,13 @@ fun ExpandablePlayButton(
|
|||
interactionSource = interactionSource,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 2.dp, top = 2.dp)
|
||||
.padding(start = 2.dp)
|
||||
.height(MinButtonSize),
|
||||
) {
|
||||
icon.invoke()
|
||||
icon.invoke(this)
|
||||
}
|
||||
AnimatedVisibility(isFocused) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
|
@ -359,6 +367,48 @@ fun TrailerButton(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val iconTint by
|
||||
animateColorAsState(
|
||||
targetValue =
|
||||
if (focused) {
|
||||
Color.Red.copy(alpha = .8f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.8f,
|
||||
)
|
||||
},
|
||||
animationSpec =
|
||||
tween(
|
||||
easing = LinearEasing,
|
||||
),
|
||||
)
|
||||
ExpandablePlayButton(
|
||||
title = R.string.delete,
|
||||
resume = Duration.ZERO,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = if (iconTint.isSpecified) iconTint else LocalContentColor.current,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 2.dp)
|
||||
.size(24.dp),
|
||||
)
|
||||
},
|
||||
onClick = { onClick.invoke() },
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewTvSpec
|
||||
@Composable
|
||||
private fun ExpandablePlayButtonsPreview() {
|
||||
|
|
@ -417,12 +467,19 @@ private fun ViewOptionsPreview() {
|
|||
onClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
DeleteButton(
|
||||
onClick = {},
|
||||
)
|
||||
SortByButton(
|
||||
sortOptions = listOf(),
|
||||
current = SortAndDirection(ItemSortBy.DEFAULT, SortOrder.ASCENDING),
|
||||
onSortChange = {},
|
||||
)
|
||||
}
|
||||
DeleteButton(
|
||||
onClick = {},
|
||||
interactionSource = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ data class MoreDialogActions(
|
|||
val onClickFavorite: (UUID, Boolean) -> Unit,
|
||||
val onClickAddPlaylist: (UUID) -> Unit,
|
||||
val onSendMediaInfo: (UUID) -> Unit,
|
||||
val onClickDelete: (BaseItem) -> Unit = {},
|
||||
)
|
||||
|
||||
enum class ClearChosenStreams {
|
||||
|
|
@ -62,6 +63,7 @@ fun buildMoreDialogItems(
|
|||
watched: Boolean,
|
||||
favorite: Boolean,
|
||||
canClearChosenStreams: Boolean,
|
||||
canDelete: Boolean = false,
|
||||
actions: MoreDialogActions,
|
||||
onChooseVersion: () -> Unit,
|
||||
onChooseTracks: (MediaStreamType) -> Unit,
|
||||
|
|
@ -139,6 +141,17 @@ fun buildMoreDialogItems(
|
|||
actions.onClickAddPlaylist.invoke(item.id)
|
||||
},
|
||||
)
|
||||
if (canDelete) {
|
||||
add(
|
||||
DialogItem(
|
||||
context.getString(R.string.delete),
|
||||
Icons.Default.Delete,
|
||||
iconColor = Color.Red.copy(alpha = .8f),
|
||||
) {
|
||||
actions.onClickDelete.invoke(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
add(
|
||||
DialogItem(
|
||||
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
||||
|
|
@ -223,6 +236,7 @@ fun buildMoreDialogItemsForHome(
|
|||
playbackPosition: Duration,
|
||||
watched: Boolean,
|
||||
favorite: Boolean,
|
||||
canDelete: Boolean = false,
|
||||
actions: MoreDialogActions,
|
||||
): List<DialogItem> =
|
||||
buildList {
|
||||
|
|
@ -290,6 +304,17 @@ fun buildMoreDialogItemsForHome(
|
|||
actions.onClickAddPlaylist.invoke(itemId)
|
||||
},
|
||||
)
|
||||
if (canDelete) {
|
||||
add(
|
||||
DialogItem(
|
||||
context.getString(R.string.delete),
|
||||
Icons.Default.Delete,
|
||||
iconColor = Color.Red.copy(alpha = .8f),
|
||||
) {
|
||||
actions.onClickDelete.invoke(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
add(
|
||||
DialogItem(
|
||||
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
|||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -111,6 +112,7 @@ fun MovieDetails(
|
|||
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
|
||||
|
||||
val moreActions =
|
||||
MoreDialogActions(
|
||||
|
|
@ -126,6 +128,7 @@ fun MovieDetails(
|
|||
showPlaylistDialog.makePresent(itemId)
|
||||
},
|
||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||
onClickDelete = { showDeleteDialog = it },
|
||||
)
|
||||
|
||||
when (val state = loading) {
|
||||
|
|
@ -201,6 +204,7 @@ fun MovieDetails(
|
|||
seriesId = null,
|
||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
|
||||
canDelete = viewModel.canDelete,
|
||||
actions = moreActions,
|
||||
onChooseVersion = {
|
||||
chooseVersion =
|
||||
|
|
@ -291,6 +295,7 @@ fun MovieDetails(
|
|||
playbackPosition = similar.playbackPosition,
|
||||
watched = similar.played,
|
||||
favorite = similar.favorite,
|
||||
canDelete = false,
|
||||
actions = moreActions,
|
||||
)
|
||||
moreDialog =
|
||||
|
|
@ -362,6 +367,16 @@ fun MovieDetails(
|
|||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
showDeleteDialog?.let { item ->
|
||||
ConfirmDeleteDialog(
|
||||
itemTitle = item.title ?: "",
|
||||
onCancel = { showDeleteDialog = null },
|
||||
onConfirm = {
|
||||
viewModel.deleteItem(item)
|
||||
showDeleteDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEADER_ROW = 0
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
|||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.ExtrasService
|
||||
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.PeopleFavorites
|
||||
|
|
@ -26,6 +27,7 @@ import com.github.damontecres.wholphin.services.StreamChoiceService
|
|||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
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.launchIO
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
|
|
@ -73,6 +75,7 @@ class MovieViewModel
|
|||
private val extrasService: ExtrasService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val backdropService: BackdropService,
|
||||
private val mediaManagementService: MediaManagementService,
|
||||
@Assisted val itemId: UUID,
|
||||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
|
|
@ -90,6 +93,9 @@ class MovieViewModel
|
|||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||
val discovered = MutableStateFlow<List<DiscoverItem>>(listOf())
|
||||
|
||||
var canDelete: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
|
@ -106,6 +112,7 @@ class MovieViewModel
|
|||
api.userLibraryApi.getItem(itemId).content.let {
|
||||
BaseItem.from(it, api)
|
||||
}
|
||||
canDelete = mediaManagementService.canDelete(item)
|
||||
this@MovieViewModel.item.setValueOnMain(item)
|
||||
item
|
||||
}
|
||||
|
|
@ -274,4 +281,10 @@ class MovieViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem(item: BaseItem) {
|
||||
deleteItem(context, mediaManagementService, item) {
|
||||
navigationManager.goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -24,12 +25,14 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -53,7 +56,9 @@ import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
|||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
|
||||
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
||||
import com.github.damontecres.wholphin.ui.components.DeleteButton
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
|
|
@ -76,6 +81,7 @@ import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
|||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||
import com.github.damontecres.wholphin.ui.discover.DiscoverRow
|
||||
import com.github.damontecres.wholphin.ui.discover.DiscoverRowData
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
|
|
@ -102,21 +108,25 @@ fun SeriesDetails(
|
|||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
|
||||
val item by viewModel.item.observeAsState()
|
||||
val canDelete by viewModel.canDeleteSeries.collectAsState()
|
||||
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||
val trailers by viewModel.trailers.observeAsState(listOf())
|
||||
val extras by viewModel.extras.observeAsState(listOf())
|
||||
val people by viewModel.people.observeAsState(listOf())
|
||||
val similar by viewModel.similar.observeAsState(listOf())
|
||||
val discovered by viewModel.discovered.collectAsState()
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
var showWatchConfirmation by remember { mutableStateOf(false) }
|
||||
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> {
|
||||
|
|
@ -150,6 +160,7 @@ fun SeriesDetails(
|
|||
similar = similar,
|
||||
played = played,
|
||||
favorite = item.data.userData?.isFavorite ?: false,
|
||||
canDelete = canDelete,
|
||||
modifier = modifier,
|
||||
onClickItem = { index, item ->
|
||||
viewModel.navigateTo(item.destination())
|
||||
|
|
@ -163,23 +174,29 @@ fun SeriesDetails(
|
|||
)
|
||||
},
|
||||
onLongClickItem = { index, season ->
|
||||
seasonDialog =
|
||||
buildDialogForSeason(
|
||||
context = context,
|
||||
s = season,
|
||||
onClickItem = { viewModel.navigateTo(it.destination()) },
|
||||
markPlayed = { played ->
|
||||
viewModel.setSeasonWatched(season.id, played)
|
||||
},
|
||||
onClickPlay = { shuffle ->
|
||||
viewModel.navigateTo(
|
||||
Destination.PlaybackList(
|
||||
itemId = season.id,
|
||||
shuffle = shuffle,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
scope.launchDefault {
|
||||
seasonDialog =
|
||||
buildDialogForSeason(
|
||||
context = context,
|
||||
s = season,
|
||||
canDelete = viewModel.canDelete(season),
|
||||
onClickItem = { viewModel.navigateTo(it.destination()) },
|
||||
markPlayed = { played ->
|
||||
viewModel.setSeasonWatched(season.id, played)
|
||||
},
|
||||
onClickPlay = { shuffle ->
|
||||
viewModel.navigateTo(
|
||||
Destination.PlaybackList(
|
||||
itemId = season.id,
|
||||
shuffle = shuffle,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClickDelete = {
|
||||
showDeleteDialog = it
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
overviewOnClick = {
|
||||
overviewDialog =
|
||||
|
|
@ -231,6 +248,9 @@ fun SeriesDetails(
|
|||
showPlaylistDialog.makePresent(itemId)
|
||||
},
|
||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||
onClickDelete = {
|
||||
showDeleteDialog = it
|
||||
},
|
||||
),
|
||||
)
|
||||
if (showWatchConfirmation) {
|
||||
|
|
@ -283,6 +303,19 @@ fun SeriesDetails(
|
|||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
showDeleteDialog?.let { item ->
|
||||
ConfirmDeleteDialog(
|
||||
itemTitle = item.title ?: "",
|
||||
onCancel = { showDeleteDialog = null },
|
||||
onConfirm = {
|
||||
if (seasons?.lastOrNull()?.id == item.id) {
|
||||
focusManager.moveFocus(FocusDirection.Previous)
|
||||
}
|
||||
viewModel.deleteItem(item)
|
||||
showDeleteDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEADER_ROW = 0
|
||||
|
|
@ -305,6 +338,7 @@ fun SeriesDetailsContent(
|
|||
discovered: List<DiscoverItem>,
|
||||
played: Boolean,
|
||||
favorite: Boolean,
|
||||
canDelete: Boolean,
|
||||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
onClickPerson: (Person) -> Unit,
|
||||
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||
|
|
@ -436,6 +470,23 @@ fun SeriesDetailsContent(
|
|||
}
|
||||
},
|
||||
)
|
||||
if (canDelete) {
|
||||
DeleteButton(
|
||||
onClick = {
|
||||
position = HEADER_ROW
|
||||
moreActions.onClickDelete.invoke(series)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
|
@ -638,9 +689,11 @@ fun SeriesDetailsHeader(
|
|||
fun buildDialogForSeason(
|
||||
context: Context,
|
||||
s: BaseItem,
|
||||
canDelete: Boolean,
|
||||
onClickItem: (BaseItem) -> Unit,
|
||||
markPlayed: (Boolean) -> Unit,
|
||||
onClickPlay: (Boolean) -> Unit,
|
||||
onClickDelete: (BaseItem) -> Unit,
|
||||
): DialogParams {
|
||||
val items =
|
||||
buildList {
|
||||
|
|
@ -679,6 +732,17 @@ fun buildDialogForSeason(
|
|||
onClickPlay.invoke(true)
|
||||
},
|
||||
)
|
||||
if (canDelete) {
|
||||
add(
|
||||
DialogItem(
|
||||
context.getString(R.string.delete),
|
||||
Icons.Default.Delete,
|
||||
iconColor = Color.Red.copy(alpha = .8f),
|
||||
) {
|
||||
onClickDelete.invoke(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
return DialogParams(
|
||||
title = s.name ?: context.getString(R.string.tv_season),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -23,6 +24,7 @@ import com.github.damontecres.wholphin.data.ChosenStreams
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
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.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -36,9 +38,11 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
|||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -89,6 +93,7 @@ fun SeriesOverview(
|
|||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val firstItemFocusRequester = remember { FocusRequester() }
|
||||
val episodeRowFocusRequester = remember { FocusRequester() }
|
||||
val castCrewRowFocusRequester = remember { FocusRequester() }
|
||||
|
|
@ -118,6 +123,7 @@ fun SeriesOverview(
|
|||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
var rowFocused by rememberInt()
|
||||
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
|
||||
|
||||
LaunchedEffect(episodes) {
|
||||
episodes?.let { episodes ->
|
||||
|
|
@ -177,7 +183,7 @@ fun SeriesOverview(
|
|||
}
|
||||
}
|
||||
|
||||
fun buildMoreForEpisode(
|
||||
suspend fun buildMoreForEpisode(
|
||||
ep: BaseItem,
|
||||
chosenStreams: ChosenStreams?,
|
||||
fromLongClick: Boolean,
|
||||
|
|
@ -194,6 +200,7 @@ fun SeriesOverview(
|
|||
seriesId = series.id,
|
||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
|
||||
canDelete = viewModel.canDelete(ep),
|
||||
actions =
|
||||
MoreDialogActions(
|
||||
navigateTo = viewModel::navigateTo,
|
||||
|
|
@ -216,6 +223,9 @@ fun SeriesOverview(
|
|||
showPlaylistDialog = it
|
||||
},
|
||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||
onClickDelete = {
|
||||
showDeleteDialog = it
|
||||
},
|
||||
),
|
||||
onChooseVersion = {
|
||||
chooseVersion =
|
||||
|
|
@ -315,7 +325,9 @@ fun SeriesOverview(
|
|||
)
|
||||
},
|
||||
onLongClick = { ep ->
|
||||
moreDialog = buildMoreForEpisode(ep, chosenStreams, true)
|
||||
scope.launchDefault {
|
||||
moreDialog = buildMoreForEpisode(ep, chosenStreams, true)
|
||||
}
|
||||
},
|
||||
playOnClick = { resume ->
|
||||
rowFocused = EPISODE_ROW
|
||||
|
|
@ -343,18 +355,22 @@ fun SeriesOverview(
|
|||
},
|
||||
moreOnClick = {
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
|
||||
moreDialog = buildMoreForEpisode(ep, chosenStreams, false)
|
||||
scope.launchDefault {
|
||||
moreDialog = buildMoreForEpisode(ep, chosenStreams, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
overviewOnClick = {
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
||||
overviewDialog =
|
||||
ItemDetailsDialogInfo(
|
||||
title = it.name ?: context.getString(R.string.unknown),
|
||||
overview = it.data.overview,
|
||||
genres = it.data.genres.orEmpty(),
|
||||
files = it.data.mediaSources.orEmpty(),
|
||||
)
|
||||
scope.launchDefault {
|
||||
overviewDialog =
|
||||
ItemDetailsDialogInfo(
|
||||
title = it.name ?: context.getString(R.string.unknown),
|
||||
overview = it.data.overview,
|
||||
genres = it.data.genres.orEmpty(),
|
||||
files = it.data.mediaSources.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
personOnClick = {
|
||||
|
|
@ -420,6 +436,17 @@ fun SeriesOverview(
|
|||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
showDeleteDialog?.let { item ->
|
||||
ConfirmDeleteDialog(
|
||||
itemTitle = item.subtitle ?: "",
|
||||
onCancel = { showDeleteDialog = null },
|
||||
onConfirm = {
|
||||
viewModel.deleteItem(item)
|
||||
episodeRowFocusRequester.tryRequestFocus()
|
||||
showDeleteDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val EPISODE_ROW = 0
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.data.model.Trailer
|
|||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.ExtrasService
|
||||
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.PeopleFavorites
|
||||
|
|
@ -24,10 +25,12 @@ import com.github.damontecres.wholphin.services.StreamChoiceService
|
|||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
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.detail.ItemViewModel
|
||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||
import com.github.damontecres.wholphin.ui.gt
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.lt
|
||||
|
|
@ -90,6 +93,7 @@ class SeriesViewModel
|
|||
private val userPreferencesService: UserPreferencesService,
|
||||
private val backdropService: BackdropService,
|
||||
private val seerrService: SeerrService,
|
||||
private val mediaManagementService: MediaManagementService,
|
||||
@Assisted val seriesId: UUID,
|
||||
@Assisted val seasonEpisodeIds: SeasonEpisodeIds?,
|
||||
@Assisted val seriesPageType: SeriesPageType,
|
||||
|
|
@ -111,6 +115,7 @@ class SeriesViewModel
|
|||
val extras = MutableLiveData<List<ExtrasItem>>(listOf())
|
||||
val people = MutableLiveData<List<Person>>(listOf())
|
||||
val similar = MutableLiveData<List<BaseItem>>()
|
||||
val canDeleteSeries = MutableStateFlow(false)
|
||||
|
||||
val peopleInEpisode = MutableLiveData<PeopleInItem>(PeopleInItem())
|
||||
val discovered = MutableStateFlow<List<DiscoverItem>>(listOf())
|
||||
|
|
@ -127,6 +132,7 @@ class SeriesViewModel
|
|||
Timber.v("Start")
|
||||
addCloseable { themeSongPlayer.stop() }
|
||||
val item = fetchItem(seriesId)
|
||||
canDeleteSeries.update { mediaManagementService.canDelete(item) }
|
||||
backdropService.submit(item)
|
||||
|
||||
val seasonsDeferred = getSeasons(item, seasonEpisodeIds?.seasonNumber)
|
||||
|
|
@ -259,9 +265,12 @@ class SeriesViewModel
|
|||
if (seriesPageType == SeriesPageType.DETAILS) {
|
||||
listOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CAN_DELETE,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
listOf(
|
||||
ItemFields.CAN_DELETE,
|
||||
)
|
||||
},
|
||||
)
|
||||
val pager =
|
||||
|
|
@ -300,6 +309,7 @@ class SeriesViewModel
|
|||
ItemFields.OVERVIEW,
|
||||
ItemFields.CUSTOM_RATING,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CAN_DELETE,
|
||||
),
|
||||
)
|
||||
Timber.v(
|
||||
|
|
@ -551,6 +561,59 @@ class SeriesViewModel
|
|||
lookUpChosenTracks(item.id, item)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem(item: BaseItem) {
|
||||
deleteItem(context, mediaManagementService, item) {
|
||||
viewModelScope.launchDefault {
|
||||
if (item.type == BaseItemKind.SERIES) {
|
||||
navigationManager.goBack()
|
||||
} else if (seriesPageType == SeriesPageType.DETAILS) {
|
||||
this@SeriesViewModel.item.value?.let { series ->
|
||||
val seasons = getSeasons(series, null).await()
|
||||
if (seasons.isEmpty()) {
|
||||
navigationManager.goBack()
|
||||
} else {
|
||||
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
position.value.let { (_, episodeIndex) ->
|
||||
val eps = episodes.value as? EpisodeList.Success
|
||||
if (eps != null) {
|
||||
val pager = eps.episodes
|
||||
val lastIndex = pager.lastIndex
|
||||
pager.refreshPagesAfter(episodeIndex)
|
||||
if (pager.isEmpty()) {
|
||||
navigationManager.goBack()
|
||||
} else {
|
||||
if (episodeIndex == lastIndex) {
|
||||
// Deleted last episode, so need to move left
|
||||
episodes.setValueOnMain(
|
||||
EpisodeList.Success(
|
||||
eps.seasonId,
|
||||
pager,
|
||||
episodeIndex - 1,
|
||||
),
|
||||
)
|
||||
position.update { it.copy(episodeRowIndex = episodeIndex - 1) }
|
||||
} else {
|
||||
episodes.setValueOnMain(
|
||||
EpisodeList.Success(
|
||||
eps.seasonId,
|
||||
pager,
|
||||
episodeIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun canDelete(item: BaseItem): Boolean = mediaManagementService.canDelete(item)
|
||||
}
|
||||
|
||||
sealed interface EpisodeList {
|
||||
|
|
|
|||
|
|
@ -166,6 +166,22 @@ class ApiRequestPager<T>(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the cache for all the pages at or after the given position and fetches a new page
|
||||
*/
|
||||
suspend fun refreshPagesAfter(position: Int) {
|
||||
val pageNumber = position / pageSize
|
||||
cachedPages.asMap().apply {
|
||||
keys.forEach { pageKey ->
|
||||
if (pageKey >= pageNumber) {
|
||||
if (DEBUG) Timber.v("refreshPagesAfter: dropping %s", pageKey)
|
||||
remove(pageKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchPageBlocking(position, true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ message InterfacePreferences {
|
|||
BackdropStyle backdrop_style = 9;
|
||||
SubtitlePreferences hdr_subtitles_preferences = 10;
|
||||
ScreensaverPreferences screensaver_preference = 11;
|
||||
bool enable_media_management = 12;
|
||||
}
|
||||
|
||||
message AdvancedPreferences {
|
||||
|
|
|
|||
|
|
@ -692,6 +692,8 @@
|
|||
<string name="no_subtitles_found">No remote subtitles were found</string>
|
||||
<string name="series_continueing">Present</string>
|
||||
<string name="in_app_screensaver">Use in-app screensaver</string>
|
||||
<string name="delete_item">Are you sure you want to delete this item?</string>
|
||||
<string name="show_media_management">Show media management options</string>
|
||||
<string-array name="backdrop_style_options">
|
||||
<item>@string/backdrop_style_dynamic</item>
|
||||
<item>@string/backdrop_style_image</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue