MediaViewer: iterate on design

This commit is contained in:
Benoit Marty 2024-11-29 13:50:19 +01:00
parent 46ab03e550
commit 23b5776474
11 changed files with 264 additions and 86 deletions

View file

@ -337,7 +337,9 @@ class MessagesFlowNode @AssistedInject constructor(
caption = event.content.caption, caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension,
senderName = event.safeSenderName,
dateSent = event.sentTime,
), ),
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
@ -355,7 +357,9 @@ class MessagesFlowNode @AssistedInject constructor(
caption = event.content.caption, caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension,
senderName = event.safeSenderName,
dateSent = event.sentTime,
), ),
mediaSource = event.content.preferredMediaSource, mediaSource = event.content.preferredMediaSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
@ -373,7 +377,9 @@ class MessagesFlowNode @AssistedInject constructor(
caption = event.content.caption, caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension,
senderName = event.safeSenderName,
dateSent = event.sentTime,
), ),
mediaSource = event.content.videoSource, mediaSource = event.content.videoSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
@ -388,7 +394,9 @@ class MessagesFlowNode @AssistedInject constructor(
caption = event.content.caption, caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension,
senderName = event.safeSenderName,
dateSent = event.sentTime,
), ),
mediaSource = event.content.fileSource, mediaSource = event.content.fileSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
@ -403,7 +411,9 @@ class MessagesFlowNode @AssistedInject constructor(
caption = event.content.caption, caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension,
senderName = event.safeSenderName,
dateSent = event.sentTime,
), ),
mediaSource = event.content.mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = null, thumbnailSource = null,

View file

@ -18,44 +18,73 @@ data class MediaInfo(
val mimeType: String, val mimeType: String,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
val senderName: String?,
val dateSent: String?,
) : Parcelable ) : Parcelable
fun anImageMediaInfo(): MediaInfo = MediaInfo( fun anImageMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "an image file.jpg", filename = "an image file.jpg",
caption = null, caption = caption,
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB", formattedFileSize = "4MB",
fileExtension = "jpg", fileExtension = "jpg",
senderName = senderName,
dateSent = dateSent,
) )
fun aVideoMediaInfo(): MediaInfo = MediaInfo( fun aVideoMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4", filename = "a video file.mp4",
caption = null, caption = caption,
mimeType = MimeTypes.Mp4, mimeType = MimeTypes.Mp4,
formattedFileSize = "14MB", formattedFileSize = "14MB",
fileExtension = "mp4", fileExtension = "mp4",
senderName = senderName,
dateSent = dateSent,
) )
fun aPdfMediaInfo(): MediaInfo = MediaInfo( fun aPdfMediaInfo(
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "a pdf file.pdf", filename = "a pdf file.pdf",
caption = null, caption = null,
mimeType = MimeTypes.Pdf, mimeType = MimeTypes.Pdf,
formattedFileSize = "23MB", formattedFileSize = "23MB",
fileExtension = "pdf", fileExtension = "pdf",
senderName = senderName,
dateSent = dateSent,
) )
fun anApkMediaInfo(): MediaInfo = MediaInfo( fun anApkMediaInfo(
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "an apk file.apk", filename = "an apk file.apk",
caption = null, caption = null,
mimeType = MimeTypes.Apk, mimeType = MimeTypes.Apk,
formattedFileSize = "50MB", formattedFileSize = "50MB",
fileExtension = "apk", fileExtension = "apk",
senderName = senderName,
dateSent = dateSent,
) )
fun anAudioMediaInfo(): MediaInfo = MediaInfo( fun anAudioMediaInfo(
senderName: String? = null,
dateSent: String? = null,
): MediaInfo = MediaInfo(
filename = "an audio file.mp3", filename = "an audio file.mp3",
caption = null, caption = null,
mimeType = MimeTypes.Mp3, mimeType = MimeTypes.Mp3,
formattedFileSize = "7MB", formattedFileSize = "7MB",
fileExtension = "mp3", fileExtension = "mp3",
senderName = senderName,
dateSent = dateSent,
) )

View file

@ -46,7 +46,9 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
caption = null, caption = null,
mimeType = mimeType, mimeType = mimeType,
formattedFileSize = "", formattedFileSize = "",
fileExtension = "" fileExtension = "",
senderName = null,
dateSent = null,
), ),
mediaSource = MediaSource(url = avatarUrl), mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null, thumbnailSource = null,

View file

@ -41,6 +41,8 @@ class AndroidLocalMediaFactory @Inject constructor(
name = mediaInfo.filename, name = mediaInfo.filename,
caption = mediaInfo.caption, caption = mediaInfo.caption,
formattedFileSize = mediaInfo.formattedFileSize, formattedFileSize = mediaInfo.formattedFileSize,
senderName = mediaInfo.senderName,
dateSent = mediaInfo.dateSent,
) )
override fun createFromUri( override fun createFromUri(
@ -54,6 +56,8 @@ class AndroidLocalMediaFactory @Inject constructor(
name = name, name = name,
caption = null, caption = null,
formattedFileSize = formattedFileSize, formattedFileSize = formattedFileSize,
senderName = null,
dateSent = null,
) )
private fun createFromUri( private fun createFromUri(
@ -61,7 +65,9 @@ class AndroidLocalMediaFactory @Inject constructor(
mimeType: String?, mimeType: String?,
name: String?, name: String?,
caption: String?, caption: String?,
formattedFileSize: String? formattedFileSize: String?,
senderName: String?,
dateSent: String?,
): LocalMedia { ): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: "" val fileName = name ?: context.getFileName(uri) ?: ""
@ -74,7 +80,9 @@ class AndroidLocalMediaFactory @Inject constructor(
filename = fileName, filename = fileName,
caption = caption, caption = caption,
formattedFileSize = fileSize, formattedFileSize = fileSize,
fileExtension = fileExtension fileExtension = fileExtension,
senderName = senderName,
dateSent = dateSent,
) )
) )
} }

View file

