[Media viewer] Improve rendering of files

Closes #6899
Closes #6908
This commit is contained in:
Benoit Marty
2026-06-02 10:22:34 +02:00
parent a095190902
commit 97135272ec
6 changed files with 78 additions and 37 deletions
@@ -103,6 +103,30 @@ fun aPdfMediaInfo(
duration = null, duration = null,
) )
fun aZipMediaInfo(
filename: String = "a zip file.zip",
caption: String? = null,
formattedCaption: CharSequence? = null,
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
fileSize = 12 * 1024 * 1024,
caption = caption,
formattedCaption = formattedCaption,
mimeType = MimeTypes.Pdf,
formattedFileSize = "45MB",
fileExtension = "zip",
senderId = UserId("@alice:server.org"),
senderName = senderName,
senderAvatar = null,
dateSent = dateSent,
dateSentFull = dateSentFull,
waveform = null,
duration = null,
)
fun anApkMediaInfo( fun anApkMediaInfo(
senderId: UserId? = UserId("@alice:server.org"), senderId: UserId? = UserId("@alice:server.org"),
senderName: String? = null, senderName: String? = null,
@@ -41,6 +41,7 @@ class DefaultLocalMediaRenderer(
textFileViewer = textFileViewer, textFileViewer = textFileViewer,
audioFocus = audioFocus, audioFocus = audioFocus,
onClick = {}, onClick = {},
onOpenWith = null,
) )
} }
} }
@@ -31,6 +31,7 @@ fun LocalMediaView(
bottomPaddingInPixels: Int, bottomPaddingInPixels: Int,
audioFocus: AudioFocus?, audioFocus: AudioFocus?,
onClick: () -> Unit, onClick: () -> Unit,
onOpenWith: (() -> Unit)?,
textFileViewer: TextFileViewer, textFileViewer: TextFileViewer,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDisplayed: Boolean = true, isDisplayed: Boolean = true,
@@ -80,7 +81,7 @@ fun LocalMediaView(
uri = localMedia?.uri, uri = localMedia?.uri,
info = mediaInfo, info = mediaInfo,
modifier = modifier, modifier = modifier,
onClick = onClick, onOpenWith = onOpenWith,
) )
} }
} }
@@ -9,23 +9,16 @@
package io.element.android.libraries.mediaviewer.impl.local.file package io.element.android.libraries.mediaviewer.impl.local.file
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -33,55 +26,54 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import io.element.android.libraries.mediaviewer.impl.R
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3361-16623
*/
@Composable @Composable
fun MediaFileView( fun MediaFileView(
localMediaViewState: LocalMediaViewState, localMediaViewState: LocalMediaViewState,
uri: Uri?, uri: Uri?,
info: MediaInfo?, info: MediaInfo?,
onClick: () -> Unit, onOpenWith: (() -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isAudio = info?.mimeType.isMimeTypeAudio().orFalse() val isAudio = info?.mimeType.isMimeTypeAudio().orFalse()
localMediaViewState.isReady = uri != null localMediaViewState.isReady = uri != null
val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = modifier modifier = modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp),
.clickable(
onClick = onClick,
interactionSource = interactionSource,
indication = null
),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box( val isApk = info?.mimeType == MimeTypes.Apk
modifier = Modifier val icon = when {
.size(72.dp) isAudio -> CompoundIcons.Audio()
.clip(CircleShape) isApk -> CompoundIcons.Bug()
.background(ElementTheme.colors.iconPrimary), else -> CompoundIcons.Files()
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = if (isAudio) CompoundIcons.Audio() else CompoundIcons.Attachment(),
contentDescription = null,
tint = ElementTheme.colors.iconOnSolidPrimary,
modifier = Modifier
.size(32.dp)
.rotate(if (isAudio) 0f else -45f),
)
} }
BigIcon(
modifier = Modifier.align(Alignment.CenterHorizontally),
style = BigIcon.Style.Default(
vectorIcon = icon,
usePrimaryTint = true,
),
)
if (info != null) { if (info != null) {
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Text( Text(
@@ -100,6 +92,20 @@ fun MediaFileView(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = ElementTheme.colors.textPrimary color = ElementTheme.colors.textPrimary
) )
if (onOpenWith != null) {
val (icon, textResId) = if (isApk) {
IconSource.Resource(R.drawable.ic_apk_install) to CommonStrings.common_install_apk_android
} else {
IconSource.Vector(CompoundIcons.PopOut()) to CommonStrings.action_open_with
}
OutlinedButton(
modifier = Modifier.padding(top = 24.dp),
size = ButtonSize.Small,
leadingIcon = icon,
onClick = onOpenWith,
text = stringResource(id = textResId),
)
}
} }
} }
} }
@@ -115,6 +121,6 @@ internal fun MediaFileViewPreview(
localMediaViewState = rememberLocalMediaViewState(), localMediaViewState = rememberLocalMediaViewState(),
uri = null, uri = null,
info = info, info = info,
onClick = {}, onOpenWith = {},
) )
} }
@@ -10,11 +10,15 @@ package io.element.android.libraries.mediaviewer.impl.local.file
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo import io.element.android.libraries.mediaviewer.api.aZipMediaInfo
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
open class MediaInfoFileProvider : PreviewParameterProvider<MediaInfo> { open class MediaInfoFileProvider : PreviewParameterProvider<MediaInfo> {
override val values: Sequence<MediaInfo> override val values: Sequence<MediaInfo>
get() = sequenceOf( get() = sequenceOf(
aPdfMediaInfo(), aZipMediaInfo(),
anAudioMediaInfo(),
anApkMediaInfo(),
) )
} }
@@ -239,6 +239,9 @@ fun MediaViewerView(
onRetry = { onRetry = {
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
}, },
onOpenWith = {
state.eventSink(MediaViewerEvent.OpenWith(dataForPage))
},
onDismissError = { onDismissError = {
state.eventSink(MediaViewerEvent.ClearLoadingError(dataForPage)) state.eventSink(MediaViewerEvent.ClearLoadingError(dataForPage))
}, },
@@ -333,6 +336,7 @@ private fun MediaViewerPage(
isUserSelected: Boolean, isUserSelected: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onRetry: () -> Unit, onRetry: () -> Unit,
onOpenWith: () -> Unit,
onDismissError: () -> Unit, onDismissError: () -> Unit,
onShowOverlayChange: (Boolean) -> Unit, onShowOverlayChange: (Boolean) -> Unit,
audioFocus: AudioFocus?, audioFocus: AudioFocus?,
@@ -387,6 +391,7 @@ private fun MediaViewerPage(
currentOnShowOverlayChange(!currentShowOverlay) currentOnShowOverlayChange(!currentShowOverlay)
} }
}, },
onOpenWith = onOpenWith,
isUserSelected = isUserSelected, isUserSelected = isUserSelected,
audioFocus = audioFocus, audioFocus = audioFocus,
) )