Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,7 @@ interface AdminRepository {
replaceAllMetadata: Boolean,
replaceAllImages: Boolean,
): Result<Unit>

suspend fun deleteItem(itemId: String): Result<Unit>
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.jellyfin.sdk.api.operations.ImageApi
import org.jellyfin.sdk.api.operations.ItemLookupApi
import org.jellyfin.sdk.api.operations.ItemRefreshApi
import org.jellyfin.sdk.api.operations.ItemUpdateApi
import org.jellyfin.sdk.api.operations.LibraryApi
import org.jellyfin.sdk.api.operations.RemoteImageApi
import org.jellyfin.sdk.api.operations.UserLibraryApi
import org.jellyfin.sdk.model.FileInfo
Expand Down Expand Up @@ -476,6 +477,26 @@ constructor(
}
}

override suspend fun deleteItem(itemId: String): Result<Unit> =
withContext(Dispatchers.IO) {
try {
val apiClient =
getApiClient()
?: return@withContext Result.failure(
IllegalStateException("No API client")
)
LibraryApi(apiClient).deleteItem(itemId = UUID.fromString(itemId))
adminChangeBroadcaster.notifyItemChanged(itemId)
Result.success(Unit)
} catch (e: ApiClientException) {
Timber.e(e, "Failed to delete item $itemId")
Result.failure(e)
} catch (e: Exception) {
Timber.e(e, "Unexpected error deleting item $itemId")
Result.failure(e)
}
}

private fun BaseItemDto.toEditableItem(availableRatings: List<String>): EditableItem =
EditableItem(
id = id.toString(),
Expand Down
102 changes: 102 additions & 0 deletions app/src/main/java/com/makd/afinity/ui/item/ItemDetailScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.makd.afinity.ui.item
import android.content.Context
import android.content.res.Configuration
import androidx.annotation.OptIn
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -26,9 +27,14 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import java.util.UUID
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -131,6 +137,7 @@ fun ItemDetailScreen(
val canDownload by viewModel.canDownload.collectAsStateWithLifecycle()
val isAdmin by viewModel.isAdmin.collectAsStateWithLifecycle()
var showEpisodeRefreshDialog by remember { mutableStateOf(false) }
var showEpisodeDeleteDialog by remember { mutableStateOf(false) }
val lifecycleOwner = LocalLifecycleOwner.current

DisposableEffect(lifecycleOwner) {
Expand Down Expand Up @@ -279,6 +286,7 @@ fun ItemDetailScreen(
)
AdminAction.Refresh -> showEpisodeRefreshDialog = true
AdminAction.Identify -> Unit
AdminAction.Delete -> showEpisodeDeleteDialog = true
}
},
)
Expand All @@ -289,6 +297,17 @@ fun ItemDetailScreen(
onDismiss = { showEpisodeRefreshDialog = false },
)
}

if (showEpisodeDeleteDialog) {
DeleteConfirmationDialog(
targetId = episode.id,
targetName = episode.name,
isMainItem = false,
viewModel = viewModel,
navController = navController,
onDismiss = { showEpisodeDeleteDialog = false },
)
}
}

LaunchedEffect(selectedEpisode, pendingNavigationSeriesId) {
Expand Down Expand Up @@ -511,6 +530,7 @@ private fun LandscapeItemDetailContent(
val canDownload by viewModel.canDownload.collectAsStateWithLifecycle()
val isAdmin by viewModel.isAdmin.collectAsStateWithLifecycle()
var showRefreshDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.getTop(density)
val displayCutoutLeft = WindowInsets.displayCutout.getLeft(density, LayoutDirection.Ltr)
Expand Down Expand Up @@ -665,6 +685,7 @@ private fun LandscapeItemDetailContent(
)
)
AdminAction.Refresh -> showRefreshDialog = true
AdminAction.Delete -> showDeleteDialog = true
}
},
modifier = Modifier.weight(2f),
Expand All @@ -676,6 +697,17 @@ private fun LandscapeItemDetailContent(
onDismiss = { showRefreshDialog = false },
)
}

if (showDeleteDialog) {
DeleteConfirmationDialog(
targetId = item.id,
targetName = item.name,
isMainItem = true,
viewModel = viewModel,
navController = navController,
onDismiss = { showDeleteDialog = false },
)
}
}

VideoQualitySelection(
Expand Down Expand Up @@ -751,6 +783,7 @@ private fun PortraitItemDetailContent(
val canDownload by viewModel.canDownload.collectAsStateWithLifecycle()
val isAdmin by viewModel.isAdmin.collectAsStateWithLifecycle()
var showRefreshDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
val playerOffset = LocalPlayerOffset.current

LazyColumn(
Expand Down Expand Up @@ -839,6 +872,7 @@ private fun PortraitItemDetailContent(
Destination.createEditImagesRoute(item.id.toString())
)
AdminAction.Refresh -> showRefreshDialog = true
AdminAction.Delete -> showDeleteDialog = true
}
},
)
Expand All @@ -850,6 +884,17 @@ private fun PortraitItemDetailContent(
)
}

if (showDeleteDialog) {
DeleteConfirmationDialog(
targetId = item.id,
targetName = item.name,
isMainItem = true,
viewModel = viewModel,
navController = navController,
onDismiss = { showDeleteDialog = false },
)
}

VideoQualitySelection(
mediaSourceOptions = mediaSourceOptions,
selectedSource = selectedMediaSource,
Expand Down Expand Up @@ -1155,3 +1200,60 @@ private fun shufflePlay(item: AfinityItem, nextEpisode: AfinityEpisode?, context
)
}
}

@Composable
private fun DeleteConfirmationDialog(
targetId: UUID,
targetName: String,
isMainItem: Boolean,
viewModel: ItemDetailViewModel,
navController: NavController,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.admin_delete_dialog_title)) },
text = { Text(stringResource(R.string.admin_delete_dialog_message, targetName)) },
confirmButton = {
Button(
onClick = {
onDismiss()
viewModel.deleteItem(
targetItemId = targetId,
onSuccess = {
Toast.makeText(
context,
R.string.admin_delete_success,
Toast.LENGTH_SHORT,
).show()
if (isMainItem) {
navController.popBackStack()
} else {
viewModel.clearSelectedEpisode()
}
},
onError = { error ->
Toast.makeText(
context,
context.getString(R.string.admin_delete_error, error),
Toast.LENGTH_LONG,
).show()
},
)
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError,
),
) {
Text(stringResource(R.string.admin_action_delete))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.action_cancel))
}
},
)
}
19 changes: 19 additions & 0 deletions app/src/main/java/com/makd/afinity/ui/item/ItemDetailViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.paging.map
import com.makd.afinity.R
import com.makd.afinity.data.database.entities.ItemMetadataCacheEntity
import com.makd.afinity.data.manager.AdminChangeBroadcaster
import com.makd.afinity.data.repository.admin.AdminRepository
import com.makd.afinity.data.manager.MediaChangeManager
import com.makd.afinity.data.manager.MediaChangeSource
import com.makd.afinity.data.manager.OfflineModeManager
Expand Down Expand Up @@ -98,6 +99,7 @@ constructor(
private val authRepository: AuthRepository,
private val playbackStateManager: PlaybackStateManager,
private val adminChangeBroadcaster: AdminChangeBroadcaster,
private val adminRepository: AdminRepository,
private val mediaChangeManager: MediaChangeManager,
private val serverRepository: ServerRepository,
private val securePreferencesRepository: SecurePreferencesRepository,
Expand Down Expand Up @@ -1574,6 +1576,23 @@ constructor(
return episodes.firstOrNull { it.playbackPositionTicks > 0 && !it.played }
?: episodes.firstOrNull { !it.played }
}

fun deleteItem(
targetItemId: UUID,
onSuccess: () -> Unit,
onError: (String) -> Unit,
) {
viewModelScope.launch {
val result = adminRepository.deleteItem(targetItemId.toString())
if (result.isSuccess) {
adminChangeBroadcaster.notifyItemChanged(targetItemId.toString())
onSuccess()
} else {
val errorMsg = result.exceptionOrNull()?.localizedMessage ?: "Unknown error"
onError(errorMsg)
}
}
}
}

data class ItemDetailUiState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,26 @@ fun EpisodeDetailOverlay(
onAdminAction(AdminAction.Refresh)
},
)
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.admin_action_delete),
color = MaterialTheme.colorScheme.error,
)
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_delete),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp),
)
},
onClick = {
menuExpanded = false
onAdminAction(AdminAction.Delete)
},
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,26 @@ fun ActionButtonsRow(
onAdminAction(AdminAction.Refresh)
},
)
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.admin_action_delete),
color = MaterialTheme.colorScheme.error,
)
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_delete),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp),
)
},
onClick = {
menuExpanded = false
onAdminAction(AdminAction.Delete)
},
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ sealed interface AdminAction {
data object Identify : AdminAction
data object EditImages : AdminAction
data object Refresh : AdminAction
data object Delete : AdminAction
}
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,11 @@
<string name="admin_action_identify">Identify</string>
<string name="admin_action_edit_images">Edit Images</string>
<string name="admin_action_refresh_metadata">Refresh Metadata</string>
<string name="admin_action_delete">Delete</string>
<string name="admin_delete_dialog_title">Delete Item</string>
<string name="admin_delete_dialog_message">Are you sure you want to delete "%1$s"? This action cannot be undone and will delete the media file(s) from your server.</string>
<string name="admin_delete_success">Item deleted successfully</string>
<string name="admin_delete_error">Failed to delete item: %1$s</string>

<!--Admin - Refresh Metadata Dialog-->
<string name="admin_refresh_title">Refresh Metadata</string>
Expand Down