@ -29,6 +29,7 @@ class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer {
) )
LocalMediaView( LocalMediaView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomPaddingInPixels = 0,
localMedia = localMedia, localMedia = localMedia,
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
onClick = {} onClick = {}

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.mediaviewer.impl.local.video.MediaVideoView
@Composable @Composable
fun LocalMediaView( fun LocalMediaView(
localMedia: LocalMedia?, localMedia: LocalMedia?,
bottomPaddingInPixels: Int,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(), localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
@ -37,6 +38,7 @@ fun LocalMediaView(
) )
mimeType.isMimeTypeVideo() -> MediaVideoView( mimeType.isMimeTypeVideo() -> MediaVideoView(
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
bottomPaddingInPixels = bottomPaddingInPixels,
localMedia = localMedia, localMedia = localMedia,
modifier = modifier, modifier = modifier,
) )

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -37,6 +38,7 @@ import androidx.media3.ui.PlayerView
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
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.text.toDp
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
@ -51,6 +53,7 @@ import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun MediaVideoView( fun MediaVideoView(
localMediaViewState: LocalMediaViewState, localMediaViewState: LocalMediaViewState,
bottomPaddingInPixels: Int,
localMedia: LocalMedia?, localMedia: LocalMedia?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -66,6 +69,7 @@ fun MediaVideoView(
} }
ExoPlayerMediaVideoView( ExoPlayerMediaVideoView(
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
bottomPaddingInPixels = bottomPaddingInPixels,
exoPlayer = exoPlayer, exoPlayer = exoPlayer,
localMedia = localMedia, localMedia = localMedia,
modifier = modifier, modifier = modifier,
@ -76,6 +80,7 @@ fun MediaVideoView(
@Composable @Composable
private fun ExoPlayerMediaVideoView( private fun ExoPlayerMediaVideoView(
localMediaViewState: LocalMediaViewState, localMediaViewState: LocalMediaViewState,
bottomPaddingInPixels: Int,
exoPlayer: ExoPlayer, exoPlayer: ExoPlayer,
localMedia: LocalMedia?, localMedia: LocalMedia?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -232,7 +237,8 @@ private fun ExoPlayerMediaVideoView(
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter)
.padding(bottom = bottomPaddingInPixels.toDp()),
) )
} }
@ -254,6 +260,7 @@ private fun ExoPlayerMediaVideoView(
internal fun MediaVideoViewPreview() = ElementPreview { internal fun MediaVideoViewPreview() = ElementPreview {
MediaVideoView( MediaVideoView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomPaddingInPixels = 0,
localMediaViewState = rememberLocalMediaViewState(), localMediaViewState = rememberLocalMediaViewState(),
localMedia = null, localMedia = null,
) )

View file

@ -24,52 +24,72 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
aMediaViewerState(), aMediaViewerState(),
aMediaViewerState(AsyncData.Loading()), aMediaViewerState(AsyncData.Loading()),
aMediaViewerState(AsyncData.Failure(IllegalStateException("error"))), aMediaViewerState(AsyncData.Failure(IllegalStateException("error"))),
aMediaViewerState( anImageMediaInfo(
AsyncData.Success( senderName = "Sally Sanderson",
LocalMedia(Uri.EMPTY, anImageMediaInfo()) dateSent = "21 NOV, 2024",
), caption = "A caption",
anImageMediaInfo(), ).let {
), aMediaViewerState(
aMediaViewerState( AsyncData.Success(
AsyncData.Success( LocalMedia(Uri.EMPTY, it)
LocalMedia(Uri.EMPTY, aVideoMediaInfo()) ),
), it,
aVideoMediaInfo(), )
), },
aMediaViewerState( aVideoMediaInfo(
AsyncData.Success( senderName = "Sally Sanderson",
LocalMedia(Uri.EMPTY, aPdfMediaInfo()) dateSent = "21 NOV, 2024",
), caption = "A caption",
aPdfMediaInfo(), ).let {
), aMediaViewerState(
AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
it,
)
},
aPdfMediaInfo().let {
aMediaViewerState(
AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
it,
)
},
aMediaViewerState( aMediaViewerState(
AsyncData.Loading(), AsyncData.Loading(),
anApkMediaInfo(), anApkMediaInfo(),
), ),
aMediaViewerState( anApkMediaInfo().let {
AsyncData.Success( aMediaViewerState(
LocalMedia(Uri.EMPTY, anApkMediaInfo()) AsyncData.Success(
), LocalMedia(Uri.EMPTY, it)
anApkMediaInfo(), ),
), it,
)
},
aMediaViewerState( aMediaViewerState(
AsyncData.Loading(), AsyncData.Loading(),
anAudioMediaInfo(), anAudioMediaInfo(),
), ),
aMediaViewerState( anAudioMediaInfo().let {
AsyncData.Success( aMediaViewerState(
LocalMedia(Uri.EMPTY, anAudioMediaInfo()) AsyncData.Success(
), LocalMedia(Uri.EMPTY, it)
anAudioMediaInfo(), ),
), it,
aMediaViewerState( )
AsyncData.Success( },
LocalMedia(Uri.EMPTY, anImageMediaInfo()) anImageMediaInfo().let {
), aMediaViewerState(
anImageMediaInfo(), AsyncData.Success(
canDownload = false, LocalMedia(Uri.EMPTY, it)
canShare = false, ),
), it,
canDownload = false,
canShare = false,
)
},
) )
} }

View file

@ -16,10 +16,14 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -28,6 +32,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
@ -36,12 +41,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
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.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
@ -51,6 +59,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
@ -80,6 +89,7 @@ fun MediaViewerView(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
var showOverlay by remember { mutableStateOf(true) } var showOverlay by remember { mutableStateOf(true) }
var bottomPaddingInPixels by remember { mutableIntStateOf(0) }
BackHandler { onBackClick() } BackHandler { onBackClick() }
Scaffold( Scaffold(
modifier, modifier,
@ -88,6 +98,7 @@ fun MediaViewerView(
) { ) {
MediaViewerPage( MediaViewerPage(
showOverlay = showOverlay, showOverlay = showOverlay,
bottomPaddingInPixels = bottomPaddingInPixels,
state = state, state = state,
onDismiss = { onDismiss = {
onBackClick() onBackClick()
@ -97,14 +108,29 @@ fun MediaViewerView(
} }
) )
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
MediaViewerTopBar( Box(
actionsEnabled = state.downloadedMedia is AsyncData.Success, modifier = Modifier
mimeType = state.mediaInfo.mimeType, .fillMaxSize()
onBackClick = onBackClick, .navigationBarsPadding()
canDownload = state.canDownload, ) {
canShare = state.canShare, MediaViewerTopBar(
eventSink = state.eventSink actionsEnabled = state.downloadedMedia is AsyncData.Success,
) senderName = state.mediaInfo.senderName,
dateSent = state.mediaInfo.dateSent,
onBackClick = onBackClick,
eventSink = state.eventSink
)
MediaViewerBottomBar(
modifier = Modifier.align(Alignment.BottomCenter),
actionsEnabled = state.downloadedMedia is AsyncData.Success,
canDownload = state.canDownload,
canShare = state.canShare,
mimeType = state.mediaInfo.mimeType,
caption = state.mediaInfo.caption,
onHeightChanged = { bottomPaddingInPixels = it },
eventSink = state.eventSink
)
}
} }
} }
} }
@ -112,6 +138,7 @@ fun MediaViewerView(
@Composable @Composable
private fun MediaViewerPage( private fun MediaViewerPage(
showOverlay: Boolean, showOverlay: Boolean,
bottomPaddingInPixels: Int,
state: MediaViewerState, state: MediaViewerState,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onShowOverlayChange: (Boolean) -> Unit, onShowOverlayChange: (Boolean) -> Unit,
@ -148,8 +175,8 @@ private fun MediaViewerPage(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.navigationBarsPadding() .navigationBarsPadding()
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val zoomableState = rememberZoomableState( val zoomableState = rememberZoomableState(
@ -168,6 +195,7 @@ private fun MediaViewerPage(
LocalMediaView( LocalMediaView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomPaddingInPixels = bottomPaddingInPixels,
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
localMedia = state.downloadedMedia.dataOrNull(), localMedia = state.downloadedMedia.dataOrNull(),
mediaInfo = state.mediaInfo, mediaInfo = state.mediaInfo,
@ -193,8 +221,8 @@ private fun MediaViewerPage(
if (showProgress) { if (showProgress) {
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(2.dp) .height(2.dp)
) )
} }
} }
@ -246,23 +274,99 @@ private fun rememberShowProgress(downloadedMedia: AsyncData<LocalMedia>): Boolea
return showProgress return showProgress
} }
@Suppress("UNUSED_PARAMETER")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MediaViewerTopBar( private fun MediaViewerTopBar(
actionsEnabled: Boolean, actionsEnabled: Boolean,
canDownload: Boolean, senderName: String?,
canShare: Boolean, dateSent: String?,
mimeType: String,
onBackClick: () -> Unit, onBackClick: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit, eventSink: (MediaViewerEvents) -> Unit,
) { ) {
TopAppBar( TopAppBar(
title = {}, title = {
if (senderName != null && dateSent != null) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(end = 48.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = senderName,
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
)
Text(
text = dateSent,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textPrimary,
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent.copy(0.6f), containerColor = Color.Transparent.copy(0.6f),
), ),
navigationIcon = { BackButton(onClick = onBackClick) }, navigationIcon = { BackButton(onClick = onBackClick) },
actions = { actions = {
// TODO Add action to open infos.
}
)
}
@Composable
private fun MediaViewerBottomBar(
actionsEnabled: Boolean,
canDownload: Boolean,
canShare: Boolean,
mimeType: String,
caption: String?,
onHeightChanged: (Int) -> Unit,
eventSink: (MediaViewerEvents) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(Color(0x99101317))
.onSizeChanged {
onHeightChanged(it.height)
},
) {
if (caption != null) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = caption,
maxLines = 5,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyLgRegular,
)
}
Row(
modifier = modifier
.fillMaxWidth()
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (canShare) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.Share)
},
modifier = Modifier.align(Alignment.CenterVertically)
) {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
contentDescription = stringResource(id = CommonStrings.action_share)
)
}
}
Spacer(modifier = Modifier.weight(1f))
IconButton( IconButton(
enabled = actionsEnabled, enabled = actionsEnabled,
onClick = { onClick = {
@ -293,21 +397,8 @@ private fun MediaViewerTopBar(
) )
} }
} }
if (canShare) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.Share)
},
) {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
contentDescription = stringResource(id = CommonStrings.action_share)
)
}
}
} }
) }
} }
@Composable @Composable

