feat(ui): show full screen image viewer when clicking on images (#578)

This commit is contained in:
junkfood 2024-02-03 23:41:37 +08:00 committed by Ash
parent 57c1d3a5b7
commit 802b14969e
6 changed files with 178 additions and 27 deletions

View File

@ -127,6 +127,9 @@ dependencies {
implementation("io.coil-kt:coil-svg:$coil")
implementation("io.coil-kt:coil-gif:$coil")
// https://saket.github.io/telephoto/zoomableimage/
implementation("me.saket.telephoto:zoomable:0.7.1")
// Cancel TLSv1.3 support pre Android10
// implementation 'org.conscrypt:conscrypt-android:2.5.2'

View File

@ -68,6 +68,7 @@ fun LazyListScope.htmlFormattedText(
subheadUpperCase: Boolean = false,
baseUrl: String,
@DrawableRes imagePlaceholder: Int,
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
onLinkClick: (String) -> Unit,
) {
Jsoup.parse(inputStream, null, baseUrl)
@ -77,6 +78,7 @@ fun LazyListScope.htmlFormattedText(
element = body,
subheadUpperCase = subheadUpperCase,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -87,6 +89,7 @@ private fun LazyListScope.formatBody(
element: Element,
subheadUpperCase: Boolean = false,
@DrawableRes imagePlaceholder: Int,
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
onLinkClick: (String) -> Unit,
baseUrl: String,
) {
@ -129,6 +132,7 @@ private fun LazyListScope.formatBody(
subheadUpperCase = subheadUpperCase,
lazyListScope = this,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -139,6 +143,7 @@ private fun LazyListScope.formatBody(
private fun LazyListScope.formatCodeBlock(
element: Element,
@DrawableRes imagePlaceholder: Int,
onImageClick: ((imgUrl: String, altText: String) -> Unit)?,
onLinkClick: (String) -> Unit,
baseUrl: String,
) {
@ -175,6 +180,7 @@ private fun LazyListScope.formatCodeBlock(
element.childNodes(), preFormatted = true,
lazyListScope = this,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -188,6 +194,7 @@ private fun TextComposer.appendTextChildren(
subheadUpperCase: Boolean = false,
lazyListScope: LazyListScope,
@DrawableRes imagePlaceholder: Int,
onImageClick: ((imgUrl: String, altText: String) -> Unit)?,
onLinkClick: (String) -> Unit,
baseUrl: String,
) {
@ -225,6 +232,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -234,6 +242,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -247,7 +256,12 @@ private fun TextComposer.appendTextChildren(
withComposableStyle(
style = { h1Style().toSpanStyle() }
) {
append("\n${if (subheadUpperCase) element.text().uppercase() else element.text()}")
append(
"\n${
if (subheadUpperCase) element.text()
.uppercase() else element.text()
}"
)
}
}
}
@ -257,7 +271,12 @@ private fun TextComposer.appendTextChildren(
withComposableStyle(
style = { h2Style().toSpanStyle() }
) {
append("\n${if (subheadUpperCase) element.text().uppercase() else element.text()}")
append(
"\n${
if (subheadUpperCase) element.text()
.uppercase() else element.text()
}"
)
}
}
}
@ -267,7 +286,12 @@ private fun TextComposer.appendTextChildren(
withComposableStyle(
style = { h3Style().toSpanStyle() }
) {
append("\n${if (subheadUpperCase) element.text().uppercase() else element.text()}")
append(
"\n${
if (subheadUpperCase) element.text()
.uppercase() else element.text()
}"
)
}
}
}
@ -277,7 +301,12 @@ private fun TextComposer.appendTextChildren(
withComposableStyle(
style = { h4Style().toSpanStyle() }
) {
append("\n${if (subheadUpperCase) element.text().uppercase() else element.text()}")
append(
"\n${
if (subheadUpperCase) element.text()
.uppercase() else element.text()
}"
)
}
}
}
@ -287,7 +316,12 @@ private fun TextComposer.appendTextChildren(
withComposableStyle(
style = { h5Style().toSpanStyle() }
) {
append("\n${if (subheadUpperCase) element.text().uppercase() else element.text()}")
append(
"\n${
if (subheadUpperCase) element.text()
.uppercase() else element.text()
}"
)
}
}
}
@ -297,7 +331,12 @@ private fun TextComposer.appendTextChildren(
withComposableStyle(
style = { h6Style().toSpanStyle() }
) {
append("\n${if (subheadUpperCase) element.text().uppercase() else element.text()}")
append(
"\n${
if (subheadUpperCase) element.text()
.uppercase() else element.text()
}"
)
}
}
}
@ -310,6 +349,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -322,6 +362,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -334,6 +375,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -346,6 +388,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -358,6 +401,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -370,6 +414,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -383,6 +428,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -395,6 +441,7 @@ private fun TextComposer.appendTextChildren(
preFormatted = true,
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -406,6 +453,7 @@ private fun TextComposer.appendTextChildren(
lazyListScope.formatCodeBlock(
element = element,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -419,6 +467,7 @@ private fun TextComposer.appendTextChildren(
preFormatted = preFormatted,
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -438,6 +487,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -454,6 +504,7 @@ private fun TextComposer.appendTextChildren(
element.childNodes(),
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -478,11 +529,11 @@ private fun TextComposer.appendTextChildren(
BoxWithConstraints(
modifier = Modifier
.clip(RectangleShape)
.clickable(
enabled = onClick != null
) {
onClick?.invoke()
}
// .clickable(
// enabled = onClick != null
// ) {
// onClick?.invoke()
// }
.fillMaxWidth()
// This makes scrolling a pain, find a way to solve that
// .pointerInput("imgzoom") {
@ -497,17 +548,26 @@ private fun TextComposer.appendTextChildren(
// }
) {
val imageSize = maxImageSize()
val imgUrl = imageCandidates.getBestImageForMaxSize(
pixelDensity = pixelDensity(),
maxSize = imageSize,
)
RYAsyncImage(
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth()
.padding(horizontal = imageHorizontalPadding().dp)
.clip(imageShape())
.clickable { },
data = imageCandidates.getBestImageForMaxSize(
pixelDensity = pixelDensity(),
maxSize = imageSize,
),
.run {
if (onImageClick != null) {
this.clickable {
onImageClick(imgUrl, alt)
}
} else {
this
}
},
data = imgUrl,
contentDescription = alt,
size = imageSize,
precision = Precision.INEXACT,
@ -547,6 +607,7 @@ private fun TextComposer.appendTextChildren(
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onLinkClick = onLinkClick,
onImageClick = onImageClick,
baseUrl = baseUrl,
)
}
@ -565,6 +626,7 @@ private fun TextComposer.appendTextChildren(
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onLinkClick = onLinkClick,
onImageClick = onImageClick,
baseUrl = baseUrl,
)
}
@ -590,6 +652,7 @@ private fun TextComposer.appendTextChildren(
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onLinkClick = onLinkClick,
onImageClick = onImageClick,
baseUrl = baseUrl,
)
ensureDoubleNewline()
@ -608,6 +671,7 @@ private fun TextComposer.appendTextChildren(
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onLinkClick = onLinkClick,
onImageClick = onImageClick,
baseUrl = baseUrl,
)
terminateCurrentText()
@ -677,6 +741,7 @@ private fun TextComposer.appendTextChildren(
subheadUpperCase = subheadUpperCase,
lazyListScope = lazyListScope,
imagePlaceholder = imagePlaceholder,
onImageClick = onImageClick,
onLinkClick = onLinkClick,
baseUrl = baseUrl,
)
@ -709,7 +774,7 @@ private fun testIt() {
inputStream = stream,
baseUrl = "https://cowboyprogrammer.org",
imagePlaceholder = R.drawable.ic_telegram,
onLinkClick = {}
onLinkClick = {},
)
}
}

View File

@ -31,6 +31,7 @@ fun LazyListScope.Reader(
subheadUpperCase: Boolean = false,
link: String,
content: String,
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
onLinkClick: (String) -> Unit
) {
Log.i("RLog", "Reader: ")
@ -38,6 +39,7 @@ fun LazyListScope.Reader(
inputStream = content.byteInputStream(),
subheadUpperCase = subheadUpperCase,
baseUrl = link,
onImageClick = onImageClick,
imagePlaceholder = R.drawable.ic_launcher_foreground,
onLinkClick = onLinkClick
)

View File

@ -31,7 +31,7 @@ fun Content(
publishedDate: Date,
listState: LazyListState,
isLoading: Boolean,
isShowToolBar: Boolean,
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
) {
val context = LocalContext.current
val subheadUpperCase = LocalReadingSubheadUpperCase.current
@ -90,6 +90,7 @@ fun Content(
subheadUpperCase = subheadUpperCase.value,
link = link ?: "",
content = content,
onImageClick = onImageClick,
onLinkClick = {
context.openURL(it, openLink, openLinkSpecificBrowser)
}

View File

@ -0,0 +1,75 @@
package me.ash.reader.ui.page.home.reading
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import me.ash.reader.R
import me.ash.reader.ui.component.base.RYAsyncImage
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
import me.saket.telephoto.zoomable.zoomable
data class ImageData(val imageUrl: String = "", val altText: String = "")
@Composable
fun ReaderImageViewer(imageData: ImageData, onDismissRequest: () -> Unit = {}) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Box(
modifier = Modifier
.fillMaxSize()
// .background(Color.Black)
.windowInsetsPadding(WindowInsets.systemBars)
) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
dialogWindowProvider?.window?.setDimAmount(1f)
val zoomableState = rememberZoomableState().apply {
contentAlignment = Alignment.Center
}
RYAsyncImage(
data = imageData.imageUrl,
contentDescription = imageData.altText,
modifier = Modifier
.align(Alignment.Center)
.zoomable(zoomableState)
.fillMaxSize(),
)
IconButton(
onClick = onDismissRequest,
colors = IconButtonDefaults.iconButtonColors(
containerColor = Color.Gray.copy(alpha = 0.5f),
contentColor = Color.White
),
modifier = Modifier.padding(12.dp)
) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(id = R.string.close)
)
}
}
}
}

View File

@ -1,28 +1,25 @@
package me.ash.reader.ui.page.home.reading
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
import me.ash.reader.ui.component.base.RYScaffold
@ -31,7 +28,6 @@ import me.ash.reader.ui.ext.isScrollDown
import me.ash.reader.ui.motion.materialSharedAxisY
import me.ash.reader.ui.page.home.HomeViewModel
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ReadingPage(
navController: NavHostController,
@ -44,6 +40,9 @@ fun ReadingPage(
val homeUiState = homeViewModel.homeUiState.collectAsStateValue()
var isReaderScrollingDown by remember { mutableStateOf(false) }
var showFullScreenImageViewer by remember { mutableStateOf(false) }
var currentImageData by remember { mutableStateOf(ImageData()) }
val isShowToolBar = if (LocalReadingAutoHideToolbar.current.value) {
readingUiState.articleId != null && !isReaderScrollingDown
@ -128,7 +127,10 @@ fun ReadingPage(
publishedDate = publishedDate,
isLoading = content is ReaderState.Loading,
listState = listState,
isShowToolBar = isShowToolBar,
onImageClick = { imgUrl, altText ->
currentImageData = ImageData(imgUrl, altText)
showFullScreenImageViewer = true
}
)
}
}
@ -159,4 +161,7 @@ fun ReadingPage(
}
}
)
if (showFullScreenImageViewer) {
ReaderImageViewer(imageData = currentImageData) { showFullScreenImageViewer = false }
}
}