Revert "Kill Legacy TextView (#1786)"
This reverts commit 62927cb6
Reason for revert: Bad performance
This commit is contained in:
1 parent
31be546e8e
commit
bd246300e0
5 files changed
+155
-113
No files matched your search
@@ -0,0 +1,40 @@
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.graphics.drawable.DrawableWrapper
|
||||
import android.text.Html
|
||||
import coil3.asDrawable
|
||||
import coil3.decode.DecodeUtils
|
||||
import coil3.imageLoader
|
||||
import coil3.request.crossfade
|
||||
import coil3.size.Scale
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import kotlin.math.roundToInt
|
||||
import splitties.init.appCtx
|
||||
|
||||
private val UrlRegex = "(?:[^-]+-){2}(\\d+)-(\\d+)-[^_]+_([^.]+)".toRegex()
|
||||
|
||||
class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter {
|
||||
override fun getDrawable(source: String) = object : DrawableWrapper(null) {}.apply {
|
||||
UrlRegex.find(source)?.run {
|
||||
val srcWidth = groupValues[1].toInt()
|
||||
val srcHeight = groupValues[2].toInt()
|
||||
val dstWidth = groupValues[3].toIntOrDefault(200)
|
||||
val dstHeight = dstWidth / 2 * 3
|
||||
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT)
|
||||
setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt())
|
||||
} ?: setBounds(0, 0, 200, 300)
|
||||
with(appCtx) {
|
||||
imageLoader.enqueue(
|
||||
imageRequest {
|
||||
data(source)
|
||||
crossfade(false)
|
||||
target { image ->
|
||||
setDrawable(image.asDrawable(resources))
|
||||
onSuccess()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import android.view.MotionEvent
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.text.getSpans
|
||||
|
||||
class LinkifyTextView(
|
||||
context: Context,
|
||||
private val onUrlClick: (String) -> Unit,
|
||||
) : AppCompatTextView(context) {
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
// Let the parent or grandparent of TextView to handles click aciton.
|
||||
// Otherwise click effect like ripple will not work, and if touch area
|
||||
// do not contain a url, the TextView will still get MotionEvent.
|
||||
// onTouchEven must be called with MotionEvent.ACTION_DOWN for each touch
|
||||
// action on it, so we analyze touched url here.
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
(text as? Spanned)?.let { text ->
|
||||
layout?.let { layout ->
|
||||
val x = event.x + scrollX - totalPaddingLeft
|
||||
val y = event.y + scrollY - totalPaddingTop
|
||||
val line = layout.getLineForVertical(y.toInt())
|
||||
val off = layout.getOffsetForHorizontal(line, x.toFloat())
|
||||
val spans = text.getSpans<URLSpan>(off, off)
|
||||
if (spans.isNotEmpty()) {
|
||||
onUrlClick(spans[0].url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,34 @@
|
||||
package com.hippo.ehviewer.ui.main
|
||||
|
||||
import android.text.Html
|
||||
import android.text.TextUtils
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.neverEqualPolicy
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import coil3.compose.AsyncImage
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.text.parseAsHtml
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.GalleryComment
|
||||
import com.hippo.ehviewer.ui.tools.thenIf
|
||||
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
||||
import com.hippo.ehviewer.ui.legacy.LinkifyTextView
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
|
||||
typealias TextOrUrls = Either<Pair<String, String>, AnnotatedString>
|
||||
|
||||
private const val IMAGE_OBJ = ''
|
||||
private val IMAGE_PATTERN = Regex("(?:<a href=\"([^\"]+)\">)?<img src=\"([^\"]+)\"")
|
||||
|
||||
fun breakToTextAndUrls(origin: String, text: AnnotatedString): List<TextOrUrls> {
|
||||
val urls = IMAGE_PATTERN.findAll(origin).toList()
|
||||
if (urls.isEmpty()) return listOf(text.right())
|
||||
val iter = urls.iterator()
|
||||
var currentOfs = 0
|
||||
return buildList<TextOrUrls> {
|
||||
while (true) {
|
||||
val index = text.text.indexOf(IMAGE_OBJ, currentOfs)
|
||||
if (index == -1) break
|
||||
add(text.subSequence(currentOfs, index).right())
|
||||
add(iter.next().groupValues.let { it[1] to it[2] }.left())
|
||||
currentOfs = index + 1
|
||||
}
|
||||
add(text.subSequence(currentOfs, text.length).right())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryCommentCard(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -60,72 +37,43 @@ fun GalleryCommentCard(
|
||||
onUserClick: () -> Unit,
|
||||
onUrlClick: (String) -> Unit,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
showImage: Boolean = false,
|
||||
processComment: @Composable (GalleryComment, TextLinkStyles, LinkInteractionListener) -> AnnotatedString = { c, s, l -> AnnotatedString.fromHtml(c.comment, s, l) },
|
||||
ellipsis: Boolean = false,
|
||||
processComment: @Composable (GalleryComment, Html.ImageGetter) -> CharSequence = { c, ig -> c.comment.parseAsHtml(imageGetter = ig) },
|
||||
) = with(comment) {
|
||||
Card(onClick = onCardClick, modifier = modifier) {
|
||||
val margin = dimensionResource(id = R.dimen.keyline_margin)
|
||||
ConstraintLayout(modifier = Modifier.padding(horizontal = margin, vertical = 8.dp).fillMaxWidth()) {
|
||||
val (userRef, timeRef, commentRef) = createRefs()
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = margin, vertical = 8.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.titleSmall) {
|
||||
val userText = if (uploader) stringResource(id = R.string.comment_user_uploader, user.orEmpty()) else user.orEmpty()
|
||||
Text(
|
||||
text = userText,
|
||||
modifier = Modifier.constrainAs(userRef) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
}.clickable(onClick = onUserClick),
|
||||
modifier = Modifier.clickable(onClick = onUserClick),
|
||||
)
|
||||
Text(
|
||||
text = ReadableTime.getTimeAgo(time),
|
||||
modifier = Modifier.constrainAs(timeRef) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.constrainAs(commentRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(userRef.bottom, margin = 8.dp)
|
||||
},
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) {
|
||||
val linkColor = MaterialTheme.colorScheme.primary
|
||||
val processed = processComment(
|
||||
comment,
|
||||
TextLinkStyles(style = SpanStyle(color = linkColor)),
|
||||
) { link ->
|
||||
check(link is LinkAnnotation.Url)
|
||||
onUrlClick(link.url)
|
||||
}
|
||||
if (showImage) {
|
||||
val list = breakToTextAndUrls(comment.comment, processed)
|
||||
list.forEach {
|
||||
when (it) {
|
||||
is Either.Left -> {
|
||||
val (url, imageUrl) = it.value
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.thenIf(url.isNotEmpty()) {
|
||||
clickable { onUrlClick(url) }
|
||||
},
|
||||
)
|
||||
}
|
||||
is Either.Right -> Text(text = it.value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = processed,
|
||||
maxLines = maxLines,
|
||||
overflow = overflow,
|
||||
)
|
||||
}
|
||||
}
|
||||
val textColor = LocalContentColor.current.toArgb()
|
||||
val linkTextColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
val redrawSignal = remember { mutableStateOf(Unit, neverEqualPolicy()) }
|
||||
val commentText = processComment(comment, CoilImageGetter { redrawSignal.value = Unit })
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
LinkifyTextView(context, onUrlClick).apply {
|
||||
setTextColor(textColor)
|
||||
setLinkTextColor(linkTextColor)
|
||||
this.maxLines = maxLines
|
||||
if (ellipsis) ellipsize = TextUtils.TruncateAt.END
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(start = margin, end = margin, bottom = 8.dp),
|
||||
) { view ->
|
||||
view.text = commentText
|
||||
redrawSignal.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package com.hippo.ehviewer.ui.screen
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Html
|
||||
import android.text.Spannable
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.URLSpan
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -50,6 +57,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
@@ -60,20 +68,15 @@ import androidx.compose.ui.platform.LocalTextToolbar
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.max
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -105,8 +108,8 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.sequences.forEach
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@@ -115,31 +118,34 @@ private val URL_PATTERN = Regex("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+
|
||||
@Composable
|
||||
fun processComment(
|
||||
comment: GalleryComment,
|
||||
linkStyle: TextLinkStyles,
|
||||
onLinkClick: LinkInteractionListener,
|
||||
) = AnnotatedString.fromHtml(comment.comment, linkStyle, onLinkClick).let { text ->
|
||||
buildAnnotatedString {
|
||||
val onSurfaceVariant = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
imageGetter: Html.ImageGetter,
|
||||
) = comment.comment.parseAsHtml(imageGetter = imageGetter).let { text ->
|
||||
buildSpannedString {
|
||||
append(text)
|
||||
URL_PATTERN.findAll(text).forEach { result ->
|
||||
val start = result.range.first
|
||||
val end = result.range.last + 1
|
||||
if (!text.hasLinkAnnotations(start, end)) {
|
||||
addLink(LinkAnnotation.Url(result.groupValues[0], linkStyle, onLinkClick), start, end)
|
||||
if (getSpans<URLSpan>(start, end).isEmpty()) {
|
||||
setSpan(URLSpan(result.groupValues[0]), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
val style = SpanStyle(fontSize = 0.8f.em, fontWeight = FontWeight.Bold, color = onSurfaceVariant)
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
|
||||
val spans = arrayOf(
|
||||
RelativeSizeSpan(0.8f),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
ForegroundColorSpan(color),
|
||||
)
|
||||
if (comment.id != 0L && comment.score != 0) {
|
||||
val score = comment.score
|
||||
val scoreString = if (score > 0) "+$score" else score.toString()
|
||||
append(" ")
|
||||
withStyle(style) {
|
||||
inSpans(*spans) {
|
||||
append(scoreString)
|
||||
}
|
||||
}
|
||||
if (comment.lastEdited != 0L) {
|
||||
append("\n\n")
|
||||
withStyle(style) {
|
||||
inSpans(*spans) {
|
||||
append(
|
||||
stringResource(
|
||||
R.string.last_edited,
|
||||
@@ -386,8 +392,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
|
||||
},
|
||||
onCardClick = { launch { doCommentAction(item) } },
|
||||
onUrlClick = { if (!jumpToReaderByPage(it, galleryDetail)) if (!navWithUrl(it)) openBrowser(it) },
|
||||
processComment = { c, s, l -> processComment(c, s, l) },
|
||||
showImage = true,
|
||||
processComment = { c, ig -> processComment(c, ig) },
|
||||
)
|
||||
}
|
||||
if (comments.hasMore) {
|
||||
|
||||
@@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.LocalPinnableContainer
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -417,8 +416,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
|
||||
onUserClick = ::onNavigateToCommentScene,
|
||||
onUrlClick = { if (!jumpToReaderByPage(it, galleryDetail)) if (!navWithUrl(it)) openBrowser(it) },
|
||||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
showImage = false,
|
||||
ellipsis = true,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
|
||||
Reference in new issue
Block a user