diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt index 6cf0ae25..7be5878a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt @@ -1,25 +1,38 @@ package com.github.damontecres.wholphin.ui.data +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text import com.github.damontecres.wholphin.ui.components.ScrollableDialog import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.letNotEmpty +import com.github.damontecres.wholphin.util.byteRateSuffixes +import com.github.damontecres.wholphin.util.formatBytes +import com.github.damontecres.wholphin.util.languageName +import org.jellyfin.sdk.model.api.AudioSpatialFormat +import org.jellyfin.sdk.model.api.MediaSourceInfo +import org.jellyfin.sdk.model.api.MediaStreamType +import java.util.Locale data class ItemDetailsDialogInfo( val title: String, val overview: String?, - val files: List, + val files: List, ) @Composable fun ItemDetailsDialog( info: ItemDetailsDialogInfo, + showFilePath: Boolean, onDismissRequest: () -> Unit, ) = ScrollableDialog( onDismissRequest = onDismissRequest, @@ -27,26 +40,107 @@ fun ItemDetailsDialog( item { Text( text = info.title, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleLarge, ) } if (info.overview.isNotNullOrBlank()) { item { Text( text = info.overview, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, ) } } if (info.files.isNotEmpty()) { item { - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) } } items(info.files) { file -> - Text( - text = "- $file", - style = MaterialTheme.typography.bodyMedium, - ) + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + MediaSourceInfo(file, showFilePath, Modifier.padding(top = 12.dp)) + } + } +} + +@Composable +fun MediaSourceInfo( + source: MediaSourceInfo, + showFilePath: Boolean, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + Text( + text = "Name: ${source.name}", + ) + Text( + text = "ID: ${source.id}", + ) + if (showFilePath) { + Text( + text = "Path: ${source.path}", + ) + } + source.size?.let { size -> + Text( + text = "Size: ${formatBytes(size)}", + ) + } + source.bitrate?.let { bitrate -> + Text( + text = "Bitrate: ${formatBytes(bitrate, byteRateSuffixes)}", + ) + } + source.mediaStreams?.letNotEmpty { streams -> + streams.filter { it.type == MediaStreamType.VIDEO }.forEach { stream -> + val data = + buildList { + if (stream.width != null && stream.height != null) { + add("${stream.width}x${stream.height}") + } + stream.averageFrameRate?.let { + add(String.format(Locale.getDefault(), "%.3f", it) + " fps") + } + stream.bitRate?.let { add(formatBytes(it, byteRateSuffixes)) } + stream.codec?.let(::add) + stream.profile?.let(::add) + } + Text( + text = "Video: " + data.joinToString(" - "), + ) + } + + streams.filter { it.type == MediaStreamType.AUDIO }.forEachIndexed { index, stream -> + val data = + buildList { + stream.language?.let { add(languageName(it)) } + stream.codec?.let(::add) + stream.channelLayout?.let(::add) + stream.bitRate?.let { add(formatBytes(it, byteRateSuffixes)) } + if (stream.audioSpatialFormat != AudioSpatialFormat.NONE) add(stream.audioSpatialFormat.serialName) + if (stream.isDefault) add("Default") + } + Text( + text = "Audio ${index + 1}: " + data.joinToString(" - "), + ) + } + + streams.filter { it.type == MediaStreamType.SUBTITLE }.forEachIndexed { index, stream -> + val data = + buildList { + stream.language?.let { add(languageName(it)) } + stream.codec?.let(::add) + if (stream.isDefault) add("Default") + if (stream.isForced) add("Forced") + if (stream.isExternal) add("External") + } + Text( + text = "Subtitle ${index + 1}: " + data.joinToString(" - "), + ) + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index 426af231..86dfbdbd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -193,6 +193,7 @@ fun PersonPage( overview = person.data.overview, files = listOf(), ), + showFilePath = false, onDismissRequest = { showOverviewDialog = false }, ) } @@ -339,8 +340,10 @@ fun PersonHeader( // .fillMaxWidth(.7f) .weight(3f) .padding(top = 16.dp, end = 32.dp) - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), RoundedCornerShape(10)) - .padding(16.dp), + .background( + MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), + RoundedCornerShape(10), + ).padding(16.dp), ) { Text( text = name, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 16c56b12..301363d1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -142,10 +142,7 @@ fun MovieDetails( ItemDetailsDialogInfo( title = movie.name ?: "Unknown", overview = movie.data.overview, - files = - movie.data.mediaSources - ?.mapNotNull { it.path } - .orEmpty(), + files = movie.data.mediaSources.orEmpty(), ) }, moreOnClick = { @@ -228,6 +225,10 @@ fun MovieDetails( overviewDialog?.let { info -> ItemDetailsDialog( info = info, + showFilePath = + viewModel.serverRepository.currentUserDto + ?.policy + ?.isAdministrator == true, onDismissRequest = { overviewDialog = null }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 50733339..85ff5a9d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -176,6 +176,7 @@ fun SeriesDetails( overviewDialog?.let { info -> ItemDetailsDialog( info = info, + showFilePath = false, onDismissRequest = { overviewDialog = null }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 04bd1f6a..4502ef07 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -315,10 +315,7 @@ fun SeriesOverview( ItemDetailsDialogInfo( title = it.name ?: "Unknown", overview = it.data.overview, - files = - it.data.mediaSources - ?.mapNotNull { it.path } - .orEmpty(), + files = it.data.mediaSources.orEmpty(), ) } }, @@ -331,6 +328,10 @@ fun SeriesOverview( overviewDialog?.let { info -> ItemDetailsDialog( info = info, + showFilePath = + viewModel.serverRepository.currentUserDto + ?.policy + ?.isAdministrator == true, onDismissRequest = { overviewDialog = null }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 606ce71f..04da9868 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -61,7 +61,7 @@ class SeriesViewModel constructor( api: ApiClient, @param:ApplicationContext val context: Context, - private val serverRepository: ServerRepository, + val serverRepository: ServerRepository, private val navigationManager: NavigationManager, private val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt index 9e9dfd41..7076276a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt @@ -140,3 +140,27 @@ fun abbreviateNumber(number: Int): String { } return String.format(Locale.getDefault(), "%.1f%s", count, abbrevSuffixes[unit]) } + +val byteSuffixes = listOf("B", "KB", "MB", "GB", "TB") +val byteRateSuffixes = listOf("bps", "kbps", "mbps", "gbps", "tbps") + +/** + * Format bytes + */ +fun formatBytes( + bytes: Int, + suffixes: List = byteSuffixes, +) = formatBytes(bytes.toLong(), suffixes) + +fun formatBytes( + bytes: Long, + suffixes: List = byteSuffixes, +): String { + var unit = 0 + var count = bytes.toDouble() + while (count >= 1024 && unit + 1 < suffixes.size) { + count /= 1024 + unit++ + } + return String.format(Locale.getDefault(), "%.2f%s", count, suffixes[unit]) +}