Merge pull request #4414 from element-hq/feature/bma/openTxtDocument
Open txt document inside the application
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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.file
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun FileContent(
|
||||
lines: ImmutableList<String>,
|
||||
colorationMode: ColorationMode,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
) {
|
||||
if (lines.isEmpty()) {
|
||||
item {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_empty_file),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(
|
||||
items = lines,
|
||||
) { index, line ->
|
||||
LineRow(
|
||||
lineNumber = index + 1,
|
||||
line = line,
|
||||
colorationMode = colorationMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LineRow(
|
||||
lineNumber: Int,
|
||||
line: String,
|
||||
colorationMode: ColorationMode,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = {
|
||||
context.copyToClipboard(
|
||||
text = line,
|
||||
toastMessage = context.getString(CommonStrings.common_line_copied_to_clipboard),
|
||||
)
|
||||
})
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 36.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
text = "$lineNumber",
|
||||
textAlign = TextAlign.End,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
val color = ElementTheme.colors.textSecondary
|
||||
val width = 0.5.dp.value
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.drawWithContent {
|
||||
// Using .height(IntrinsicSize.Min) on the Row does not work well inside LazyColumn
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(0f, size.height),
|
||||
strokeWidth = width
|
||||
)
|
||||
drawContent()
|
||||
}
|
||||
.padding(horizontal = 4.dp),
|
||||
text = line,
|
||||
color = line.toColor(colorationMode),
|
||||
style = ElementTheme.typography.fontBodyMdRegular
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a line to a color.
|
||||
* Ex for logcat:
|
||||
* `01-23 13:14:50.740 25818 25818 D org.matrix.rust.sdk: elementx: SyncIndicator = Hide | RustRoomListService.kt:81`
|
||||
* ^ use this char to determine the color
|
||||
* Ex for Rust logs:
|
||||
* `2024-01-26T10:22:26.947416Z WARN elementx: Restore with non-empty map | MatrixClientsHolder.kt:68`
|
||||
* ^ use this char to determine the color, see [LogLevel]
|
||||
*/
|
||||
@Composable
|
||||
private fun String.toColor(colorationMode: ColorationMode): Color {
|
||||
return when (colorationMode) {
|
||||
ColorationMode.Logcat -> when (getOrNull(31)) {
|
||||
'D' -> colorDebug
|
||||
'I' -> colorInfo
|
||||
'W' -> colorWarning
|
||||
'E' -> colorError
|
||||
'A' -> colorError
|
||||
else -> ElementTheme.colors.textPrimary
|
||||
}
|
||||
ColorationMode.RustLogs -> when (getOrNull(32)) {
|
||||
'E' -> ElementTheme.colors.textPrimary
|
||||
'G' -> colorDebug
|
||||
'O' -> colorInfo
|
||||
'N' -> colorWarning
|
||||
'R' -> colorError
|
||||
else -> ElementTheme.colors.textPrimary
|
||||
}
|
||||
ColorationMode.None -> ElementTheme.colors.textPrimary
|
||||
}
|
||||
}
|
||||
|
||||
private val colorDebug = Color(0xFF299999)
|
||||
private val colorInfo = Color(0xFFABC023)
|
||||
private val colorWarning = Color(0xFFBBB529)
|
||||
private val colorError = Color(0xFFFF6B68)
|
||||
@@ -7,32 +7,16 @@
|
||||
|
||||
package io.element.android.features.viewfolder.impl.file
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
@@ -46,7 +30,6 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -114,125 +97,6 @@ fun ViewFileView(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileContent(
|
||||
lines: ImmutableList<String>,
|
||||
colorationMode: ColorationMode,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
) {
|
||||
if (lines.isEmpty()) {
|
||||
item {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
Text(
|
||||
text = "Empty file",
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(
|
||||
items = lines,
|
||||
) { index, line ->
|
||||
LineRow(
|
||||
lineNumber = index + 1,
|
||||
line = line,
|
||||
colorationMode = colorationMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LineRow(
|
||||
lineNumber: Int,
|
||||
line: String,
|
||||
colorationMode: ColorationMode,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = {
|
||||
context.copyToClipboard(
|
||||
line,
|
||||
"Line copied to clipboard",
|
||||
)
|
||||
})
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 36.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
text = "$lineNumber",
|
||||
textAlign = TextAlign.End,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
val color = ElementTheme.colors.textSecondary
|
||||
val width = 0.5.dp.value
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.drawWithContent {
|
||||
// Using .height(IntrinsicSize.Min) on the Row does not work well inside LazyColumn
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(0f, size.height),
|
||||
strokeWidth = width
|
||||
)
|
||||
drawContent()
|
||||
}
|
||||
.padding(horizontal = 4.dp),
|
||||
text = line,
|
||||
color = line.toColor(colorationMode),
|
||||
style = ElementTheme.typography.fontBodyMdRegular
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a line to a color.
|
||||
* Ex for logcat:
|
||||
* `01-23 13:14:50.740 25818 25818 D org.matrix.rust.sdk: elementx: SyncIndicator = Hide | RustRoomListService.kt:81`
|
||||
* ^ use this char to determine the color
|
||||
* Ex for Rust logs:
|
||||
* `2024-01-26T10:22:26.947416Z WARN elementx: Restore with non-empty map | MatrixClientsHolder.kt:68`
|
||||
* ^ use this char to determine the color, see [LogLevel]
|
||||
*/
|
||||
@Composable
|
||||
private fun String.toColor(colorationMode: ColorationMode): Color {
|
||||
return when (colorationMode) {
|
||||
ColorationMode.Logcat -> when (getOrNull(31)) {
|
||||
'D' -> colorDebug
|
||||
'I' -> colorInfo
|
||||
'W' -> colorWarning
|
||||
'E' -> colorError
|
||||
'A' -> colorError
|
||||
else -> ElementTheme.colors.textPrimary
|
||||
}
|
||||
ColorationMode.RustLogs -> when (getOrNull(32)) {
|
||||
'E' -> ElementTheme.colors.textPrimary
|
||||
'G' -> colorDebug
|
||||
'O' -> colorInfo
|
||||
'N' -> colorWarning
|
||||
'R' -> colorError
|
||||
else -> ElementTheme.colors.textPrimary
|
||||
}
|
||||
ColorationMode.None -> ElementTheme.colors.textPrimary
|
||||
}
|
||||
}
|
||||
|
||||
private val colorDebug = Color(0xFF299999)
|
||||
private val colorInfo = Color(0xFFABC023)
|
||||
private val colorWarning = Color(0xFFBBB529)
|
||||
private val colorError = Color(0xFFFF6B68)
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ViewFileViewPreview(@PreviewParameter(ViewFileStateProvider::class) state: ViewFileState) = ElementPreview {
|
||||
|
||||
@@ -156,3 +156,24 @@ fun aVoiceMediaInfo(
|
||||
waveform = waveForm,
|
||||
duration = duration,
|
||||
)
|
||||
|
||||
fun aTxtMediaInfo(
|
||||
filename: String = "a text file.txt",
|
||||
caption: String? = null,
|
||||
senderName: String? = null,
|
||||
dateSent: String? = null,
|
||||
dateSentFull: String? = null,
|
||||
): MediaInfo = MediaInfo(
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
mimeType = MimeTypes.PlainText,
|
||||
formattedFileSize = "2kB",
|
||||
fileExtension = "txt",
|
||||
senderId = UserId("@alice:server.org"),
|
||||
senderName = senderName,
|
||||
senderAvatar = null,
|
||||
dateSent = dateSent,
|
||||
dateSentFull = dateSentFull,
|
||||
waveform = null,
|
||||
duration = null,
|
||||
)
|
||||
|
||||
@@ -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.TextFileView
|
||||
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 -> TextFileView(
|
||||
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 TextFileContentProvider : 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 TextFileView(
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
TextFileContentView(
|
||||
data = data.value,
|
||||
textFileViewer = textFileViewer,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextFileContentView(
|
||||
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 TextFileContentViewPreview(
|
||||
@PreviewParameter(TextFileContentProvider::class) text: AsyncData<ImmutableList<String>>,
|
||||
) = ElementPreview {
|
||||
TextFileContentView(
|
||||
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>()
|
||||
@@ -125,6 +127,7 @@ class MediaViewerNode @AssistedInject constructor(
|
||||
val state = presenter.present()
|
||||
MediaViewerView(
|
||||
state = state,
|
||||
textFileViewer = textFileViewer,
|
||||
modifier = modifier,
|
||||
onBackClick = ::onDone
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ 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.aPdfMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.aTxtMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
||||
@@ -159,6 +160,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||
MediaViewerPageData.Failure(Exception("error"))
|
||||
),
|
||||
),
|
||||
aMediaViewerState(
|
||||
listOf(
|
||||
aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Loading(),
|
||||
mediaInfo = aTxtMediaInfo(),
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -252,6 +252,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMedia
|
||||
setContent {
|
||||
MediaViewerView(
|
||||
state = state,
|
||||
textFileViewer = { _, _ -> },
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
<string name="common_editing">"Editing"</string>
|
||||
<string name="common_editing_caption">"Editing caption"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_empty_file">"Empty file"</string>
|
||||
<string name="common_encryption">"Encryption"</string>
|
||||
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
||||
<string name="common_enter_your_pin">"Enter your PIN"</string>
|
||||
@@ -184,6 +185,7 @@ Reason: %1$s."</string>
|
||||
<string name="common_invite_unknown_profile">"This Matrix ID can\'t be found, so the invite might not be received."</string>
|
||||
<string name="common_leaving_room">"Leaving room"</string>
|
||||
<string name="common_light">"Light"</string>
|
||||
<string name="common_line_copied_to_clipboard">"Line copied to clipboard"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string>
|
||||
<string name="common_loading">"Loading…"</string>
|
||||
<string name="common_loading_more">"Loading more…"</string>
|
||||
|
||||
@@ -51,6 +51,7 @@ class KonsistClassNameTest {
|
||||
.withoutName(
|
||||
"AspectRatioProvider",
|
||||
"OverlapRatioProvider",
|
||||
"TextFileContentProvider",
|
||||
)
|
||||
.also {
|
||||
// Check that classes are actually found
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user