Render txt files content.
PdfViewer increase the top padding.
This commit is contained in:
committed by
Benoit Marty
parent
7194477465
commit
b6bbb0bc7a
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.features.viewfolder.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
fun interface TextFileViewer {
|
||||
@Composable
|
||||
fun Render(
|
||||
lines: ImmutableList<String>,
|
||||
modifier: Modifier,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.features.viewfolder.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.features.viewfolder.impl.file.ColorationMode
|
||||
import io.element.android.features.viewfolder.impl.file.FileContent
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultTextFileViewer @Inject constructor() : TextFileViewer {
|
||||
@Composable
|
||||
override fun Render(
|
||||
lines: ImmutableList<String>,
|
||||
modifier: Modifier
|
||||
) {
|
||||
FileContent(
|
||||
lines = lines,
|
||||
colorationMode = ColorationMode.None,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ fun ViewFileView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileContent(
|
||||
internal fun FileContent(
|
||||
lines: ImmutableList<String>,
|
||||
colorationMode: ColorationMode,
|
||||
modifier: Modifier = Modifier,
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
implementation(libs.telephoto.flick)
|
||||
|
||||
implementation(projects.features.viewfolder.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
|
||||
@@ -20,7 +21,9 @@ import me.saket.telephoto.zoomable.rememberZoomableState
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer {
|
||||
class DefaultLocalMediaRenderer @Inject constructor(
|
||||
private val textFileViewer: TextFileViewer,
|
||||
) : LocalMediaRenderer {
|
||||
@Composable
|
||||
override fun Render(localMedia: LocalMedia) {
|
||||
val localMediaViewState = rememberLocalMediaViewState(
|
||||
@@ -33,6 +36,7 @@ class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer {
|
||||
bottomPaddingInPixels = 0,
|
||||
localMedia = localMedia,
|
||||
localMediaViewState = localMediaViewState,
|
||||
textFileViewer = textFileViewer,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.local
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
||||
@@ -19,6 +20,7 @@ import io.element.android.libraries.mediaviewer.impl.local.audio.MediaAudioView
|
||||
import io.element.android.libraries.mediaviewer.impl.local.file.MediaFileView
|
||||
import io.element.android.libraries.mediaviewer.impl.local.image.MediaImageView
|
||||
import io.element.android.libraries.mediaviewer.impl.local.pdf.MediaPdfView
|
||||
import io.element.android.libraries.mediaviewer.impl.local.txt.TxtFileView
|
||||
import io.element.android.libraries.mediaviewer.impl.local.video.MediaVideoView
|
||||
|
||||
@Composable
|
||||
@@ -26,6 +28,7 @@ fun LocalMediaView(
|
||||
localMedia: LocalMedia?,
|
||||
bottomPaddingInPixels: Int,
|
||||
onClick: () -> Unit,
|
||||
textFileViewer: TextFileViewer,
|
||||
modifier: Modifier = Modifier,
|
||||
isDisplayed: Boolean = true,
|
||||
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
|
||||
@@ -46,6 +49,11 @@ fun LocalMediaView(
|
||||
localMedia = localMedia,
|
||||
modifier = modifier,
|
||||
)
|
||||
mimeType == MimeTypes.PlainText -> TxtFileView(
|
||||
localMedia = localMedia,
|
||||
textFileViewer = textFileViewer,
|
||||
modifier = modifier,
|
||||
)
|
||||
mimeType == MimeTypes.Pdf -> MediaPdfView(
|
||||
localMediaViewState = localMediaViewState,
|
||||
localMedia = localMedia,
|
||||
|
||||
@@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.roundToPx
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.mediaviewer.impl.viewer.topAppBarHeight
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import me.saket.telephoto.zoomable.zoomable
|
||||
@@ -126,7 +127,7 @@ private fun PdfPagesContentView(
|
||||
) {
|
||||
// Add a fake item to the top so that the first item is not at the top of the screen.
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(80.dp))
|
||||
Spacer(modifier = Modifier.height(topAppBarHeight))
|
||||
}
|
||||
items(pdfPages.size) { index ->
|
||||
val pdfPage = pdfPages[index]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.local.txt
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class TxtFileProvider : PreviewParameterProvider<AsyncData<ImmutableList<String>>> {
|
||||
override val values: Sequence<AsyncData<ImmutableList<String>>>
|
||||
get() = sequenceOf(
|
||||
AsyncData.Uninitialized,
|
||||
AsyncData.Loading(),
|
||||
AsyncData.Success(persistentListOf("Hello, World!")),
|
||||
AsyncData.Failure(Exception("Failed to load text")),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.local.txt
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.impl.viewer.topAppBarHeight
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun TxtFileView(
|
||||
localMedia: LocalMedia?,
|
||||
textFileViewer: TextFileViewer,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val data = remember { mutableStateOf<AsyncData<ImmutableList<String>>>(AsyncData.Uninitialized) }
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(localMedia?.uri) {
|
||||
data.value = AsyncData.Loading()
|
||||
if (localMedia?.uri != null) {
|
||||
// Load the file content
|
||||
val result = runCatching {
|
||||
context.contentResolver.openInputStream(localMedia.uri).use {
|
||||
it?.bufferedReader()?.readLines()?.toList().orEmpty()
|
||||
}
|
||||
}
|
||||
data.value = if (result.isSuccess) {
|
||||
AsyncData.Success(result.getOrNull().orEmpty().toImmutableList())
|
||||
} else {
|
||||
AsyncData.Failure(result.exceptionOrNull() ?: Exception("An error occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
TxtFileViewContent(
|
||||
data = data.value,
|
||||
textFileViewer = textFileViewer,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TxtFileViewContent(
|
||||
data: AsyncData<ImmutableList<String>>,
|
||||
textFileViewer: TextFileViewer,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (data) {
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading -> Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
is AsyncData.Failure -> Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = data.error.message ?: stringResource(id = CommonStrings.error_unknown))
|
||||
}
|
||||
is AsyncData.Success -> {
|
||||
textFileViewer.Render(
|
||||
lines = data.data,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = topAppBarHeight),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TxtFileViewPreview(
|
||||
@PreviewParameter(TxtFileProvider::class) text: AsyncData<ImmutableList<String>>,
|
||||
) = ElementPreview {
|
||||
TxtFileViewContent(
|
||||
data = text,
|
||||
textFileViewer = { lines, modifier ->
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = lines.firstOrNull() ?: "File content"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ForcedDarkElementTheme
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
@@ -42,6 +43,7 @@ class MediaViewerNode @AssistedInject constructor(
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
systemClock: SystemClock,
|
||||
pagerKeysHandler: PagerKeysHandler,
|
||||
private val textFileViewer: TextFileViewer,
|
||||
) : Node(buildContext, plugins = plugins),
|
||||
MediaViewerNavigator {
|
||||
private val inputs = inputs<MediaViewerEntryPoint.Params>()
|
||||
@@ -128,6 +130,7 @@ class MediaViewerNode @AssistedInject constructor(
|
||||
val state = presenter.present()
|
||||
MediaViewerView(
|
||||
state = state,
|
||||
textFileViewer = textFileViewer,
|
||||
modifier = modifier,
|
||||
onBackClick = ::onDone
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||
@@ -83,9 +84,12 @@ import me.saket.telephoto.zoomable.OverzoomEffect
|
||||
import me.saket.telephoto.zoomable.ZoomSpec
|
||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
||||
|
||||
val topAppBarHeight = 88.dp
|
||||
|
||||
@Composable
|
||||
fun MediaViewerView(
|
||||
state: MediaViewerState,
|
||||
textFileViewer: TextFileViewer,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -143,6 +147,7 @@ fun MediaViewerView(
|
||||
showOverlay = showOverlay,
|
||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||
data = dataForPage,
|
||||
textFileViewer = textFileViewer,
|
||||
onDismiss = onBackClick,
|
||||
onRetry = {
|
||||
state.eventSink(MediaViewerEvents.LoadMedia(dataForPage))
|
||||
@@ -267,6 +272,7 @@ private fun MediaViewerPage(
|
||||
showOverlay: Boolean,
|
||||
bottomPaddingInPixels: Int,
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
textFileViewer: TextFileViewer,
|
||||
onDismiss: () -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onDismissError: () -> Unit,
|
||||
@@ -316,6 +322,7 @@ private fun MediaViewerPage(
|
||||
localMediaViewState = localMediaViewState,
|
||||
localMedia = downloadedMedia.dataOrNull(),
|
||||
mediaInfo = data.mediaInfo,
|
||||
textFileViewer = textFileViewer,
|
||||
onClick = {
|
||||
if (playableState is PlayableState.NotPlayable) {
|
||||
currentOnShowOverlayChange(!currentShowOverlay)
|
||||
@@ -563,6 +570,7 @@ private fun ErrorView(
|
||||
internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = ElementPreviewDark {
|
||||
MediaViewerView(
|
||||
state = state,
|
||||
textFileViewer = { _, _ -> },
|
||||
onBackClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user