Render txt files content.

PdfViewer increase the top padding.
This commit is contained in:
Benoit Marty
2025-03-14 14:27:51 +01:00
committed by Benoit Marty
parent 7194477465
commit b6bbb0bc7a
12 changed files with 215 additions and 4 deletions

View File

@@ -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 {

View File

@@ -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,
)
}

View File

@@ -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
)
}
}

View File

@@ -115,7 +115,7 @@ fun ViewFileView(
}
@Composable
private fun FileContent(
internal fun FileContent(
lines: ImmutableList<String>,
colorationMode: ColorationMode,
modifier: Modifier = Modifier,

View File

@@ -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)

View File

@@ -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 = {}
)
}

View File

@@ -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,

View File

@@ -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]

View File

@@ -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")),
)
}

View File

@@ -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"
)
}
)
}

View File

@@ -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
)

View File

@@ -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 = {}
)
}