Improvements to overview/media info dialog (#580)

## Description
* Splits video details into two columns
* Show index number if multiple streams of the same type
* Different font color for media section titles
* Adjustments to spacing
* Add hearing impaired flag to & remove AVC from subtitle info

### Related issues
Closes #549
This commit is contained in:
Ray 2025-12-27 12:47:01 -05:00 committed by GitHub
parent a797bd8267
commit fd1feddab3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,8 +8,10 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -57,30 +59,28 @@ fun ItemDetailsDialog(
ScrollableDialog( ScrollableDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
width = 720.dp, width = 680.dp,
maxHeight = 480.dp, maxHeight = 440.dp,
itemSpacing = 4.dp, itemSpacing = 8.dp,
) { ) {
item { item {
Text( Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
text = info.title,
style = MaterialTheme.typography.titleLarge,
)
}
if (info.genres.isNotEmpty()) {
item {
Text( Text(
text = info.genres.joinToString(", "), text = info.title,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.headlineSmall,
)
}
}
if (info.overview.isNotNullOrBlank()) {
item {
Text(
text = info.overview,
style = MaterialTheme.typography.bodyLarge,
) )
if (info.genres.isNotEmpty()) {
Text(
text = info.genres.joinToString(", "),
style = MaterialTheme.typography.titleSmall,
)
}
if (info.overview.isNotNullOrBlank()) {
Text(
text = info.overview,
style = MaterialTheme.typography.bodyMedium,
)
}
} }
} }
@ -89,13 +89,19 @@ fun ItemDetailsDialog(
source.mediaStreams?.letNotEmpty { mediaStreams -> source.mediaStreams?.letNotEmpty { mediaStreams ->
item { item {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
HorizontalDivider()
} }
// General file information // General file information
item { item {
val containerLabel = stringResource(R.string.container) val containerLabel = stringResource(R.string.container)
MediaInfoSection( MediaInfoSection(
title = stringResource(R.string.general), title =
titleIndex(
stringResource(R.string.general),
index,
info.files.size,
),
items = items =
buildList { buildList {
source.container?.let { add(containerLabel to it) } source.container?.let { add(containerLabel to it) }
@ -116,26 +122,30 @@ fun ItemDetailsDialog(
} }
// Video streams // Video streams
items(mediaStreams.filter { it.type == MediaStreamType.VIDEO }) { stream -> val videoStreams = mediaStreams.filter { it.type == MediaStreamType.VIDEO }
itemsIndexed(videoStreams) { index, stream ->
MediaInfoSection( MediaInfoSection(
title = videoLabel, title = titleIndex(videoLabel, index, videoStreams.size),
items = buildVideoStreamInfo(context, stream), items = remember { buildVideoStreamInfo(context, stream) },
additional = remember { buildVideoStreamInfoAdditional(context, stream) },
) )
} }
// Audio streams - display multiple per row // Audio streams - display multiple per row
items( val audioStreams = mediaStreams.filter { it.type == MediaStreamType.AUDIO }
mediaStreams itemsIndexed(audioStreams.chunked(3)) { groupIndex, streamGroup ->
.filter { it.type == MediaStreamType.AUDIO }
.chunked(3),
) { streamGroup ->
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
streamGroup.forEach { stream -> streamGroup.forEachIndexed { index, stream ->
MediaInfoSection( MediaInfoSection(
title = audioLabel, title =
titleIndex(
audioLabel,
groupIndex * 3 + index,
audioStreams.size,
),
items = buildAudioStreamInfo(context, stream), items = buildAudioStreamInfo(context, stream),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
@ -148,19 +158,21 @@ fun ItemDetailsDialog(
} }
// Subtitle streams - display multiple per row // Subtitle streams - display multiple per row
items( val subtitleStreams = mediaStreams.filter { it.type == MediaStreamType.SUBTITLE }
mediaStreams itemsIndexed(subtitleStreams.chunked(3)) { groupIndex, streamGroup ->
.filter { it.type == MediaStreamType.SUBTITLE }
.chunked(3),
) { streamGroup ->
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
streamGroup.forEach { stream -> streamGroup.forEachIndexed { index, stream ->
MediaInfoSection( MediaInfoSection(
title = subtitleLabel, title =
items = buildSubtitleStreamInfo(context, stream), titleIndex(
subtitleLabel,
groupIndex * 3 + index,
subtitleStreams.size,
),
items = buildSubtitleStreamInfo(context, stream, showFilePath),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
@ -185,6 +197,7 @@ private fun MediaInfoSection(
title: String, title: String,
items: List<Pair<String, String>>, items: List<Pair<String, String>>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
additional: List<Pair<String, String>> = listOf(),
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
@ -192,66 +205,86 @@ private fun MediaInfoSection(
) { ) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.onSurface,
) )
items.forEach { (label, value) -> Row(
Row( horizontalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier.padding(start = 12.dp), modifier = Modifier.padding(start = 12.dp),
) { ) {
Text( Column(modifier = Modifier.weight(1f, fill = false)) {
text = "$label: ", items.forEach { (label, value) ->
style = MaterialTheme.typography.bodyMedium, Row(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier,
) ) {
Text( Text(
text = value, text = "$label: ",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
if (additional.isNotEmpty()) {
Column(modifier = Modifier.weight(1f, fill = false)) {
additional.forEach { (label, value) ->
Row(
modifier = Modifier,
) {
Text(
text = "$label: ",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
} }
} }
} }
} }
fun titleIndex(
title: String,
index: Int,
total: Int,
) = if (total > 1) {
"$title (${index + 1})"
} else {
title
}
private fun buildVideoStreamInfo( private fun buildVideoStreamInfo(
context: Context, context: Context,
stream: MediaStream, stream: MediaStream,
): List<Pair<String, String>> = ): List<Pair<String, String>> =
buildList { buildList {
val titleLabel = context.getString(R.string.title)
val codecLabel = context.getString(R.string.codec)
val avcLabel = context.getString(R.string.avc)
val profileLabel = context.getString(R.string.profile)
val levelLabel = context.getString(R.string.level)
val resolutionLabel = context.getString(R.string.resolution)
val aspectRatioLabel = context.getString(R.string.aspect_ratio)
val anamorphicLabel = context.getString(R.string.anamorphic)
val interlacedLabel = context.getString(R.string.interlaced)
val framerateLabel = context.getString(R.string.framerate)
val bitrateLabel = context.getString(R.string.bitrate)
val bitDepthLabel = context.getString(R.string.bit_depth)
val videoRangeLabel = context.getString(R.string.video_range)
val videoRangeTypeLabel = context.getString(R.string.video_range_type)
val colorSpaceLabel = context.getString(R.string.color_space)
val colorTransferLabel = context.getString(R.string.color_transfer)
val colorPrimariesLabel = context.getString(R.string.color_primaries)
val pixelFormatLabel = context.getString(R.string.pixel_format)
val refFramesLabel = context.getString(R.string.ref_frames)
val nalLabel = context.getString(R.string.nal)
val yesStr = context.getString(R.string.yes) val yesStr = context.getString(R.string.yes)
val noStr = context.getString(R.string.no) val noStr = context.getString(R.string.no)
val titleLabel = context.getString(R.string.title)
val codecLabel = context.getString(R.string.codec)
val resolutionLabel = context.getString(R.string.resolution)
val aspectRatioLabel = context.getString(R.string.aspect_ratio)
val framerateLabel = context.getString(R.string.framerate)
val bitrateLabel = context.getString(R.string.bitrate)
val profileLabel = context.getString(R.string.profile)
val levelLabel = context.getString(R.string.level)
val interlacedLabel = context.getString(R.string.interlaced)
val videoRangeLabel = context.getString(R.string.video_range)
val sdrStr = context.getString(R.string.sdr) val sdrStr = context.getString(R.string.sdr)
val hdrStr = context.getString(R.string.hdr) val hdrStr = context.getString(R.string.hdr)
val hdr10Str = context.getString(R.string.hdr10)
val hdr10PlusStr = context.getString(R.string.hdr10_plus)
val hlgStr = context.getString(R.string.hlg)
val bitUnit = context.getString(R.string.bit_unit)
stream.title?.let { add(titleLabel to it) } stream.title?.let { add(titleLabel to it) }
stream.codec?.let { add(codecLabel to it.uppercase()) } stream.codec?.let { add(codecLabel to it.uppercase()) }
stream.isAvc?.let { add(avcLabel to if (it) yesStr else noStr) }
stream.profile?.let { add(profileLabel to it) }
stream.level?.let { add(levelLabel to it.toString()) }
if (stream.width != null && stream.height != null) { if (stream.width != null && stream.height != null) {
add(resolutionLabel to "${stream.width}x${stream.height}") add(resolutionLabel to "${stream.width}x${stream.height}")
} }
@ -259,23 +292,55 @@ private fun buildVideoStreamInfo(
val aspectRatio = calculateAspectRatio(stream.width!!, stream.height!!) val aspectRatio = calculateAspectRatio(stream.width!!, stream.height!!)
add(aspectRatioLabel to aspectRatio) add(aspectRatioLabel to aspectRatio)
} }
stream.isAnamorphic?.let { add(anamorphicLabel to if (it) yesStr else noStr) } stream.bitRate?.let { add(bitrateLabel to formatBytes(it, byteRateSuffixes)) }
stream.isInterlaced?.let { add(interlacedLabel to if (it) yesStr else noStr) }
stream.averageFrameRate?.let { stream.averageFrameRate?.let {
add(framerateLabel to String.format(Locale.getDefault(), "%.3f", it)) add(framerateLabel to String.format(Locale.getDefault(), "%.3f", it))
} }
stream.bitRate?.let { add(bitrateLabel to formatBytes(it, byteRateSuffixes)) }
stream.bitDepth?.let { add(bitDepthLabel to "$it $bitUnit") } stream.videoRange.let {
stream.videoRange?.let {
val rangeStr = val rangeStr =
when (it) { when (it) {
VideoRange.SDR -> sdrStr VideoRange.SDR -> sdrStr
VideoRange.HDR -> hdrStr VideoRange.HDR -> hdrStr
VideoRange.UNKNOWN -> null VideoRange.UNKNOWN -> null
else -> null
} }
rangeStr?.let { add(videoRangeLabel to it) } rangeStr?.let { add(videoRangeLabel to it) }
} }
stream.profile?.let { add(profileLabel to it) }
stream.level?.let { add(levelLabel to it.toString()) }
stream.isInterlaced.let { add(interlacedLabel to if (it) yesStr else noStr) }
}
private fun buildVideoStreamInfoAdditional(
context: Context,
stream: MediaStream,
): List<Pair<String, String>> =
buildList {
val yesStr = context.getString(R.string.yes)
val noStr = context.getString(R.string.no)
val avcLabel = context.getString(R.string.avc)
val anamorphicLabel = context.getString(R.string.anamorphic)
val bitDepthLabel = context.getString(R.string.bit_depth)
val videoRangeTypeLabel = context.getString(R.string.video_range_type)
val colorSpaceLabel = context.getString(R.string.color_space)
val colorTransferLabel = context.getString(R.string.color_transfer)
val colorPrimariesLabel = context.getString(R.string.color_primaries)
val pixelFormatLabel = context.getString(R.string.pixel_format)
val refFramesLabel = context.getString(R.string.ref_frames)
val nalLabel = context.getString(R.string.nal)
val dolbyVisionLabel = context.getString(R.string.dolby_vision)
val sdrStr = context.getString(R.string.sdr)
val hdr10Str = context.getString(R.string.hdr10)
val hdr10PlusStr = context.getString(R.string.hdr10_plus)
val hlgStr = context.getString(R.string.hlg)
val bitUnit = context.getString(R.string.bit_unit)
stream.isAvc?.let { add(avcLabel to if (it) yesStr else noStr) }
stream.isAnamorphic?.let { add(anamorphicLabel to if (it) yesStr else noStr) }
stream.bitDepth?.let { add(bitDepthLabel to "$it $bitUnit") }
stream.videoRangeType?.let { stream.videoRangeType?.let {
val rangeTypeStr = val rangeTypeStr =
when (it) { when (it) {
@ -304,7 +369,8 @@ private fun buildVideoStreamInfo(
stream.colorPrimaries?.let { add(colorPrimariesLabel to it) } stream.colorPrimaries?.let { add(colorPrimariesLabel to it) }
stream.pixelFormat?.let { add(pixelFormatLabel to it) } stream.pixelFormat?.let { add(pixelFormatLabel to it) }
stream.refFrames?.let { add(refFramesLabel to it.toString()) } stream.refFrames?.let { add(refFramesLabel to it.toString()) }
stream.nalLengthSize?.let { add(nalLabel to it.toString()) } stream.nalLengthSize?.let { add(nalLabel to it) }
stream.videoDoViTitle?.let { add(dolbyVisionLabel to it) }
} }
private fun buildAudioStreamInfo( private fun buildAudioStreamInfo(
@ -341,17 +407,18 @@ private fun buildAudioStreamInfo(
private fun buildSubtitleStreamInfo( private fun buildSubtitleStreamInfo(
context: Context, context: Context,
stream: MediaStream, stream: MediaStream,
showPath: Boolean,
): List<Pair<String, String>> = ): List<Pair<String, String>> =
buildList { buildList {
val titleLabel = context.getString(R.string.title) val titleLabel = context.getString(R.string.title)
val languageLabel = context.getString(R.string.language) val languageLabel = context.getString(R.string.language)
val codecLabel = context.getString(R.string.codec) val codecLabel = context.getString(R.string.codec)
val avcLabel = context.getString(R.string.avc)
val defaultLabel = context.getString(R.string.default_track) val defaultLabel = context.getString(R.string.default_track)
val forcedLabel = context.getString(R.string.forced_track) val forcedLabel = context.getString(R.string.forced_track)
val externalLabel = context.getString(R.string.external_track) val externalLabel = context.getString(R.string.external_track)
val yesStr = context.getString(R.string.yes) val yesStr = context.getString(R.string.yes)
val noStr = context.getString(R.string.no) val noStr = context.getString(R.string.no)
val pathLabel = context.getString(R.string.path)
stream.title?.let { add(titleLabel to it) } stream.title?.let { add(titleLabel to it) }
stream.language?.let { add(languageLabel to languageName(it)) } stream.language?.let { add(languageLabel to languageName(it)) }
@ -359,10 +426,15 @@ private fun buildSubtitleStreamInfo(
val formattedCodec = formatSubtitleCodec(it) ?: it.uppercase() val formattedCodec = formatSubtitleCodec(it) ?: it.uppercase()
add(codecLabel to formattedCodec) add(codecLabel to formattedCodec)
} }
stream.isAvc?.let { add(avcLabel to if (it) yesStr else noStr) }
stream.isDefault?.let { add(defaultLabel to if (it) yesStr else noStr) } stream.isDefault?.let { add(defaultLabel to if (it) yesStr else noStr) }
stream.isForced?.let { add(forcedLabel to if (it) yesStr else noStr) } stream.isForced?.let { add(forcedLabel to if (it) yesStr else noStr) }
stream.isExternal?.let { add(externalLabel to if (it) yesStr else noStr) } stream.isExternal?.let { add(externalLabel to if (it) yesStr else noStr) }
stream.isHearingImpaired?.let {
add((stream.localizedHearingImpaired ?: "SDH") to if (it) yesStr else noStr)
}
if (showPath) {
stream.path?.let { pathLabel to it }
}
} }
private fun calculateAspectRatio( private fun calculateAspectRatio(