View file

@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.media.FakeMediaFile import io.element.android.libraries.matrix.test.media.FakeMediaFile
import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
@ -25,7 +26,10 @@ class AndroidLocalMediaFactoryTest {
@Test @Test
fun `test AndroidLocalMediaFactory`() { fun `test AndroidLocalMediaFactory`() {
val sut = createAndroidLocalMediaFactory() val sut = createAndroidLocalMediaFactory()
val result = sut.createFromMediaFile(aMediaFile(), anImageMediaInfo()) val result = sut.createFromMediaFile(aMediaFile(), anImageMediaInfo(
senderName = A_USER_NAME,
dateSent = "12:34",
))
assertThat(result.uri.toString()).endsWith("aPath") assertThat(result.uri.toString()).endsWith("aPath")
assertThat(result.info).isEqualTo( assertThat(result.info).isEqualTo(
MediaInfo( MediaInfo(
@ -34,6 +38,8 @@ class AndroidLocalMediaFactoryTest {
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB", formattedFileSize = "4MB",
fileExtension = "jpg", fileExtension = "jpg",
senderName = A_USER_NAME,
dateSent = "12:34"
) )
) )
} }

View file

@ -36,7 +36,9 @@ class FakeLocalMediaFactory(
caption = null, caption = null,
mimeType = mimeType ?: fallbackMimeType, mimeType = mimeType ?: fallbackMimeType,
formattedFileSize = formattedFileSize ?: fallbackFileSize, formattedFileSize = formattedFileSize ?: fallbackFileSize,
fileExtension = fileExtensionExtractor.extractFromName(safeName) fileExtension = fileExtensionExtractor.extractFromName(safeName),
senderName = null,
dateSent = null
) )
return aLocalMedia(uri, mediaInfo) return aLocalMedia(uri, mediaInfo)
} }