Merge pull request #2438 from element-hq/feature/fga/improve_media_viewer

[Improvement] MediaViewer/Attachment experience
This commit is contained in:
ganfra 2024-02-26 10:39:02 +01:00 committed by GitHub
commit 3213847a5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 330 additions and 138 deletions

1
changelog.d/2390.feature Normal file
View file

@ -0,0 +1 @@
MediaViewer : introduce fullscreen and flick to dismiss behavior.

View file

@ -16,11 +16,12 @@
package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -28,6 +29,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -42,7 +44,10 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
@Composable
fun AttachmentsPreviewView(
@ -66,16 +71,11 @@ fun AttachmentsPreviewView(
}
Scaffold(modifier) {
Box(
modifier = Modifier.padding(it),
contentAlignment = Alignment.Center
) {
AttachmentPreviewContent(
attachment = state.attachment,
onSendClicked = ::postSendAttachment,
onDismiss = onDismiss
)
}
AttachmentPreviewContent(
attachment = state.attachment,
onSendClicked = ::postSendAttachment,
onDismiss = onDismiss
)
}
AttachmentSendStateView(
sendActionState = state.sendActionState,
@ -119,21 +119,30 @@ private fun AttachmentPreviewContent(
onSendClicked: () -> Unit,
onDismiss: () -> Unit,
) {
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 24.dp)
.navigationBarsPadding(),
contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when (attachment) {
is Attachment.Media -> LocalMediaView(
localMedia = attachment.localMedia
)
is Attachment.Media -> {
val localMediaViewState = rememberLocalMediaViewState(
zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false)
)
)
LocalMediaView(
modifier = Modifier.fillMaxSize(),
localMedia = attachment.localMedia,
localMediaViewState = localMediaViewState,
onClick = {}
)
}
}
}
AttachmentsPreviewBottomActions(
@ -141,8 +150,9 @@ private fun AttachmentPreviewContent(
onSendClicked = onSendClicked,
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 120.dp)
.padding(all = 24.dp)
.background(Color.Black.copy(alpha = 0.7f))
.padding(horizontal = 24.dp)
.defaultMinSize(minHeight = 80.dp)
)
}
}
@ -153,9 +163,7 @@ private fun AttachmentsPreviewBottomActions(
onSendClicked: () -> Unit,
modifier: Modifier = Modifier
) {
ButtonRowMolecule(
modifier = modifier,
) {
ButtonRowMolecule(modifier = modifier) {
TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClicked)
TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClicked)
}

View file

@ -39,6 +39,7 @@ showkase = "1.0.2"
appyx = "1.4.0"
sqldelight = "2.0.1"
wysiwyg = "2.29.0"
telephoto = "0.8.0"
# DI
dagger = "2.50"
@ -163,7 +164,8 @@ sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
vanniktech_blurhash = "com.vanniktech:blurhash:0.2.0"
telephoto_zoomableimage = "me.saket.telephoto:zoomable-image-coil:0.8.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.1"
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2"

View file

@ -39,6 +39,7 @@ dependencies {
implementation(libs.dagger)
implementation(libs.telephoto.zoomableimage)
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.flick)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)

View file

@ -18,14 +18,16 @@ package io.element.android.libraries.mediaviewer.api.local
import android.annotation.SuppressLint
import android.net.Uri
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -35,7 +37,10 @@ import androidx.compose.material.icons.outlined.GraphicEq
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -72,48 +77,45 @@ import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWra
import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer
import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState
import io.element.android.libraries.ui.strings.CommonStrings
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
import me.saket.telephoto.zoomable.rememberZoomableState
@SuppressLint("UnsafeOptInUsageError")
@Composable
fun LocalMediaView(
localMedia: LocalMedia?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
mediaInfo: MediaInfo? = localMedia?.info,
) {
val zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 5f)
)
val mimeType = mediaInfo?.mimeType
when {
mimeType.isMimeTypeImage() -> MediaImageView(
localMediaViewState = localMediaViewState,
localMedia = localMedia,
zoomableState = zoomableState,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
mimeType.isMimeTypeVideo() -> MediaVideoView(
localMediaViewState = localMediaViewState,
localMedia = localMedia,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
mimeType == MimeTypes.Pdf -> MediaPDFView(
localMediaViewState = localMediaViewState,
localMedia = localMedia,
zoomableState = zoomableState,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
// TODO handle audio with exoplayer
else -> MediaFileView(
localMediaViewState = localMediaViewState,
uri = localMedia?.uri,
info = mediaInfo,
modifier = modifier
modifier = modifier,
onClick = onClick,
)
}
}
@ -122,24 +124,25 @@ fun LocalMediaView(
private fun MediaImageView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?,
zoomableState: ZoomableState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalInspectionMode.current) {
Image(
painter = painterResource(id = CommonDrawables.sample_background),
modifier = modifier.fillMaxSize(),
modifier = modifier,
contentDescription = null,
)
} else {
val zoomableImageState = rememberZoomableImageState(zoomableState)
val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState)
localMediaViewState.isReady = zoomableImageState.isImageDisplayed
ZoomableAsyncImage(
modifier = modifier.fillMaxSize(),
modifier = modifier,
state = zoomableImageState,
model = localMedia?.uri,
contentDescription = stringResource(id = CommonStrings.common_image),
contentScale = ContentScale.Fit,
onClick = { onClick() }
)
}
}
@ -149,8 +152,14 @@ private fun MediaImageView(
private fun MediaVideoView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
var playableState: PlayableState.Playable by remember {
mutableStateOf(PlayableState.Playable(isPlaying = false, isShowingControls = false))
}
localMediaViewState.playableState = playableState
val context = LocalContext.current
val playerListener = object : Player.Listener {
override fun onRenderedFirstFrame() {
@ -158,7 +167,7 @@ private fun MediaVideoView(
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
localMediaViewState.isPlaying = isPlaying
playableState = playableState.copy(isPlaying = isPlaying)
}
}
val exoPlayer = remember {
@ -176,19 +185,34 @@ private fun MediaVideoView(
} else {
exoPlayer.setMediaItems(emptyList())
}
KeepScreenOn(localMediaViewState.isPlaying)
KeepScreenOn(playableState.isPlaying)
AndroidView(
factory = {
PlayerView(context).apply {
player = exoPlayer
setShowPreviousButton(false)
setShowNextButton(false)
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
controllerShowTimeoutMs = 3000
setOnClickListener {
onClick()
}
setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { visibility ->
val isShowingControls = visibility == View.VISIBLE
playableState = playableState.copy(isShowingControls = isShowingControls)
})
controllerShowTimeoutMs = 1500
setShowPreviousButton(false)
setShowFastForwardButton(false)
setShowRewindButton(false)
setShowNextButton(false)
showController()
}
},
modifier = modifier.fillMaxSize()
onRelease = { playerView ->
playerView.setOnClickListener(null)
playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?)
playerView.player = null
},
modifier = modifier
)
OnLifecycleEvent { _, event ->
@ -208,15 +232,19 @@ private fun MediaVideoView(
private fun MediaPDFView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?,
zoomableState: ZoomableState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val pdfViewerState = rememberPdfViewerState(
model = localMedia?.uri,
zoomableState = zoomableState
zoomableState = localMediaViewState.zoomableState,
)
localMediaViewState.isReady = pdfViewerState.isLoaded
PdfViewer(pdfViewerState = pdfViewerState, modifier = modifier)
PdfViewer(
pdfViewerState = pdfViewerState,
onClick = onClick,
modifier = modifier,
)
}
@Composable
@ -224,11 +252,23 @@ private fun MediaFileView(
localMediaViewState: LocalMediaViewState,
uri: Uri?,
info: MediaInfo?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val isAudio = info?.mimeType.isMimeTypeAudio().orFalse()
localMediaViewState.isReady = uri != null
Box(modifier = modifier.padding(horizontal = 8.dp), contentAlignment = Alignment.Center) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = modifier
.padding(horizontal = 8.dp)
.clickable(
onClick = onClick,
interactionSource = interactionSource,
indication = null
),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
modifier = Modifier

View file

@ -17,21 +17,35 @@
package io.element.android.libraries.mediaviewer.api.local
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.rememberZoomableState
@Stable
class LocalMediaViewState {
class LocalMediaViewState internal constructor(
val zoomableState: ZoomableState,
) {
var isReady: Boolean by mutableStateOf(false)
var isPlaying: Boolean by mutableStateOf(false)
var playableState: PlayableState by mutableStateOf(PlayableState.NotPlayable)
}
@Immutable
sealed interface PlayableState {
data object NotPlayable : PlayableState
data class Playable(
val isPlaying: Boolean,
val isShowingControls: Boolean
) : PlayableState
}
@Composable
fun rememberLocalMediaViewState(): LocalMediaViewState {
return remember {
LocalMediaViewState()
fun rememberLocalMediaViewState(zoomableState: ZoomableState = rememberZoomableState()): LocalMediaViewState {
return remember(zoomableState) {
LocalMediaViewState(zoomableState)
}
}

View file

@ -21,6 +21,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -47,10 +48,15 @@ import me.saket.telephoto.zoomable.zoomable
@Composable
fun PdfViewer(
pdfViewerState: PdfViewerState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(
modifier = modifier.zoomable(pdfViewerState.zoomableState),
modifier = modifier
.zoomable(
state = pdfViewerState.zoomableState,
onClick = { onClick() }
),
contentAlignment = Alignment.Center
) {
val maxWidthInPx = maxWidth.roundToPx()
@ -61,7 +67,10 @@ fun PdfViewer(
}
}
val pdfPages = pdfViewerState.getPages()
PdfPagesView(pdfPages.toImmutableList(), pdfViewerState.lazyListState)
PdfPagesView(
pdfPages = pdfPages.toImmutableList(),
lazyListState = pdfViewerState.lazyListState,
)
}
}
@ -74,8 +83,12 @@ private fun PdfPagesView(
LazyColumn(
modifier = modifier.fillMaxSize(),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically)
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
) {
// Add a fake item to the top so that the first item is not at the top of the screen.
item {
Spacer(modifier = Modifier.height(80.dp))
}
items(pdfPages.size) { index ->
val pdfPage = pdfPages[index]
PdfPageView(pdfPage)

View file

@ -19,34 +19,36 @@
package io.element.android.libraries.mediaviewer.api.viewer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.mimetype.MimeTypes
@ -65,15 +67,64 @@ import io.element.android.libraries.mediaviewer.api.R
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.PlayableState
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.delay
import me.saket.telephoto.flick.FlickToDismiss
import me.saket.telephoto.flick.FlickToDismissState
import me.saket.telephoto.flick.rememberFlickToDismissState
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
import me.saket.telephoto.zoomable.rememberZoomableState
import kotlin.time.Duration
@Composable
fun MediaViewerView(
state: MediaViewerState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
var showOverlay by remember { mutableStateOf(true) }
Scaffold(
modifier,
containerColor = Color.Transparent,
snackbarHost = { SnackbarHost(snackbarHostState) },
) {
MediaViewerPage(
showOverlay = showOverlay,
state = state,
onDismiss = {
onBackPressed()
},
onShowOverlayChanged = {
showOverlay = it
}
)
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is AsyncData.Success,
mimeType = state.mediaInfo.mimeType,
onBackPressed = onBackPressed,
canDownload = state.canDownload,
canShare = state.canShare,
eventSink = state.eventSink
)
}
}
}
@Composable
private fun MediaViewerPage(
showOverlay: Boolean,
state: MediaViewerState,
onDismiss: () -> Unit,
onShowOverlayChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
fun onRetry() {
state.eventSink(MediaViewerEvents.RetryLoading)
@ -83,65 +134,107 @@ fun MediaViewerView(
state.eventSink(MediaViewerEvents.ClearLoadingError)
}
val localMediaViewState = rememberLocalMediaViewState()
val showThumbnail = !localMediaViewState.isReady
val showProgress = rememberShowProgress(state.downloadedMedia)
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val currentShowOverlay by rememberUpdatedState(showOverlay)
val currentOnShowOverlayChanged by rememberUpdatedState(onShowOverlayChanged)
val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false)
Scaffold(
modifier,
topBar = {
MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is AsyncData.Success,
mimeType = state.mediaInfo.mimeType,
onBackPressed = onBackPressed,
canDownload = state.canDownload,
canShare = state.canShare,
eventSink = state.eventSink
)
DismissFlickEffects(
flickState = flickState,
onDismissing = { animationDuration ->
delay(animationDuration / 3)
onDismiss()
},
snackbarHost = { SnackbarHost(snackbarHostState) },
onDragging = {
currentOnShowOverlayChanged(false)
}
)
FlickToDismiss(
state = flickState,
modifier = modifier.background(backgroundColorFor(flickState))
) {
Column(
val showProgress = rememberShowProgress(state.downloadedMedia)
Box(
modifier = Modifier
.fillMaxSize()
.padding(it),
.navigationBarsPadding()
) {
if (showProgress) {
LinearProgressIndicator(
Modifier
.fillMaxWidth()
.height(2.dp)
Box(contentAlignment = Alignment.Center) {
val zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false)
)
} else {
Spacer(Modifier.height(2.dp))
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (state.downloadedMedia is AsyncData.Failure) {
val localMediaViewState = rememberLocalMediaViewState(zoomableState)
val showThumbnail = !localMediaViewState.isReady
val playableState = localMediaViewState.playableState
val showError = state.downloadedMedia is AsyncData.Failure
LaunchedEffect(playableState) {
if (playableState is PlayableState.Playable) {
currentOnShowOverlayChanged(playableState.isShowingControls)
}
}
LocalMediaView(
modifier = Modifier.fillMaxSize(),
localMediaViewState = localMediaViewState,
localMedia = state.downloadedMedia.dataOrNull(),
mediaInfo = state.mediaInfo,
onClick = {
if (playableState is PlayableState.NotPlayable) {
currentOnShowOverlayChanged(!currentShowOverlay)
}
},
)
ThumbnailView(
mediaInfo = state.mediaInfo,
thumbnailSource = state.thumbnailSource,
isVisible = showThumbnail,
zoomableState = zoomableState
)
if (showError) {
ErrorView(
errorMessage = stringResource(id = CommonStrings.error_unknown),
onRetry = ::onRetry,
onDismiss = ::onDismissError
)
}
LocalMediaView(
localMediaViewState = localMediaViewState,
localMedia = state.downloadedMedia.dataOrNull(),
mediaInfo = state.mediaInfo,
)
ThumbnailView(
mediaInfo = state.mediaInfo,
thumbnailSource = state.thumbnailSource,
showThumbnail = showThumbnail,
}
if (showProgress) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
)
}
}
}
}
@Composable
private fun DismissFlickEffects(
flickState: FlickToDismissState,
onDismissing: suspend (Duration) -> Unit,
onDragging: suspend () -> Unit,
) {
val currentOnDismissing by rememberUpdatedState(onDismissing)
val currentOnDragging by rememberUpdatedState(onDragging)
when (val gestureState = flickState.gestureState) {
is FlickToDismissState.GestureState.Dismissing -> {
LaunchedEffect(Unit) {
currentOnDismissing(gestureState.animationDuration)
}
}
is FlickToDismissState.GestureState.Dragging -> {
LaunchedEffect(Unit) {
currentOnDragging()
}
}
else -> Unit
}
}
@Composable
private fun rememberShowProgress(downloadedMedia: AsyncData<LocalMedia>): Boolean {
var showProgress by remember {
@ -175,6 +268,9 @@ private fun MediaViewerTopBar(
) {
TopAppBar(
title = {},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent.copy(0.6f),
),
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
IconButton(
@ -227,26 +323,28 @@ private fun MediaViewerTopBar(
@Composable
private fun ThumbnailView(
thumbnailSource: MediaSource?,
showThumbnail: Boolean,
isVisible: Boolean,
mediaInfo: MediaInfo,
zoomableState: ZoomableState,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = showThumbnail,
visible = isVisible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val mediaRequestData = MediaRequestData(
source = thumbnailSource,
kind = MediaRequestData.Kind.File(mediaInfo.name, mediaInfo.mimeType)
)
AsyncImage(
ZoomableAsyncImage(
state = rememberZoomableImageState(zoomableState),
modifier = Modifier.fillMaxSize(),
model = mediaRequestData,
alpha = 0.8f,
contentScale = ContentScale.Fit,
contentDescription = null,
)
@ -267,6 +365,21 @@ private fun ErrorView(
)
}
@Composable
private fun backgroundColorFor(flickState: FlickToDismissState): Color {
val animatedAlpha by animateFloatAsState(
targetValue = when (flickState.gestureState) {
is FlickToDismissState.GestureState.Dismissed,
is FlickToDismissState.GestureState.Dismissing -> 0f
is FlickToDismissState.GestureState.Dragging,
is FlickToDismissState.GestureState.Idle,
is FlickToDismissState.GestureState.Resetting -> 1f - flickState.offsetFraction
},
label = "Background alpha",
)
return Color.Black.copy(alpha = animatedAlpha)
}
// Only preview in dark, dark theme is forced on the Node.
@Preview
@Composable

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3008d0953bbe1135796e50e1c1175c25f3e138892c7bd431444525213c9c91b8
size 396053
oid sha256:5d8c8580f22a7dbf6ac546e0c741259109e039f9e2dbc1bdc48966031b4f7cdb
size 396496

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2909eab36362e1c4ca470ef65bb6fabfe9c410e9a6666a2d14bb65f8b753c203
size 16237
oid sha256:4b209f5c4f0775eeb4059fa26a0fe9468301b0b82c5bf5627a93a01cb7a3e6d8
size 16609

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:09d54a1201ffbbfdc0f63eaf075f66c2c446b6b773f8929226a2455118710de7
size 64620
oid sha256:28a52e7e011a59fbeeaf2477d4fc15f3ea8f615906d0dbf3642a3e7473d32cbc
size 49465

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f50bf3f0c23362762b9911352179d428821da26cbf5cdfc88dd31285ae8b478
size 100137
oid sha256:8507b04315e9f112e962e4aa3d572f85f5985f6dd9153b9fe5e0572500de1dfe
size 90187

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e380cb38a6efcf9421ad6f86fbcebe82191c4d202dc6fbfb408b0c20b9c4928
size 395461
oid sha256:4454f74e8b205627df523773938a9f6f689116446a29fe3098da03c158bd1d37
size 396004

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d53a17bf0c1e43175d282a85923d184ea5bb913f73367916121eefc74ea82d4c
size 395464
oid sha256:a9e99773ba190bc9e08faa644568ec576064967b9367ee0f6e324f720b454680
size 396007

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9da218de660ad16ae8befe015703afe654dc9eef033d4edb00467ad3da4923a9
size 394418
oid sha256:617afc64adfcf660ba28a9c0cc7a8089efe76b0b9d382dfc4ae9ea29e02130e0
size 394862

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dfb06486f14c7ca54d49c69659a2861fc6c1a0b09b8bcd3e390c4126061bd83a
size 114684
oid sha256:e20d3b49668069aa81d457b2fd6ce6ce2e4e72f8897b55db29ae8049bba13669
size 100033

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:52486cc2cf9a3e521916c794d41df91b35b3384a47b38d97fb33bfde8e03e630
size 395696
oid sha256:5fff1c9ca69312b781cc6f61b5731e46fefdbe4aab0773ac0f82c11a1d537186
size 396253

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8d17201eaa89a6ac0cd503796508718d6ae515240e521acfa666a19daa2be14
size 6662
oid sha256:cf994c9d5bc4845c3c949e034cc2a920085e823749e60817f75e1bfbb4d8e2ad
size 6769

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e75209484e3ed4f8d89b82004530af59a7c8e8035ce38bcbd8a0e5d64e32016c
size 15920
oid sha256:5bee1fe2ec77bc2d15e3b05202c8897724c89ce774db62211ddc7a94d220146c
size 16891

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00491f1a53c4f05c96f019e97ef006dfa2d13f9980d4b567b14e63ffdf06a870
size 16074
oid sha256:ece7a72c5f5d3c0aec96568ad0188733dfb34b5169bb57a395f28a2da070f877
size 17121

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3809d38f5bd32392612b943ea17113948d8215707c92f919eb7d4e147a2cc3b
size 14428
oid sha256:df72ca3aead93edbb4dab70efc7728894a899845c05092fe5af2c73b65d8c213
size 15520

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be54d8f8ea0d5e376aae0304aefe9c236b8eb681b059f7a5e35ec32d0741dba9
size 14549
oid sha256:584fa976463bed507362846401b73662051dc8b8475301b0f48054f2464e9e60
size 15685