Add audio file support in the gallery and in the media viewer.
Make MediaPlayerControllerView compatible to control audio playback
This commit is contained in:
parent
c40d468e4f
commit
caf8c7392e
23 changed files with 866 additions and 25 deletions
|
|
@ -47,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||||
|
|
@ -447,6 +448,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
timestamp = event.sentTimeMillis,
|
timestamp = event.sentTimeMillis,
|
||||||
mode = DateFormatterMode.Full,
|
mode = DateFormatterMode.Full,
|
||||||
),
|
),
|
||||||
|
waveform = (content as? TimelineItemVoiceContent)?.waveform,
|
||||||
),
|
),
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
thumbnailSource = thumbnailSource,
|
thumbnailSource = thumbnailSource,
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ internal fun WaveformPlaybackViewPreview() = ElementPreview {
|
||||||
showCursor = false,
|
showCursor = false,
|
||||||
playbackProgress = 0.5f,
|
playbackProgress = 0.5f,
|
||||||
onSeek = {},
|
onSeek = {},
|
||||||
waveform = persistentListOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f),
|
waveform = aWaveForm().toPersistentList(),
|
||||||
)
|
)
|
||||||
WaveformPlaybackView(
|
WaveformPlaybackView(
|
||||||
modifier = Modifier.height(34.dp),
|
modifier = Modifier.height(34.dp),
|
||||||
|
|
@ -219,3 +219,45 @@ private fun ImmutableList<Float>.normalisedData(maxSamplesCount: Int): Immutable
|
||||||
|
|
||||||
return result.toPersistentList()
|
return result.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun aWaveForm(): List<Float> {
|
||||||
|
return listOf(
|
||||||
|
0.000f,
|
||||||
|
0.000f,
|
||||||
|
0.000f,
|
||||||
|
0.003f,
|
||||||
|
0.354f,
|
||||||
|
0.353f,
|
||||||
|
0.365f,
|
||||||
|
0.790f,
|
||||||
|
0.787f,
|
||||||
|
0.167f,
|
||||||
|
0.333f,
|
||||||
|
0.975f,
|
||||||
|
0.000f,
|
||||||
|
0.102f,
|
||||||
|
0.003f,
|
||||||
|
0.531f,
|
||||||
|
0.584f,
|
||||||
|
0.317f,
|
||||||
|
0.140f,
|
||||||
|
0.475f,
|
||||||
|
0.496f,
|
||||||
|
0.561f,
|
||||||
|
0.042f,
|
||||||
|
0.263f,
|
||||||
|
0.169f,
|
||||||
|
0.829f,
|
||||||
|
0.349f,
|
||||||
|
0.010f,
|
||||||
|
0.000f,
|
||||||
|
0.000f,
|
||||||
|
1.000f,
|
||||||
|
0.334f,
|
||||||
|
0.321f,
|
||||||
|
0.011f,
|
||||||
|
0.000f,
|
||||||
|
0.000f,
|
||||||
|
0.003f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ data class MediaInfo(
|
||||||
val senderAvatar: String?,
|
val senderAvatar: String?,
|
||||||
val dateSent: String?,
|
val dateSent: String?,
|
||||||
val dateSentFull: String?,
|
val dateSentFull: String?,
|
||||||
|
val waveform: List<Float>?,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
fun anImageMediaInfo(
|
fun anImageMediaInfo(
|
||||||
|
|
@ -43,6 +44,7 @@ fun anImageMediaInfo(
|
||||||
senderAvatar = null,
|
senderAvatar = null,
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aVideoMediaInfo(
|
fun aVideoMediaInfo(
|
||||||
|
|
@ -61,6 +63,7 @@ fun aVideoMediaInfo(
|
||||||
senderAvatar = null,
|
senderAvatar = null,
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aPdfMediaInfo(
|
fun aPdfMediaInfo(
|
||||||
|
|
@ -80,6 +83,7 @@ fun aPdfMediaInfo(
|
||||||
senderAvatar = null,
|
senderAvatar = null,
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anApkMediaInfo(
|
fun anApkMediaInfo(
|
||||||
|
|
@ -98,15 +102,19 @@ fun anApkMediaInfo(
|
||||||
senderAvatar = null,
|
senderAvatar = null,
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anAudioMediaInfo(
|
fun anAudioMediaInfo(
|
||||||
|
filename: String = "an audio file.mp3",
|
||||||
|
caption: String? = null,
|
||||||
senderName: String? = null,
|
senderName: String? = null,
|
||||||
dateSent: String? = null,
|
dateSent: String? = null,
|
||||||
dateSentFull: String? = null,
|
dateSentFull: String? = null,
|
||||||
|
waveForm: List<Float>? = null,
|
||||||
): MediaInfo = MediaInfo(
|
): MediaInfo = MediaInfo(
|
||||||
filename = "an audio file.mp3",
|
filename = filename,
|
||||||
caption = null,
|
caption = caption,
|
||||||
mimeType = MimeTypes.Mp3,
|
mimeType = MimeTypes.Mp3,
|
||||||
formattedFileSize = "7MB",
|
formattedFileSize = "7MB",
|
||||||
fileExtension = "mp3",
|
fileExtension = "mp3",
|
||||||
|
|
@ -115,4 +123,5 @@ fun anAudioMediaInfo(
|
||||||
senderAvatar = null,
|
senderAvatar = null,
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = waveForm,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
|
||||||
senderAvatar = null,
|
senderAvatar = null,
|
||||||
dateSent = null,
|
dateSent = null,
|
||||||
dateSentFull = null,
|
dateSentFull = null,
|
||||||
|
waveform = null,
|
||||||
),
|
),
|
||||||
mediaSource = MediaSource(url = avatarUrl),
|
mediaSource = MediaSource(url = avatarUrl),
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ class EventItemFactory @Inject constructor(
|
||||||
Timber.w("Should not happen: ${content.type}")
|
Timber.w("Should not happen: ${content.type}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
is AudioMessageType -> MediaItem.File(
|
is AudioMessageType -> MediaItem.Audio(
|
||||||
id = currentTimelineItem.uniqueId,
|
id = currentTimelineItem.uniqueId,
|
||||||
eventId = currentTimelineItem.eventId,
|
eventId = currentTimelineItem.eventId,
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
|
|
@ -100,8 +100,11 @@ class EventItemFactory @Inject constructor(
|
||||||
senderAvatar = event.senderProfile.getAvatarUrl(),
|
senderAvatar = event.senderProfile.getAvatarUrl(),
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
|
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||||
|
waveform = null,
|
||||||
)
|
)
|
||||||
is FileMessageType -> MediaItem.File(
|
is FileMessageType -> MediaItem.File(
|
||||||
id = currentTimelineItem.uniqueId,
|
id = currentTimelineItem.uniqueId,
|
||||||
|
|
@ -117,6 +120,7 @@ class EventItemFactory @Inject constructor(
|
||||||
senderAvatar = event.senderProfile.getAvatarUrl(),
|
senderAvatar = event.senderProfile.getAvatarUrl(),
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
)
|
)
|
||||||
|
|
@ -134,6 +138,7 @@ class EventItemFactory @Inject constructor(
|
||||||
senderAvatar = event.senderProfile.getAvatarUrl(),
|
senderAvatar = event.senderProfile.getAvatarUrl(),
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
|
|
@ -152,6 +157,7 @@ class EventItemFactory @Inject constructor(
|
||||||
senderAvatar = event.senderProfile.getAvatarUrl(),
|
senderAvatar = event.senderProfile.getAvatarUrl(),
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
|
|
@ -170,12 +176,13 @@ class EventItemFactory @Inject constructor(
|
||||||
senderAvatar = event.senderProfile.getAvatarUrl(),
|
senderAvatar = event.senderProfile.getAvatarUrl(),
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = null,
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
thumbnailSource = type.info?.thumbnailSource,
|
thumbnailSource = type.info?.thumbnailSource,
|
||||||
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||||
)
|
)
|
||||||
is VoiceMessageType -> MediaItem.File(
|
is VoiceMessageType -> MediaItem.Audio(
|
||||||
id = currentTimelineItem.uniqueId,
|
id = currentTimelineItem.uniqueId,
|
||||||
eventId = currentTimelineItem.eventId,
|
eventId = currentTimelineItem.eventId,
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
|
|
@ -189,8 +196,11 @@ class EventItemFactory @Inject constructor(
|
||||||
senderAvatar = event.senderProfile.getAvatarUrl(),
|
senderAvatar = event.senderProfile.getAvatarUrl(),
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = type.details?.waveform,
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
|
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||||
|
waveform = type.details?.waveform,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
|
||||||
thumbnailSource = when (event.mediaItem) {
|
thumbnailSource = when (event.mediaItem) {
|
||||||
is MediaItem.Image -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
|
is MediaItem.Image -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
|
||||||
is MediaItem.Video -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
|
is MediaItem.Video -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
|
||||||
|
is MediaItem.Audio -> null
|
||||||
is MediaItem.File -> null
|
is MediaItem.File -> null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ package io.element.android.libraries.mediaviewer.impl.gallery
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemAudio
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
||||||
|
|
@ -62,7 +64,11 @@ open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryStat
|
||||||
formattedDate = "September 2004",
|
formattedDate = "September 2004",
|
||||||
),
|
),
|
||||||
aMediaItemFile(id = UniqueId("3")),
|
aMediaItemFile(id = UniqueId("3")),
|
||||||
aMediaItemFile(id = UniqueId("4")),
|
aMediaItemAudio(id = UniqueId("4")),
|
||||||
|
aMediaItemAudio(
|
||||||
|
id = UniqueId("5"),
|
||||||
|
waveform = aWaveForm(),
|
||||||
|
),
|
||||||
aMediaItemLoadingIndicator(),
|
aMediaItemLoadingIndicator(),
|
||||||
).toImmutableList()
|
).toImmutableList()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ import io.element.android.libraries.mediaviewer.impl.R
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
|
import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
|
import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.AudioItemView
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.DateItemView
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.DateItemView
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.FileItemView
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.FileItemView
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.ImageItemView
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.ImageItemView
|
||||||
|
|
@ -257,6 +258,13 @@ private fun MediaGalleryFilesList(
|
||||||
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
|
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
|
||||||
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
|
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
|
||||||
)
|
)
|
||||||
|
is MediaItem.Audio -> AudioItemView(
|
||||||
|
item,
|
||||||
|
onClick = { onItemClick(item) },
|
||||||
|
onShareClick = { eventSink(MediaGalleryEvents.Share(item)) },
|
||||||
|
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
|
||||||
|
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
|
||||||
|
)
|
||||||
is MediaItem.DateSeparator -> DateItemView(item)
|
is MediaItem.DateSeparator -> DateItemView(item)
|
||||||
is MediaItem.Image,
|
is MediaItem.Image,
|
||||||
is MediaItem.Video -> {
|
is MediaItem.Video -> {
|
||||||
|
|
@ -312,6 +320,9 @@ private fun MediaGalleryImageGrid(
|
||||||
is MediaItem.DateSeparator -> {
|
is MediaItem.DateSeparator -> {
|
||||||
DateItemView(item)
|
DateItemView(item)
|
||||||
}
|
}
|
||||||
|
is MediaItem.Audio -> {
|
||||||
|
// Should not happen
|
||||||
|
}
|
||||||
is MediaItem.File -> {
|
is MediaItem.File -> {
|
||||||
// Should not happen
|
// Should not happen
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
sealed interface MediaItem {
|
sealed interface MediaItem {
|
||||||
data class DateSeparator(
|
data class DateSeparator(
|
||||||
|
|
@ -51,6 +52,15 @@ sealed interface MediaItem {
|
||||||
get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
|
get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Audio(
|
||||||
|
val id: UniqueId,
|
||||||
|
val eventId: EventId?,
|
||||||
|
val mediaInfo: MediaInfo,
|
||||||
|
val mediaSource: MediaSource,
|
||||||
|
val duration: String?,
|
||||||
|
val waveform: ImmutableList<Float>?,
|
||||||
|
) : Event
|
||||||
|
|
||||||
data class File(
|
data class File(
|
||||||
val id: UniqueId,
|
val id: UniqueId,
|
||||||
val eventId: EventId?,
|
val eventId: EventId?,
|
||||||
|
|
@ -66,6 +76,7 @@ fun MediaItem.id(): UniqueId {
|
||||||
is MediaItem.Image -> id
|
is MediaItem.Image -> id
|
||||||
is MediaItem.Video -> id
|
is MediaItem.Video -> id
|
||||||
is MediaItem.File -> id
|
is MediaItem.File -> id
|
||||||
|
is MediaItem.Audio -> id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +85,7 @@ fun MediaItem.Event.eventId(): EventId? {
|
||||||
is MediaItem.Image -> eventId
|
is MediaItem.Image -> eventId
|
||||||
is MediaItem.Video -> eventId
|
is MediaItem.Video -> eventId
|
||||||
is MediaItem.File -> eventId
|
is MediaItem.File -> eventId
|
||||||
|
is MediaItem.Audio -> eventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +94,7 @@ fun MediaItem.Event.mediaInfo(): MediaInfo {
|
||||||
is MediaItem.Image -> mediaInfo
|
is MediaItem.Image -> mediaInfo
|
||||||
is MediaItem.Video -> mediaInfo
|
is MediaItem.Video -> mediaInfo
|
||||||
is MediaItem.File -> mediaInfo
|
is MediaItem.File -> mediaInfo
|
||||||
|
is MediaItem.Audio -> mediaInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +103,7 @@ fun MediaItem.Event.mediaSource(): MediaSource {
|
||||||
is MediaItem.Image -> mediaSource
|
is MediaItem.Image -> mediaSource
|
||||||
is MediaItem.Video -> mediaSource
|
is MediaItem.Video -> mediaSource
|
||||||
is MediaItem.File -> mediaSource
|
is MediaItem.File -> mediaSource
|
||||||
|
is MediaItem.Audio -> mediaSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,5 +112,6 @@ fun MediaItem.Event.thumbnailSource(): MediaSource? {
|
||||||
is MediaItem.Image -> thumbnailSource
|
is MediaItem.Image -> thumbnailSource
|
||||||
is MediaItem.Video -> thumbnailSource
|
is MediaItem.Video -> thumbnailSource
|
||||||
is MediaItem.File -> null
|
is MediaItem.File -> null
|
||||||
|
is MediaItem.Audio -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ class MediaItemsPostProcessor @Inject constructor() {
|
||||||
is MediaItem.Video -> {
|
is MediaItem.Video -> {
|
||||||
imageAndVideoItemsSubList.add(0, item)
|
imageAndVideoItemsSubList.add(0, item)
|
||||||
}
|
}
|
||||||
|
is MediaItem.Audio,
|
||||||
is MediaItem.File -> {
|
is MediaItem.File -> {
|
||||||
fileItemsSublist.add(0, item)
|
fileItemsSublist.add(0, item)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.core.extensions.withBrackets
|
||||||
|
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
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.Text
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AudioItemView(
|
||||||
|
audio: MediaItem.Audio,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onShareClick: () -> Unit,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
|
onInfoClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 20.dp, start = 16.dp, end = 16.dp),
|
||||||
|
) {
|
||||||
|
FilenameRow(
|
||||||
|
audio = audio,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
val caption = audio.mediaInfo.caption
|
||||||
|
if (caption != null) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Caption(caption)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ActionIconsRow(
|
||||||
|
onShareClick = onShareClick,
|
||||||
|
onDownloadClick = onDownloadClick,
|
||||||
|
onInfoClick = onInfoClick,
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FilenameRow(
|
||||||
|
audio: MediaItem.Audio,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
)
|
||||||
|
.clickable { onClick() }
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = ElementTheme.colors.bgCanvasDefault,
|
||||||
|
shape = CircleShape,
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = ElementTheme.colors.borderInteractiveSecondary,
|
||||||
|
shape = CircleShape,
|
||||||
|
)
|
||||||
|
.size(36.dp)
|
||||||
|
.padding(6.dp),
|
||||||
|
imageVector = CompoundIcons.PlaySolid(),
|
||||||
|
tint = ElementTheme.colors.iconSecondary,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
audio.duration?.let {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = audio.duration,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
val waveform = audio.waveform
|
||||||
|
if (waveform == null) {
|
||||||
|
Text(
|
||||||
|
text = audio.mediaInfo.filename,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
val formattedSize = audio.mediaInfo.formattedFileSize
|
||||||
|
if (formattedSize.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = formattedSize.withBrackets(),
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
WaveformPlaybackView(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(34.dp),
|
||||||
|
playbackProgress = 0f,
|
||||||
|
showCursor = false,
|
||||||
|
waveform = waveform.toPersistentList(),
|
||||||
|
onSeek = {},
|
||||||
|
seekEnabled = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Caption(caption: String) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = caption,
|
||||||
|
maxLines = 5,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionIconsRow(
|
||||||
|
onShareClick: () -> Unit,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
|
onInfoClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onShareClick,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.ShareAndroid(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = onDownloadClick,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Download(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = onInfoClick,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Info(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun AudioItemViewPreview(
|
||||||
|
@PreviewParameter(MediaItemAudioProvider::class) audio: MediaItem.Audio,
|
||||||
|
) = ElementPreview {
|
||||||
|
AudioItemView(
|
||||||
|
audio = audio,
|
||||||
|
onClick = {},
|
||||||
|
onShareClick = {},
|
||||||
|
onDownloadClick = {},
|
||||||
|
onInfoClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.core.preview.loremIpsum
|
||||||
|
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
||||||
|
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
||||||
|
override val values: Sequence<MediaItem.Audio>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aMediaItemAudio(),
|
||||||
|
aMediaItemAudio(
|
||||||
|
filename = "A long filename that should be truncated.mp3",
|
||||||
|
caption = "A caption",
|
||||||
|
),
|
||||||
|
aMediaItemAudio(
|
||||||
|
caption = loremIpsum,
|
||||||
|
),
|
||||||
|
aMediaItemAudio(
|
||||||
|
waveform = aWaveForm(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aMediaItemAudio(
|
||||||
|
id: UniqueId = UniqueId("fileId"),
|
||||||
|
filename: String = "filename",
|
||||||
|
caption: String? = null,
|
||||||
|
duration: String? = "1:23",
|
||||||
|
waveform: List<Float>? = null,
|
||||||
|
): MediaItem.Audio {
|
||||||
|
return MediaItem.Audio(
|
||||||
|
id = id,
|
||||||
|
eventId = null,
|
||||||
|
mediaInfo = anAudioMediaInfo(
|
||||||
|
filename = filename,
|
||||||
|
caption = caption,
|
||||||
|
),
|
||||||
|
mediaSource = MediaSource(""),
|
||||||
|
duration = duration,
|
||||||
|
waveform = waveform?.toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,7 @@ class AndroidLocalMediaFactory @Inject constructor(
|
||||||
senderAvatar = mediaInfo.senderAvatar,
|
senderAvatar = mediaInfo.senderAvatar,
|
||||||
dateSent = mediaInfo.dateSent,
|
dateSent = mediaInfo.dateSent,
|
||||||
dateSentFull = mediaInfo.dateSentFull,
|
dateSentFull = mediaInfo.dateSentFull,
|
||||||
|
waveform = mediaInfo.waveform,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun createFromUri(
|
override fun createFromUri(
|
||||||
|
|
@ -65,6 +66,7 @@ class AndroidLocalMediaFactory @Inject constructor(
|
||||||
senderAvatar = null,
|
senderAvatar = null,
|
||||||
dateSent = null,
|
dateSent = null,
|
||||||
dateSentFull = null,
|
dateSentFull = null,
|
||||||
|
waveform = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createFromUri(
|
private fun createFromUri(
|
||||||
|
|
@ -78,6 +80,7 @@ class AndroidLocalMediaFactory @Inject constructor(
|
||||||
senderAvatar: String?,
|
senderAvatar: String?,
|
||||||
dateSent: String?,
|
dateSent: String?,
|
||||||
dateSentFull: String?,
|
dateSentFull: String?,
|
||||||
|
waveform: List<Float>?,
|
||||||
): 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) ?: ""
|
||||||
|
|
@ -96,6 +99,7 @@ class AndroidLocalMediaFactory @Inject constructor(
|
||||||
senderAvatar = senderAvatar,
|
senderAvatar = senderAvatar,
|
||||||
dateSent = dateSent,
|
dateSent = dateSent,
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = waveform,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ package io.element.android.libraries.mediaviewer.impl.local
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
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.isMimeTypeImage
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.audio.MediaAudioView
|
||||||
import io.element.android.libraries.mediaviewer.impl.local.file.MediaFileView
|
import io.element.android.libraries.mediaviewer.impl.local.file.MediaFileView
|
||||||
import io.element.android.libraries.mediaviewer.impl.local.image.MediaImageView
|
import io.element.android.libraries.mediaviewer.impl.local.image.MediaImageView
|
||||||
import io.element.android.libraries.mediaviewer.impl.local.pdf.MediaPdfView
|
import io.element.android.libraries.mediaviewer.impl.local.pdf.MediaPdfView
|
||||||
|
|
@ -48,7 +50,13 @@ fun LocalMediaView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
// TODO handle audio with exoplayer
|
mimeType.isMimeTypeAudio() -> MediaAudioView(
|
||||||
|
localMediaViewState = localMediaViewState,
|
||||||
|
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||||
|
localMedia = localMedia,
|
||||||
|
info = mediaInfo,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
else -> MediaFileView(
|
else -> MediaFileView(
|
||||||
localMediaViewState = localMediaViewState,
|
localMediaViewState = localMediaViewState,
|
||||||
uri = localMedia?.uri,
|
uri = localMedia?.uri,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.mediaviewer.impl.local.audio
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.GraphicEq
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.Timeline
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
||||||
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.PlayableState
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.video.ExoPlayerForPreview
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.video.ExoPlayerWrapper
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.video.MediaPlayerControllerState
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.local.video.MediaPlayerControllerView
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
@Composable
|
||||||
|
fun MediaAudioView(
|
||||||
|
localMediaViewState: LocalMediaViewState,
|
||||||
|
bottomPaddingInPixels: Int,
|
||||||
|
localMedia: LocalMedia?,
|
||||||
|
info: MediaInfo?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val exoPlayer = if (LocalInspectionMode.current) {
|
||||||
|
remember {
|
||||||
|
ExoPlayerForPreview()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val context = LocalContext.current
|
||||||
|
remember {
|
||||||
|
ExoPlayerWrapper.create(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExoPlayerMediaAudioView(
|
||||||
|
localMediaViewState = localMediaViewState,
|
||||||
|
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||||
|
exoPlayer = exoPlayer,
|
||||||
|
localMedia = localMedia,
|
||||||
|
info = info,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
@Composable
|
||||||
|
private fun ExoPlayerMediaAudioView(
|
||||||
|
localMediaViewState: LocalMediaViewState,
|
||||||
|
bottomPaddingInPixels: Int,
|
||||||
|
exoPlayer: ExoPlayer,
|
||||||
|
localMedia: LocalMedia?,
|
||||||
|
info: MediaInfo?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var mediaPlayerControllerState: MediaPlayerControllerState by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
MediaPlayerControllerState(
|
||||||
|
isVisible = true,
|
||||||
|
isPlaying = false,
|
||||||
|
progressInMillis = 0,
|
||||||
|
durationInMillis = 0,
|
||||||
|
canMute = false,
|
||||||
|
isMuted = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata: MediaMetadata? by remember {
|
||||||
|
mutableStateOf(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val playableState: PlayableState.Playable by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
PlayableState.Playable(
|
||||||
|
isShowingControls = mediaPlayerControllerState.isVisible,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localMediaViewState.playableState = playableState
|
||||||
|
|
||||||
|
val playerListener = object : Player.Listener {
|
||||||
|
override fun onRenderedFirstFrame() {
|
||||||
|
localMediaViewState.isReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||||
|
isPlaying = isPlaying,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||||
|
if (reason == Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) {
|
||||||
|
exoPlayer.duration.takeIf { it >= 0 }
|
||||||
|
?.let {
|
||||||
|
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||||
|
durationInMillis = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||||
|
metadata = mediaMetadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(exoPlayer.isPlaying) {
|
||||||
|
if (exoPlayer.isPlaying) {
|
||||||
|
while (true) {
|
||||||
|
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||||
|
progressInMillis = exoPlayer.currentPosition,
|
||||||
|
)
|
||||||
|
delay(200)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ensure we render the final state
|
||||||
|
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||||
|
progressInMillis = exoPlayer.currentPosition,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (localMedia?.uri != null) {
|
||||||
|
LaunchedEffect(localMedia.uri) {
|
||||||
|
val mediaItem = MediaItem.fromUri(localMedia.uri)
|
||||||
|
exoPlayer.setMediaItem(mediaItem)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exoPlayer.setMediaItems(emptyList())
|
||||||
|
}
|
||||||
|
val context = LocalContext.current
|
||||||
|
val waveform = info?.waveform
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(ElementTheme.colors.bgSubtlePrimary),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.Center),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (LocalInspectionMode.current) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.width(240.dp),
|
||||||
|
text = "An audio Player may render an image here if the audio file contains some artwork.",
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(shape = RoundedCornerShape(12.dp))
|
||||||
|
.clipToBounds()
|
||||||
|
.width(240.dp),
|
||||||
|
factory = {
|
||||||
|
PlayerView(context).apply {
|
||||||
|
player = exoPlayer
|
||||||
|
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
|
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||||
|
useController = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { playerView ->
|
||||||
|
playerView.isVisible = metadata.hasArtwork()
|
||||||
|
},
|
||||||
|
onRelease = { playerView ->
|
||||||
|
playerView.player = null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (waveform != null) {
|
||||||
|
WaveformPlaybackView(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp),
|
||||||
|
playbackProgress = mediaPlayerControllerState.progressAsFloat,
|
||||||
|
showCursor = true,
|
||||||
|
waveform = waveform.toPersistentList(),
|
||||||
|
onSeek = {
|
||||||
|
if (exoPlayer.isPlaying.not()) {
|
||||||
|
exoPlayer.play()
|
||||||
|
}
|
||||||
|
exoPlayer.seekTo((it * exoPlayer.duration).toLong())
|
||||||
|
},
|
||||||
|
seekEnabled = true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (!metadata.hasArtwork()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.onBackground),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.GraphicEq,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.background,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (waveform == null) {
|
||||||
|
// Display the info below the player
|
||||||
|
AudioInfoView(
|
||||||
|
info = info,
|
||||||
|
metadata = metadata,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaPlayerControllerView(
|
||||||
|
state = mediaPlayerControllerState,
|
||||||
|
onTogglePlay = {
|
||||||
|
if (exoPlayer.isPlaying) {
|
||||||
|
exoPlayer.pause()
|
||||||
|
} else {
|
||||||
|
if (exoPlayer.playbackState == Player.STATE_ENDED) {
|
||||||
|
exoPlayer.seekTo(0)
|
||||||
|
} else {
|
||||||
|
exoPlayer.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSeekChange = {
|
||||||
|
if (exoPlayer.isPlaying.not()) {
|
||||||
|
exoPlayer.play()
|
||||||
|
}
|
||||||
|
exoPlayer.seekTo(it.toLong())
|
||||||
|
},
|
||||||
|
onToggleMute = {
|
||||||
|
// Cannot happen for audio files
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = bottomPaddingInPixels.toDp()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLifecycleEvent { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_CREATE -> exoPlayer.addListener(playerListener)
|
||||||
|
Lifecycle.Event.ON_RESUME -> exoPlayer.prepare()
|
||||||
|
Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
|
||||||
|
Lifecycle.Event.ON_DESTROY -> {
|
||||||
|
exoPlayer.release()
|
||||||
|
exoPlayer.removeListener(playerListener)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.AudioInfoView(
|
||||||
|
info: MediaInfo?,
|
||||||
|
metadata: MediaMetadata?,
|
||||||
|
) {
|
||||||
|
// Render the info about the file and from the metadata
|
||||||
|
val metaDataInfo = metadata.buildInfo()
|
||||||
|
if (metaDataInfo.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = metaDataInfo,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info != null) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = info.filename,
|
||||||
|
maxLines = 2,
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = formatFileExtensionAndSize(info.fileExtension, info.formattedFileSize),
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun MediaFileViewPreview(
|
||||||
|
@PreviewParameter(MediaInfoAudioProvider::class) info: MediaInfo
|
||||||
|
) = ElementPreview {
|
||||||
|
MediaAudioView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
bottomPaddingInPixels = 0,
|
||||||
|
localMediaViewState = rememberLocalMediaViewState(),
|
||||||
|
info = info,
|
||||||
|
localMedia = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.mediaviewer.impl.local.audio
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
||||||
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
||||||
|
|
||||||
|
open class MediaInfoAudioProvider : PreviewParameterProvider<MediaInfo> {
|
||||||
|
override val values: Sequence<MediaInfo>
|
||||||
|
get() = sequenceOf(
|
||||||
|
anAudioMediaInfo(),
|
||||||
|
anAudioMediaInfo(
|
||||||
|
waveForm = aWaveForm(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.mediaviewer.impl.local.audio
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
|
|
||||||
|
fun MediaMetadata?.hasArtwork(): Boolean {
|
||||||
|
return this?.artworkData != null || this?.artworkUri != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaMetadata?.buildInfo(): String {
|
||||||
|
this ?: return ""
|
||||||
|
return buildString {
|
||||||
|
if (artist != null) {
|
||||||
|
append(artist)
|
||||||
|
}
|
||||||
|
if (title != null) {
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
append(" - ")
|
||||||
|
}
|
||||||
|
append(title)
|
||||||
|
}
|
||||||
|
if (recordingYear != null) {
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
append(" - ")
|
||||||
|
}
|
||||||
|
append(recordingYear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,12 +10,10 @@ 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.aPdfMediaInfo
|
||||||
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(),
|
aPdfMediaInfo(),
|
||||||
anAudioMediaInfo(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,16 @@
|
||||||
|
|
||||||
package io.element.android.libraries.mediaviewer.impl.local.video
|
package io.element.android.libraries.mediaviewer.impl.local.video
|
||||||
|
|
||||||
|
import androidx.annotation.FloatRange
|
||||||
|
|
||||||
data class MediaPlayerControllerState(
|
data class MediaPlayerControllerState(
|
||||||
val isVisible: Boolean,
|
val isVisible: Boolean,
|
||||||
val isPlaying: Boolean,
|
val isPlaying: Boolean,
|
||||||
val progressInMillis: Long,
|
val progressInMillis: Long,
|
||||||
val durationInMillis: Long,
|
val durationInMillis: Long,
|
||||||
|
val canMute: Boolean,
|
||||||
val isMuted: Boolean,
|
val isMuted: Boolean,
|
||||||
)
|
) {
|
||||||
|
@FloatRange(from = 0.0, to = 1.0)
|
||||||
|
val progressAsFloat = (progressInMillis.toFloat() / durationInMillis.toFloat()).coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ open class MediaPlayerControllerStateProvider : PreviewParameterProvider<MediaPl
|
||||||
durationInMillis = 83_000,
|
durationInMillis = 83_000,
|
||||||
isMuted = true,
|
isMuted = true,
|
||||||
),
|
),
|
||||||
|
aMediaPlayerControllerState(
|
||||||
|
canMute = false,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,11 +30,13 @@ private fun aMediaPlayerControllerState(
|
||||||
progressInMillis: Long = 0,
|
progressInMillis: Long = 0,
|
||||||
// Default to 1 minute and 23 seconds
|
// Default to 1 minute and 23 seconds
|
||||||
durationInMillis: Long = 83_000,
|
durationInMillis: Long = 83_000,
|
||||||
|
canMute: Boolean = true,
|
||||||
isMuted: Boolean = false,
|
isMuted: Boolean = false,
|
||||||
) = MediaPlayerControllerState(
|
) = MediaPlayerControllerState(
|
||||||
isVisible = isVisible,
|
isVisible = isVisible,
|
||||||
isPlaying = isPlaying,
|
isPlaying = isPlaying,
|
||||||
progressInMillis = progressInMillis,
|
progressInMillis = progressInMillis,
|
||||||
durationInMillis = durationInMillis,
|
durationInMillis = durationInMillis,
|
||||||
|
canMute = canMute,
|
||||||
isMuted = isMuted,
|
isMuted = isMuted,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -133,21 +133,23 @@ fun MediaPlayerControllerView(
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
style = ElementTheme.typography.fontBodyXsMedium,
|
style = ElementTheme.typography.fontBodyXsMedium,
|
||||||
)
|
)
|
||||||
IconButton(
|
if (state.canMute) {
|
||||||
onClick = onToggleMute,
|
IconButton(
|
||||||
) {
|
onClick = onToggleMute,
|
||||||
if (state.isMuted) {
|
) {
|
||||||
Icon(
|
if (state.isMuted) {
|
||||||
imageVector = CompoundIcons.VolumeOffSolid(),
|
Icon(
|
||||||
tint = ElementTheme.colors.iconPrimary,
|
imageVector = CompoundIcons.VolumeOffSolid(),
|
||||||
contentDescription = stringResource(CommonStrings.common_unmute)
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
)
|
contentDescription = stringResource(CommonStrings.common_unmute)
|
||||||
} else {
|
)
|
||||||
Icon(
|
} else {
|
||||||
imageVector = CompoundIcons.VolumeOnSolid(),
|
Icon(
|
||||||
tint = ElementTheme.colors.iconPrimary,
|
imageVector = CompoundIcons.VolumeOnSolid(),
|
||||||
contentDescription = stringResource(CommonStrings.common_mute)
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
)
|
contentDescription = stringResource(CommonStrings.common_mute)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ private fun ExoPlayerMediaVideoView(
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
progressInMillis = 0,
|
progressInMillis = 0,
|
||||||
durationInMillis = 0,
|
durationInMillis = 0,
|
||||||
|
canMute = true,
|
||||||
isMuted = false,
|
isMuted = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
||||||
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.aPdfMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
||||||
|
|
@ -100,6 +101,16 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||||
aMediaViewerState(
|
aMediaViewerState(
|
||||||
mediaBottomSheetState = aMediaDeleteConfirmationState(),
|
mediaBottomSheetState = aMediaDeleteConfirmationState(),
|
||||||
),
|
),
|
||||||
|
anAudioMediaInfo(
|
||||||
|
waveForm = aWaveForm(),
|
||||||
|
).let {
|
||||||
|
aMediaViewerState(
|
||||||
|
downloadedMedia = AsyncData.Success(
|
||||||
|
LocalMedia(Uri.EMPTY, it)
|
||||||
|
),
|
||||||
|
mediaInfo = it,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue