media viewer : create MediaViewerDataSource
This commit is contained in:
parent
b02c12d92a
commit
24a2458e4a
7 changed files with 255 additions and 174 deletions
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ sealed interface MediaViewerEvents {
|
||||||
data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
||||||
data class Share(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
data class Share(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
||||||
data class OpenWith(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 ViewInTimeline(val eventId: EventId) : MediaViewerEvents
|
||||||
data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
||||||
data class ConfirmDelete(
|
data class ConfirmDelete(
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,13 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ForcedDarkElementTheme
|
import io.element.android.compound.theme.ForcedDarkElementTheme
|
||||||
import io.element.android.libraries.architecture.inputs
|
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.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.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.SingleMediaGalleryDataSource
|
||||||
import io.element.android.libraries.mediaviewer.impl.gallery.TimelineMediaGalleryDataSource
|
import io.element.android.libraries.mediaviewer.impl.gallery.TimelineMediaGalleryDataSource
|
||||||
|
|
||||||
|
|
@ -30,6 +34,9 @@ class MediaViewerNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: MediaViewerPresenter.Factory,
|
presenterFactory: MediaViewerPresenter.Factory,
|
||||||
timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource,
|
timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource,
|
||||||
|
mediaLoader: MatrixMediaLoader,
|
||||||
|
localMediaFactory: LocalMediaFactory,
|
||||||
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : Node(buildContext, plugins = plugins),
|
) : Node(buildContext, plugins = plugins),
|
||||||
MediaViewerNavigator {
|
MediaViewerNavigator {
|
||||||
private val inputs = inputs<MediaViewerEntryPoint.Params>()
|
private val inputs = inputs<MediaViewerEntryPoint.Params>()
|
||||||
|
|
@ -50,14 +57,28 @@ class MediaViewerNode @AssistedInject constructor(
|
||||||
onDone()
|
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(
|
private val presenter = presenterFactory.create(
|
||||||
inputs = inputs,
|
inputs = inputs,
|
||||||
navigator = this,
|
navigator = this,
|
||||||
mediaGalleryDataSource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) {
|
dataSource = MediaViewerDataSource(
|
||||||
SingleMediaGalleryDataSource.createFrom(inputs)
|
dispatcher = coroutineDispatchers.computation,
|
||||||
} else {
|
galleryMode = galleryMode,
|
||||||
timelineMediaGalleryDataSource
|
galleryDataSource = mediaGallerySource,
|
||||||
},
|
mediaLoader = mediaLoader,
|
||||||
|
localMediaFactory = localMediaFactory
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.SnackbarMessage
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
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.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.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
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.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.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
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.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.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.mediaviewer.impl.local.LocalMediaActions
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.androidutils.R as UtilsR
|
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(
|
class MediaViewerPresenter @AssistedInject constructor(
|
||||||
@Assisted private val inputs: MediaViewerEntryPoint.Params,
|
@Assisted private val inputs: MediaViewerEntryPoint.Params,
|
||||||
@Assisted private val navigator: MediaViewerNavigator,
|
@Assisted private val navigator: MediaViewerNavigator,
|
||||||
@Assisted private val mediaGalleryDataSource: MediaGalleryDataSource,
|
@Assisted private val dataSource: MediaViewerDataSource,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val localMediaFactory: LocalMediaFactory,
|
|
||||||
private val mediaLoader: MatrixMediaLoader,
|
|
||||||
private val localMediaActions: LocalMediaActions,
|
private val localMediaActions: LocalMediaActions,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<MediaViewerState> {
|
) : Presenter<MediaViewerState> {
|
||||||
|
|
@ -68,97 +54,46 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
fun create(
|
fun create(
|
||||||
inputs: MediaViewerEntryPoint.Params,
|
inputs: MediaViewerEntryPoint.Params,
|
||||||
navigator: MediaViewerNavigator,
|
navigator: MediaViewerNavigator,
|
||||||
mediaGalleryDataSource: MediaGalleryDataSource,
|
dataSource: MediaViewerDataSource,
|
||||||
): MediaViewerPresenter
|
): MediaViewerPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val galleryMode = when (inputs.mode) {
|
|
||||||
MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
|
||||||
MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
|
|
||||||
MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): MediaViewerState {
|
override fun present(): MediaViewerState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
LaunchedEffect(Unit) {
|
val data: ImmutableList<MediaViewerPageData> by dataSource.dataFlow().collectAsState(persistentListOf())
|
||||||
mediaGalleryDataSource.start()
|
var currentIndex by remember { mutableIntStateOf(dataSource.initialPageIndex(inputs.eventId)) }
|
||||||
}
|
|
||||||
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 snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
localMediaActions.Configure()
|
|
||||||
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
|
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
dataSource.setup()
|
||||||
|
onDispose {
|
||||||
|
dataSource.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localMediaActions.Configure()
|
||||||
|
|
||||||
fun handleEvents(event: MediaViewerEvents) {
|
fun handleEvents(event: MediaViewerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is MediaViewerEvents.LoadMedia -> {
|
is MediaViewerEvents.LoadMedia -> {
|
||||||
// It's OK to suppress the warning since mediaFile and localMedia are remembered
|
coroutineScope.downloadMedia(data = event.data)
|
||||||
@Suppress("RememberMissing")
|
|
||||||
coroutineScope.downloadMedia(
|
|
||||||
data = event.data,
|
|
||||||
mediaFile = mediaFile.getOrPut(event.data.eventId) { mutableStateOf(null) },
|
|
||||||
localMedia = localMedia.getOrPut(event.data.eventId) { mutableStateOf(AsyncData.Uninitialized) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is MediaViewerEvents.ClearLoadingError -> {
|
is MediaViewerEvents.ClearLoadingError -> {
|
||||||
// It's OK to suppress the warning since localMedia is remembered
|
dataSource.clearLoadingError(event.data)
|
||||||
@Suppress("RememberMissing")
|
|
||||||
localMedia.getOrPut(event.eventId) { mutableStateOf(AsyncData.Uninitialized) }.value = AsyncData.Uninitialized
|
|
||||||
}
|
}
|
||||||
is MediaViewerEvents.SaveOnDisk -> {
|
is MediaViewerEvents.SaveOnDisk -> {
|
||||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
coroutineScope.saveOnDisk(event.data.downloadedMedia)
|
coroutineScope.saveOnDisk(event.data.downloadedMedia.value)
|
||||||
}
|
}
|
||||||
is MediaViewerEvents.Share -> {
|
is MediaViewerEvents.Share -> {
|
||||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
coroutineScope.share(event.data.downloadedMedia)
|
coroutineScope.share(event.data.downloadedMedia.value)
|
||||||
}
|
}
|
||||||
is MediaViewerEvents.OpenWith -> {
|
is MediaViewerEvents.OpenWith -> {
|
||||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
coroutineScope.open(event.data.downloadedMedia)
|
coroutineScope.open(event.data.downloadedMedia.value)
|
||||||
}
|
}
|
||||||
is MediaViewerEvents.Delete -> {
|
is MediaViewerEvents.Delete -> {
|
||||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
|
|
@ -194,7 +129,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
currentIndex = event.index
|
currentIndex = event.index
|
||||||
}
|
}
|
||||||
is MediaViewerEvents.LoadMore -> coroutineScope.launch {
|
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(
|
private fun CoroutineScope.downloadMedia(
|
||||||
data: MediaViewerPageData.MediaViewerData,
|
data: MediaViewerPageData.MediaViewerData,
|
||||||
mediaFile: MutableState<MediaFile?>,
|
|
||||||
localMedia: MutableState<AsyncData<LocalMedia>>,
|
|
||||||
) = launch {
|
) = launch {
|
||||||
localMedia.value = AsyncData.Loading()
|
dataSource.loadMedia(data)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.saveOnDisk(localMedia: AsyncData<LocalMedia>) = launch {
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.mediaviewer.impl.viewer
|
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.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
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.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.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class MediaViewerState(
|
data class MediaViewerState(
|
||||||
val listData: List<MediaViewerPageData>,
|
val listData: ImmutableList<MediaViewerPageData>,
|
||||||
val currentIndex: Int,
|
val currentIndex: Int,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canShowInfo: Boolean,
|
val canShowInfo: Boolean,
|
||||||
|
|
@ -35,6 +37,6 @@ sealed interface MediaViewerPageData {
|
||||||
val mediaInfo: MediaInfo,
|
val mediaInfo: MediaInfo,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val downloadedMedia: AsyncData<LocalMedia>,
|
val downloadedMedia: State<AsyncData<LocalMedia>>,
|
||||||
) : MediaViewerPageData
|
) : MediaViewerPageData
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.libraries.mediaviewer.impl.viewer
|
package io.element.android.libraries.mediaviewer.impl.viewer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
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.MediaBottomSheetState
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirmationState
|
import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirmationState
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
|
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
|
||||||
override val values: Sequence<MediaViewerState>
|
override val values: Sequence<MediaViewerState>
|
||||||
|
|
@ -158,7 +160,7 @@ fun aMediaViewerPageData(
|
||||||
mediaInfo = mediaInfo,
|
mediaInfo = mediaInfo,
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
thumbnailSource = null,
|
thumbnailSource = null,
|
||||||
downloadedMedia = downloadedMedia,
|
downloadedMedia = mutableStateOf(downloadedMedia),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aMediaViewerState(
|
fun aMediaViewerState(
|
||||||
|
|
@ -168,7 +170,7 @@ fun aMediaViewerState(
|
||||||
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
||||||
eventSink: (MediaViewerEvents) -> Unit = {},
|
eventSink: (MediaViewerEvents) -> Unit = {},
|
||||||
) = MediaViewerState(
|
) = MediaViewerState(
|
||||||
listData = listData,
|
listData = listData.toPersistentList(),
|
||||||
currentIndex = currentIndex,
|
currentIndex = currentIndex,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
canShowInfo = canShowInfo,
|
canShowInfo = canShowInfo,
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ import me.saket.telephoto.flick.FlickToDismissState
|
||||||
import me.saket.telephoto.flick.rememberFlickToDismissState
|
import me.saket.telephoto.flick.rememberFlickToDismissState
|
||||||
import me.saket.telephoto.zoomable.ZoomSpec
|
import me.saket.telephoto.zoomable.ZoomSpec
|
||||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
import me.saket.telephoto.zoomable.rememberZoomableState
|
||||||
|
import timber.log.Timber
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -98,7 +99,7 @@ fun MediaViewerView(
|
||||||
|
|
||||||
val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0
|
val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0
|
||||||
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
||||||
val currentData = state.listData[state.currentIndex]
|
val currentData = state.listData.getOrNull(state.currentIndex)
|
||||||
BackHandler { onBackClick() }
|
BackHandler { onBackClick() }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier,
|
modifier,
|
||||||
|
|
@ -113,6 +114,9 @@ fun MediaViewerView(
|
||||||
state.eventSink(MediaViewerEvents.OnNavigateTo(page))
|
state.eventSink(MediaViewerEvents.OnNavigateTo(page))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(state.listData) {
|
||||||
|
Timber.d("MediaViewerView: state.listData: ${state.listData}")
|
||||||
|
}
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
|
|
@ -139,10 +143,15 @@ fun MediaViewerView(
|
||||||
showOverlay = showOverlay,
|
showOverlay = showOverlay,
|
||||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||||
data = dataForPage,
|
data = dataForPage,
|
||||||
state = state,
|
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
onBackClick()
|
onBackClick()
|
||||||
},
|
},
|
||||||
|
onRetry = {
|
||||||
|
state.eventSink(MediaViewerEvents.LoadMedia(dataForPage))
|
||||||
|
},
|
||||||
|
onDismissError = {
|
||||||
|
state.eventSink(MediaViewerEvents.ClearLoadingError(dataForPage))
|
||||||
|
},
|
||||||
onShowOverlayChange = {
|
onShowOverlayChange = {
|
||||||
showOverlay = it
|
showOverlay = it
|
||||||
}
|
}
|
||||||
|
|
@ -151,8 +160,8 @@ fun MediaViewerView(
|
||||||
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
) {
|
) {
|
||||||
MediaViewerBottomBar(
|
MediaViewerBottomBar(
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
|
@ -169,19 +178,10 @@ fun MediaViewerView(
|
||||||
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
) {
|
) {
|
||||||
when (currentData) {
|
when (currentData) {
|
||||||
is MediaViewerPageData.Loading -> {
|
|
||||||
TopAppBar(
|
|
||||||
title = {},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = Color.Transparent.copy(0.6f),
|
|
||||||
),
|
|
||||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MediaViewerPageData.MediaViewerData -> {
|
is MediaViewerPageData.MediaViewerData -> {
|
||||||
MediaViewerTopBar(
|
MediaViewerTopBar(
|
||||||
data = currentData,
|
data = currentData,
|
||||||
|
|
@ -193,6 +193,15 @@ fun MediaViewerView(
|
||||||
eventSink = state.eventSink
|
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,
|
isDisplayed: Boolean,
|
||||||
showOverlay: Boolean,
|
showOverlay: Boolean,
|
||||||
bottomPaddingInPixels: Int,
|
bottomPaddingInPixels: Int,
|
||||||
state: MediaViewerState,
|
|
||||||
data: MediaViewerPageData.MediaViewerData,
|
data: MediaViewerPageData.MediaViewerData,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onDismissError: () -> Unit,
|
||||||
onShowOverlayChange: (Boolean) -> Unit,
|
onShowOverlayChange: (Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
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 currentShowOverlay by rememberUpdatedState(showOverlay)
|
||||||
val currentOnShowOverlayChange by rememberUpdatedState(onShowOverlayChange)
|
val currentOnShowOverlayChange by rememberUpdatedState(onShowOverlayChange)
|
||||||
|
|
@ -285,12 +286,13 @@ private fun MediaViewerPage(
|
||||||
state = flickState,
|
state = flickState,
|
||||||
modifier = modifier.background(backgroundColorFor(flickState))
|
modifier = modifier.background(backgroundColorFor(flickState))
|
||||||
) {
|
) {
|
||||||
val showProgress = rememberShowProgress(data.downloadedMedia)
|
val downloadedMedia by data.downloadedMedia
|
||||||
|
val showProgress = rememberShowProgress(downloadedMedia)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
val zoomableState = rememberZoomableState(
|
val zoomableState = rememberZoomableState(
|
||||||
|
|
@ -299,7 +301,7 @@ private fun MediaViewerPage(
|
||||||
val localMediaViewState = rememberLocalMediaViewState(zoomableState)
|
val localMediaViewState = rememberLocalMediaViewState(zoomableState)
|
||||||
val showThumbnail = !localMediaViewState.isReady
|
val showThumbnail = !localMediaViewState.isReady
|
||||||
val playableState = localMediaViewState.playableState
|
val playableState = localMediaViewState.playableState
|
||||||
val showError = data.downloadedMedia is AsyncData.Failure
|
val showError = downloadedMedia.isFailure()
|
||||||
|
|
||||||
LaunchedEffect(playableState) {
|
LaunchedEffect(playableState) {
|
||||||
if (playableState is PlayableState.Playable) {
|
if (playableState is PlayableState.Playable) {
|
||||||
|
|
@ -312,7 +314,7 @@ private fun MediaViewerPage(
|
||||||
isDisplayed = isDisplayed,
|
isDisplayed = isDisplayed,
|
||||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||||
localMediaViewState = localMediaViewState,
|
localMediaViewState = localMediaViewState,
|
||||||
localMedia = data.downloadedMedia.dataOrNull(),
|
localMedia = downloadedMedia.dataOrNull(),
|
||||||
mediaInfo = data.mediaInfo,
|
mediaInfo = data.mediaInfo,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (playableState is PlayableState.NotPlayable) {
|
if (playableState is PlayableState.NotPlayable) {
|
||||||
|
|
@ -328,16 +330,16 @@ private fun MediaViewerPage(
|
||||||
if (showError) {
|
if (showError) {
|
||||||
ErrorView(
|
ErrorView(
|
||||||
errorMessage = stringResource(id = CommonStrings.error_unknown),
|
errorMessage = stringResource(id = CommonStrings.error_unknown),
|
||||||
onRetry = ::onRetry,
|
onRetry = onRetry,
|
||||||
onDismiss = ::onDismissError
|
onDismiss = onDismissError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showProgress) {
|
if (showProgress) {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(2.dp)
|
.height(2.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -366,8 +368,8 @@ private fun MediaViewerLoadingPage(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding(),
|
.navigationBarsPadding(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
AsyncLoading()
|
AsyncLoading()
|
||||||
|
|
@ -429,7 +431,8 @@ private fun MediaViewerTopBar(
|
||||||
onInfoClick: () -> Unit,
|
onInfoClick: () -> Unit,
|
||||||
eventSink: (MediaViewerEvents) -> 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 mimeType = data.mediaInfo.mimeType
|
||||||
val senderName = data.mediaInfo.senderName
|
val senderName = data.mediaInfo.senderName
|
||||||
val dateSent = data.mediaInfo.dateSent
|
val dateSent = data.mediaInfo.dateSent
|
||||||
|
|
@ -503,11 +506,11 @@ private fun MediaViewerBottomBar(
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0x99101317))
|
.background(Color(0x99101317))
|
||||||
.onSizeChanged {
|
.onSizeChanged {
|
||||||
onHeightChange(it.height)
|
onHeightChange(it.height)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (caption != null) {
|
if (caption != null) {
|
||||||
if (showDivider) {
|
if (showDivider) {
|
||||||
|
|
@ -515,8 +518,8 @@ private fun MediaViewerBottomBar(
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
text = caption,
|
text = caption,
|
||||||
maxLines = 5,
|
maxLines = 5,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue