From 802b14969e84f1d563e99b1f823719e1c525605d Mon Sep 17 00:00:00 2001 From: junkfood <69683722+JunkFood02@users.noreply.github.com> Date: Sat, 3 Feb 2024 23:41:37 +0800 Subject: [PATCH] feat(ui): show full screen image viewer when clicking on images (#578) --- app/build.gradle | 3 + .../ui/component/reader/HtmlToComposable.kt | 99 +++++++++++++++---- .../ash/reader/ui/component/reader/Reader.kt | 2 + .../reader/ui/page/home/reading/Content.kt | 3 +- .../ui/page/home/reading/ReaderImagePage.kt | 75 ++++++++++++++ .../ui/page/home/reading/ReadingPage.kt | 23 +++-- 6 files changed, 178 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt diff --git a/app/build.gradle b/app/build.gradle index 691456b33..8d14c3ba3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt b/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt index 95964a897..043574ab6 100644 --- a/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt +++ b/app/src/main/java/me/ash/reader/ui/component/reader/HtmlToComposable.kt @@ -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 = {}, ) } } diff --git a/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt b/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt index 10b9d99d7..d9b668749 100644 --- a/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt +++ b/app/src/main/java/me/ash/reader/ui/component/reader/Reader.kt @@ -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 ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt index c48850ad6..dd21e8bc6 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt @@ -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) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt new file mode 100644 index 000000000..c4ea76c63 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt @@ -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) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt index dcbcd6251..4b25571a9 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt @@ -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 } + } }