-
Notifications
You must be signed in to change notification settings - Fork 171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Media navigation with swipe gesture #4161
Changes from 1 commit
35cc5df
cbf7bf0
a06607f
32db42a
7df65b0
ca5c06f
8d6550b
98a786b
d26414f
bad4566
3248041
c0542c8
d6ebec8
6c904a4
aa68a35
a4cf1d7
f286f6d
428b81c
18c3b5b
621558a
e496bc3
dfda13a
bac69c4
b72f4bb
74be5a5
1580c73
77887f9
dff9005
54182b0
2fde015
afd8161
a5515b0
7dd797b
beb835c
05660fb
4d2edfa
ba0502c
da22758
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add some comments to this function? It wasn't easy to understand why passing an empty list was still necessary and the side effects this method has with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, done |
||
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coerceAtLeast(0)
here too?