Merge pull request #6933 from element-hq/feature/bma/filePreviewImprovement

[Media viewer] File preview improvement
This commit is contained in:
Benoit Marty
2026-06-02 15:19:09 +02:00
committed by GitHub
55 changed files with 181 additions and 128 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,
@@ -12,8 +12,8 @@ fun formatFileExtensionAndSize(extension: String, size: String?): String {
return buildString { return buildString {
append(extension.uppercase()) append(extension.uppercase())
if (size != null) { if (size != null) {
append(' ') append(" • ")
append("($size)") append(size)
} }
} }
} }
@@ -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(),
) )
} }
@@ -106,7 +106,7 @@ import me.saket.telephoto.zoomable.OverzoomEffect
import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState import me.saket.telephoto.zoomable.rememberZoomableState
val topAppBarHeight = 88.dp val topAppBarHeight = 112.dp
/** /**
* Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3361-16623 * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3361-16623
@@ -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,
) )
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:43ab4873fc5fc812bf18af50cbe620c83b273ee70305b5ea06a7aeabdf8dbc93 oid sha256:58e3353ce8aecff415158d146347206b1dbf167d15523301847f47c2e81b1c5b
size 137637 size 137215