Merge pull request #4071 from element-hq/feature/bma/galleryUiTweak
Media gallery UI update
This commit is contained in:
commit
26ffb1eb0e
81 changed files with 443 additions and 298 deletions
|
|
@ -251,8 +251,6 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
mediaSource = navTarget.mediaSource,
|
mediaSource = navTarget.mediaSource,
|
||||||
thumbnailSource = navTarget.thumbnailSource,
|
thumbnailSource = navTarget.thumbnailSource,
|
||||||
canShowInfo = true,
|
canShowInfo = true,
|
||||||
canDownload = true,
|
|
||||||
canShare = true,
|
|
||||||
)
|
)
|
||||||
val callback = object : MediaViewerEntryPoint.Callback {
|
val callback = object : MediaViewerEntryPoint.Callback {
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ enum class FeatureFlags(
|
||||||
key = "feature.media_gallery",
|
key = "feature.media_gallery",
|
||||||
title = "Allow user to open the media gallery",
|
title = "Allow user to open the media gallery",
|
||||||
description = null,
|
description = null,
|
||||||
defaultValue = { buildMeta -> buildMeta.buildType != BuildType.RELEASE },
|
defaultValue = { true },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
EventCache(
|
EventCache(
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,5 @@ interface MediaViewerEntryPoint : FeatureEntryPoint {
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val canShowInfo: Boolean,
|
val canShowInfo: Boolean,
|
||||||
val canDownload: Boolean,
|
|
||||||
val canShare: Boolean,
|
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,6 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
|
||||||
mediaSource = MediaSource(url = avatarUrl),
|
mediaSource = MediaSource(url = avatarUrl),
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
canShowInfo = false,
|
canShowInfo = false,
|
||||||
canDownload = false,
|
|
||||||
canShare = false,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun MediaDetailsBottomSheet(
|
fun MediaDetailsBottomSheet(
|
||||||
state: MediaBottomSheetState.MediaDetailsBottomSheetState,
|
state: MediaBottomSheetState.MediaDetailsBottomSheetState,
|
||||||
onViewInTimeline: (EventId) -> Unit,
|
onViewInTimeline: (EventId) -> Unit,
|
||||||
|
onShare: (EventId) -> Unit,
|
||||||
|
onDownload: (EventId) -> Unit,
|
||||||
onDelete: (EventId) -> Unit,
|
onDelete: (EventId) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -92,6 +94,22 @@ fun MediaDetailsBottomSheet(
|
||||||
onViewInTimeline(state.eventId)
|
onViewInTimeline(state.eventId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ShareAndroid())),
|
||||||
|
headlineContent = { Text(stringResource(CommonStrings.action_share)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
|
onClick = {
|
||||||
|
onShare(state.eventId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Download())),
|
||||||
|
headlineContent = { Text(stringResource(CommonStrings.action_save)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
|
onClick = {
|
||||||
|
onDownload(state.eventId)
|
||||||
|
}
|
||||||
|
)
|
||||||
if (state.canDelete) {
|
if (state.canDelete) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
@ -196,6 +214,8 @@ internal fun MediaDetailsBottomSheetPreview() = ElementPreview {
|
||||||
MediaDetailsBottomSheet(
|
MediaDetailsBottomSheet(
|
||||||
state = aMediaDetailsBottomSheetState(),
|
state = aMediaDetailsBottomSheetState(),
|
||||||
onViewInTimeline = {},
|
onViewInTimeline = {},
|
||||||
|
onShare = {},
|
||||||
|
onDownload = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@ import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||||
|
|
||||||
fun aMediaDetailsBottomSheetState(
|
fun aMediaDetailsBottomSheetState(
|
||||||
dateSentFull: String = "December 6, 2024 at 12:59",
|
dateSentFull: String = "December 6, 2024 at 12:59",
|
||||||
|
canDelete: Boolean = true,
|
||||||
): MediaBottomSheetState.MediaDetailsBottomSheetState {
|
): MediaBottomSheetState.MediaDetailsBottomSheetState {
|
||||||
return MediaBottomSheetState.MediaDetailsBottomSheetState(
|
return MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||||
eventId = EventId("\$eventId"),
|
eventId = EventId("\$eventId"),
|
||||||
canDelete = true,
|
canDelete = canDelete,
|
||||||
mediaInfo = anImageMediaInfo(
|
mediaInfo = anImageMediaInfo(
|
||||||
senderName = "Alice",
|
senderName = "Alice",
|
||||||
dateSentFull = dateSentFull,
|
dateSentFull = dateSentFull,
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
sealed interface MediaGalleryEvents {
|
sealed interface MediaGalleryEvents {
|
||||||
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
|
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
|
||||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents
|
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents
|
||||||
data class Share(val mediaItem: MediaItem.Event) : MediaGalleryEvents
|
data class Share(val eventId: EventId?) : MediaGalleryEvents
|
||||||
data class SaveOnDisk(val mediaItem: MediaItem.Event) : MediaGalleryEvents
|
data class SaveOnDisk(val eventId: EventId?) : MediaGalleryEvents
|
||||||
data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents
|
data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents
|
||||||
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents
|
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,16 @@ class MediaGalleryPresenter @AssistedInject constructor(
|
||||||
timeline.dataOrNull()?.paginate(event.direction)
|
timeline.dataOrNull()?.paginate(event.direction)
|
||||||
}
|
}
|
||||||
is MediaGalleryEvents.Delete -> coroutineScope.delete(timeline, event.eventId)
|
is MediaGalleryEvents.Delete -> coroutineScope.delete(timeline, event.eventId)
|
||||||
is MediaGalleryEvents.SaveOnDisk -> coroutineScope.saveOnDisk(event.mediaItem)
|
is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch {
|
||||||
is MediaGalleryEvents.Share -> coroutineScope.share(event.mediaItem)
|
mediaItems.dataOrNull().find(event.eventId)?.let {
|
||||||
|
saveOnDisk(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MediaGalleryEvents.Share -> coroutineScope.launch {
|
||||||
|
mediaItems.dataOrNull().find(event.eventId)?.let {
|
||||||
|
share(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
is MediaGalleryEvents.ViewInTimeline -> {
|
is MediaGalleryEvents.ViewInTimeline -> {
|
||||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
navigator.onViewInTimelineClick(event.eventId)
|
navigator.onViewInTimelineClick(event.eventId)
|
||||||
|
|
@ -221,7 +229,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.saveOnDisk(mediaItem: MediaItem.Event) = launch {
|
private suspend fun saveOnDisk(mediaItem: MediaItem.Event) {
|
||||||
downloadMedia(mediaItem)
|
downloadMedia(mediaItem)
|
||||||
.mapCatching { localMedia ->
|
.mapCatching { localMedia ->
|
||||||
localMediaActions.saveOnDisk(localMedia)
|
localMediaActions.saveOnDisk(localMedia)
|
||||||
|
|
@ -236,7 +244,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.share(mediaItem: MediaItem.Event) = launch {
|
private suspend fun share(mediaItem: MediaItem.Event) {
|
||||||
downloadMedia(mediaItem)
|
downloadMedia(mediaItem)
|
||||||
.mapCatching { localMedia ->
|
.mapCatching { localMedia ->
|
||||||
localMediaActions.share(localMedia)
|
localMediaActions.share(localMedia)
|
||||||
|
|
@ -255,3 +263,11 @@ class MediaGalleryPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<MediaItem>?.find(eventId: EventId?): MediaItem.Event? {
|
||||||
|
if (this == null || eventId == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return filterIsInstance<MediaItem.Event>()
|
||||||
|
.firstOrNull { it.eventId() == eventId }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.PageTitle
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||||
|
|
@ -106,7 +107,8 @@ fun MediaGalleryView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.consumeWindowInsets(paddingValues)
|
.consumeWindowInsets(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
SingleChoiceSegmentedButtonRow(
|
SingleChoiceSegmentedButtonRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -151,6 +153,12 @@ fun MediaGalleryView(
|
||||||
onViewInTimeline = { eventId ->
|
onViewInTimeline = { eventId ->
|
||||||
state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId))
|
state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId))
|
||||||
},
|
},
|
||||||
|
onShare = { eventId ->
|
||||||
|
state.eventSink(MediaGalleryEvents.Share(eventId))
|
||||||
|
},
|
||||||
|
onDownload = { eventId ->
|
||||||
|
state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
|
||||||
|
},
|
||||||
onDelete = { eventId ->
|
onDelete = { eventId ->
|
||||||
state.eventSink(
|
state.eventSink(
|
||||||
MediaGalleryEvents.ConfirmDelete(
|
MediaGalleryEvents.ConfirmDelete(
|
||||||
|
|
@ -274,11 +282,17 @@ private fun MediaGalleryFilesList(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
file = item,
|
file = item,
|
||||||
onClick = { onItemClick(item) },
|
onClick = { onItemClick(item) },
|
||||||
|
onLongClick = {
|
||||||
|
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
is MediaItem.Audio -> AudioItemView(
|
is MediaItem.Audio -> AudioItemView(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
audio = item,
|
audio = item,
|
||||||
onClick = { onItemClick(item) },
|
onClick = { onItemClick(item) },
|
||||||
|
onLongClick = {
|
||||||
|
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
is MediaItem.Voice -> {
|
is MediaItem.Voice -> {
|
||||||
val presenter: Presenter<VoiceMessageState> = presenterFactories.rememberPresenter(item)
|
val presenter: Presenter<VoiceMessageState> = presenterFactories.rememberPresenter(item)
|
||||||
|
|
@ -286,9 +300,9 @@ private fun MediaGalleryFilesList(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
state = presenter.present(),
|
state = presenter.present(),
|
||||||
voice = item,
|
voice = item,
|
||||||
onShareClick = { eventSink(MediaGalleryEvents.Share(item)) },
|
onLongClick = {
|
||||||
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
|
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||||
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is MediaItem.DateSeparator -> DateItemView(
|
is MediaItem.DateSeparator -> DateItemView(
|
||||||
|
|
@ -426,6 +440,7 @@ private fun EmptyContent(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
|
OnboardingBackground()
|
||||||
PageTitle(
|
PageTitle(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,6 @@ class MediaGalleryRootNode @AssistedInject constructor(
|
||||||
mediaSource = navTarget.mediaSource,
|
mediaSource = navTarget.mediaSource,
|
||||||
thumbnailSource = navTarget.thumbnailSource,
|
thumbnailSource = navTarget.thumbnailSource,
|
||||||
canShowInfo = true,
|
canShowInfo = true,
|
||||||
canDownload = true,
|
|
||||||
canShare = true,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -41,6 +42,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||||
fun AudioItemView(
|
fun AudioItemView(
|
||||||
audio: MediaItem.Audio,
|
audio: MediaItem.Audio,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -52,6 +54,7 @@ fun AudioItemView(
|
||||||
FilenameRow(
|
FilenameRow(
|
||||||
audio = audio,
|
audio = audio,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
)
|
)
|
||||||
val caption = audio.mediaInfo.caption
|
val caption = audio.mediaInfo.caption
|
||||||
if (caption != null) {
|
if (caption != null) {
|
||||||
|
|
@ -63,10 +66,12 @@ fun AudioItemView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun FilenameRow(
|
private fun FilenameRow(
|
||||||
audio: MediaItem.Audio,
|
audio: MediaItem.Audio,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -75,7 +80,7 @@ private fun FilenameRow(
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
)
|
)
|
||||||
.clickable { onClick() }
|
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -119,5 +124,6 @@ internal fun AudioItemViewPreview(
|
||||||
AudioItemView(
|
AudioItemView(
|
||||||
audio = audio,
|
audio = audio,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -40,6 +41,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||||
fun FileItemView(
|
fun FileItemView(
|
||||||
file: MediaItem.File,
|
file: MediaItem.File,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -51,6 +53,7 @@ fun FileItemView(
|
||||||
FilenameRow(
|
FilenameRow(
|
||||||
file = file,
|
file = file,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
)
|
)
|
||||||
val caption = file.mediaInfo.caption
|
val caption = file.mediaInfo.caption
|
||||||
if (caption != null) {
|
if (caption != null) {
|
||||||
|
|
@ -62,10 +65,12 @@ fun FileItemView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun FilenameRow(
|
private fun FilenameRow(
|
||||||
file: MediaItem.File,
|
file: MediaItem.File,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -74,7 +79,7 @@ private fun FilenameRow(
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
)
|
)
|
||||||
.clickable { onClick() }
|
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -118,5 +123,6 @@ internal fun FileItemViewPreview(
|
||||||
FileItemView(
|
FileItemView(
|
||||||
file = file,
|
file = file,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
|
@ -84,6 +85,14 @@ private fun VideoInfoRow(
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
ElementTheme.colors.bgCanvasDefault.copy(alpha = 0f),
|
||||||
|
ElementTheme.colors.bgCanvasDefault,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@
|
||||||
|
|
||||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -58,9 +59,7 @@ import kotlinx.coroutines.delay
|
||||||
fun VoiceItemView(
|
fun VoiceItemView(
|
||||||
state: VoiceMessageState,
|
state: VoiceMessageState,
|
||||||
voice: MediaItem.Voice,
|
voice: MediaItem.Voice,
|
||||||
onShareClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onDownloadClick: () -> Unit,
|
|
||||||
onInfoClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -72,6 +71,7 @@ fun VoiceItemView(
|
||||||
VoiceInfoRow(
|
VoiceInfoRow(
|
||||||
state = state,
|
state = state,
|
||||||
voice = voice,
|
voice = voice,
|
||||||
|
onLongClick = onLongClick,
|
||||||
)
|
)
|
||||||
val caption = voice.mediaInfo.caption
|
val caption = voice.mediaInfo.caption
|
||||||
if (caption != null) {
|
if (caption != null) {
|
||||||
|
|
@ -79,19 +79,16 @@ fun VoiceItemView(
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
ActionIconsRow(
|
|
||||||
onShareClick = onShareClick,
|
|
||||||
onDownloadClick = onDownloadClick,
|
|
||||||
onInfoClick = onInfoClick,
|
|
||||||
)
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun VoiceInfoRow(
|
private fun VoiceInfoRow(
|
||||||
state: VoiceMessageState,
|
state: VoiceMessageState,
|
||||||
voice: MediaItem.Voice,
|
voice: MediaItem.Voice,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
fun playPause() {
|
fun playPause() {
|
||||||
state.eventSink(VoiceMessageEvents.PlayPause)
|
state.eventSink(VoiceMessageEvents.PlayPause)
|
||||||
|
|
@ -104,6 +101,7 @@ private fun VoiceInfoRow(
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
)
|
)
|
||||||
|
.combinedClickable(onClick = {}, onLongClick = onLongClick)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -257,43 +255,6 @@ private fun CustomIconButton(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun VoiceItemViewPreview(
|
internal fun VoiceItemViewPreview(
|
||||||
|
|
@ -302,9 +263,7 @@ internal fun VoiceItemViewPreview(
|
||||||
VoiceItemView(
|
VoiceItemView(
|
||||||
state = aVoiceMessageState(),
|
state = aVoiceMessageState(),
|
||||||
voice = voice,
|
voice = voice,
|
||||||
onShareClick = {},
|
onLongClick = {},
|
||||||
onDownloadClick = {},
|
|
||||||
onInfoClick = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,8 +275,6 @@ internal fun VoiceItemViewPlayPreview(
|
||||||
VoiceItemView(
|
VoiceItemView(
|
||||||
state = state,
|
state = state,
|
||||||
voice = aMediaItemVoice(),
|
voice = aMediaItemVoice(),
|
||||||
onShareClick = {},
|
onLongClick = {},
|
||||||
onDownloadClick = {},
|
|
||||||
onInfoClick = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,18 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
when (mediaViewerEvents) {
|
when (mediaViewerEvents) {
|
||||||
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
|
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
|
||||||
MediaViewerEvents.ClearLoadingError -> localMedia.value = AsyncData.Uninitialized
|
MediaViewerEvents.ClearLoadingError -> localMedia.value = AsyncData.Uninitialized
|
||||||
MediaViewerEvents.SaveOnDisk -> coroutineScope.saveOnDisk(localMedia.value)
|
MediaViewerEvents.SaveOnDisk -> {
|
||||||
MediaViewerEvents.Share -> coroutineScope.share(localMedia.value)
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
MediaViewerEvents.OpenWith -> coroutineScope.open(localMedia.value)
|
coroutineScope.saveOnDisk(localMedia.value)
|
||||||
|
}
|
||||||
|
MediaViewerEvents.Share -> {
|
||||||
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
|
coroutineScope.share(localMedia.value)
|
||||||
|
}
|
||||||
|
MediaViewerEvents.OpenWith -> {
|
||||||
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
|
coroutineScope.open(localMedia.value)
|
||||||
|
}
|
||||||
is MediaViewerEvents.Delete -> {
|
is MediaViewerEvents.Delete -> {
|
||||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
coroutineScope.delete(mediaViewerEvents.eventId)
|
coroutineScope.delete(mediaViewerEvents.eventId)
|
||||||
|
|
@ -126,8 +135,6 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
downloadedMedia = localMedia.value,
|
downloadedMedia = localMedia.value,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canShowInfo = inputs.canShowInfo,
|
canShowInfo = inputs.canShowInfo,
|
||||||
canDownload = inputs.canDownload,
|
|
||||||
canShare = inputs.canShare,
|
|
||||||
mediaBottomSheetState = mediaBottomSheetState,
|
mediaBottomSheetState = mediaBottomSheetState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ data class MediaViewerState(
|
||||||
val downloadedMedia: AsyncData<LocalMedia>,
|
val downloadedMedia: AsyncData<LocalMedia>,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canShowInfo: Boolean,
|
val canShowInfo: Boolean,
|
||||||
val canDownload: Boolean,
|
|
||||||
val canShare: Boolean,
|
|
||||||
val mediaBottomSheetState: MediaBottomSheetState,
|
val mediaBottomSheetState: MediaBottomSheetState,
|
||||||
val eventSink: (MediaViewerEvents) -> Unit,
|
val eventSink: (MediaViewerEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,6 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||||
),
|
),
|
||||||
mediaInfo = it,
|
mediaInfo = it,
|
||||||
canShowInfo = false,
|
canShowInfo = false,
|
||||||
canDownload = false,
|
|
||||||
canShare = false,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
aMediaViewerState(
|
aMediaViewerState(
|
||||||
|
|
@ -118,8 +116,6 @@ fun aMediaViewerState(
|
||||||
downloadedMedia: AsyncData<LocalMedia> = AsyncData.Uninitialized,
|
downloadedMedia: AsyncData<LocalMedia> = AsyncData.Uninitialized,
|
||||||
mediaInfo: MediaInfo = anImageMediaInfo(),
|
mediaInfo: MediaInfo = anImageMediaInfo(),
|
||||||
canShowInfo: Boolean = true,
|
canShowInfo: Boolean = true,
|
||||||
canDownload: Boolean = true,
|
|
||||||
canShare: Boolean = true,
|
|
||||||
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
||||||
eventSink: (MediaViewerEvents) -> Unit = {},
|
eventSink: (MediaViewerEvents) -> Unit = {},
|
||||||
) = MediaViewerState(
|
) = MediaViewerState(
|
||||||
|
|
@ -129,8 +125,6 @@ fun aMediaViewerState(
|
||||||
downloadedMedia = downloadedMedia,
|
downloadedMedia = downloadedMedia,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
canShowInfo = canShowInfo,
|
canShowInfo = canShowInfo,
|
||||||
canDownload = canDownload,
|
|
||||||
canShare = canShare,
|
|
||||||
mediaBottomSheetState = mediaBottomSheetState,
|
mediaBottomSheetState = mediaBottomSheetState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,6 @@ fun MediaViewerView(
|
||||||
) {
|
) {
|
||||||
MediaViewerTopBar(
|
MediaViewerTopBar(
|
||||||
actionsEnabled = state.downloadedMedia is AsyncData.Success,
|
actionsEnabled = state.downloadedMedia is AsyncData.Success,
|
||||||
canDownload = state.canDownload,
|
|
||||||
canShare = state.canShare,
|
|
||||||
mimeType = state.mediaInfo.mimeType,
|
mimeType = state.mediaInfo.mimeType,
|
||||||
senderName = state.mediaInfo.senderName,
|
senderName = state.mediaInfo.senderName,
|
||||||
dateSent = state.mediaInfo.dateSent,
|
dateSent = state.mediaInfo.dateSent,
|
||||||
|
|
@ -148,6 +146,12 @@ fun MediaViewerView(
|
||||||
onViewInTimeline = {
|
onViewInTimeline = {
|
||||||
state.eventSink(MediaViewerEvents.ViewInTimeline(it))
|
state.eventSink(MediaViewerEvents.ViewInTimeline(it))
|
||||||
},
|
},
|
||||||
|
onShare = {
|
||||||
|
state.eventSink(MediaViewerEvents.Share)
|
||||||
|
},
|
||||||
|
onDownload = {
|
||||||
|
state.eventSink(MediaViewerEvents.SaveOnDisk)
|
||||||
|
},
|
||||||
onDelete = { eventId ->
|
onDelete = { eventId ->
|
||||||
state.eventSink(MediaViewerEvents.ConfirmDelete(eventId))
|
state.eventSink(MediaViewerEvents.ConfirmDelete(eventId))
|
||||||
},
|
},
|
||||||
|
|
@ -313,8 +317,6 @@ private fun rememberShowProgress(downloadedMedia: AsyncData<LocalMedia>): Boolea
|
||||||
@Composable
|
@Composable
|
||||||
private fun MediaViewerTopBar(
|
private fun MediaViewerTopBar(
|
||||||
actionsEnabled: Boolean,
|
actionsEnabled: Boolean,
|
||||||
canDownload: Boolean,
|
|
||||||
canShare: Boolean,
|
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
senderName: String?,
|
senderName: String?,
|
||||||
dateSent: String?,
|
dateSent: String?,
|
||||||
|
|
@ -348,19 +350,6 @@ private fun MediaViewerTopBar(
|
||||||
),
|
),
|
||||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||||
actions = {
|
actions = {
|
||||||
if (canShare) {
|
|
||||||
IconButton(
|
|
||||||
enabled = actionsEnabled,
|
|
||||||
onClick = {
|
|
||||||
eventSink(MediaViewerEvents.Share)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = CompoundIcons.ShareAndroid(),
|
|
||||||
contentDescription = stringResource(id = CommonStrings.action_share)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = actionsEnabled,
|
enabled = actionsEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -378,19 +367,6 @@ private fun MediaViewerTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (canDownload) {
|
|
||||||
IconButton(
|
|
||||||
enabled = actionsEnabled,
|
|
||||||
onClick = {
|
|
||||||
eventSink(MediaViewerEvents.SaveOnDisk)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = CompoundIcons.Download(),
|
|
||||||
contentDescription = stringResource(id = CommonStrings.action_save),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (canShowInfo) {
|
if (canShowInfo) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onInfoClick,
|
onClick = onInfoClick,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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.details
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MediaDeleteConfirmationBottomSheetTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Cancel invokes expected callback`() {
|
||||||
|
val state = aMediaDeleteConfirmationState()
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
rule.setMediaDeleteConfirmationBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onDismiss = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Remove invokes expected callback`() {
|
||||||
|
val state = aMediaDeleteConfirmationState()
|
||||||
|
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||||
|
rule.setMediaDeleteConfirmationBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onDelete = callback,
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists()
|
||||||
|
rule.clickOn(CommonStrings.action_remove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMediaDeleteConfirmationBottomSheet(
|
||||||
|
state: MediaBottomSheetState.MediaDeleteConfirmationState,
|
||||||
|
onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onDismiss: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
MediaDeleteConfirmationBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onDelete = onDelete,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* 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.details
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MediaDetailsBottomSheetTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on View in timeline invokes expected callback`() {
|
||||||
|
val state = aMediaDetailsBottomSheetState()
|
||||||
|
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||||
|
rule.setMediaDetailsBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onViewInTimeline = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_view_in_timeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Share invokes expected callback`() {
|
||||||
|
val state = aMediaDetailsBottomSheetState()
|
||||||
|
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||||
|
rule.setMediaDetailsBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onShare = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_share)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Save invokes expected callback`() {
|
||||||
|
val state = aMediaDetailsBottomSheetState()
|
||||||
|
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||||
|
rule.setMediaDetailsBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onDownload = callback,
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_save)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking on Remove invokes expected callback`() {
|
||||||
|
val state = aMediaDetailsBottomSheetState()
|
||||||
|
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||||
|
rule.setMediaDetailsBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onDelete = callback,
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists()
|
||||||
|
rule.clickOn(CommonStrings.action_remove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `Remove is not present if canDelete is false`() {
|
||||||
|
val state = aMediaDetailsBottomSheetState(
|
||||||
|
canDelete = false,
|
||||||
|
)
|
||||||
|
rule.setMediaDetailsBottomSheet(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMediaDetailsBottomSheet(
|
||||||
|
state: MediaBottomSheetState.MediaDetailsBottomSheetState,
|
||||||
|
onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onDownload: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onDismiss: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
MediaDetailsBottomSheet(
|
||||||
|
state = state,
|
||||||
|
onViewInTimeline = onViewInTimeline,
|
||||||
|
onShare = onShare,
|
||||||
|
onDownload = onDownload,
|
||||||
|
onDelete = onDelete,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,8 +66,6 @@ class MediaViewerPresenterTest {
|
||||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
assertThat(initialState.canShowInfo).isTrue()
|
assertThat(initialState.canShowInfo).isTrue()
|
||||||
assertThat(initialState.canDownload).isTrue()
|
|
||||||
assertThat(initialState.canShare).isTrue()
|
|
||||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,48 +84,6 @@ class MediaViewerPresenterTest {
|
||||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
assertThat(initialState.canShowInfo).isFalse()
|
assertThat(initialState.canShowInfo).isFalse()
|
||||||
assertThat(initialState.canDownload).isTrue()
|
|
||||||
assertThat(initialState.canShare).isTrue()
|
|
||||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state cannot share`() = runTest {
|
|
||||||
val presenter = createMediaViewerPresenter(
|
|
||||||
canShare = false,
|
|
||||||
room = FakeMatrixRoom(
|
|
||||||
canRedactOwnResult = { Result.success(true) },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
skipItems(2)
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
|
||||||
assertThat(initialState.canShowInfo).isTrue()
|
|
||||||
assertThat(initialState.canDownload).isTrue()
|
|
||||||
assertThat(initialState.canShare).isFalse()
|
|
||||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state cannot download`() = runTest {
|
|
||||||
val presenter = createMediaViewerPresenter(
|
|
||||||
canDownload = false,
|
|
||||||
room = FakeMatrixRoom(
|
|
||||||
canRedactOwnResult = { Result.success(true) },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
skipItems(2)
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
|
||||||
assertThat(initialState.canShowInfo).isTrue()
|
|
||||||
assertThat(initialState.canDownload).isFalse()
|
|
||||||
assertThat(initialState.canShare).isTrue()
|
|
||||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -146,8 +102,6 @@ class MediaViewerPresenterTest {
|
||||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
assertThat(initialState.canShowInfo).isTrue()
|
assertThat(initialState.canShowInfo).isTrue()
|
||||||
assertThat(initialState.canDownload).isTrue()
|
|
||||||
assertThat(initialState.canShare).isTrue()
|
|
||||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,8 +121,6 @@ class MediaViewerPresenterTest {
|
||||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
assertThat(initialState.canShowInfo).isTrue()
|
assertThat(initialState.canShowInfo).isTrue()
|
||||||
assertThat(initialState.canDownload).isTrue()
|
|
||||||
assertThat(initialState.canShare).isTrue()
|
|
||||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -350,8 +302,6 @@ class MediaViewerPresenterTest {
|
||||||
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
|
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
|
||||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
canShowInfo: Boolean = true,
|
canShowInfo: Boolean = true,
|
||||||
canShare: Boolean = true,
|
|
||||||
canDownload: Boolean = true,
|
|
||||||
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
|
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
|
||||||
room: MatrixRoom = FakeMatrixRoom(
|
room: MatrixRoom = FakeMatrixRoom(
|
||||||
liveTimeline = FakeTimeline(),
|
liveTimeline = FakeTimeline(),
|
||||||
|
|
@ -364,8 +314,6 @@ class MediaViewerPresenterTest {
|
||||||
mediaSource = aMediaSource(),
|
mediaSource = aMediaSource(),
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
canShowInfo = canShowInfo,
|
canShowInfo = canShowInfo,
|
||||||
canShare = canShare,
|
|
||||||
canDownload = canDownload,
|
|
||||||
),
|
),
|
||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
mediaLoader = matrixMediaLoader,
|
mediaLoader = matrixMediaLoader,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||||
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.details.aMediaDetailsBottomSheetState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
|
@ -54,16 +55,6 @@ class MediaViewerViewTest {
|
||||||
testMenuAction(CommonStrings.action_open_with, MediaViewerEvents.OpenWith)
|
testMenuAction(CommonStrings.action_open_with, MediaViewerEvents.OpenWith)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on save emit expected Event`() {
|
|
||||||
testMenuAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on share emit expected Event`() {
|
|
||||||
testMenuAction(CommonStrings.action_share, MediaViewerEvents.Share)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
|
private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
|
||||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||||
rule.setMediaViewerView(
|
rule.setMediaViewerView(
|
||||||
|
|
@ -80,6 +71,32 @@ class MediaViewerViewTest {
|
||||||
eventsRecorder.assertSingle(expectedEvent)
|
eventsRecorder.assertSingle(expectedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on save emit expected Event`() {
|
||||||
|
testBottomSheetAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on share emit expected Event`() {
|
||||||
|
testBottomSheetAction(CommonStrings.action_share, MediaViewerEvents.Share)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testBottomSheetAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
|
||||||
|
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||||
|
rule.setMediaViewerView(
|
||||||
|
aMediaViewerState(
|
||||||
|
downloadedMedia = AsyncData.Success(
|
||||||
|
LocalMedia(Uri.EMPTY, anImageMediaInfo())
|
||||||
|
),
|
||||||
|
mediaInfo = anImageMediaInfo(),
|
||||||
|
mediaBottomSheetState = aMediaDetailsBottomSheetState(),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(contentDescriptionRes)
|
||||||
|
eventsRecorder.assertSingle(expectedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on image hides the overlay`() {
|
fun `clicking on image hides the overlay`() {
|
||||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2c7ba307cf21056623bf35c8558809fdbc6deaacf4e9365a99ffcf829e8d9188
|
oid sha256:4cded4f64be360fbd6ba607f9303e17154da24220712cf2e8da2d495b50bda26
|
||||||
size 34477
|
size 38103
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:724bff130c547e7f0065ccb4c1b3319162ded2e6c1c1db666f4e08e01289a5a0
|
oid sha256:de4cec2f60dda00375c6583fb2926cc0fdfa02d4673bd3d99bbe0ca3a2193952
|
||||||
size 32776
|
size 36454
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:70f93330adb987d6f98d654670ab0898957d765ad3d92a47c9a1c781b24f9059
|
oid sha256:00159ed8d968d53970e4a3b7f82ab542fb5eabfc4513dd68d0eef05f0615373e
|
||||||
size 5317
|
size 7290
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3ffac5911f928411fa0ac94e9ac59f6b8bb8bce1016e06f348d946f9d10053e5
|
oid sha256:efb12b63fde67256b255503d00848d64480e142c54619329b75aeed451a3dd17
|
||||||
size 4539
|
size 6375
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e0ee87589ec0e4f7cf67775dfa69cd289aeb27f22087bd91d54102923a28557d
|
oid sha256:0f6835bd79d18d202c1d21b00a1afa4fa2c7cbfaf8f586a1dd1f48afdd5f69e5
|
||||||
size 4775
|
size 7644
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6504e09eb09a9e28bc70594e669ec87abd290b4fe20e2ee9b3588c2116a049cd
|
oid sha256:452afc2e04191eb82de772597ee97987eda5667ff56ecb684bb3b9e0bef90435
|
||||||
size 3994
|
size 6737
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9d0b3f44a9a0ed9ab16192b23ccf95b7d34abccd025bb4a7ccd8ebb7a9379965
|
oid sha256:4444ea352a367bb8617e9be4c86368b0a2292916a20a0c166933b219617e6055
|
||||||
size 10824
|
size 8502
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:60f281d970321442e39b350f4f697c7ecfc9bc32000cd19676ea7ed6468ee63a
|
oid sha256:eb103b365f83667834ccf8e6a181ab59d1c9dcbecf6fd4eb7f16bb236444b4e0
|
||||||
size 11411
|
size 9087
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:25386b28846ae826701c9e53530cdd1e5fda4d0899673394bf6d81dac1ca751f
|
oid sha256:0ce431cefbd22d8232af06626336e4c3baccbf9ad32e88f662efab66c7640b0d
|
||||||
size 11057
|
size 8737
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:740923d3e740dc98a5d6890f8f8dfb5810606d99e81a4e6597078566194f076d
|
oid sha256:d2a9292c525e7fe4d72362f2fe04a9f1184c7dff951f3b517e55f6f369214324
|
||||||
size 11290
|
size 8992
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e18741850e92314b40da89c9e1cafd1795397960151f7bb4e221ae3866d25f9d
|
oid sha256:d10b446f168e8a1295c811c57a08897db6636935646cd6c314eb7b6b7e310d5c
|
||||||
size 11497
|
size 9184
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:73fecb12c33892a9bea41e5a4bbbf4db43990e44b2a36b96d03d7a81046c8f92
|
oid sha256:44b37f78992762569431bed9330140037b5fe9e49bb0043e2bb94cdfc2b53844
|
||||||
size 10076
|
size 7989
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19c209ec0dca3c3b722af63dcd33444d1074749afd47a8364da4a7a071fce8aa
|
oid sha256:54f3371f7003d7fe40dea4968037fbd4e148449e431356e693c7951a2814dab6
|
||||||
size 10680
|
size 8592
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dabd54db78ae93b72cf9ee31fc4b153fe2ccf64869d861fd16995e16494f4d67
|
oid sha256:bbe8efde3b6f6f13558eb323b7774640a269c66a5fe4b0c44e5b60fc6bccf4c6
|
||||||
size 10423
|
size 8329
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5ea12dad2ab5c70aa30bc506343f0fce05e08a2940460ec595216d07a244fc84
|
oid sha256:e3935b49178db454d1cd20270a1641d380f5f3e576548d74e194fe7619bff4c8
|
||||||
size 10583
|
size 8492
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:470f7d0a1ceedcc0f9ade603c590101da8231b24745283976b3528a84e72d721
|
oid sha256:0796d9962b93631f27338f9e93b9f7812964cd830cc1d62903829fd8012865cf
|
||||||
size 10743
|
size 8653
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8f09ea644568c7ab55f0cfc3b8148d8e7821313852cc3fcd78dca2b473a07555
|
oid sha256:b98131ca183aaa3fd550f2b78317d73fb63ad03122c298d56d451d4023fd61d3
|
||||||
size 10871
|
size 8548
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a45e80573b7db0c47cbe9048c1bb95d66e4b8437dd4816cf6cb3548305549715
|
oid sha256:e0fb23e74fded45ea3d56cd69c2675efa2c0c0190b17bc8b4cc3ffbb2715650e
|
||||||
size 13017
|
size 10772
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:339a6ae86094fb3d0b99d6ea3b23bed14adf7bbe90bc55613d6ece3356f7be16
|
oid sha256:a52ddb20b4f07ecdfc23f5d19a26832d8d61cbc25159ac2868e9ff56d844e755
|
||||||
size 38566
|
size 36271
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19b6feb0228df79d7f6ec505a8b073852e4400366027d93feb61c09186494ac6
|
oid sha256:7a52b93594d74e66ac4c5145254feb4a5b82941783defd0e8820c6acc21cac97
|
||||||
size 9228
|
size 6900
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b7fdc491e73aa3fe1afb52ac6a22b3ced09c73573dc175c415d116293c04c95d
|
oid sha256:3d11986925680f2b7a9827858396e85ad13792b84412d01f5589a32f1a48c631
|
||||||
size 10129
|
size 8038
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f816378db978ff00cd722fd44b8219b6181d9cc6fbf4d3b31fea33430e37580e
|
oid sha256:d440f08f846bec01f07d10ab2347dd5e863ae594109038dc9e6e4e40fe9528d0
|
||||||
size 12309
|
size 10239
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a0b97a65622307f289695d3a19aa0957fe739d35070b1f0cd972f7ff51987a7d
|
oid sha256:ee9ef033060816da98e228abfeea3d09c2afc8822b884f1d656646dc3d15ce02
|
||||||
size 36824
|
size 34685
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8b71ca3fcc1f94d89264f3d5c5751f46681014b662bcf7d910263dfb23178e0f
|
oid sha256:1957dbe8c580b048fc32877639d42e3761b10a298dc80804634481ef391dcf76
|
||||||
size 8623
|
size 6544
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:29d55f260237060e5ac280d6c87f5eefbdaa9dc6710572cab38fbd41dea77090
|
oid sha256:1853de49049bcd1448cc4e7c4a38ed8ab3cf11c3d11b1ddf296f2b6a37e985b4
|
||||||
size 15465
|
size 15428
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:925df53e0666d800b0f047f45abda50f1744ef1571594be50efd6008ed988b72
|
oid sha256:d90abb10774208a842a3284346c3fa1d3c8e34b53daf6ea0f14f61bf4be5bb87
|
||||||
size 14549
|
size 14551
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:29d55f260237060e5ac280d6c87f5eefbdaa9dc6710572cab38fbd41dea77090
|
oid sha256:1853de49049bcd1448cc4e7c4a38ed8ab3cf11c3d11b1ddf296f2b6a37e985b4
|
||||||
size 15465
|
size 15428
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:08b47d8b631032d7cb33cdd150eab67e4dc9e6498813e0f5107007c30143d249
|
oid sha256:e74c3ab733e969059c4d8bb72903ab53aeb855f49857656bfe61f4dee574dbe4
|
||||||
size 26058
|
size 70317
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:873e124ea0560fe577b93b5e55e959baf443eedcb07d6b229f0370621ee0dbf3
|
oid sha256:4ee8d9e829efc0723d62411e2069a8dac90b18069d1d8cd5dc5ec6a5b9899a14
|
||||||
size 20621
|
size 21572
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:88c68143c2bc7b098664d881bb6c0f2c35ac10ec690c0c7eb7ae6d9366144e83
|
oid sha256:7d1cdeba30c6efca096c3625613ce5ad33b6f09e08eb09b2758d9a039e4206fb
|
||||||
size 15136
|
size 15106
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:88c68143c2bc7b098664d881bb6c0f2c35ac10ec690c0c7eb7ae6d9366144e83
|
oid sha256:7d1cdeba30c6efca096c3625613ce5ad33b6f09e08eb09b2758d9a039e4206fb
|
||||||
size 15136
|
size 15106
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d2d9ef7383d17436738abf01c450ad189ebffebcfbee12de253fdc92f6feda93
|
oid sha256:cbcf086763463eaa1dbf9cb52620c430f7a7982f01d3abcd039ebd307544f8e7
|
||||||
size 28593
|
size 72986
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c8b23c6c8f47f7e49cdb56098408873aa9d3f02af22cae01add580f0e623678d
|
oid sha256:ff96b22724d7f82b3003a73a560da0a34c9c196757b4336706b5823bbfa32589
|
||||||
size 34814
|
size 32398
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:25ebb7460550b31bf2cebfca7bdab32e5f89e327b68f6687bf490e5d14cb9220
|
oid sha256:492b5ae698dc52672d8d0a4599c9cd9a5b6f414e8a0a6f42c91e765e5a5b221d
|
||||||
size 40950
|
size 40921
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fff7687206e1b1c03ecc4da231e8d030ec730573070550f1d6a7c355ba0c90d2
|
oid sha256:782fa9e5501e399d4840c0aab6ee317aa4fa8137eab93ee85924ec512b071be1
|
||||||
size 14525
|
size 14525
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e379008ac3a5346b727bd9355a4a59b75a1a2eca8855ad4ef5b6d7c239da129b
|
oid sha256:739e2618bac9233b0ff7335d734d7fb594e3ee8860f9e61ef80d2dc4d7736a27
|
||||||
size 15076
|
size 15026
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d3fe3f3e7668a3d529132172366622db970391dcc8718f87a7fc90ec67b93ed1
|
oid sha256:49e6a6bda914fc5e77bd0a864900f4fd7f654f4017a331be6008825b2150340d
|
||||||
size 13992
|
size 14013
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e379008ac3a5346b727bd9355a4a59b75a1a2eca8855ad4ef5b6d7c239da129b
|
oid sha256:739e2618bac9233b0ff7335d734d7fb594e3ee8860f9e61ef80d2dc4d7736a27
|
||||||
size 15076
|
size 15026
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a99c2ae96a72551e0da2e6e3cce08a76fffc2f99763c81d5c3fe65a9604bbdc7
|
oid sha256:9833ad112df471f8be9587999f444ad371ae1eebadfc351cea83c6db5685c9ad
|
||||||
size 25564
|
size 62028
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f7c98ab469dd76beea196cfceff2ea9f72c9225f15b701ab7965b54cc9064dd8
|
oid sha256:fcb5bc041286ba863ae982b2ad03873a76e48ed6ebd5d35c82dea269d86363a7
|
||||||
size 19505
|
size 21189
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d414cb2e4268815943ce6d78fe8d85f576e055bd8ff4f652b7e15a664e3d8cb3
|
oid sha256:8be4c40bcec9cb84bd087dc395be694b614f88ff5b44a33dafc3ad87576d23c6
|
||||||
size 14597
|
size 14578
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d414cb2e4268815943ce6d78fe8d85f576e055bd8ff4f652b7e15a664e3d8cb3
|
oid sha256:8be4c40bcec9cb84bd087dc395be694b614f88ff5b44a33dafc3ad87576d23c6
|
||||||
size 14597
|
size 14578
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bfe8f753f87d3b67ad3ffde246dba0438120bb2a28efe033cbfd2204664fee95
|
oid sha256:267cc528f2fdaba66bfad4f8c8622087b76c2e3409f5fda8ce25009039278a22
|
||||||
size 27633
|
size 64356
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e361247dd137a828f1170fc1497ace365e04bb551edcd4984c26b15c26fe65f1
|
oid sha256:7f5831741183467f1d05517097f2617aee405a9d6752cdf8a8e193e5851376a3
|
||||||
size 33011
|
size 30904
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:39ecc9a50285df492417f5f22adcc391be2dcad0cc388efb756274c83aba077d
|
oid sha256:de98370531bc9342539bbf98b6f3534b72e327a94e34b1c6d827e2330291340c
|
||||||
size 39030
|
size 39235
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bd4903a8a383c5fe3c8c5bfeaf26ab16d3c785fccaab5b66de8b31f3380e9272
|
oid sha256:c08080c2814f8e8273949b39359ad105f0305ee6c7b91ddf9b437ce925489b40
|
||||||
size 14104
|
size 14125
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:043da4a779363f5162b1fbb6b1159ab3ae3f6a1635473146a5b73242525b5e53
|
oid sha256:66437179fb0b851d4d4d647d00cab94cc7422d625f559839c675b378dbf1af38
|
||||||
size 390373
|
size 389408
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2d5f183f53f9e8d0dbbae473f2f853f4372dbff15b1d6ea17e78b4770781fa34
|
oid sha256:8165bcb4b0d52a227aad4e1f3951fc3628ef647947ef2584ed50d8ede8a6a344
|
||||||
size 35468
|
size 38248
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:514f67acd325e01d010466786eb85e7db8071c36e2f21454be8f30c4a6a57425
|
oid sha256:c9efb4ed1ee82bba30351bf213f0873637e8194140a3bbea669321bf76bc6483
|
||||||
size 32356
|
size 31449
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8a6a599e68f0955d84ea737603f0db83be412433691f7b7b3729a01999808830
|
oid sha256:5d2882d79b9f66726c6d16c8c6fc84cb9f65a5674222fe85794229dc5ba12a6b
|
||||||
size 25827
|
size 24679
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a2c00187eb25b297f7debb7424969e5535e48366d139b7f073b6a7628f155d60
|
oid sha256:da172fdf40dc8702bc6dcb89bcc75e93bd279f6bbb9454f5283febe4da25d399
|
||||||
size 390395
|
size 389440
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:94520145bde5de6a0d7820dd44d7f0243d9fb06eca062b19b72cb2457abdfb7d
|
oid sha256:bb79e754f9b4caeb40508bdc067d68a4e115e8a50467fc006be6f5db0684ea5b
|
||||||
size 95438
|
size 94672
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:82710704b6daff1c1e12a4a3c782f68744c0d6b3ee30b3fa1e38739176c6eaef
|
oid sha256:7ad6e45382dec9bb27593b6e2ed92ed633204479040ccebaf4d362bb2f41fec7
|
||||||
size 397724
|
size 396403
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1547883dfc7ae3d742d2da2f6e5c6c2d6182d6039b3ce6075dbbe1d24f4ba341
|
oid sha256:77913c010877d13d82196182d32e19168e77102d2f245ab321c2224a7108768a
|
||||||
size 23370
|
size 21874
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:66add7cb3b696075b7e931b06d1d8472cfddc2fc1902081864c3d88754f7404a
|
oid sha256:69b5d7572ae6e4ff084867fac1bae41a55c75c9a5236cb6ccb4c31b89ef77898
|
||||||
size 6702
|
size 5442
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a86ec4a40a63f646e62b1c6a3e481f0b68be442923e39b6750836ae4e5ac3045
|
oid sha256:f8d3e3f8733424870b254be90599ed1ff6ba784089600bcb200fbef62c81537c
|
||||||
size 15577
|
size 14562
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0abf1fdae34ea898f817f9667a02adb551d5e3bb528f73b4c2c01826f4ca375a
|
oid sha256:d1785d90957316791969f047e66bd779da62d675004914099f2af2b69bebe405
|
||||||
size 15881
|
size 14700
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c7642e9e20a551e59f4529c52fa7fbe5b3f4dfcb8c26caeb716ccb2bdfc63dde
|
oid sha256:1ddcd8e9e20de4171a3d9f8175806a268723e47a14dca431849c2c29edaf5d0b
|
||||||
size 27176
|
size 26267
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1fd53c24dd38a12b4ceefa54e1d6d096d8f443d1cdd1d1f1cc71f92c1d603a51
|
oid sha256:035ef0079af6e9825a52b86e2eab50667404a66d70dd2756596b60cc1cea376a
|
||||||
size 27419
|
size 26404
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue