Compose GalleryCommentsScreen (#187)
* Initial Stub Compose GalleryCommentsScreen * Initial Fab and edittext * Refine comment edittext * Support load more comments * Animated loading state * Refine, cleanup migrated things * Initial comment dialog * Refine * Introduce buildAction dsl * Impl voteComment * Refine and cleanup migrated things * Impl check vote status * Refine * Refine * Padding a ime * Support send commnet * Cleanup all the code * Let edittext and sendButton padding a navbar * Let LazyColumn padding a ime * Let LazyColumn contentPadding a edittext and fab * Handle unresolved span with onCardClick * Modify how LinkifyTextView works * Adjust padding * Adjust comment textstyle --------- Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
This commit is contained in:
1 parent
747fd23b96
commit
1238edfe2b
19 files changed
+590
-977
No files matched your search
@@ -19,7 +19,7 @@ import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class GalleryCommentList(
|
||||
var comments: Array<GalleryComment>,
|
||||
data class GalleryCommentList(
|
||||
val comments: List<GalleryComment>,
|
||||
val hasMore: Boolean,
|
||||
) : Parcelable
|
||||
@@ -77,7 +77,7 @@ object GalleryDetailParser {
|
||||
private val PATTERN_FAVORITE_SLOT =
|
||||
Regex("/fav.png\\); background-position:0px -(\\d+)px")
|
||||
private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()
|
||||
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)
|
||||
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(emptyList(), false)
|
||||
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
|
||||
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
||||
private const val OFFENSIVE_STRING =
|
||||
@@ -438,7 +438,7 @@ object GalleryDetailParser {
|
||||
},
|
||||
chd!!,
|
||||
)
|
||||
GalleryCommentList(list.toTypedArray(), hasMore)
|
||||
GalleryCommentList(list, hasMore)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -352,8 +352,6 @@ private const val MAX_FAVNOTE_CHAR = 200
|
||||
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () -> Unit = {}) {
|
||||
var checked by mutableStateOf(Settings.removeImageFiles)
|
||||
awaitPermissionOrCancel(
|
||||
confirmText = android.R.string.ok,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.download_remove_dialog_title,
|
||||
onDismiss = onDismiss,
|
||||
text = {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.Spanned
|
||||
import android.text.style.ClickableSpan
|
||||
@@ -26,15 +25,8 @@ import androidx.appcompat.widget.AppCompatTextView
|
||||
class LinkifyTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
private val onCurrentSpanUpdate: (ClickableSpan?) -> Unit,
|
||||
) : AppCompatTextView(context, attrs) {
|
||||
var currentSpan: ClickableSpan? = null
|
||||
private set
|
||||
|
||||
fun clearCurrentSpan() {
|
||||
currentSpan = null
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
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
|
||||
@@ -42,10 +34,8 @@ class LinkifyTextView @JvmOverloads constructor(
|
||||
// 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) {
|
||||
currentSpan = null
|
||||
onCurrentSpanUpdate(null)
|
||||
if (text is Spanned) {
|
||||
// Get this code from android.text.method.LinkMovementMethod.
|
||||
// Work fine !
|
||||
var x = event.x.toInt()
|
||||
var y = event.y.toInt()
|
||||
x -= totalPaddingLeft
|
||||
@@ -58,7 +48,7 @@ class LinkifyTextView @JvmOverloads constructor(
|
||||
val off = layout.getOffsetForHorizontal(line, x.toFloat())
|
||||
val spans = (text as Spanned).getSpans(off, off, ClickableSpan::class.java)
|
||||
if (spans.isNotEmpty()) {
|
||||
currentSpan = spans[0]
|
||||
onCurrentSpanUpdate(spans[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ fun CookieSignInScene(navigator: DestinationsNavigator) {
|
||||
EhCookieStore.signOut()
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.get_it,
|
||||
showCancelButton = false,
|
||||
title = R.string.sign_in_failed,
|
||||
text = {
|
||||
Text(
|
||||
|
||||
@@ -122,6 +122,7 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
focusManager.clearFocus()
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.get_it,
|
||||
showCancelButton = false,
|
||||
title = R.string.sign_in_failed,
|
||||
text = {
|
||||
Text(
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.hippo.ehviewer.ui.main
|
||||
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.URLSpan
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.GalleryComment
|
||||
import com.hippo.ehviewer.ui.legacy.LinkifyTextView
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
|
||||
@Composable
|
||||
fun GalleryCommentCard(
|
||||
modifier: Modifier = Modifier,
|
||||
comment: GalleryComment,
|
||||
onCardClick: () -> Unit,
|
||||
onUserClick: () -> Unit,
|
||||
onUrlClick: (String) -> Unit,
|
||||
update: LinkifyTextView.() -> Unit,
|
||||
) = comment.run {
|
||||
var currentSpan by remember(comment) { mutableStateOf<ClickableSpan?>(null) }
|
||||
Card(
|
||||
onClick = {
|
||||
val span = currentSpan.apply { currentSpan = null }
|
||||
if (span is URLSpan) {
|
||||
onUrlClick(span.url)
|
||||
} else {
|
||||
onCardClick()
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
val keylineMargin = dimensionResource(id = R.dimen.keyline_margin)
|
||||
ConstraintLayout(modifier = Modifier.padding(horizontal = keylineMargin, vertical = 8.dp).fillMaxWidth()) {
|
||||
val (userRef, timeRef, commentRef) = createRefs()
|
||||
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),
|
||||
)
|
||||
Text(
|
||||
text = ReadableTime.getTimeAgo(time),
|
||||
modifier = Modifier.constrainAs(timeRef) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
)
|
||||
}
|
||||
AndroidView(
|
||||
factory = { context -> LinkifyTextView(context) { currentSpan = it } },
|
||||
modifier = Modifier.constrainAs(commentRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(userRef.bottom, margin = 8.dp)
|
||||
},
|
||||
update = update,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,7 +400,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
lifecycleScope.launchIO {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.delete,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.delete_favorites_dialog_title,
|
||||
onDismiss = { mAdapter!!.notifyItemChanged(position) },
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
/*
|
||||
* Copyright 2016 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.scene
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text2.BasicTextField2
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.currentRecomposeScope
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhFilter.remember
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.GalleryComment
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.dao.FilterMode
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
import com.hippo.ehviewer.ui.jumpToReaderByPage
|
||||
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
||||
import com.hippo.ehviewer.ui.main.GalleryCommentCard
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.TextUrl
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.findActivity
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import rikka.core.res.resolveColor
|
||||
|
||||
interface ActionScope {
|
||||
infix fun String.thenDo(that: suspend () -> Unit)
|
||||
}
|
||||
|
||||
private inline fun buildAction(builder: ActionScope.() -> Unit) = buildList {
|
||||
builder(object : ActionScope {
|
||||
override fun String.thenDo(that: suspend () -> Unit) {
|
||||
add(this to that)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun Context.generateComment(
|
||||
textView: TextView,
|
||||
comment: GalleryComment,
|
||||
): CharSequence {
|
||||
val sp = comment.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(textView))
|
||||
val ssb = SpannableStringBuilder(sp)
|
||||
if (0L != comment.id && 0 != comment.score) {
|
||||
val score = comment.score
|
||||
val scoreString = if (score > 0) "+$score" else score.toString()
|
||||
ssb.append(" ").inSpans(
|
||||
RelativeSizeSpan(0.8f),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
|
||||
) {
|
||||
append(scoreString)
|
||||
}
|
||||
}
|
||||
if (comment.lastEdited != 0L) {
|
||||
val str = getString(
|
||||
R.string.last_edited,
|
||||
ReadableTime.getTimeAgo(comment.lastEdited),
|
||||
)
|
||||
ssb.append("\n\n").inSpans(
|
||||
RelativeSizeSpan(0.8f),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
|
||||
) {
|
||||
append(str)
|
||||
}
|
||||
}
|
||||
return TextUrl.handleTextUrl(ssb)
|
||||
}
|
||||
|
||||
@Destination
|
||||
@Composable
|
||||
fun GalleryCommentsScreen(galleryDetail: GalleryDetail, navigator: NavController) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val dialogState = LocalDialogState.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var commenting by rememberSaveable { mutableStateOf(false) }
|
||||
var userComment by rememberSaveable { mutableStateOf("") }
|
||||
var comments by rememberSaveable { mutableStateOf(galleryDetail.comments) }
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
suspend fun refreshComment(showAll: Boolean) {
|
||||
val url = EhUrl.getGalleryDetailUrl(galleryDetail.gid, galleryDetail.token, 0, showAll)
|
||||
val detail = EhEngine.getGalleryDetail(url)
|
||||
comments = detail.comments
|
||||
}
|
||||
|
||||
val copyComment = stringResource(R.string.copy_comment_text)
|
||||
val blockCommenter = stringResource(R.string.block_commenter)
|
||||
val cancelVoteUp = stringResource(R.string.cancel_vote_up)
|
||||
val cancelVoteDown = stringResource(R.string.cancel_vote_down)
|
||||
val voteUp = stringResource(R.string.vote_up)
|
||||
val voteDown = stringResource(R.string.vote_down)
|
||||
val checkVoteStatus = stringResource(R.string.check_vote_status)
|
||||
val editCommentSuccess = stringResource(R.string.edit_comment_successfully)
|
||||
val commentSuccess = stringResource(R.string.comment_successfully)
|
||||
val editCommentFail = stringResource(R.string.edit_comment_failed)
|
||||
val commentFail = stringResource(R.string.comment_failed)
|
||||
|
||||
suspend fun Context.sendComment() {
|
||||
commenting = false
|
||||
val url = EhUrl.getGalleryDetailUrl(galleryDetail.gid, galleryDetail.token, 0, false)
|
||||
runSuspendCatching {
|
||||
EhEngine.commentGallery(url, userComment, null)
|
||||
}.onSuccess {
|
||||
findActivity<MainActivity>().showTip(
|
||||
if (false) editCommentSuccess else commentSuccess,
|
||||
BaseScene.LENGTH_SHORT,
|
||||
)
|
||||
userComment = ""
|
||||
comments = it
|
||||
}.onFailure {
|
||||
val text = if (false) editCommentFail else commentFail
|
||||
findActivity<MainActivity>().showTip(
|
||||
text + "\n" + ExceptionUtils.getReadableString(it),
|
||||
BaseScene.LENGTH_LONG,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.showFilterCommenter(comment: GalleryComment) {
|
||||
val commenter = comment.user ?: return
|
||||
dialogState.awaitPermissionOrCancel { Text(text = stringResource(R.string.filter_the_commenter, commenter)) }
|
||||
Filter(FilterMode.COMMENTER, commenter).remember()
|
||||
comments = comments.copy(comments = comments.comments.filter { it == comment })
|
||||
findActivity<MainActivity>().showTip(R.string.filter_added, BaseScene.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
suspend fun showCommentVoteStatus(comment: GalleryComment) {
|
||||
val statusStr = comment.voteState ?: return
|
||||
val data = statusStr.split(',').map {
|
||||
val str = it.trim()
|
||||
val index = str.lastIndexOf(' ')
|
||||
if (index < 0) {
|
||||
str to ""
|
||||
} else {
|
||||
str.substring(0, index).trim() to str.substring(index + 1).trim()
|
||||
}
|
||||
}
|
||||
// Wait cancellation
|
||||
dialogState.showNoButton<Unit> {
|
||||
Column {
|
||||
data.forEach { (name, vote) ->
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = name)
|
||||
},
|
||||
trailingContent = {
|
||||
Text(text = vote)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(commenting) {
|
||||
commenting = false
|
||||
}
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.gallery_comments)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navigator.popBackStack() }) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!commenting) {
|
||||
FloatingActionButton(onClick = { commenting = true }) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Default.Reply, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
val keylineMargin = dimensionResource(id = R.dimen.keyline_margin)
|
||||
var editTextMeasured by remember { mutableStateOf(0.dp) }
|
||||
Box(modifier = Modifier.fillMaxSize().imePadding()) {
|
||||
val additionalPadding = if (commenting) {
|
||||
editTextMeasured
|
||||
} else {
|
||||
if (!comments.hasMore) {
|
||||
16.dp + 56.dp // Fab space + Fab size
|
||||
} else {
|
||||
16.dp // Fab space
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(horizontal = keylineMargin),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding() + additionalPadding,
|
||||
),
|
||||
) {
|
||||
items(comments.comments) { item ->
|
||||
val recomposeScope = currentRecomposeScope
|
||||
|
||||
suspend fun Context.voteComment(comment: GalleryComment, isUp: Boolean) {
|
||||
galleryDetail.runSuspendCatching {
|
||||
EhEngine.voteComment(apiUid, apiKey, gid, token, comment.id, if (isUp) 1 else -1)
|
||||
}.onSuccess { result ->
|
||||
findActivity<MainActivity>().showTip(
|
||||
if (isUp) (if (0 != result.vote) R.string.vote_up_successfully else R.string.cancel_vote_up_successfully) else if (0 != result.vote) R.string.vote_down_successfully else R.string.cancel_vote_down_successfully,
|
||||
BaseScene.LENGTH_SHORT,
|
||||
)
|
||||
comment.score = result.score
|
||||
if (isUp) {
|
||||
comment.voteUpEd = 0 != result.vote
|
||||
comment.voteDownEd = false
|
||||
} else {
|
||||
comment.voteDownEd = 0 != result.vote
|
||||
comment.voteUpEd = false
|
||||
}
|
||||
recomposeScope.invalidate()
|
||||
}.onFailure {
|
||||
findActivity<MainActivity>().showTip(R.string.vote_failed, BaseScene.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.doCommentAction(comment: GalleryComment) {
|
||||
val actions = buildAction {
|
||||
copyComment thenDo { findActivity<MainActivity>().addTextToClipboard(comment.comment.orEmpty().parseAsHtml()) }
|
||||
if (!comment.uploader && !comment.editable) {
|
||||
blockCommenter thenDo { showFilterCommenter(comment) }
|
||||
}
|
||||
if (comment.voteUpAble) {
|
||||
(if (comment.voteUpEd) cancelVoteUp else voteUp) thenDo { voteComment(comment, true) }
|
||||
}
|
||||
if (comment.voteDownAble) {
|
||||
(if (comment.voteDownEd) cancelVoteDown else voteDown) thenDo { voteComment(comment, false) }
|
||||
}
|
||||
if (!comment.voteState.isNullOrEmpty()) {
|
||||
checkVoteStatus thenDo { showCommentVoteStatus(comment) }
|
||||
}
|
||||
}
|
||||
dialogState.showSelectItem(*actions.toTypedArray()).invoke()
|
||||
}
|
||||
|
||||
GalleryCommentCard(
|
||||
comment = item,
|
||||
onUserClick = {
|
||||
val lub = ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_UPLOADER,
|
||||
mKeyword = item.user,
|
||||
)
|
||||
navigator.navAnimated(R.id.galleryListScene, lub.toStartArgs(), true)
|
||||
},
|
||||
onCardClick = {
|
||||
coroutineScope.launch {
|
||||
context.doCommentAction(item)
|
||||
}
|
||||
},
|
||||
onUrlClick = {
|
||||
val activity = context.findActivity<MainActivity>()
|
||||
if (!activity.jumpToReaderByPage(it, galleryDetail)) {
|
||||
if (!navigator.navWithUrl(it)) {
|
||||
activity.openBrowser(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
maxLines = 5
|
||||
text = context.generateComment(this, item)
|
||||
}
|
||||
}
|
||||
if (comments.hasMore) {
|
||||
item {
|
||||
AnimatedVisibility(refreshing) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center).padding(keylineMargin),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(!refreshing) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
coroutineScope.launchIO {
|
||||
refreshing = true
|
||||
runSuspendCatching {
|
||||
refreshComment(true)
|
||||
}
|
||||
refreshing = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(keylineMargin),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.click_more_comments))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (commenting) {
|
||||
Surface(
|
||||
modifier = Modifier.align(Alignment.BottomCenter).onGloballyPositioned { coordinates ->
|
||||
editTextMeasured = with(density) { coordinates.size.height.toDp() }
|
||||
},
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth().navigationBarsPadding()) {
|
||||
BasicTextField2(
|
||||
value = userComment,
|
||||
onValueChange = { userComment = it },
|
||||
modifier = Modifier.weight(1f).padding(keylineMargin),
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launchIO {
|
||||
context.sendComment()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.CenterVertically).padding(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.Send,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryCommentsFragment : BaseScene() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
return ComposeWithMD3 {
|
||||
val galleryDetail = remember { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
|
||||
val navController = remember { findNavController() }
|
||||
GalleryCommentsScreen(galleryDetail = galleryDetail, navigator = navController)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_GALLERY_DETAIL = "gallery_detail"
|
||||
}
|
||||
}
|
||||
@@ -1,894 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 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.scene
|
||||
|
||||
import android.animation.Animator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import android.util.Log
|
||||
import android.view.ActionMode
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewAnimationUtils
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.text.set
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsAnimationCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhFilter.remember
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.GalleryComment
|
||||
import com.hippo.ehviewer.client.data.GalleryCommentList
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.parser.VoteCommentResult
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.dao.FilterMode
|
||||
import com.hippo.ehviewer.databinding.ItemDrawerFavoritesBinding
|
||||
import com.hippo.ehviewer.databinding.ItemGalleryCommentBinding
|
||||
import com.hippo.ehviewer.databinding.ItemGalleryCommentMoreBinding
|
||||
import com.hippo.ehviewer.databinding.ItemGalleryCommentProgressBinding
|
||||
import com.hippo.ehviewer.databinding.SceneGalleryCommentsBinding
|
||||
import com.hippo.ehviewer.ui.jumpToReaderByPage
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
||||
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.ViewTransition
|
||||
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
||||
import com.hippo.ehviewer.util.AnimationUtils
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.IntList
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
||||
import com.hippo.ehviewer.util.TextUrl
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.applyNavigationBarsPadding
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
import com.hippo.ehviewer.util.toBBCode
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlin.math.hypot
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import rikka.core.res.resolveColor
|
||||
|
||||
class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefreshListener {
|
||||
private var _binding: SceneGalleryCommentsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val callback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!mInAnimation && binding.editPanel.visibility == View.VISIBLE) {
|
||||
hideEditPanel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
private var mGalleryDetail: GalleryDetail? = null
|
||||
private var mAdapter: CommentAdapter? = null
|
||||
private var mViewTransition: ViewTransition? = null
|
||||
private var mSendDrawable: Drawable? = null
|
||||
private var mPencilDrawable: Drawable? = null
|
||||
private var mCommentId: Long = 0
|
||||
private var mInAnimation = false
|
||||
private var mShowAllComments = false
|
||||
private var mRefreshingComments = false
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(callback)
|
||||
if (savedInstanceState == null) {
|
||||
onInit()
|
||||
} else {
|
||||
onRestore(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleArgs(args: Bundle?) {
|
||||
if (args == null) {
|
||||
return
|
||||
}
|
||||
mGalleryDetail = args.getParcelableCompat(KEY_GALLERY_DETAIL)
|
||||
mShowAllComments =
|
||||
mGalleryDetail != null && true && !mGalleryDetail!!.comments.hasMore
|
||||
}
|
||||
|
||||
private fun onInit() {
|
||||
handleArgs(arguments)
|
||||
}
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
mGalleryDetail = savedInstanceState.getParcelableCompat(KEY_GALLERY_DETAIL)
|
||||
mShowAllComments = mGalleryDetail != null && !mGalleryDetail!!.comments.hasMore
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail)
|
||||
}
|
||||
|
||||
override fun onCreateViewWithToolbar(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = SceneGalleryCommentsBinding.inflate(inflater, container!!)
|
||||
setLiftOnScrollTargetView(binding.recyclerView)
|
||||
val tip = binding.tip
|
||||
ViewCompat.setWindowInsetsAnimationCallback(
|
||||
binding.root,
|
||||
WindowInsetsAnimationHelper(
|
||||
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
|
||||
binding.editPanel,
|
||||
binding.fabLayout,
|
||||
),
|
||||
)
|
||||
binding.refreshLayout.setOnRefreshListener(this)
|
||||
val context = requireContext()
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)
|
||||
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
tip.setCompoundDrawables(null, drawable, null, null)
|
||||
mSendDrawable = ContextCompat.getDrawable(context, R.drawable.v_send_dark_x24)
|
||||
mPencilDrawable = ContextCompat.getDrawable(context, R.drawable.v_pencil_dark_x24)
|
||||
mAdapter = CommentAdapter()
|
||||
binding.recyclerView.adapter = mAdapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
RecyclerView.VERTICAL,
|
||||
false,
|
||||
)
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
// Cancel change animator
|
||||
val itemAnimator = binding.recyclerView.itemAnimator
|
||||
if (itemAnimator is DefaultItemAnimator) {
|
||||
itemAnimator.supportsChangeAnimations = false
|
||||
}
|
||||
binding.send.setOnClickListener(this)
|
||||
binding.editText.customSelectionActionModeCallback = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
requireActivity().menuInflater.inflate(R.menu.context_comment, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
item?.let {
|
||||
val text = binding.editText.editableText
|
||||
val start = binding.editText.selectionStart
|
||||
val end = binding.editText.selectionEnd
|
||||
when (item.itemId) {
|
||||
R.id.action_bold -> text[start, end] = StyleSpan(Typeface.BOLD)
|
||||
|
||||
R.id.action_italic -> text[start, end] = StyleSpan(Typeface.ITALIC)
|
||||
|
||||
R.id.action_underline -> text[start, end] = UnderlineSpan()
|
||||
|
||||
R.id.action_strikethrough -> text[start, end] = StrikethroughSpan()
|
||||
|
||||
R.id.action_url -> {
|
||||
val oldSpans = text.getSpans<URLSpan>(start, end)
|
||||
var oldUrl = "https://"
|
||||
oldSpans.forEach {
|
||||
if (!it.url.isNullOrEmpty()) {
|
||||
oldUrl = it.url
|
||||
}
|
||||
}
|
||||
val builder = EditTextDialogBuilder(
|
||||
context,
|
||||
oldUrl,
|
||||
getString(R.string.format_url),
|
||||
)
|
||||
builder.setTitle(getString(R.string.format_url))
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
val dialog = builder.show()
|
||||
val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
button?.setOnClickListener(
|
||||
View.OnClickListener {
|
||||
val url = builder.text.trim()
|
||||
if (url.isEmpty()) {
|
||||
builder.setError(getString(R.string.text_is_empty))
|
||||
return@OnClickListener
|
||||
} else {
|
||||
builder.setError(null)
|
||||
}
|
||||
text.clearSpan(start, end, true)
|
||||
text[start, end] = URLSpan(url)
|
||||
dialog.dismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
R.id.action_clear -> {
|
||||
text.clearSpan(start, end, false)
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
mode?.finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
}
|
||||
}
|
||||
binding.fab.setOnClickListener(this)
|
||||
mViewTransition = ViewTransition(binding.refreshLayout, tip)
|
||||
updateView(false)
|
||||
binding.editPanel.applyInsetter {
|
||||
type(ime = true, navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
binding.recyclerView.applyInsetter {
|
||||
type(ime = true, navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
binding.fabLayout.applyNavigationBarsPadding()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun Spannable.clearSpan(start: Int, end: Int, url: Boolean) {
|
||||
val spans = if (url) getSpans<URLSpan>(start, end) else getSpans<CharacterStyle>(start, end)
|
||||
spans.forEach {
|
||||
val spanStart = getSpanStart(it)
|
||||
val spanEnd = getSpanEnd(it)
|
||||
removeSpan(it)
|
||||
if (spanStart < start) {
|
||||
this[spanStart, start] = it
|
||||
}
|
||||
if (spanEnd > end) {
|
||||
this[end, spanEnd] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
callback.remove()
|
||||
binding.recyclerView.stopScroll()
|
||||
mAdapter = null
|
||||
mViewTransition = null
|
||||
_binding = null
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setTitle(R.string.gallery_comments)
|
||||
setNavigationIcon(R.drawable.v_arrow_left_dark_x24)
|
||||
}
|
||||
|
||||
private fun showFilterCommenterDialog(commenter: String?, position: Int) {
|
||||
val context = context
|
||||
if (context == null || commenter == null) {
|
||||
return
|
||||
}
|
||||
BaseDialogBuilder(context)
|
||||
.setMessage(getString(R.string.filter_the_commenter, commenter))
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
Filter(FilterMode.COMMENTER, commenter).remember()
|
||||
hideComment(position)
|
||||
showTip(R.string.filter_added, LENGTH_SHORT)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun hideComment(position: Int) {
|
||||
if (mGalleryDetail == null) {
|
||||
return
|
||||
}
|
||||
val oldCommentsList = mGalleryDetail!!.comments.comments
|
||||
val newCommentsList = arrayOfNulls<GalleryComment>(
|
||||
oldCommentsList.size - 1,
|
||||
)
|
||||
var i = 0
|
||||
var j = 0
|
||||
while (i < oldCommentsList.size) {
|
||||
if (i != position) {
|
||||
newCommentsList[j] = oldCommentsList[i]
|
||||
j++
|
||||
}
|
||||
i++
|
||||
}
|
||||
mGalleryDetail!!.comments.comments = newCommentsList.requireNoNulls()
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
updateView(true)
|
||||
}
|
||||
|
||||
private fun voteComment(id: Long, vote: Int) {
|
||||
val gd = mGalleryDetail ?: return
|
||||
lifecycleScope.launchIO {
|
||||
runSuspendCatching {
|
||||
EhEngine.voteComment(gd.apiUid, gd.apiKey, gd.gid, gd.token, id, vote)
|
||||
}.onSuccess { result ->
|
||||
val voteUp = vote > 0
|
||||
showTip(
|
||||
if (voteUp) (if (0 != result.vote) R.string.vote_up_successfully else R.string.cancel_vote_up_successfully) else if (0 != result.vote) R.string.vote_down_successfully else R.string.cancel_vote_down_successfully,
|
||||
LENGTH_SHORT,
|
||||
)
|
||||
withUIContext { onVoteCommentSuccess(result, voteUp) }
|
||||
}.onFailure {
|
||||
showTip(R.string.vote_failed, LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
fun showVoteStatusDialog(context: Context, voteStatus: String) {
|
||||
val temp = voteStatus.split(',')
|
||||
val length = temp.size
|
||||
val userArray = arrayOfNulls<String>(length)
|
||||
val voteArray = arrayOfNulls<String>(length)
|
||||
for (i in 0 until length) {
|
||||
val str = temp[i].trim()
|
||||
val index = str.lastIndexOf(' ')
|
||||
if (index < 0) {
|
||||
Log.d(TAG, "Something wrong happened about vote state")
|
||||
userArray[i] = str
|
||||
voteArray[i] = ""
|
||||
} else {
|
||||
userArray[i] = str.substring(0, index).trim()
|
||||
voteArray[i] = str.substring(index + 1).trim()
|
||||
}
|
||||
}
|
||||
val builder = BaseDialogBuilder(context)
|
||||
val builderContext = builder.context
|
||||
val inflater = LayoutInflater.from(builderContext)
|
||||
val rv = inflater.inflate(R.layout.dialog_recycler_view, null) as RecyclerView
|
||||
rv.adapter = object : RecyclerView.Adapter<VoteHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VoteHolder {
|
||||
return VoteHolder(ItemDrawerFavoritesBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VoteHolder, position: Int) {
|
||||
holder.bind(userArray[position], voteArray[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return length
|
||||
}
|
||||
}
|
||||
rv.layoutManager = LinearLayoutManager(builderContext)
|
||||
rv.clipToPadding = false
|
||||
builder.setView(rv).show()
|
||||
}
|
||||
|
||||
private fun showCommentDialog(position: Int, text: CharSequence) {
|
||||
val context = context
|
||||
if (context == null || mGalleryDetail == null || position >= mGalleryDetail!!.comments.comments.size || position < 0) {
|
||||
return
|
||||
}
|
||||
val comment = mGalleryDetail!!.comments.comments[position]
|
||||
val menu: MutableList<String> = ArrayList()
|
||||
val menuId = IntList()
|
||||
val resources = context.resources
|
||||
menu.add(resources.getString(R.string.copy_comment_text))
|
||||
menuId.add(R.id.copy)
|
||||
if (!comment.uploader && !comment.editable) {
|
||||
menu.add(resources.getString(R.string.block_commenter))
|
||||
menuId.add(R.id.block_commenter)
|
||||
}
|
||||
if (comment.editable) {
|
||||
menu.add(resources.getString(R.string.edit_comment))
|
||||
menuId.add(R.id.edit_comment)
|
||||
}
|
||||
if (comment.voteUpAble) {
|
||||
menu.add(resources.getString(if (comment.voteUpEd) R.string.cancel_vote_up else R.string.vote_up))
|
||||
menuId.add(R.id.vote_up)
|
||||
}
|
||||
if (comment.voteDownAble) {
|
||||
menu.add(resources.getString(if (comment.voteDownEd) R.string.cancel_vote_down else R.string.vote_down))
|
||||
menuId.add(R.id.vote_down)
|
||||
}
|
||||
if (!comment.voteState.isNullOrEmpty()) {
|
||||
menu.add(resources.getString(R.string.check_vote_status))
|
||||
menuId.add(R.id.check_vote_status)
|
||||
}
|
||||
BaseDialogBuilder(context)
|
||||
.setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->
|
||||
if (which < 0 || which >= menuId.size) {
|
||||
return@setItems
|
||||
}
|
||||
when (menuId[which]) {
|
||||
R.id.copy -> requireActivity().addTextToClipboard(text)
|
||||
R.id.block_commenter -> showFilterCommenterDialog(comment.user, position)
|
||||
R.id.vote_up -> voteComment(comment.id, 1)
|
||||
R.id.vote_down -> voteComment(comment.id, -1)
|
||||
R.id.check_vote_status -> showVoteStatusDialog(context, comment.voteState!!)
|
||||
R.id.edit_comment -> {
|
||||
prepareEditComment(comment.id, text)
|
||||
if (!mInAnimation && binding.editPanel.visibility != View.VISIBLE) {
|
||||
showEditPanel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun onItemClick(parent: RecyclerView?, view2: View?, position: Int): Boolean {
|
||||
val activity = mainActivity ?: return false
|
||||
val holder = parent!!.getChildViewHolder(view2!!)
|
||||
if (holder is ActualCommentHolder) {
|
||||
val span = holder.binding.comment.currentSpan
|
||||
holder.binding.comment.clearCurrentSpan()
|
||||
val detail = mGalleryDetail ?: return false
|
||||
if (span is URLSpan) {
|
||||
if (!activity.jumpToReaderByPage(span.url, detail)) {
|
||||
if (!findNavController().navWithUrl(span.url)) {
|
||||
activity.openBrowser(span.url)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showCommentDialog(position, holder.sp)
|
||||
}
|
||||
} else if (holder is MoreCommentHolder && !mRefreshingComments && mAdapter != null) {
|
||||
mRefreshingComments = true
|
||||
mShowAllComments = true
|
||||
mAdapter!!.notifyItemChanged(position)
|
||||
galleryDetailUrl?.let { refreshComment(it) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun refreshComment(url: String) {
|
||||
lifecycleScope.launchIO {
|
||||
runSuspendCatching {
|
||||
EhEngine.getGalleryDetail(url)
|
||||
}.onSuccess {
|
||||
withUIContext {
|
||||
onRefreshGallerySuccess(it.comments)
|
||||
}
|
||||
}.onFailure {
|
||||
withUIContext {
|
||||
onRefreshGalleryFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateView(animation: Boolean) {
|
||||
if (null == mViewTransition) {
|
||||
return
|
||||
}
|
||||
if (mGalleryDetail == null || mGalleryDetail!!.comments.comments.isEmpty()) {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
} else {
|
||||
mViewTransition!!.showView(0, animation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareNewComment() {
|
||||
mCommentId = 0
|
||||
binding.send.setImageDrawable(mSendDrawable)
|
||||
}
|
||||
|
||||
private fun prepareEditComment(commentId: Long, text: CharSequence) {
|
||||
mCommentId = commentId
|
||||
binding.editText.setText(text)
|
||||
binding.send.setImageDrawable(mPencilDrawable)
|
||||
}
|
||||
|
||||
private fun showEditPanelWithAnimation() {
|
||||
mInAnimation = true
|
||||
binding.fab.translationX = 0.0f
|
||||
binding.fab.translationY = 0.0f
|
||||
binding.fab.scaleX = 1.0f
|
||||
binding.fab.scaleY = 1.0f
|
||||
val fabEndX = binding.editPanel.left + binding.editPanel.width / 2 - binding.fab.width / 2
|
||||
val fabEndY = binding.editPanel.top + binding.editPanel.height / 2 - binding.fab.height / 2
|
||||
binding.fab.animate().x(fabEndX.toFloat()).y(fabEndY.toFloat()).scaleX(0.0f).scaleY(0.0f)
|
||||
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
|
||||
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
(binding.fab as View).visibility = View.INVISIBLE
|
||||
binding.editPanel.visibility = View.VISIBLE
|
||||
val halfW = binding.editPanel.width / 2
|
||||
val halfH = binding.editPanel.height / 2
|
||||
val animator = ViewAnimationUtils.createCircularReveal(
|
||||
binding.editPanel,
|
||||
halfW,
|
||||
halfH,
|
||||
0f,
|
||||
hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),
|
||||
).setDuration(300L)
|
||||
animator.addListener(object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
mInAnimation = false
|
||||
}
|
||||
})
|
||||
animator.start()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
private fun showEditPanel(animation: Boolean) {
|
||||
callback.isEnabled = true
|
||||
if (animation) {
|
||||
showEditPanelWithAnimation()
|
||||
} else {
|
||||
(binding.fab as View).visibility = View.INVISIBLE
|
||||
binding.editPanel.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideEditPanelWithAnimation() {
|
||||
mInAnimation = true
|
||||
val halfW = binding.editPanel.width / 2
|
||||
val halfH = binding.editPanel.height / 2
|
||||
val animator = ViewAnimationUtils.createCircularReveal(
|
||||
binding.editPanel,
|
||||
halfW,
|
||||
halfH,
|
||||
hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),
|
||||
0.0f,
|
||||
).setDuration(300L)
|
||||
animator.addListener(object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
// Some devices may run this block in non-UI thread.
|
||||
// It might be a bug of Android OS.
|
||||
// Check it here to avoid crash.
|
||||
return
|
||||
}
|
||||
binding.editPanel.visibility = View.GONE
|
||||
(binding.fab as View).visibility = View.VISIBLE
|
||||
val fabStartX =
|
||||
binding.editPanel.left + binding.editPanel.width / 2 - binding.fab.width / 2
|
||||
val fabStartY =
|
||||
binding.editPanel.top + binding.editPanel.height / 2 - binding.fab.height / 2
|
||||
binding.fab.x = fabStartX.toFloat()
|
||||
binding.fab.y = fabStartY.toFloat()
|
||||
binding.fab.scaleX = 0.0f
|
||||
binding.fab.scaleY = 0.0f
|
||||
binding.fab.rotation = -45.0f
|
||||
binding.fab.animate().translationX(0.0f).translationY(0.0f).scaleX(1.0f)
|
||||
.scaleY(1.0f)
|
||||
.rotation(0.0f)
|
||||
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
|
||||
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
mInAnimation = false
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
})
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun hideEditPanel(animation: Boolean) {
|
||||
callback.isEnabled = false
|
||||
hideSoftInput()
|
||||
if (animation) {
|
||||
hideEditPanelWithAnimation()
|
||||
} else {
|
||||
(binding.fab as View).visibility = View.VISIBLE
|
||||
binding.editPanel.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private val galleryDetailUrl: String?
|
||||
get() = if (mGalleryDetail != null && mGalleryDetail!!.gid != -1L && mGalleryDetail!!.token != null) {
|
||||
EhUrl.getGalleryDetailUrl(
|
||||
mGalleryDetail!!.gid,
|
||||
mGalleryDetail!!.token,
|
||||
0,
|
||||
mShowAllComments,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val context = context
|
||||
val activity = mainActivity
|
||||
if (null == context || null == activity) {
|
||||
return
|
||||
}
|
||||
if (binding.fab === v) {
|
||||
if (!mInAnimation) {
|
||||
prepareNewComment()
|
||||
showEditPanel(true)
|
||||
}
|
||||
} else if (binding.send === v) {
|
||||
if (!mInAnimation) {
|
||||
val comment = binding.editText.text?.toBBCode()?.takeIf { it.isNotBlank() } ?: return
|
||||
val url = galleryDetailUrl ?: return
|
||||
lifecycleScope.launchIO {
|
||||
runSuspendCatching {
|
||||
EhEngine.commentGallery(
|
||||
url,
|
||||
comment,
|
||||
if (mCommentId != 0L) mCommentId.toString() else null,
|
||||
)
|
||||
}.onSuccess {
|
||||
showTip(
|
||||
if (mCommentId != 0L) R.string.edit_comment_successfully else R.string.comment_successfully,
|
||||
LENGTH_SHORT,
|
||||
)
|
||||
withUIContext {
|
||||
onCommentGallerySuccess(it)
|
||||
}
|
||||
}.onFailure {
|
||||
showTip(
|
||||
"""
|
||||
${getString(if (mCommentId != 0L) R.string.edit_comment_failed else R.string.comment_failed)}
|
||||
${ExceptionUtils.getReadableString(it)}
|
||||
""".trimIndent(),
|
||||
LENGTH_LONG,
|
||||
)
|
||||
}
|
||||
}
|
||||
hideSoftInput()
|
||||
hideEditPanel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRefreshGallerySuccess(result: GalleryCommentList) {
|
||||
if (mGalleryDetail == null || mAdapter == null) {
|
||||
return
|
||||
}
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
mRefreshingComments = false
|
||||
mGalleryDetail!!.comments = result
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
updateView(true)
|
||||
}
|
||||
|
||||
private fun onRefreshGalleryFailure() {
|
||||
if (mAdapter == null) {
|
||||
return
|
||||
}
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
mRefreshingComments = false
|
||||
val position = mAdapter!!.itemCount - 1
|
||||
if (position >= 0) {
|
||||
mAdapter!!.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCommentGallerySuccess(result: GalleryCommentList) {
|
||||
if (mGalleryDetail == null || mAdapter == null) {
|
||||
return
|
||||
}
|
||||
mGalleryDetail!!.comments = result
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
|
||||
// Remove text
|
||||
binding.editText.setText("")
|
||||
updateView(true)
|
||||
}
|
||||
|
||||
private fun onVoteCommentSuccess(result: VoteCommentResult, voteUp: Boolean) {
|
||||
if (mAdapter == null) {
|
||||
return
|
||||
}
|
||||
var position = -1
|
||||
var i = 0
|
||||
val n = mGalleryDetail!!.comments.comments.size
|
||||
while (i < n) {
|
||||
val comment = mGalleryDetail!!.comments.comments[i]
|
||||
if (comment.id == result.id) {
|
||||
position = i
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (-1 == position) {
|
||||
Log.d(TAG, "Can't find comment with id " + result.id)
|
||||
return
|
||||
}
|
||||
|
||||
// Update comment
|
||||
val comment = mGalleryDetail!!.comments.comments[position]
|
||||
comment.score = result.score
|
||||
if (voteUp) {
|
||||
comment.voteUpEd = 0 != result.vote
|
||||
comment.voteDownEd = false
|
||||
} else {
|
||||
comment.voteDownEd = 0 != result.vote
|
||||
comment.voteUpEd = false
|
||||
}
|
||||
mAdapter!!.notifyItemChanged(position)
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
if (!mRefreshingComments && mAdapter != null) {
|
||||
mRefreshingComments = true
|
||||
galleryDetailUrl?.let { refreshComment(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private class VoteHolder(private val binding: ItemDrawerFavoritesBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(user: String?, vote: String?) {
|
||||
binding.key.text = user
|
||||
binding.value.text = vote
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class CommentHolder(binding: ViewBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
private class MoreCommentHolder(binding: ItemGalleryCommentMoreBinding) : CommentHolder(binding)
|
||||
|
||||
private class ProgressCommentHolder(binding: ItemGalleryCommentProgressBinding) :
|
||||
CommentHolder(binding)
|
||||
|
||||
private inner class ActualCommentHolder(val binding: ItemGalleryCommentBinding) :
|
||||
CommentHolder(binding) {
|
||||
lateinit var sp: CharSequence
|
||||
|
||||
private fun generateComment(
|
||||
context: Context,
|
||||
textView: TextView,
|
||||
comment: GalleryComment,
|
||||
): CharSequence {
|
||||
sp = comment.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(textView))
|
||||
val ssb = SpannableStringBuilder(sp)
|
||||
if (0L != comment.id && 0 != comment.score) {
|
||||
val score = comment.score
|
||||
val scoreString = if (score > 0) "+$score" else score.toString()
|
||||
ssb.append(" ").inSpans(
|
||||
RelativeSizeSpan(0.8f),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
|
||||
) {
|
||||
append(scoreString)
|
||||
}
|
||||
}
|
||||
if (comment.lastEdited != 0L) {
|
||||
val str = context.getString(
|
||||
R.string.last_edited,
|
||||
ReadableTime.getTimeAgo(comment.lastEdited),
|
||||
)
|
||||
ssb.append("\n\n").inSpans(
|
||||
RelativeSizeSpan(0.8f),
|
||||
StyleSpan(Typeface.BOLD),
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
|
||||
) {
|
||||
append(str)
|
||||
}
|
||||
}
|
||||
return TextUrl.handleTextUrl(ssb)
|
||||
}
|
||||
|
||||
fun bind(value: GalleryComment) {
|
||||
binding.run {
|
||||
user.text = value.user?.let {
|
||||
if (value.uploader) getString(R.string.comment_user_uploader, it) else it
|
||||
}
|
||||
user.setOnClickListener {
|
||||
value.user?.let {
|
||||
val lub = ListUrlBuilder().apply {
|
||||
mode = ListUrlBuilder.MODE_UPLOADER
|
||||
keyword = it
|
||||
}
|
||||
navAnimated(R.id.galleryListScene, lub.toStartArgs(), true)
|
||||
}
|
||||
}
|
||||
time.text = ReadableTime.getTimeAgo(value.time)
|
||||
comment.text = generateComment(binding.comment.context, binding.comment, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CommentAdapter : RecyclerView.Adapter<CommentHolder>() {
|
||||
private val mInflater: LayoutInflater = layoutInflater
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentHolder {
|
||||
return when (viewType) {
|
||||
TYPE_COMMENT -> ActualCommentHolder(
|
||||
ItemGalleryCommentBinding.inflate(mInflater, parent, false),
|
||||
)
|
||||
|
||||
TYPE_MORE -> MoreCommentHolder(
|
||||
ItemGalleryCommentMoreBinding.inflate(mInflater, parent, false),
|
||||
)
|
||||
|
||||
TYPE_PROGRESS -> ProgressCommentHolder(
|
||||
ItemGalleryCommentProgressBinding.inflate(mInflater, parent, false),
|
||||
)
|
||||
|
||||
else -> throw IllegalStateException("Invalid view type: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: CommentHolder, position: Int) {
|
||||
val context = context
|
||||
if (context == null || mGalleryDetail == null) {
|
||||
return
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClick(
|
||||
binding.recyclerView,
|
||||
holder.itemView,
|
||||
position,
|
||||
)
|
||||
}
|
||||
holder.itemView.isClickable = true
|
||||
holder.itemView.isFocusable = true
|
||||
if (holder is ActualCommentHolder) {
|
||||
holder.bind(mGalleryDetail!!.comments.comments[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (mGalleryDetail == null) {
|
||||
0
|
||||
} else if (mGalleryDetail!!.comments.hasMore) {
|
||||
mGalleryDetail!!.comments.comments.size + 1
|
||||
} else {
|
||||
mGalleryDetail!!.comments.comments.size
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position >= mGalleryDetail!!.comments.comments.size) {
|
||||
if (mRefreshingComments) TYPE_PROGRESS else TYPE_MORE
|
||||
} else {
|
||||
TYPE_COMMENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = GalleryCommentsScene::class.java.simpleName
|
||||
const val KEY_API_UID = "api_uid"
|
||||
const val KEY_API_KEY = "api_key"
|
||||
const val KEY_GID = "gid"
|
||||
const val KEY_TOKEN = "token"
|
||||
const val KEY_COMMENT_LIST = "comment_list"
|
||||
const val KEY_GALLERY_DETAIL = "gallery_detail"
|
||||
private const val TYPE_COMMENT = 0
|
||||
private const val TYPE_MORE = 1
|
||||
private const val TYPE_PROGRESS = 2
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.app.DownloadManager
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
@@ -91,7 +90,7 @@ 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.compose.ui.viewinterop.AndroidViewBinding
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@@ -129,7 +128,6 @@ import com.hippo.ehviewer.dao.FilterMode
|
||||
import com.hippo.ehviewer.databinding.DialogArchiveListBinding
|
||||
import com.hippo.ehviewer.databinding.DialogRateBinding
|
||||
import com.hippo.ehviewer.databinding.DialogTorrentListBinding
|
||||
import com.hippo.ehviewer.databinding.ItemGalleryCommentBinding
|
||||
import com.hippo.ehviewer.download.DownloadManager as EhDownloadManager
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
@@ -138,12 +136,14 @@ import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
import com.hippo.ehviewer.ui.getFavoriteIcon
|
||||
import com.hippo.ehviewer.ui.jumpToReaderByPage
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.CheckBoxDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
||||
import com.hippo.ehviewer.ui.legacy.GalleryRatingBar.OnUserRateListener
|
||||
import com.hippo.ehviewer.ui.legacy.calculateSuitableSpanCount
|
||||
import com.hippo.ehviewer.ui.main.EhPreviewItem
|
||||
import com.hippo.ehviewer.ui.main.GalleryCommentCard
|
||||
import com.hippo.ehviewer.ui.main.GalleryDetailErrorTip
|
||||
import com.hippo.ehviewer.ui.main.GalleryDetailHeaderCard
|
||||
import com.hippo.ehviewer.ui.main.GalleryTags
|
||||
@@ -161,8 +161,8 @@ import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.IntList
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.findActivity
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.requestPermission
|
||||
@@ -330,7 +330,6 @@ class GalleryDetailScene : BaseScene() {
|
||||
lifecycleScope.launchIO {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.clear_all,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.clear_image_cache,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
|
||||
@@ -974,28 +973,15 @@ class GalleryDetailScene : BaseScene() {
|
||||
|
||||
private fun onNavigateToCommentScene() {
|
||||
val galleryDetail = composeBindingGD ?: return
|
||||
val args = Bundle()
|
||||
args.putLong(GalleryCommentsScene.KEY_API_UID, galleryDetail.apiUid)
|
||||
args.putString(
|
||||
GalleryCommentsScene.KEY_API_KEY,
|
||||
galleryDetail.apiKey,
|
||||
navAnimated(
|
||||
R.id.galleryCommentsScene,
|
||||
bundleOf(GalleryCommentsFragment.KEY_GALLERY_DETAIL to galleryDetail),
|
||||
)
|
||||
args.putLong(GalleryCommentsScene.KEY_GID, galleryDetail.gid)
|
||||
args.putString(GalleryCommentsScene.KEY_TOKEN, galleryDetail.token)
|
||||
args.putParcelable(
|
||||
GalleryCommentsScene.KEY_COMMENT_LIST,
|
||||
galleryDetail.comments,
|
||||
)
|
||||
args.putParcelable(
|
||||
GalleryCommentsScene.KEY_GALLERY_DETAIL,
|
||||
galleryDetail,
|
||||
)
|
||||
navAnimated(R.id.galleryCommentsScene, args)
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@Composable
|
||||
private fun GalleryDetailComment(commentsList: Array<GalleryComment>) {
|
||||
private fun GalleryDetailComment(commentsList: List<GalleryComment>) {
|
||||
val maxShowCount = 2
|
||||
val commentText = if (commentsList.isEmpty()) {
|
||||
stringResource(R.string.no_comments)
|
||||
@@ -1008,13 +994,27 @@ class GalleryDetailScene : BaseScene() {
|
||||
val length = maxShowCount.coerceAtMost(commentsList.size)
|
||||
for (i in 0 until length) {
|
||||
val item = commentsList[i]
|
||||
AndroidViewBinding(factory = ItemGalleryCommentBinding::inflate) {
|
||||
card.setOnClickListener { onNavigateToCommentScene() }
|
||||
user.text = item.user
|
||||
user.setBackgroundColor(Color.TRANSPARENT)
|
||||
time.text = ReadableTime.getTimeAgo(item.time)
|
||||
comment.maxLines = 5
|
||||
comment.text = item.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(comment))
|
||||
GalleryCommentCard(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
comment = item,
|
||||
onCardClick = {
|
||||
onNavigateToCommentScene()
|
||||
},
|
||||
onUserClick = {
|
||||
onNavigateToCommentScene()
|
||||
},
|
||||
onUrlClick = {
|
||||
val activity = context?.findActivity<MainActivity>() ?: return@GalleryCommentCard
|
||||
val galleryDetail = composeBindingGD ?: return@GalleryCommentCard
|
||||
if (!activity.jumpToReaderByPage(it, galleryDetail)) {
|
||||
if (!findNavController().navWithUrl(it)) {
|
||||
activity.openBrowser(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
maxLines = 5
|
||||
text = item.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(this))
|
||||
}
|
||||
}
|
||||
Box(
|
||||
|
||||
@@ -388,11 +388,7 @@ class GalleryListScene : SearchBarScene() {
|
||||
binding.tip.text = ExceptionUtils.getReadableString(state.error)
|
||||
transition.showView(2)
|
||||
if (state.error.cause is CloudflareBypassException) {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
android.R.string.ok,
|
||||
android.R.string.cancel,
|
||||
R.string.cloudflare_bypass_failed,
|
||||
) {
|
||||
dialogState.awaitPermissionOrCancel(title = R.string.cloudflare_bypass_failed) {
|
||||
Text(text = stringResource(id = R.string.open_in_webview))
|
||||
}
|
||||
navAnimated(R.id.webView, bundleOf(WebViewActivity.KEY_URL to EhUrl.host))
|
||||
|
||||
@@ -108,7 +108,6 @@ fun HistoryScreen(navigator: NavController) {
|
||||
coroutineScope.launchIO {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.clear_all,
|
||||
dismissText = android.R.string.cancel,
|
||||
text = { Text(text = stringResource(id = R.string.clear_all_history)) },
|
||||
)
|
||||
EhDB.clearHistoryInfo()
|
||||
|
||||
@@ -133,7 +133,6 @@ fun AboutScreen(navigator: DestinationsNavigator) {
|
||||
suspend fun DialogState.showNewVersion(context: Context, release: Release) {
|
||||
awaitPermissionOrCancel(
|
||||
confirmText = R.string.download,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.new_version_available,
|
||||
) {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
|
||||
@@ -199,17 +199,17 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
in 0..9 -> Settings.favCat[defaultFavSlot]
|
||||
else -> stringResource(id = R.string.default_favorites_warning)
|
||||
}
|
||||
val items = if (signin) {
|
||||
arrayOf(disabled, localFav, *Settings.favCat)
|
||||
} else {
|
||||
arrayOf(disabled, localFav)
|
||||
}
|
||||
Preference(
|
||||
title = stringResource(id = R.string.default_favorites_collection),
|
||||
summary = summary,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
|
||||
defaultFavSlot = dialogState.showSelectItem(
|
||||
disabled,
|
||||
localFav,
|
||||
*Settings.favCat.takeIf { signin } ?: emptyArray(),
|
||||
title = R.string.default_favorites_collection,
|
||||
) - 2
|
||||
}
|
||||
}
|
||||
SimpleMenuPreferenceInt(
|
||||
|
||||
@@ -169,10 +169,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.delete,
|
||||
dismissText = android.R.string.cancel,
|
||||
) {
|
||||
dialogState.awaitPermissionOrCancel(confirmText = R.string.delete) {
|
||||
Text(text = stringResource(id = R.string.delete_filter, filter.text))
|
||||
}
|
||||
filter.forget {
|
||||
|
||||
@@ -81,7 +81,6 @@ fun PrivacyScreen(navigator: DestinationsNavigator) {
|
||||
coroutineScope.launch {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.clear_all,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.clear_search_history_confirm,
|
||||
)
|
||||
searchDatabase.searchDao().clear()
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@@ -49,6 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||
import com.jamal.composeprefs3.ui.ifTrueThen
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -157,8 +159,9 @@ class DialogState {
|
||||
}
|
||||
|
||||
suspend fun awaitPermissionOrCancel(
|
||||
@StringRes confirmText: Int? = null,
|
||||
@StringRes dismissText: Int? = null,
|
||||
@StringRes confirmText: Int = android.R.string.ok,
|
||||
@StringRes dismissText: Int = android.R.string.cancel,
|
||||
showCancelButton: Boolean = true,
|
||||
@StringRes title: Int? = null,
|
||||
onDismiss: () -> Unit = {},
|
||||
text: (@Composable () -> Unit)? = null,
|
||||
@@ -171,15 +174,15 @@ class DialogState {
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { cont.resume(Unit) }) {
|
||||
Text(text = stringResource(id = confirmText ?: android.R.string.ok))
|
||||
Text(text = stringResource(id = confirmText))
|
||||
}
|
||||
},
|
||||
dismissButton = dismissText.ifNotNullThen {
|
||||
dismissButton = showCancelButton.ifTrueThen {
|
||||
TextButton(onClick = {
|
||||
onDismiss()
|
||||
cont.cancel()
|
||||
}) {
|
||||
Text(text = stringResource(id = dismissText!!))
|
||||
Text(text = stringResource(id = dismissText))
|
||||
}
|
||||
},
|
||||
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
||||
@@ -188,7 +191,7 @@ class DialogState {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <R> showNoButton(respectDefaultWidth: Boolean = true, block: @Composable DismissDialogScope<R>.() -> Unit): R {
|
||||
suspend fun <R> showNoButton(respectDefaultWidth: Boolean = true, block: @Composable DismissDialogScope<R>.() -> Unit): R {
|
||||
return dialog { cont ->
|
||||
val impl = remember(cont) {
|
||||
DismissDialogScope<R> {
|
||||
@@ -212,21 +215,31 @@ class DialogState {
|
||||
}
|
||||
|
||||
suspend fun showSelectItem(
|
||||
vararg items: String,
|
||||
vararg items: String?,
|
||||
@StringRes title: Int,
|
||||
): Int = showNoButton {
|
||||
) = showSelectItem(
|
||||
*items.filterNotNull().mapIndexed { a, b -> b to a }.toTypedArray(),
|
||||
title = title,
|
||||
)
|
||||
|
||||
suspend fun <R> showSelectItem(
|
||||
vararg items: Pair<String, R>,
|
||||
@StringRes title: Int? = null,
|
||||
): R = showNoButton {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
title.ifNotNullThen {
|
||||
Text(
|
||||
text = stringResource(id = title!!),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
LazyColumn {
|
||||
itemsIndexed(items) { index, text ->
|
||||
items(items) { (text, item) ->
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth()
|
||||
modifier = Modifier.clickable { dismissWith(item) }.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
android:label="GalleryDetailScene" />
|
||||
<fragment
|
||||
android:id="@+id/galleryCommentsScene"
|
||||
android:name="com.hippo.ehviewer.ui.scene.GalleryCommentsScene"
|
||||
android:name="com.hippo.ehviewer.ui.scene.GalleryCommentsFragment"
|
||||
android:label="GalleryCommentsScene" />
|
||||
<fragment
|
||||
android:id="@+id/galleryPreviewsScene"
|
||||
|
||||
Reference in new issue
Block a user