Distinguish Audio and Voice media items.
This commit is contained in:
parent
83e474f118
commit
df72ab6a86
13 changed files with 324 additions and 51 deletions
|
|
@ -125,3 +125,24 @@ fun anAudioMediaInfo(
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
waveform = waveForm,
|
waveform = waveForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aVoiceMediaInfo(
|
||||||
|
filename: String = "a voice file.ogg",
|
||||||
|
caption: String? = null,
|
||||||
|
senderName: String? = null,
|
||||||
|
dateSent: String? = null,
|
||||||
|
dateSentFull: String? = null,
|
||||||
|
waveForm: List<Float>? = null,
|
||||||
|
): MediaInfo = MediaInfo(
|
||||||
|
filename = filename,
|
||||||
|
caption = caption,
|
||||||
|
mimeType = MimeTypes.Ogg,
|
||||||
|
formattedFileSize = "3MB",
|
||||||
|
fileExtension = "ogg",
|
||||||
|
senderId = UserId("@alice:server.org"),
|
||||||
|
senderName = senderName,
|
||||||
|
senderAvatar = null,
|
||||||
|
dateSent = dateSent,
|
||||||
|
dateSentFull = dateSentFull,
|
||||||
|
waveform = waveForm,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
|
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -104,7 +105,6 @@ class EventItemFactory @Inject constructor(
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||||
waveform = null,
|
|
||||||
)
|
)
|
||||||
is FileMessageType -> MediaItem.File(
|
is FileMessageType -> MediaItem.File(
|
||||||
id = currentTimelineItem.uniqueId,
|
id = currentTimelineItem.uniqueId,
|
||||||
|
|
@ -182,7 +182,7 @@ class EventItemFactory @Inject constructor(
|
||||||
thumbnailSource = type.info?.thumbnailSource,
|
thumbnailSource = type.info?.thumbnailSource,
|
||||||
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||||
)
|
)
|
||||||
is VoiceMessageType -> MediaItem.Audio(
|
is VoiceMessageType -> MediaItem.Voice(
|
||||||
id = currentTimelineItem.uniqueId,
|
id = currentTimelineItem.uniqueId,
|
||||||
eventId = currentTimelineItem.eventId,
|
eventId = currentTimelineItem.eventId,
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
|
|
@ -200,7 +200,7 @@ class EventItemFactory @Inject constructor(
|
||||||
),
|
),
|
||||||
mediaSource = type.source,
|
mediaSource = type.source,
|
||||||
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||||
waveform = type.details?.waveform,
|
waveform = type.details?.waveform ?: persistentListOf(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
|
||||||
is MediaItem.Video -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
|
is MediaItem.Video -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
|
||||||
is MediaItem.Audio -> null
|
is MediaItem.Audio -> null
|
||||||
is MediaItem.File -> null
|
is MediaItem.File -> null
|
||||||
|
is MediaItem.Voice -> null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ 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
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryState> {
|
open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryState> {
|
||||||
|
|
@ -65,7 +66,7 @@ open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryStat
|
||||||
),
|
),
|
||||||
aMediaItemFile(id = UniqueId("3")),
|
aMediaItemFile(id = UniqueId("3")),
|
||||||
aMediaItemAudio(id = UniqueId("4")),
|
aMediaItemAudio(id = UniqueId("4")),
|
||||||
aMediaItemAudio(
|
aMediaItemVoice(
|
||||||
id = UniqueId("5"),
|
id = UniqueId("5"),
|
||||||
waveform = aWaveForm(),
|
waveform = aWaveForm(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ 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
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VideoItemView
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VideoItemView
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VoiceItemView
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
@ -274,6 +275,12 @@ 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.Voice -> VoiceItemView(
|
||||||
|
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 -> {
|
||||||
|
|
@ -332,6 +339,9 @@ private fun MediaGalleryImageGrid(
|
||||||
is MediaItem.Audio -> {
|
is MediaItem.Audio -> {
|
||||||
// Should not happen
|
// Should not happen
|
||||||
}
|
}
|
||||||
|
is MediaItem.Voice -> {
|
||||||
|
// Should not happen
|
||||||
|
}
|
||||||
is MediaItem.File -> {
|
is MediaItem.File -> {
|
||||||
// Should not happen
|
// Should not happen
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,15 @@ sealed interface MediaItem {
|
||||||
val mediaInfo: MediaInfo,
|
val mediaInfo: MediaInfo,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val duration: String?,
|
val duration: String?,
|
||||||
val waveform: ImmutableList<Float>?,
|
) : Event
|
||||||
|
|
||||||
|
data class Voice(
|
||||||
|
val id: UniqueId,
|
||||||
|
val eventId: EventId?,
|
||||||
|
val mediaInfo: MediaInfo,
|
||||||
|
val mediaSource: MediaSource,
|
||||||
|
val duration: String?,
|
||||||
|
val waveform: ImmutableList<Float>,
|
||||||
) : Event
|
) : Event
|
||||||
|
|
||||||
data class File(
|
data class File(
|
||||||
|
|
@ -77,6 +85,7 @@ fun MediaItem.id(): UniqueId {
|
||||||
is MediaItem.Video -> id
|
is MediaItem.Video -> id
|
||||||
is MediaItem.File -> id
|
is MediaItem.File -> id
|
||||||
is MediaItem.Audio -> id
|
is MediaItem.Audio -> id
|
||||||
|
is MediaItem.Voice -> id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,6 +95,7 @@ fun MediaItem.Event.eventId(): EventId? {
|
||||||
is MediaItem.Video -> eventId
|
is MediaItem.Video -> eventId
|
||||||
is MediaItem.File -> eventId
|
is MediaItem.File -> eventId
|
||||||
is MediaItem.Audio -> eventId
|
is MediaItem.Audio -> eventId
|
||||||
|
is MediaItem.Voice -> eventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +105,7 @@ fun MediaItem.Event.mediaInfo(): MediaInfo {
|
||||||
is MediaItem.Video -> mediaInfo
|
is MediaItem.Video -> mediaInfo
|
||||||
is MediaItem.File -> mediaInfo
|
is MediaItem.File -> mediaInfo
|
||||||
is MediaItem.Audio -> mediaInfo
|
is MediaItem.Audio -> mediaInfo
|
||||||
|
is MediaItem.Voice -> mediaInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,6 +115,7 @@ fun MediaItem.Event.mediaSource(): MediaSource {
|
||||||
is MediaItem.Video -> mediaSource
|
is MediaItem.Video -> mediaSource
|
||||||
is MediaItem.File -> mediaSource
|
is MediaItem.File -> mediaSource
|
||||||
is MediaItem.Audio -> mediaSource
|
is MediaItem.Audio -> mediaSource
|
||||||
|
is MediaItem.Voice -> mediaSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,5 +125,6 @@ fun MediaItem.Event.thumbnailSource(): MediaSource? {
|
||||||
is MediaItem.Video -> thumbnailSource
|
is MediaItem.Video -> thumbnailSource
|
||||||
is MediaItem.File -> null
|
is MediaItem.File -> null
|
||||||
is MediaItem.Audio -> null
|
is MediaItem.Audio -> null
|
||||||
|
is MediaItem.Voice -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ class MediaItemsPostProcessor @Inject constructor() {
|
||||||
imageAndVideoItemsSubList.add(0, item)
|
imageAndVideoItemsSubList.add(0, item)
|
||||||
}
|
}
|
||||||
is MediaItem.Audio,
|
is MediaItem.Audio,
|
||||||
|
is MediaItem.Voice,
|
||||||
is MediaItem.File -> {
|
is MediaItem.File -> {
|
||||||
fileItemsSublist.add(0, item)
|
fileItemsSublist.add(0, item)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -21,6 +20,8 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.GraphicEq
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -31,7 +32,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.core.extensions.withBrackets
|
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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
|
@ -39,7 +39,6 @@ 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.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioItemView(
|
fun AudioItemView(
|
||||||
|
|
@ -94,18 +93,12 @@ private fun FilenameRow(
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(
|
||||||
color = ElementTheme.colors.bgCanvasDefault,
|
color = ElementTheme.colors.bgActionSecondaryRest,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
)
|
)
|
||||||
.border(
|
.size(32.dp)
|
||||||
width = 1.dp,
|
|
||||||
color = ElementTheme.colors.borderInteractiveSecondary,
|
|
||||||
shape = CircleShape,
|
|
||||||
)
|
|
||||||
.size(36.dp)
|
|
||||||
.padding(6.dp),
|
.padding(6.dp),
|
||||||
imageVector = CompoundIcons.PlaySolid(),
|
imageVector = Icons.Outlined.GraphicEq,
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
audio.duration?.let {
|
audio.duration?.let {
|
||||||
|
|
@ -119,34 +112,20 @@ private fun FilenameRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
val waveform = audio.waveform
|
Text(
|
||||||
if (waveform == null) {
|
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(
|
||||||
text = audio.mediaInfo.filename,
|
text = formattedSize.withBrackets(),
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
color = ElementTheme.colors.textPrimary,
|
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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,10 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.core.preview.loremIpsum
|
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.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
||||||
override val values: Sequence<MediaItem.Audio>
|
override val values: Sequence<MediaItem.Audio>
|
||||||
|
|
@ -27,9 +25,6 @@ class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
||||||
aMediaItemAudio(
|
aMediaItemAudio(
|
||||||
caption = loremIpsum,
|
caption = loremIpsum,
|
||||||
),
|
),
|
||||||
aMediaItemAudio(
|
|
||||||
waveform = aWaveForm(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +33,6 @@ fun aMediaItemAudio(
|
||||||
filename: String = "filename",
|
filename: String = "filename",
|
||||||
caption: String? = null,
|
caption: String? = null,
|
||||||
duration: String? = "1:23",
|
duration: String? = "1:23",
|
||||||
waveform: List<Float>? = null,
|
|
||||||
): MediaItem.Audio {
|
): MediaItem.Audio {
|
||||||
return MediaItem.Audio(
|
return MediaItem.Audio(
|
||||||
id = id,
|
id = id,
|
||||||
|
|
@ -49,6 +43,5 @@ fun aMediaItemAudio(
|
||||||
),
|
),
|
||||||
mediaSource = MediaSource(""),
|
mediaSource = MediaSource(""),
|
||||||
duration = duration,
|
duration = duration,
|
||||||
waveform = waveform?.toImmutableList(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.aVoiceMediaInfo
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> {
|
||||||
|
override val values: Sequence<MediaItem.Voice>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aMediaItemVoice(),
|
||||||
|
aMediaItemVoice(
|
||||||
|
filename = "A long filename that should be truncated.ogg",
|
||||||
|
caption = "A caption",
|
||||||
|
),
|
||||||
|
aMediaItemVoice(
|
||||||
|
caption = loremIpsum,
|
||||||
|
),
|
||||||
|
aMediaItemVoice(
|
||||||
|
waveform = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aMediaItemVoice(
|
||||||
|
id: UniqueId = UniqueId("fileId"),
|
||||||
|
filename: String = "filename.ogg",
|
||||||
|
caption: String? = null,
|
||||||
|
duration: String? = "1:23",
|
||||||
|
waveform: List<Float> = aWaveForm(),
|
||||||
|
): MediaItem.Voice {
|
||||||
|
return MediaItem.Voice(
|
||||||
|
id = id,
|
||||||
|
eventId = null,
|
||||||
|
mediaInfo = aVoiceMediaInfo(
|
||||||
|
filename = filename,
|
||||||
|
caption = caption,
|
||||||
|
),
|
||||||
|
mediaSource = MediaSource(""),
|
||||||
|
duration = duration,
|
||||||
|
waveform = waveform.toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* 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.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.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 VoiceItemView(
|
||||||
|
voice: MediaItem.Voice,
|
||||||
|
onShareClick: () -> Unit,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
|
onInfoClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 20.dp, start = 16.dp, end = 16.dp),
|
||||||
|
) {
|
||||||
|
VoiceInfoRow(
|
||||||
|
voice = voice,
|
||||||
|
)
|
||||||
|
val caption = voice.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 VoiceInfoRow(
|
||||||
|
voice: MediaItem.Voice,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
)
|
||||||
|
.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,
|
||||||
|
)
|
||||||
|
voice.duration?.let {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = voice.duration,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
WaveformPlaybackView(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(34.dp),
|
||||||
|
playbackProgress = 0f,
|
||||||
|
showCursor = false,
|
||||||
|
waveform = voice.waveform.toPersistentList(),
|
||||||
|
onSeek = {
|
||||||
|
|
||||||
|
},
|
||||||
|
seekEnabled = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 VoiceItemViewPreview(
|
||||||
|
@PreviewParameter(MediaItemVoiceProvider::class) voice: MediaItem.Voice,
|
||||||
|
) = ElementPreview {
|
||||||
|
VoiceItemView(
|
||||||
|
voice = voice,
|
||||||
|
onShareClick = {},
|
||||||
|
onDownloadClick = {},
|
||||||
|
onInfoClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -263,7 +263,6 @@ class DefaultEventItemFactoryTest {
|
||||||
),
|
),
|
||||||
mediaSource = MediaSource(""),
|
mediaSource = MediaSource(""),
|
||||||
duration = "7:36",
|
duration = "7:36",
|
||||||
waveform = null,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +347,7 @@ class DefaultEventItemFactoryTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
MediaItem.Audio(
|
MediaItem.Voice(
|
||||||
id = A_UNIQUE_ID,
|
id = A_UNIQUE_ID,
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
mediaInfo = MediaInfo(
|
mediaInfo = MediaInfo(
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ 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
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
||||||
|
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -27,6 +28,9 @@ class MediaItemsPostProcessorTest {
|
||||||
private val audio1 = aMediaItemAudio(id = UniqueId("1"))
|
private val audio1 = aMediaItemAudio(id = UniqueId("1"))
|
||||||
private val audio2 = aMediaItemAudio(id = UniqueId("2"))
|
private val audio2 = aMediaItemAudio(id = UniqueId("2"))
|
||||||
private val audio3 = aMediaItemAudio(id = UniqueId("3"))
|
private val audio3 = aMediaItemAudio(id = UniqueId("3"))
|
||||||
|
private val voice1 = aMediaItemVoice(id = UniqueId("1"))
|
||||||
|
private val voice2 = aMediaItemVoice(id = UniqueId("2"))
|
||||||
|
private val voice3 = aMediaItemVoice(id = UniqueId("3"))
|
||||||
private val image1 = aMediaItemImage(id = UniqueId("1"))
|
private val image1 = aMediaItemImage(id = UniqueId("1"))
|
||||||
private val image2 = aMediaItemImage(id = UniqueId("2"))
|
private val image2 = aMediaItemImage(id = UniqueId("2"))
|
||||||
private val image3 = aMediaItemImage(id = UniqueId("3"))
|
private val image3 = aMediaItemImage(id = UniqueId("3"))
|
||||||
|
|
@ -163,6 +167,9 @@ class MediaItemsPostProcessorTest {
|
||||||
fun `process will handle complex case`() {
|
fun `process will handle complex case`() {
|
||||||
test(
|
test(
|
||||||
mediaItems = listOf(
|
mediaItems = listOf(
|
||||||
|
voice3,
|
||||||
|
voice2,
|
||||||
|
voice1,
|
||||||
audio3,
|
audio3,
|
||||||
audio2,
|
audio2,
|
||||||
audio1,
|
audio1,
|
||||||
|
|
@ -192,6 +199,9 @@ class MediaItemsPostProcessorTest {
|
||||||
audio1,
|
audio1,
|
||||||
audio2,
|
audio2,
|
||||||
audio3,
|
audio3,
|
||||||
|
voice1,
|
||||||
|
voice2,
|
||||||
|
voice3,
|
||||||
date3,
|
date3,
|
||||||
file3,
|
file3,
|
||||||
loading1,
|
loading1,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue