media viewer : create MediaViewerDataSource

This commit is contained in:
ganfra
2025-01-20 22:35:59 +01:00
committed by Benoit Marty
parent b02c12d92a
commit 24a2458e4a
7 changed files with 255 additions and 174 deletions

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.impl.viewer
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource
import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
import io.element.android.libraries.mediaviewer.impl.gallery.eventId
import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo
import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource
import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
class MediaViewerDataSource(
private val galleryMode: MediaGalleryMode,
private val dispatcher: CoroutineDispatcher,
private val galleryDataSource: MediaGalleryDataSource,
private val mediaLoader: MatrixMediaLoader,
private val localMediaFactory: LocalMediaFactory,
) {
// List of media files that are currently being loaded
private val mediaFiles: MutableList<MediaFile> = mutableListOf()
// Map of sourceUrl to local media state
private val localMediaStates: MutableMap<String, MutableState<AsyncData<LocalMedia>>> =
mutableMapOf()
fun setup() {
galleryDataSource.start()
}
fun dispose() {
mediaFiles.forEach { it.close() }
mediaFiles.clear()
localMediaStates.clear()
}
fun initialPageIndex(eventId: EventId?): Int {
if (eventId == null) {
return 0
}
val mediaItems =
galleryDataSource.getLastData().dataOrNull()?.getItems(galleryMode).orEmpty()
val pageList = buildMediaViewerPageList(mediaItems)
return pageList.indexOfFirst { data ->
when (data) {
is MediaViewerPageData.MediaViewerData -> data.eventId == eventId
else -> false
}
}
.takeIf { it != -1 }
?: 0
}
fun dataFlow(): Flow<PersistentList<MediaViewerPageData>> {
return galleryDataSource.groupedMediaItemsFlow()
.map {
val groupedItems = it.dataOrNull()?.getItems(galleryMode).orEmpty()
withContext(dispatcher) {
buildMediaViewerPageList(groupedItems)
}
}
}
private fun buildMediaViewerPageList(groupedItems: List<MediaItem>) = buildList {
groupedItems.forEach { mediaItem ->
when (mediaItem) {
is MediaItem.DateSeparator -> Unit
is MediaItem.Event -> {
val sourceUrl = mediaItem.mediaSource().url
val localMedia = localMediaStates.getOrPut(sourceUrl) {
mutableStateOf(AsyncData.Uninitialized)
}
add(
MediaViewerPageData.MediaViewerData(
eventId = mediaItem.eventId(),
mediaInfo = mediaItem.mediaInfo(),
mediaSource = mediaItem.mediaSource(),
thumbnailSource = mediaItem.thumbnailSource(),
downloadedMedia = localMedia,
)
)
}
is MediaItem.LoadingIndicator -> add(
MediaViewerPageData.Loading(mediaItem.direction)
)
}
}
if (isEmpty()) {
MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS)
}
}.toPersistentList()
suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) {
Timber.d("loadMedia for ${data.eventId}")
val localMediaState = localMediaStates.getOrPut(data.mediaSource.url) {
mutableStateOf(AsyncData.Uninitialized)
}
localMediaState.value = AsyncData.Loading()
mediaLoader
.downloadMediaFile(
source = data.mediaSource,
mimeType = data.mediaInfo.mimeType,
filename = data.mediaInfo.filename
)
.onSuccess { mediaFile ->
mediaFiles.add(mediaFile)
}
.mapCatching { mediaFile ->
localMediaFactory.createFromMediaFile(
mediaFile = mediaFile,
mediaInfo = data.mediaInfo
)
}
.onSuccess {
localMediaState.value = AsyncData.Success(it)
}
.onFailure {
localMediaState.value = AsyncData.Failure(it)
}
}
fun clearLoadingError(data: MediaViewerPageData.MediaViewerData) {
localMediaStates[data.mediaSource.url]?.value = AsyncData.Uninitialized
}
suspend fun loadMore(direction: Timeline.PaginationDirection) {
galleryDataSource.loadMore(direction)
}
}

View File

@@ -15,7 +15,7 @@ sealed interface MediaViewerEvents {
data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
data class Share(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
data class OpenWith(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
data class ClearLoadingError(val eventId: EventId) : MediaViewerEvents
data class ClearLoadingError(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
data class ViewInTimeline(val eventId: EventId) : MediaViewerEvents
data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
data class ConfirmDelete(

View File

@@ -18,9 +18,13 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode
import io.element.android.libraries.mediaviewer.impl.gallery.SingleMediaGalleryDataSource
import io.element.android.libraries.mediaviewer.impl.gallery.TimelineMediaGalleryDataSource
@@ -30,6 +34,9 @@ class MediaViewerNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
presenterFactory: MediaViewerPresenter.Factory,
timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource,
mediaLoader: MatrixMediaLoader,
localMediaFactory: LocalMediaFactory,
coroutineDispatchers: CoroutineDispatchers,
) : Node(buildContext, plugins = plugins),
MediaViewerNavigator {
private val inputs = inputs<MediaViewerEntryPoint.Params>()
@@ -50,14 +57,28 @@ class MediaViewerNode @AssistedInject constructor(
onDone()
}
private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) {
SingleMediaGalleryDataSource.createFrom(inputs)
} else {
timelineMediaGalleryDataSource
}
private val galleryMode = when (inputs.mode) {
MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
}
private val presenter = presenterFactory.create(
inputs = inputs,
navigator = this,
mediaGalleryDataSource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) {
SingleMediaGalleryDataSource.createFrom(inputs)
} else {
timelineMediaGalleryDataSource
},
dataSource = MediaViewerDataSource(
dispatcher = coroutineDispatchers.computation,
galleryMode = galleryMode,
galleryDataSource = mediaGallerySource,
mediaLoader = mediaLoader,
localMediaFactory = localMediaFactory
)
)
@Composable

View File

@@ -10,10 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer
import android.content.ActivityNotFoundException
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -29,26 +26,17 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource
import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
import io.element.android.libraries.mediaviewer.impl.gallery.eventId
import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo
import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource
import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import io.element.android.libraries.androidutils.R as UtilsR
@@ -56,10 +44,8 @@ import io.element.android.libraries.androidutils.R as UtilsR
class MediaViewerPresenter @AssistedInject constructor(
@Assisted private val inputs: MediaViewerEntryPoint.Params,
@Assisted private val navigator: MediaViewerNavigator,
@Assisted private val mediaGalleryDataSource: MediaGalleryDataSource,
@Assisted private val dataSource: MediaViewerDataSource,
private val room: MatrixRoom,
private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader,
private val localMediaActions: LocalMediaActions,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<MediaViewerState> {
@@ -68,97 +54,46 @@ class MediaViewerPresenter @AssistedInject constructor(
fun create(
inputs: MediaViewerEntryPoint.Params,
navigator: MediaViewerNavigator,
mediaGalleryDataSource: MediaGalleryDataSource,
dataSource: MediaViewerDataSource,
): MediaViewerPresenter
}
private val galleryMode = when (inputs.mode) {
MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
}
@Composable
override fun present(): MediaViewerState {
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
mediaGalleryDataSource.start()
}
val groupedMediaItem by remember { mediaGalleryDataSource.groupedMediaItemsFlow() }
.collectAsState(mediaGalleryDataSource.getLastData())
val mediaFile: MutableMap<EventId?, MutableState<MediaFile?>> = remember { mutableMapOf() }
val localMedia: MutableMap<EventId?, MutableState<AsyncData<LocalMedia>>> = remember { mutableMapOf() }
DisposableEffect(Unit) {
onDispose {
mediaFile.values.forEach { it.value?.close() }
}
}
val data: List<MediaViewerPageData> by remember {
derivedStateOf {
buildList {
val data = groupedMediaItem.dataOrNull()
if (data != null) {
if (data.getItems(galleryMode).firstOrNull() is MediaItem.LoadingIndicator) {
add(MediaViewerPageData.Loading(Timeline.PaginationDirection.FORWARDS))
}
data.getItems(galleryMode).filterIsInstance<MediaItem.Event>().forEach { mediaItem ->
val eventId = mediaItem.eventId()
add(
MediaViewerPageData.MediaViewerData(
eventId = eventId,
mediaInfo = mediaItem.mediaInfo(),
mediaSource = mediaItem.mediaSource(),
thumbnailSource = mediaItem.thumbnailSource(),
downloadedMedia = localMedia.getOrPut(eventId) {
mutableStateOf(AsyncData.Uninitialized)
}.value,
)
)
}
if (data.getItems(galleryMode).lastOrNull() is MediaItem.LoadingIndicator) {
add(MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS))
}
}
if (isEmpty()) {
add(MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS))
}
}
}
}
var currentIndex by remember { mutableIntStateOf(searchIndex(data, inputs.eventId)) }
val data: ImmutableList<MediaViewerPageData> by dataSource.dataFlow().collectAsState(persistentListOf())
var currentIndex by remember { mutableIntStateOf(dataSource.initialPageIndex(inputs.eventId)) }
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
localMediaActions.Configure()
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
DisposableEffect(Unit) {
dataSource.setup()
onDispose {
dataSource.dispose()
}
}
localMediaActions.Configure()
fun handleEvents(event: MediaViewerEvents) {
when (event) {
is MediaViewerEvents.LoadMedia -> {
// It's OK to suppress the warning since mediaFile and localMedia are remembered
@Suppress("RememberMissing")
coroutineScope.downloadMedia(
data = event.data,
mediaFile = mediaFile.getOrPut(event.data.eventId) { mutableStateOf(null) },
localMedia = localMedia.getOrPut(event.data.eventId) { mutableStateOf(AsyncData.Uninitialized) },
)
coroutineScope.downloadMedia(data = event.data)
}
is MediaViewerEvents.ClearLoadingError -> {
// It's OK to suppress the warning since localMedia is remembered
@Suppress("RememberMissing")
localMedia.getOrPut(event.eventId) { mutableStateOf(AsyncData.Uninitialized) }.value = AsyncData.Uninitialized
dataSource.clearLoadingError(event.data)
}
is MediaViewerEvents.SaveOnDisk -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
coroutineScope.saveOnDisk(event.data.downloadedMedia)
coroutineScope.saveOnDisk(event.data.downloadedMedia.value)
}
is MediaViewerEvents.Share -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
coroutineScope.share(event.data.downloadedMedia)
coroutineScope.share(event.data.downloadedMedia.value)
}
is MediaViewerEvents.OpenWith -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
coroutineScope.open(event.data.downloadedMedia)
coroutineScope.open(event.data.downloadedMedia.value)
}
is MediaViewerEvents.Delete -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
@@ -194,7 +129,7 @@ class MediaViewerPresenter @AssistedInject constructor(
currentIndex = event.index
}
is MediaViewerEvents.LoadMore -> coroutineScope.launch {
mediaGalleryDataSource.loadMore(event.direction)
dataSource.loadMore(event.direction)
}
}
}
@@ -211,30 +146,8 @@ class MediaViewerPresenter @AssistedInject constructor(
private fun CoroutineScope.downloadMedia(
data: MediaViewerPageData.MediaViewerData,
mediaFile: MutableState<MediaFile?>,
localMedia: MutableState<AsyncData<LocalMedia>>,
) = launch {
localMedia.value = AsyncData.Loading()
mediaLoader.downloadMediaFile(
source = data.mediaSource,
mimeType = data.mediaInfo.mimeType,
filename = data.mediaInfo.filename
)
.onSuccess {
mediaFile.value = it
}
.mapCatching { mediaFile ->
localMediaFactory.createFromMediaFile(
mediaFile = mediaFile,
mediaInfo = data.mediaInfo
)
}
.onSuccess {
localMedia.value = AsyncData.Success(it)
}
.onFailure {
localMedia.value = AsyncData.Failure(it)
}
dataSource.loadMedia(data)
}
private fun CoroutineScope.saveOnDisk(localMedia: AsyncData<LocalMedia>) = launch {
@@ -290,14 +203,3 @@ class MediaViewerPresenter @AssistedInject constructor(
}
}
}
private fun searchIndex(data: List<MediaViewerPageData>, eventId: EventId?): Int {
if (eventId == null) {
return 0
}
return data.indexOfFirst {
(it as? MediaViewerPageData.MediaViewerData)?.eventId == eventId
}
.takeIf { it != -1 }
?: 0
}

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.mediaviewer.impl.viewer
import androidx.compose.runtime.State
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.EventId
@@ -15,9 +16,10 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
import kotlinx.collections.immutable.ImmutableList
data class MediaViewerState(
val listData: List<MediaViewerPageData>,
val listData: ImmutableList<MediaViewerPageData>,
val currentIndex: Int,
val snackbarMessage: SnackbarMessage?,
val canShowInfo: Boolean,
@@ -35,6 +37,6 @@ sealed interface MediaViewerPageData {
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val downloadedMedia: AsyncData<LocalMedia>,
val downloadedMedia: State<AsyncData<LocalMedia>>,
) : MediaViewerPageData
}

View File

@@ -8,6 +8,7 @@
package io.element.android.libraries.mediaviewer.impl.viewer
import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.media.aWaveForm
@@ -22,6 +23,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirmationState
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
import kotlinx.collections.immutable.toPersistentList
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
override val values: Sequence<MediaViewerState>
@@ -158,7 +160,7 @@ fun aMediaViewerPageData(
mediaInfo = mediaInfo,
mediaSource = mediaSource,
thumbnailSource = null,
downloadedMedia = downloadedMedia,
downloadedMedia = mutableStateOf(downloadedMedia),
)
fun aMediaViewerState(
@@ -168,7 +170,7 @@ fun aMediaViewerState(
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
eventSink: (MediaViewerEvents) -> Unit = {},
) = MediaViewerState(
listData = listData,
listData = listData.toPersistentList(),
currentIndex = currentIndex,
snackbarMessage = null,
canShowInfo = canShowInfo,

View File

@@ -85,6 +85,7 @@ import me.saket.telephoto.flick.FlickToDismissState
import me.saket.telephoto.flick.rememberFlickToDismissState
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
import timber.log.Timber
import kotlin.time.Duration
@Composable
@@ -98,7 +99,7 @@ fun MediaViewerView(
val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
val currentData = state.listData[state.currentIndex]
val currentData = state.listData.getOrNull(state.currentIndex)
BackHandler { onBackClick() }
Scaffold(
modifier,
@@ -113,6 +114,9 @@ fun MediaViewerView(
state.eventSink(MediaViewerEvents.OnNavigateTo(page))
}
}
LaunchedEffect(state.listData) {
Timber.d("MediaViewerView: state.listData: ${state.listData}")
}
HorizontalPager(
state = pagerState,
modifier = Modifier,
@@ -139,10 +143,15 @@ fun MediaViewerView(
showOverlay = showOverlay,
bottomPaddingInPixels = bottomPaddingInPixels,
data = dataForPage,
state = state,
onDismiss = {
onBackClick()
},
onRetry = {
state.eventSink(MediaViewerEvents.LoadMedia(dataForPage))
},
onDismissError = {
state.eventSink(MediaViewerEvents.ClearLoadingError(dataForPage))
},
onShowOverlayChange = {
showOverlay = it
}
@@ -151,8 +160,8 @@ fun MediaViewerView(
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
.fillMaxSize()
.navigationBarsPadding()
) {
MediaViewerBottomBar(
modifier = Modifier.align(Alignment.BottomCenter),
@@ -169,19 +178,10 @@ fun MediaViewerView(
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
.fillMaxSize()
.navigationBarsPadding()
) {
when (currentData) {
is MediaViewerPageData.Loading -> {
TopAppBar(
title = {},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent.copy(0.6f),
),
navigationIcon = { BackButton(onClick = onBackClick) },
)
}
is MediaViewerPageData.MediaViewerData -> {
MediaViewerTopBar(
data = currentData,
@@ -193,6 +193,15 @@ fun MediaViewerView(
eventSink = state.eventSink
)
}
else -> {
TopAppBar(
title = {},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent.copy(0.6f),
),
navigationIcon = { BackButton(onClick = onBackClick) },
)
}
}
}
}
@@ -250,21 +259,13 @@ private fun MediaViewerPage(
isDisplayed: Boolean,
showOverlay: Boolean,
bottomPaddingInPixels: Int,
state: MediaViewerState,
data: MediaViewerPageData.MediaViewerData,
onDismiss: () -> Unit,
onRetry: () -> Unit,
onDismissError: () -> Unit,
onShowOverlayChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
fun onRetry() {
state.eventSink(MediaViewerEvents.LoadMedia(data))
}
fun onDismissError() {
data.eventId?.let {
state.eventSink(MediaViewerEvents.ClearLoadingError(it))
}
}
val currentShowOverlay by rememberUpdatedState(showOverlay)
val currentOnShowOverlayChange by rememberUpdatedState(onShowOverlayChange)
@@ -285,12 +286,13 @@ private fun MediaViewerPage(
state = flickState,
modifier = modifier.background(backgroundColorFor(flickState))
) {
val showProgress = rememberShowProgress(data.downloadedMedia)
val downloadedMedia by data.downloadedMedia
val showProgress = rememberShowProgress(downloadedMedia)
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
.fillMaxSize()
.navigationBarsPadding()
) {
Box(contentAlignment = Alignment.Center) {
val zoomableState = rememberZoomableState(
@@ -299,7 +301,7 @@ private fun MediaViewerPage(
val localMediaViewState = rememberLocalMediaViewState(zoomableState)
val showThumbnail = !localMediaViewState.isReady
val playableState = localMediaViewState.playableState
val showError = data.downloadedMedia is AsyncData.Failure
val showError = downloadedMedia.isFailure()
LaunchedEffect(playableState) {
if (playableState is PlayableState.Playable) {
@@ -312,7 +314,7 @@ private fun MediaViewerPage(
isDisplayed = isDisplayed,
bottomPaddingInPixels = bottomPaddingInPixels,
localMediaViewState = localMediaViewState,
localMedia = data.downloadedMedia.dataOrNull(),
localMedia = downloadedMedia.dataOrNull(),
mediaInfo = data.mediaInfo,
onClick = {
if (playableState is PlayableState.NotPlayable) {
@@ -328,16 +330,16 @@ private fun MediaViewerPage(
if (showError) {
ErrorView(
errorMessage = stringResource(id = CommonStrings.error_unknown),
onRetry = ::onRetry,
onDismiss = ::onDismissError
onRetry = onRetry,
onDismiss = onDismissError
)
}
}
if (showProgress) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
.fillMaxWidth()
.height(2.dp)
)
}
}
@@ -366,8 +368,8 @@ private fun MediaViewerLoadingPage(
) {
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
.fillMaxSize()
.navigationBarsPadding(),
contentAlignment = Alignment.Center
) {
AsyncLoading()
@@ -429,7 +431,8 @@ private fun MediaViewerTopBar(
onInfoClick: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit,
) {
val actionsEnabled = data.downloadedMedia is AsyncData.Success
val downloadedMedia by data.downloadedMedia
val actionsEnabled = downloadedMedia.isSuccess()
val mimeType = data.mediaInfo.mimeType
val senderName = data.mediaInfo.senderName
val dateSent = data.mediaInfo.dateSent
@@ -503,11 +506,11 @@ private fun MediaViewerBottomBar(
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(Color(0x99101317))
.onSizeChanged {
onHeightChange(it.height)
},
.fillMaxWidth()
.background(Color(0x99101317))
.onSizeChanged {
onHeightChange(it.height)
},
) {
if (caption != null) {
if (showDivider) {
@@ -515,8 +518,8 @@ private fun MediaViewerBottomBar(
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.fillMaxWidth()
.padding(16.dp),
text = caption,
maxLines = 5,
overflow = TextOverflow.Ellipsis,