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