Let MediaGalleryDataSource be an interface
This commit is contained in:
committed by
Benoit Marty
parent
c8ca4d7425
commit
d691a3f6a2
@@ -26,22 +26,32 @@ import kotlinx.coroutines.flow.onEach
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
interface MediaGalleryDataSource {
|
||||
fun start()
|
||||
fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>>
|
||||
fun getLastData(): AsyncData<GroupedMediaItems>
|
||||
suspend fun loadMore(direction: Timeline.PaginationDirection)
|
||||
suspend fun deleteItem(eventId: EventId)
|
||||
}
|
||||
|
||||
@SingleIn(RoomScope::class)
|
||||
class MediaGalleryDataSource @Inject constructor(
|
||||
class TimelineMediaGalleryDataSource @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
|
||||
private val mediaItemsPostProcessor: MediaItemsPostProcessor,
|
||||
) {
|
||||
) : MediaGalleryDataSource {
|
||||
private var timeline: Timeline? = null
|
||||
|
||||
private val groupedMediaItemsFlow = MutableSharedFlow<AsyncData<GroupedMediaItems>>(replay = 1)
|
||||
|
||||
fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>> = groupedMediaItemsFlow
|
||||
override fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>> = groupedMediaItemsFlow
|
||||
|
||||
override fun getLastData(): AsyncData<GroupedMediaItems> = groupedMediaItemsFlow.replayCache.firstOrNull() ?: AsyncData.Uninitialized
|
||||
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun start() {
|
||||
override fun start() {
|
||||
if (!isStarted.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
@@ -73,11 +83,11 @@ class MediaGalleryDataSource @Inject constructor(
|
||||
.launchIn(room.roomCoroutineScope)
|
||||
}
|
||||
|
||||
suspend fun loadMore(direction: Timeline.PaginationDirection) {
|
||||
override suspend fun loadMore(direction: Timeline.PaginationDirection) {
|
||||
timeline?.paginate(direction)
|
||||
}
|
||||
|
||||
suspend fun deleteItem(eventId: EventId) {
|
||||
override suspend fun deleteItem(eventId: EventId) {
|
||||
timeline?.redactEvent(
|
||||
eventOrTransactionId = eventId.toEventOrTransactionId(),
|
||||
reason = null,
|
||||
|
||||
@@ -40,7 +40,7 @@ import kotlinx.coroutines.launch
|
||||
class MediaGalleryPresenter @AssistedInject constructor(
|
||||
@Assisted private val navigator: MediaGalleryNavigator,
|
||||
private val room: MatrixRoom,
|
||||
private val mediaGalleryDataSource: MediaGalleryDataSource,
|
||||
private val mediaGalleryDataSource: TimelineMediaGalleryDataSource,
|
||||
private val localMediaFactory: LocalMediaFactory,
|
||||
private val mediaLoader: MatrixMediaLoader,
|
||||
private val localMediaActions: LocalMediaActions,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.gallery
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import javax.inject.Inject
|
||||
|
||||
class SingleMediaGalleryDataSource @Inject constructor(
|
||||
private val data: GroupedMediaItems,
|
||||
) : MediaGalleryDataSource {
|
||||
override fun start() = Unit
|
||||
override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data))
|
||||
override fun getLastData(): AsyncData<GroupedMediaItems> = AsyncData.Success(data)
|
||||
override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit
|
||||
override suspend fun deleteItem(eventId: EventId) = Unit
|
||||
|
||||
companion object {
|
||||
fun createFrom(params: MediaViewerEntryPoint.Params) = SingleMediaGalleryDataSource(
|
||||
data = when {
|
||||
params.mediaInfo.mimeType.isMimeTypeImage() -> {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(
|
||||
MediaItem.Image(
|
||||
id = UniqueId("dummy"),
|
||||
eventId = params.eventId,
|
||||
mediaInfo = params.mediaInfo,
|
||||
mediaSource = params.mediaSource,
|
||||
thumbnailSource = params.thumbnailSource,
|
||||
)
|
||||
),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
}
|
||||
params.mediaInfo.mimeType.isMimeTypeVideo() -> {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(
|
||||
MediaItem.Video(
|
||||
id = UniqueId("dummy"),
|
||||
eventId = params.eventId,
|
||||
mediaInfo = params.mediaInfo,
|
||||
mediaSource = params.mediaSource,
|
||||
thumbnailSource = params.thumbnailSource,
|
||||
duration = "TODO", // TODO Duration
|
||||
)
|
||||
),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
}
|
||||
params.mediaInfo.mimeType.isMimeTypeAudio() -> {
|
||||
if (params.mediaInfo.waveform == null) {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(
|
||||
MediaItem.Audio(
|
||||
id = UniqueId("dummy"),
|
||||
eventId = params.eventId,
|
||||
mediaInfo = params.mediaInfo,
|
||||
mediaSource = params.mediaSource,
|
||||
)
|
||||
),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
} else {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(
|
||||
MediaItem.Voice(
|
||||
id = UniqueId("dummy"),
|
||||
eventId = params.eventId,
|
||||
mediaInfo = params.mediaInfo,
|
||||
mediaSource = params.mediaSource,
|
||||
duration = "TODO", // TODO Duration
|
||||
waveform = params.mediaInfo.waveform.orEmpty().toImmutableList(),
|
||||
)
|
||||
),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(
|
||||
MediaItem.File(
|
||||
id = UniqueId("dummy"),
|
||||
eventId = params.eventId,
|
||||
mediaInfo = params.mediaInfo,
|
||||
mediaSource = params.mediaSource,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,24 @@
|
||||
package io.element.android.libraries.mediaviewer.impl.viewer
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
||||
sealed interface MediaViewerEvents {
|
||||
data object SaveOnDisk : MediaViewerEvents
|
||||
data object Share : MediaViewerEvents
|
||||
data object OpenWith : MediaViewerEvents
|
||||
data object RetryLoading : MediaViewerEvents
|
||||
data object ClearLoadingError : MediaViewerEvents
|
||||
data class LoadMedia(val data: MediaViewerPageData.MediaViewerData) : 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 RetryLoading(val eventId: EventId) : MediaViewerEvents
|
||||
data class ClearLoadingError(val eventId: EventId) : MediaViewerEvents
|
||||
data class ViewInTimeline(val eventId: EventId) : MediaViewerEvents
|
||||
data object OpenInfo : MediaViewerEvents
|
||||
data class ConfirmDelete(val eventId: EventId) : MediaViewerEvents
|
||||
data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
||||
data class ConfirmDelete(
|
||||
val eventId: EventId,
|
||||
val data: MediaViewerPageData.MediaViewerData,
|
||||
) : MediaViewerEvents
|
||||
|
||||
data object CloseBottomSheet : MediaViewerEvents
|
||||
data class Delete(val eventId: EventId) : MediaViewerEvents
|
||||
data class OnNavigateTo(val index: Int) : MediaViewerEvents
|
||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvents
|
||||
}
|
||||
|
||||
@@ -21,12 +21,15 @@ import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.SingleMediaGalleryDataSource
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.TimelineMediaGalleryDataSource
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class MediaViewerNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: MediaViewerPresenter.Factory,
|
||||
timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource,
|
||||
) : Node(buildContext, plugins = plugins),
|
||||
MediaViewerNavigator {
|
||||
private val inputs = inputs<MediaViewerEntryPoint.Params>()
|
||||
@@ -50,6 +53,11 @@ class MediaViewerNode @AssistedInject constructor(
|
||||
private val presenter = presenterFactory.create(
|
||||
inputs = inputs,
|
||||
navigator = this,
|
||||
mediaGalleryDataSource = if (inputs.eventId != null) {
|
||||
timelineMediaGalleryDataSource
|
||||
} else {
|
||||
SingleMediaGalleryDataSource.createFrom(inputs)
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -10,7 +10,11 @@ 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.MutableIntState
|
||||
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
|
||||
@@ -31,11 +35,18 @@ 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.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.coroutines.CoroutineScope
|
||||
@@ -45,6 +56,7 @@ 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,
|
||||
private val room: MatrixRoom,
|
||||
private val localMediaFactory: LocalMediaFactory,
|
||||
private val mediaLoader: MatrixMediaLoader,
|
||||
@@ -56,83 +68,132 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||
fun create(
|
||||
inputs: MediaViewerEntryPoint.Params,
|
||||
navigator: MediaViewerNavigator,
|
||||
mediaGalleryDataSource: MediaGalleryDataSource,
|
||||
): MediaViewerPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): MediaViewerState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var loadMediaTrigger by remember { mutableIntStateOf(0) }
|
||||
val mediaFile: MutableState<MediaFile?> = remember {
|
||||
mutableStateOf(null)
|
||||
LaunchedEffect(Unit) {
|
||||
mediaGalleryDataSource.start()
|
||||
}
|
||||
val localMedia: MutableState<AsyncData<LocalMedia>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
localMediaActions.Configure()
|
||||
DisposableEffect(loadMediaTrigger) {
|
||||
coroutineScope.downloadMedia(mediaFile, localMedia)
|
||||
val groupedMediaItem by remember { mediaGalleryDataSource.groupedMediaItemsFlow() }
|
||||
.collectAsState(mediaGalleryDataSource.getLastData())
|
||||
|
||||
val loadMediaTrigger: MutableMap<EventId?, MutableIntState> = remember { mutableMapOf() }
|
||||
val mediaFile: MutableMap<EventId?, MutableState<MediaFile?>> = remember { mutableMapOf() }
|
||||
val localMedia: MutableMap<EventId?, MutableState<AsyncData<LocalMedia>>> = remember { mutableMapOf() }
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
mediaFile.value?.close()
|
||||
mediaFile.values.forEach { it.value?.close() }
|
||||
}
|
||||
}
|
||||
|
||||
val data: List<MediaViewerPageData> by remember {
|
||||
derivedStateOf {
|
||||
buildList {
|
||||
val data = groupedMediaItem.dataOrNull()
|
||||
if (data != null) {
|
||||
if (data.imageAndVideoItems.firstOrNull() is MediaItem.LoadingIndicator) {
|
||||
add(MediaViewerPageData.Loading(Timeline.PaginationDirection.FORWARDS))
|
||||
}
|
||||
data.imageAndVideoItems.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.imageAndVideoItems.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 snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
localMediaActions.Configure()
|
||||
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
|
||||
|
||||
fun handleEvents(mediaViewerEvents: MediaViewerEvents) {
|
||||
when (mediaViewerEvents) {
|
||||
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
|
||||
MediaViewerEvents.ClearLoadingError -> localMedia.value = AsyncData.Uninitialized
|
||||
MediaViewerEvents.SaveOnDisk -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.saveOnDisk(localMedia.value)
|
||||
fun handleEvents(event: MediaViewerEvents) {
|
||||
when (event) {
|
||||
is MediaViewerEvents.LoadMedia -> coroutineScope.downloadMedia(
|
||||
data = event.data,
|
||||
mediaFile = mediaFile.getOrPut(event.data.eventId) { mutableStateOf(null) },
|
||||
localMedia = localMedia.getOrPut(event.data.eventId) { mutableStateOf(AsyncData.Uninitialized) },
|
||||
)
|
||||
is MediaViewerEvents.RetryLoading -> {
|
||||
loadMediaTrigger.getOrPut(event.eventId) { mutableIntStateOf(0) }.intValue++
|
||||
}
|
||||
MediaViewerEvents.Share -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.share(localMedia.value)
|
||||
is MediaViewerEvents.ClearLoadingError -> {
|
||||
localMedia.getOrPut(event.eventId) { mutableStateOf(AsyncData.Uninitialized) }.value = AsyncData.Uninitialized
|
||||
}
|
||||
MediaViewerEvents.OpenWith -> {
|
||||
is MediaViewerEvents.SaveOnDisk -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.open(localMedia.value)
|
||||
coroutineScope.saveOnDisk(event.data.downloadedMedia)
|
||||
}
|
||||
is MediaViewerEvents.Share -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.share(event.data.downloadedMedia)
|
||||
}
|
||||
is MediaViewerEvents.OpenWith -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.open(event.data.downloadedMedia)
|
||||
}
|
||||
is MediaViewerEvents.Delete -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.delete(mediaViewerEvents.eventId)
|
||||
coroutineScope.delete(event.eventId)
|
||||
}
|
||||
is MediaViewerEvents.ViewInTimeline -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
navigator.onViewInTimelineClick(mediaViewerEvents.eventId)
|
||||
navigator.onViewInTimelineClick(event.eventId)
|
||||
}
|
||||
MediaViewerEvents.OpenInfo -> coroutineScope.launch {
|
||||
is MediaViewerEvents.OpenInfo -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||
eventId = inputs.eventId,
|
||||
canDelete = when (inputs.mediaInfo.senderId) {
|
||||
eventId = event.data.eventId,
|
||||
canDelete = when (event.data.mediaInfo.senderId) {
|
||||
null -> false
|
||||
room.sessionId -> room.canRedactOwn().getOrElse { false } && inputs.eventId != null
|
||||
else -> room.canRedactOther().getOrElse { false } && inputs.eventId != null
|
||||
room.sessionId -> room.canRedactOwn().getOrElse { false } && event.data.eventId != null
|
||||
else -> room.canRedactOther().getOrElse { false } && event.data.eventId != null
|
||||
},
|
||||
mediaInfo = inputs.mediaInfo,
|
||||
thumbnailSource = inputs.thumbnailSource,
|
||||
mediaInfo = event.data.mediaInfo,
|
||||
thumbnailSource = event.data.thumbnailSource,
|
||||
)
|
||||
}
|
||||
is MediaViewerEvents.ConfirmDelete -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.MediaDeleteConfirmationState(
|
||||
eventId = mediaViewerEvents.eventId,
|
||||
mediaInfo = inputs.mediaInfo,
|
||||
thumbnailSource = inputs.thumbnailSource ?: inputs.mediaSource,
|
||||
eventId = event.eventId,
|
||||
mediaInfo = event.data.mediaInfo,
|
||||
thumbnailSource = event.data.thumbnailSource ?: event.data.mediaSource,
|
||||
)
|
||||
}
|
||||
MediaViewerEvents.CloseBottomSheet -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
}
|
||||
is MediaViewerEvents.OnNavigateTo -> {
|
||||
currentIndex = event.index
|
||||
}
|
||||
is MediaViewerEvents.LoadMore -> coroutineScope.launch {
|
||||
mediaGalleryDataSource.loadMore(event.direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MediaViewerState(
|
||||
eventId = inputs.eventId,
|
||||
mediaInfo = inputs.mediaInfo,
|
||||
thumbnailSource = inputs.thumbnailSource,
|
||||
downloadedMedia = localMedia.value,
|
||||
listData = data,
|
||||
currentIndex = currentIndex,
|
||||
snackbarMessage = snackbarMessage,
|
||||
canShowInfo = inputs.canShowInfo,
|
||||
mediaBottomSheetState = mediaBottomSheetState,
|
||||
@@ -140,12 +201,16 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.downloadMedia(mediaFile: MutableState<MediaFile?>, localMedia: MutableState<AsyncData<LocalMedia>>) = launch {
|
||||
private fun CoroutineScope.downloadMedia(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
mediaFile: MutableState<MediaFile?>,
|
||||
localMedia: MutableState<AsyncData<LocalMedia>>,
|
||||
) = launch {
|
||||
localMedia.value = AsyncData.Loading()
|
||||
mediaLoader.downloadMediaFile(
|
||||
source = inputs.mediaSource,
|
||||
mimeType = inputs.mediaInfo.mimeType,
|
||||
filename = inputs.mediaInfo.filename
|
||||
source = data.mediaSource,
|
||||
mimeType = data.mediaInfo.mimeType,
|
||||
filename = data.mediaInfo.filename
|
||||
)
|
||||
.onSuccess {
|
||||
mediaFile.value = it
|
||||
@@ -153,7 +218,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||
.mapCatching { mediaFile ->
|
||||
localMediaFactory.createFromMediaFile(
|
||||
mediaFile = mediaFile,
|
||||
mediaInfo = inputs.mediaInfo
|
||||
mediaInfo = data.mediaInfo
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
@@ -217,3 +282,14 @@ 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
|
||||
}
|
||||
|
||||
@@ -11,17 +11,30 @@ 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
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
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
|
||||
|
||||
data class MediaViewerState(
|
||||
val eventId: EventId?,
|
||||
val mediaInfo: MediaInfo,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val downloadedMedia: AsyncData<LocalMedia>,
|
||||
val listData: List<MediaViewerPageData>,
|
||||
val currentIndex: Int,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val canShowInfo: Boolean,
|
||||
val mediaBottomSheetState: MediaBottomSheetState,
|
||||
val eventSink: (MediaViewerEvents) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface MediaViewerPageData {
|
||||
data class Loading(
|
||||
val direction: Timeline.PaginationDirection,
|
||||
) : MediaViewerPageData
|
||||
|
||||
data class MediaViewerData(
|
||||
val eventId: EventId?,
|
||||
val mediaInfo: MediaInfo,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val downloadedMedia: AsyncData<LocalMedia>,
|
||||
) : MediaViewerPageData
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.net.Uri
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
||||
@@ -26,18 +27,22 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||
override val values: Sequence<MediaViewerState>
|
||||
get() = sequenceOf(
|
||||
aMediaViewerState(),
|
||||
aMediaViewerState(AsyncData.Loading()),
|
||||
aMediaViewerState(AsyncData.Failure(IllegalStateException("error"))),
|
||||
aMediaViewerState(listOf(aMediaViewerPageData(AsyncData.Loading()))),
|
||||
aMediaViewerState(listOf(aMediaViewerPageData(AsyncData.Failure(IllegalStateException("error"))))),
|
||||
anImageMediaInfo(
|
||||
senderName = "Sally Sanderson",
|
||||
dateSent = "21 NOV, 2024",
|
||||
caption = "A caption",
|
||||
).let {
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
aVideoMediaInfo(
|
||||
@@ -46,50 +51,78 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||
caption = "A caption",
|
||||
).let {
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
aPdfMediaInfo().let {
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Loading(),
|
||||
mediaInfo = anApkMediaInfo(),
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Loading(),
|
||||
mediaInfo = anApkMediaInfo(),
|
||||
)
|
||||
)
|
||||
),
|
||||
anApkMediaInfo().let {
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Loading(),
|
||||
mediaInfo = anAudioMediaInfo(),
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Loading(),
|
||||
mediaInfo = anAudioMediaInfo(),
|
||||
)
|
||||
)
|
||||
),
|
||||
anAudioMediaInfo().let {
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
anImageMediaInfo().let {
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
)
|
||||
),
|
||||
mediaInfo = it,
|
||||
canShowInfo = false,
|
||||
)
|
||||
},
|
||||
@@ -103,26 +136,40 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||
waveForm = aWaveForm(),
|
||||
).let {
|
||||
aMediaViewerState(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(
|
||||
LocalMedia(Uri.EMPTY, it)
|
||||
),
|
||||
mediaInfo = it,
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun aMediaViewerState(
|
||||
fun aMediaViewerPageData(
|
||||
downloadedMedia: AsyncData<LocalMedia> = AsyncData.Uninitialized,
|
||||
mediaInfo: MediaInfo = anImageMediaInfo(),
|
||||
mediaSource: MediaSource = MediaSource(""),
|
||||
): MediaViewerPageData.MediaViewerData = MediaViewerPageData.MediaViewerData(
|
||||
eventId = null,
|
||||
mediaInfo = mediaInfo,
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = null,
|
||||
downloadedMedia = downloadedMedia,
|
||||
)
|
||||
|
||||
fun aMediaViewerState(
|
||||
listData: List<MediaViewerPageData> = listOf(aMediaViewerPageData()),
|
||||
currentIndex: Int = 0,
|
||||
canShowInfo: Boolean = true,
|
||||
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
||||
eventSink: (MediaViewerEvents) -> Unit = {},
|
||||
) = MediaViewerState(
|
||||
eventId = null,
|
||||
mediaInfo = mediaInfo,
|
||||
thumbnailSource = null,
|
||||
downloadedMedia = downloadedMedia,
|
||||
listData = listData,
|
||||
currentIndex = currentIndex,
|
||||
snackbarMessage = null,
|
||||
canShowInfo = canShowInfo,
|
||||
mediaBottomSheetState = mediaBottomSheetState,
|
||||
|
||||
@@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -35,6 +37,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -52,6 +55,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
@@ -94,50 +98,105 @@ fun MediaViewerView(
|
||||
|
||||
val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0
|
||||
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
||||
val currentData = state.listData[state.currentIndex]
|
||||
BackHandler { onBackClick() }
|
||||
Scaffold(
|
||||
modifier,
|
||||
containerColor = Color.Transparent,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) {
|
||||
MediaViewerPage(
|
||||
showOverlay = showOverlay,
|
||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||
state = state,
|
||||
onDismiss = {
|
||||
onBackClick()
|
||||
},
|
||||
onShowOverlayChange = {
|
||||
showOverlay = it
|
||||
val pagerState = rememberPagerState(state.currentIndex, 0f) {
|
||||
state.listData.size
|
||||
}
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }.collect { page ->
|
||||
state.eventSink(MediaViewerEvents.OnNavigateTo(page))
|
||||
}
|
||||
)
|
||||
}
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier,
|
||||
// Pre-load previous and next pages
|
||||
beyondViewportPageCount = 1,
|
||||
) { page ->
|
||||
when (val dataForPage = state.listData[page]) {
|
||||
is MediaViewerPageData.Loading -> {
|
||||
LaunchedEffect(Unit) {
|
||||
state.eventSink(MediaViewerEvents.LoadMore(dataForPage.direction))
|
||||
}
|
||||
MediaViewerLoadingPage(
|
||||
onDismiss = {
|
||||
onBackClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
is MediaViewerPageData.MediaViewerData -> {
|
||||
LaunchedEffect(Unit) {
|
||||
state.eventSink(MediaViewerEvents.LoadMedia(dataForPage))
|
||||
}
|
||||
MediaViewerPage(
|
||||
showOverlay = showOverlay,
|
||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||
data = dataForPage,
|
||||
state = state,
|
||||
onDismiss = {
|
||||
onBackClick()
|
||||
},
|
||||
onShowOverlayChange = {
|
||||
showOverlay = it
|
||||
}
|
||||
)
|
||||
// Bottom bar
|
||||
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
MediaViewerBottomBar(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
showDivider = dataForPage.mediaInfo.mimeType.isMimeTypeVideo(),
|
||||
caption = dataForPage.mediaInfo.caption,
|
||||
onHeightChange = { bottomPaddingInPixels = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Top bar
|
||||
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
MediaViewerTopBar(
|
||||
actionsEnabled = state.downloadedMedia is AsyncData.Success,
|
||||
mimeType = state.mediaInfo.mimeType,
|
||||
senderName = state.mediaInfo.senderName,
|
||||
dateSent = state.mediaInfo.dateSent,
|
||||
canShowInfo = state.canShowInfo,
|
||||
onBackClick = onBackClick,
|
||||
onInfoClick = {
|
||||
state.eventSink(MediaViewerEvents.OpenInfo)
|
||||
},
|
||||
eventSink = state.eventSink
|
||||
)
|
||||
MediaViewerBottomBar(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
showDivider = state.mediaInfo.mimeType.isMimeTypeVideo(),
|
||||
caption = state.mediaInfo.caption,
|
||||
onHeightChange = { bottomPaddingInPixels = it },
|
||||
)
|
||||
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,
|
||||
canShowInfo = state.canShowInfo,
|
||||
onBackClick = onBackClick,
|
||||
onInfoClick = {
|
||||
state.eventSink(MediaViewerEvents.OpenInfo(currentData))
|
||||
},
|
||||
eventSink = state.eventSink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val bottomSheetState = state.mediaBottomSheetState) {
|
||||
MediaBottomSheetState.Hidden -> Unit
|
||||
is MediaBottomSheetState.MediaDetailsBottomSheetState -> {
|
||||
@@ -147,13 +206,24 @@ fun MediaViewerView(
|
||||
state.eventSink(MediaViewerEvents.ViewInTimeline(it))
|
||||
},
|
||||
onShare = {
|
||||
state.eventSink(MediaViewerEvents.Share)
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(MediaViewerEvents.Share(currentData))
|
||||
}
|
||||
},
|
||||
onDownload = {
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk)
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk(currentData))
|
||||
}
|
||||
},
|
||||
onDelete = { eventId ->
|
||||
state.eventSink(MediaViewerEvents.ConfirmDelete(eventId))
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(
|
||||
MediaViewerEvents.ConfirmDelete(
|
||||
eventId,
|
||||
currentData,
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(MediaViewerEvents.CloseBottomSheet)
|
||||
@@ -179,16 +249,21 @@ private fun MediaViewerPage(
|
||||
showOverlay: Boolean,
|
||||
bottomPaddingInPixels: Int,
|
||||
state: MediaViewerState,
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
onDismiss: () -> Unit,
|
||||
onShowOverlayChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onRetry() {
|
||||
state.eventSink(MediaViewerEvents.RetryLoading)
|
||||
data.eventId?.let {
|
||||
state.eventSink(MediaViewerEvents.RetryLoading(it))
|
||||
}
|
||||
}
|
||||
|
||||
fun onDismissError() {
|
||||
state.eventSink(MediaViewerEvents.ClearLoadingError)
|
||||
data.eventId?.let {
|
||||
state.eventSink(MediaViewerEvents.ClearLoadingError(it))
|
||||
}
|
||||
}
|
||||
|
||||
val currentShowOverlay by rememberUpdatedState(showOverlay)
|
||||
@@ -210,7 +285,7 @@ private fun MediaViewerPage(
|
||||
state = flickState,
|
||||
modifier = modifier.background(backgroundColorFor(flickState))
|
||||
) {
|
||||
val showProgress = rememberShowProgress(state.downloadedMedia)
|
||||
val showProgress = rememberShowProgress(data.downloadedMedia)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -224,7 +299,7 @@ private fun MediaViewerPage(
|
||||
val localMediaViewState = rememberLocalMediaViewState(zoomableState)
|
||||
val showThumbnail = !localMediaViewState.isReady
|
||||
val playableState = localMediaViewState.playableState
|
||||
val showError = state.downloadedMedia is AsyncData.Failure
|
||||
val showError = data.downloadedMedia is AsyncData.Failure
|
||||
|
||||
LaunchedEffect(playableState) {
|
||||
if (playableState is PlayableState.Playable) {
|
||||
@@ -236,8 +311,8 @@ private fun MediaViewerPage(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||
localMediaViewState = localMediaViewState,
|
||||
localMedia = state.downloadedMedia.dataOrNull(),
|
||||
mediaInfo = state.mediaInfo,
|
||||
localMedia = data.downloadedMedia.dataOrNull(),
|
||||
mediaInfo = data.mediaInfo,
|
||||
onClick = {
|
||||
if (playableState is PlayableState.NotPlayable) {
|
||||
currentOnShowOverlayChange(!currentShowOverlay)
|
||||
@@ -245,8 +320,8 @@ private fun MediaViewerPage(
|
||||
},
|
||||
)
|
||||
ThumbnailView(
|
||||
mediaInfo = state.mediaInfo,
|
||||
thumbnailSource = state.thumbnailSource,
|
||||
mediaInfo = data.mediaInfo,
|
||||
thumbnailSource = data.thumbnailSource,
|
||||
isVisible = showThumbnail,
|
||||
)
|
||||
if (showError) {
|
||||
@@ -268,6 +343,37 @@ private fun MediaViewerPage(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaViewerLoadingPage(
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false)
|
||||
|
||||
DismissFlickEffects(
|
||||
flickState = flickState,
|
||||
onDismissing = { animationDuration ->
|
||||
delay(animationDuration / 3)
|
||||
onDismiss()
|
||||
},
|
||||
onDragging = {},
|
||||
)
|
||||
|
||||
FlickToDismiss(
|
||||
state = flickState,
|
||||
modifier = modifier.background(backgroundColorFor(flickState))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AsyncLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DismissFlickEffects(
|
||||
flickState: FlickToDismissState,
|
||||
@@ -316,15 +422,16 @@ private fun rememberShowProgress(downloadedMedia: AsyncData<LocalMedia>): Boolea
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MediaViewerTopBar(
|
||||
actionsEnabled: Boolean,
|
||||
mimeType: String,
|
||||
senderName: String?,
|
||||
dateSent: String?,
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
canShowInfo: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onInfoClick: () -> Unit,
|
||||
eventSink: (MediaViewerEvents) -> Unit,
|
||||
) {
|
||||
val actionsEnabled = data.downloadedMedia is AsyncData.Success
|
||||
val mimeType = data.mediaInfo.mimeType
|
||||
val senderName = data.mediaInfo.senderName
|
||||
val dateSent = data.mediaInfo.dateSent
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (senderName != null && dateSent != null) {
|
||||
@@ -357,7 +464,7 @@ private fun MediaViewerTopBar(
|
||||
IconButton(
|
||||
enabled = actionsEnabled,
|
||||
onClick = {
|
||||
eventSink(MediaViewerEvents.OpenWith)
|
||||
eventSink(MediaViewerEvents.OpenWith(data))
|
||||
},
|
||||
) {
|
||||
when (mimeType) {
|
||||
|
||||
Reference in New Issue
Block a user