Media: some more improvements over MediaViewer
This commit is contained in:
parent
5e5737aa61
commit
e0106fe907
16 changed files with 226 additions and 74 deletions
|
|
@ -26,7 +26,6 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
|
|
@ -41,8 +40,8 @@ import io.element.android.libraries.architecture.BackstackNode
|
|||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class MessagesFlowNode @AssistedInject constructor(
|
||||
|
|
@ -65,7 +64,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
data class MediaViewer(
|
||||
val title: String,
|
||||
val mediaSource: MediaSource,
|
||||
val mimeType: String?
|
||||
val thumbnailSource: MediaSource?,
|
||||
val mimeType: String?,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
|
|
@ -93,7 +93,12 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
createNode<MessagesNode>(buildContext, listOf(callback))
|
||||
}
|
||||
is NavTarget.MediaViewer -> {
|
||||
val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource, navTarget.mimeType)
|
||||
val inputs = MediaViewerNode.Inputs(
|
||||
name = navTarget.title,
|
||||
mediaSource = navTarget.mediaSource,
|
||||
thumbnailSource = navTarget.thumbnailSource,
|
||||
mimeType = navTarget.mimeType,
|
||||
)
|
||||
createNode<MediaViewerNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
is NavTarget.AttachmentPreview -> {
|
||||
|
|
@ -106,13 +111,22 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
private fun processEventClicked(event: TimelineItem.Event) {
|
||||
when (event.content) {
|
||||
is TimelineItemImageContent -> {
|
||||
val mediaSource = event.content.mediaSource
|
||||
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType)
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
title = event.content.body,
|
||||
mediaSource = event.content.mediaSource,
|
||||
thumbnailSource = event.content.mediaSource,
|
||||
mimeType = event.content.mimeType
|
||||
)
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
is TimelineItemVideoContent -> {
|
||||
val mediaSource = event.content.videoSource
|
||||
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType)
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
title = event.content.body,
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = event.content.thumbnailSource,
|
||||
mimeType = event.content.mimeType,
|
||||
)
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
else -> Unit
|
||||
|
|
@ -124,7 +138,6 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = rememberBackstackFader()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ package io.element.android.features.messages.impl.media.local
|
|||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Immutable
|
||||
data class LocalMedia(
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
|
|
@ -47,16 +48,20 @@ import me.saket.telephoto.zoomable.rememberZoomableState
|
|||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Composable
|
||||
fun LocalMediaView(
|
||||
localMedia: LocalMedia,
|
||||
modifier: Modifier = Modifier
|
||||
localMedia: LocalMedia?,
|
||||
modifier: Modifier = Modifier,
|
||||
mimeType: String? = localMedia?.mimeType,
|
||||
onReady: () -> Unit = {},
|
||||
) {
|
||||
when {
|
||||
MimeTypes.isImage(localMedia.mimeType) -> MediaImageView(
|
||||
MimeTypes.isImage(mimeType) -> MediaImageView(
|
||||
localMedia = localMedia,
|
||||
onReady = onReady,
|
||||
modifier = modifier
|
||||
)
|
||||
MimeTypes.isVideo(localMedia.mimeType) -> MediaVideoView(
|
||||
MimeTypes.isVideo(mimeType) -> MediaVideoView(
|
||||
localMedia = localMedia,
|
||||
onReady = onReady,
|
||||
modifier = modifier
|
||||
)
|
||||
else -> Unit
|
||||
|
|
@ -65,7 +70,8 @@ fun LocalMediaView(
|
|||
|
||||
@Composable
|
||||
private fun MediaImageView(
|
||||
localMedia: LocalMedia,
|
||||
localMedia: LocalMedia?,
|
||||
onReady: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
|
|
@ -78,10 +84,16 @@ private fun MediaImageView(
|
|||
val zoomableState = rememberZoomableState(
|
||||
zoomSpec = ZoomSpec(maxZoomFactor = 3f)
|
||||
)
|
||||
val zoomableImageState = rememberZoomableImageState(zoomableState)
|
||||
LaunchedEffect(zoomableImageState.isImageDisplayed) {
|
||||
if (zoomableImageState.isImageDisplayed) {
|
||||
onReady()
|
||||
}
|
||||
}
|
||||
ZoomableAsyncImage(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
state = rememberZoomableImageState(zoomableState),
|
||||
model = localMedia.model,
|
||||
state = zoomableImageState,
|
||||
model = localMedia?.model,
|
||||
contentDescription = "Image",
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
|
|
@ -91,22 +103,32 @@ private fun MediaImageView(
|
|||
@UnstableApi
|
||||
@Composable
|
||||
fun MediaVideoView(
|
||||
localMedia: LocalMedia,
|
||||
localMedia: LocalMedia?,
|
||||
onReady: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val playerListener = object : Player.Listener {
|
||||
override fun onRenderedFirstFrame() {
|
||||
onReady()
|
||||
}
|
||||
}
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context).build()
|
||||
ExoPlayer.Builder(context)
|
||||
.build()
|
||||
.apply {
|
||||
this.playWhenReady = true
|
||||
addListener(playerListener)
|
||||
this.prepare()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(localMedia.uri) {
|
||||
val mediaItem = MediaItem.fromUri(localMedia.uri)
|
||||
exoPlayer.setMediaItem(mediaItem)
|
||||
if (localMedia?.uri != null) {
|
||||
LaunchedEffect(localMedia.uri) {
|
||||
val mediaItem = MediaItem.fromUri(localMedia.uri)
|
||||
exoPlayer.setMediaItem(mediaItem)
|
||||
}
|
||||
} else {
|
||||
exoPlayer.setMediaItems(emptyList())
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = {
|
||||
PlayerView(context).apply {
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ package io.element.android.features.messages.impl.media.viewer
|
|||
|
||||
sealed interface MediaViewerEvents {
|
||||
object RetryLoading : MediaViewerEvents
|
||||
object ClearLoadingError : MediaViewerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class MediaViewerNode @AssistedInject constructor(
|
|||
data class Inputs(
|
||||
val name: String,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val mimeType: String?
|
||||
) : NodeInputs
|
||||
|
||||
|
|
|
|||
|
|
@ -67,11 +67,14 @@ class MediaViewerPresenter @AssistedInject constructor(
|
|||
fun handleEvents(mediaViewerEvents: MediaViewerEvents) {
|
||||
when (mediaViewerEvents) {
|
||||
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
|
||||
MediaViewerEvents.ClearLoadingError -> localMedia.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
return MediaViewerState(
|
||||
name = inputs.name,
|
||||
mimeType = inputs.mimeType,
|
||||
thumbnailSource = inputs.thumbnailSource,
|
||||
downloadedMedia = localMedia.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ package io.element.android.features.messages.impl.media.viewer
|
|||
|
||||
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
|
||||
data class MediaViewerState(
|
||||
val name: String,
|
||||
val mimeType: String?,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val downloadedMedia: Async<LocalMedia>,
|
||||
val eventSink: (MediaViewerEvents) -> Unit
|
||||
val eventSink: (MediaViewerEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
|||
aMediaViewerState(
|
||||
Async.Success(
|
||||
LocalMedia(
|
||||
Uri.EMPTY, MimeTypes.IMAGE_JPEG, "a file", 100L
|
||||
Uri.EMPTY, MimeTypes.IMAGE_JPEG, "an image file", 100L
|
||||
)
|
||||
),
|
||||
),
|
||||
aMediaViewerState(
|
||||
Async.Success(
|
||||
LocalMedia(
|
||||
Uri.EMPTY, MimeTypes.VIDEO_MP4, "a video file", 100L
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
@ -40,6 +47,7 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
|||
|
||||
fun aMediaViewerState(downloadedMedia: Async<LocalMedia> = Async.Uninitialized) = MediaViewerState(
|
||||
name = "A media",
|
||||
mimeType = MimeTypes.IMAGE_JPEG,
|
||||
thumbnailSource = null,
|
||||
downloadedMedia = downloadedMedia,
|
||||
eventSink = {}
|
||||
)
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -14,33 +14,36 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.features.messages.impl.media.viewer
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import kotlinx.coroutines.delay
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
|
|
@ -53,6 +56,32 @@ fun MediaViewerView(
|
|||
state.eventSink(MediaViewerEvents.RetryLoading)
|
||||
}
|
||||
|
||||
fun onDismissError() {
|
||||
state.eventSink(MediaViewerEvents.ClearLoadingError)
|
||||
}
|
||||
|
||||
var showProgress by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
// Trick to avoid showing progress indicator if the media is already on disk.
|
||||
// When sdk will expose download progress we'll be able to remove this.
|
||||
LaunchedEffect(state.downloadedMedia) {
|
||||
showProgress = false
|
||||
delay(100)
|
||||
if (state.downloadedMedia.isLoading()) {
|
||||
showProgress = true
|
||||
}
|
||||
}
|
||||
|
||||
var showThumbnail by remember {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
|
||||
fun onMediaReady() {
|
||||
showThumbnail = false
|
||||
}
|
||||
|
||||
Scaffold(modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -60,10 +89,55 @@ fun MediaViewerView(
|
|||
.padding(it),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (state.downloadedMedia) {
|
||||
is Async.Success -> LocalMediaView(state.downloadedMedia.state)
|
||||
is Async.Failure -> ErrorView(stringResource(id = StringR.string.error_unknown), ::onRetry)
|
||||
else -> CircularProgressIndicator()
|
||||
if (state.downloadedMedia is Async.Failure) {
|
||||
ErrorView(
|
||||
errorMessage = stringResource(id = StringR.string.error_unknown),
|
||||
onRetry = ::onRetry,
|
||||
onDismiss = ::onDismissError
|
||||
)
|
||||
}
|
||||
LocalMediaView(
|
||||
localMedia = state.downloadedMedia.dataOrNull(),
|
||||
mimeType = state.mimeType,
|
||||
onReady = ::onMediaReady
|
||||
)
|
||||
ThumbnailView(
|
||||
thumbnailSource = state.thumbnailSource,
|
||||
showThumbnail = showThumbnail,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThumbnailView(
|
||||
thumbnailSource: MediaSource?,
|
||||
showThumbnail: Boolean,
|
||||
showProgress: Boolean,
|
||||
) {
|
||||
if (!showThumbnail) return
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val mediaRequestData = MediaRequestData(
|
||||
source = thumbnailSource,
|
||||
kind = MediaRequestData.Kind.Content
|
||||
)
|
||||
AsyncImage(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
model = mediaRequestData,
|
||||
contentScale = ContentScale.Fit,
|
||||
contentDescription = null,
|
||||
)
|
||||
if (showProgress) {
|
||||
Box(
|
||||
modifier = Modifier.roundedBackground(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,21 +147,15 @@ fun MediaViewerView(
|
|||
private fun ErrorView(
|
||||
errorMessage: String,
|
||||
onRetry: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(text = errorMessage)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
onClick = onRetry
|
||||
) {
|
||||
Text(text = stringResource(id = StringR.string.action_retry))
|
||||
}
|
||||
|
||||
}
|
||||
RetryDialog(
|
||||
modifier = modifier,
|
||||
content = errorMessage,
|
||||
onRetry = onRetry,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -17,26 +17,22 @@
|
|||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
||||
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
|
|
@ -54,15 +50,12 @@ fun TimelineItemVideoView(
|
|||
) {
|
||||
BlurHashAsyncImage(
|
||||
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content),
|
||||
blurHash = content.blurhash,
|
||||
blurHash = content.blurHash,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(50.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = Color.Black.copy(alpha = 0.5f)),
|
||||
modifier = Modifier.roundedBackground(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
|
|
|
|||
|
|
@ -46,11 +46,11 @@ class TimelineItemContentMessageFactory @Inject constructor() {
|
|||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||
TimelineItemImageContent(
|
||||
body = messageType.body,
|
||||
height = messageType.info?.height?.toInt(),
|
||||
width = messageType.info?.width?.toInt(),
|
||||
mimeType = messageType.info?.mimetype,
|
||||
mediaSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype,
|
||||
blurhash = messageType.info?.blurhash,
|
||||
width = messageType.info?.width?.toInt(),
|
||||
height = messageType.info?.height?.toInt(),
|
||||
aspectRatio = aspectRatio
|
||||
)
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ class TimelineItemContentMessageFactory @Inject constructor() {
|
|||
width = messageType.info?.width?.toInt(),
|
||||
height = messageType.info?.height?.toInt(),
|
||||
duration = messageType.info?.duration ?: 0L,
|
||||
blurhash = messageType.info?.blurhash,
|
||||
blurHash = messageType.info?.blurhash,
|
||||
aspectRatio = aspectRatio
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
|
|||
fun aTimelineItemImageContent() = TimelineItemImageContent(
|
||||
body = "a body",
|
||||
mediaSource = MediaSource(""),
|
||||
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||
aspectRatio = 0.5f,
|
||||
mimeType = MimeTypes.IMAGE_JPEG,
|
||||
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||
width = null,
|
||||
height = 300,
|
||||
width = null
|
||||
aspectRatio = 0.5f
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ data class TimelineItemVideoContent(
|
|||
val videoSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val aspectRatio: Float,
|
||||
val blurhash: String?,
|
||||
val blurHash: String?,
|
||||
val height: Int?,
|
||||
val width: Int?,
|
||||
val mimeType: String?,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineI
|
|||
fun aTimelineItemVideoContent() = TimelineItemVideoContent(
|
||||
body = "a video",
|
||||
thumbnailSource = MediaSource(url = ""),
|
||||
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||
blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||
aspectRatio = 0.5f,
|
||||
duration = 100,
|
||||
videoSource = MediaSource(""),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.modifiers
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* This modifier can be use to provide a nice background for Icon or ProgressIndicator.
|
||||
*/
|
||||
fun Modifier.roundedBackground(
|
||||
size: Dp = 48.dp,
|
||||
color: Color = Color.Black,
|
||||
alpha: Float = 0.5f,
|
||||
) = this
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(color = color.copy(alpha = alpha))
|
||||
.padding(8.dp)
|
||||
|
|
@ -23,8 +23,6 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
const val A_USER_NAME = "alice"
|
||||
const val A_PASSWORD = "password"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue