feat: 楼层长按菜单

This commit is contained in:
HuanCheng65 2023-07-21 18:57:07 +08:00
parent d5f0b1c61a
commit fad05912ae
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
9 changed files with 202 additions and 115 deletions

View File

@ -159,16 +159,9 @@ private val PbContent.picUrl: String
cdnSrcActive, cdnSrcActive,
src src
) )
val List<PbContent>.plainText: String val List<PbContent>.plainText: String
get() = joinToString(separator = "") { get() = renders.joinToString("\n") { toString() }
when (it.type) {
0, 1, 4, 9, 27 -> it.text
2 -> "#(${it.c})"
3, 20 -> "[图片]"
5 -> "[视频]"
else -> ""
}
}
@OptIn(ExperimentalTextApi::class) @OptIn(ExperimentalTextApi::class)
val List<PbContent>.renders: ImmutableList<PbContentRender> val List<PbContent>.renders: ImmutableList<PbContentRender>
@ -307,6 +300,7 @@ val List<PbContent>.renders: ImmutableList<PbContentRender>
return renders.toImmutableList() return renders.toImmutableList()
} }
val Post.contentRenders: ImmutableList<PbContentRender> val Post.contentRenders: ImmutableList<PbContentRender>
get() { get() {
val renders = content.renders val renders = content.renders
@ -325,6 +319,7 @@ val Post.contentRenders: ImmutableList<PbContentRender>
} else it } else it
}.toImmutableList() }.toImmutableList()
} }
val User.bawuType: String? val User.bawuType: String?
get() = if (is_bawu == 1) { get() = if (is_bawu == 1) {
if (bawu_type == "manager") "吧主" else "小吧主" if (bawu_type == "manager") "吧主" else "小吧主"

View File

@ -64,6 +64,10 @@ data class TextContentRender(
) : PbContentRender { ) : PbContentRender {
constructor(text: String) : this(AnnotatedString(text)) constructor(text: String) : this(AnnotatedString(text))
override fun toString(): String {
return text.toString()
}
@Composable @Composable
override fun Render() { override fun Render() {
PbContentText(text = text, fontSize = 15.sp, style = MaterialTheme.typography.body1) PbContentText(text = text, fontSize = 15.sp, style = MaterialTheme.typography.body1)
@ -134,6 +138,10 @@ data class PicContentRender(
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }
override fun toString(): String {
return "[图片]"
}
} }
@Stable @Stable
@ -148,6 +156,10 @@ data class VoiceContentRender(
} }
VoicePlayer(url = voiceUrl, duration = duration) VoicePlayer(url = voiceUrl, duration = duration)
} }
override fun toString(): String {
return "[视频]"
}
} }
@Stable @Stable
@ -189,6 +201,10 @@ data class VideoContentRender(
} }
} }
} }
override fun toString(): String {
return "[语音]"
}
} }
@Composable @Composable

View File

@ -217,7 +217,6 @@ private fun HistoryItem(
) { ) {
val menuState = rememberMenuState() val menuState = rememberMenuState()
LongClickMenu( LongClickMenu(
menuState = menuState,
menuContent = { menuContent = {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
onDelete(info) onDelete(info)
@ -226,6 +225,7 @@ private fun HistoryItem(
Text(text = stringResource(id = R.string.title_delete)) Text(text = stringResource(id = R.string.title_delete))
} }
}, },
menuState = menuState,
onClick = { onClick(info) } onClick = { onClick(info) }
) { ) {
Column( Column(

View File

@ -268,7 +268,6 @@ private fun ForumItem(
} }
} }
LongClickMenu( LongClickMenu(
menuState = menuState,
menuContent = { menuContent = {
if (isTopForum) { if (isTopForum) {
DropdownMenuItem( DropdownMenuItem(
@ -306,6 +305,7 @@ private fun ForumItem(
Text(text = stringResource(id = R.string.button_unfollow)) Text(text = stringResource(id = R.string.button_unfollow))
} }
}, },
menuState = menuState,
onClick = { onClick = {
navigator.navigate(ForumPageDestination(item.forumName)) navigator.navigate(ForumPageDestination(item.forumName))
} }

View File

@ -298,7 +298,7 @@ internal fun SubPostsContent(
) )
) )
}, },
onClickContent = { onReplyClick = {
navigator.navigate( navigator.navigate(
ReplyPageDestination( ReplyPageDestination(
forumId = forumId, forumId = forumId,

View File

@ -28,8 +28,8 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -96,6 +96,7 @@ import com.huanchengfly.tieba.post.api.models.protos.SimpleForum
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.User import com.huanchengfly.tieba.post.api.models.protos.User
import com.huanchengfly.tieba.post.api.models.protos.bawuType import com.huanchengfly.tieba.post.api.models.protos.bawuType
import com.huanchengfly.tieba.post.api.models.protos.plainText
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
@ -127,6 +128,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.HorizontalDivider
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.ListMenuItem import com.huanchengfly.tieba.post.ui.widgets.compose.ListMenuItem
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.LongClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.MyBackHandler import com.huanchengfly.tieba.post.ui.widgets.compose.MyBackHandler
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
import com.huanchengfly.tieba.post.ui.widgets.compose.PromptDialog import com.huanchengfly.tieba.post.ui.widgets.compose.PromptDialog
@ -1041,7 +1043,9 @@ fun ThreadPage(
subPostContents = subPostContents[index], subPostContents = subPostContents[index],
threadAuthorId = author?.get { id } ?: 0L, threadAuthorId = author?.get { id } ?: 0L,
blocked = blocked, blocked = blocked,
canDelete = { it.author_id == user.get { id } },
immersiveMode = isImmersiveMode, immersiveMode = isImmersiveMode,
isCollected = { it.id == thread?.get { collectMarkPid.toLongOrNull() } },
onAgree = { onAgree = {
val postHasAgreed = val postHasAgreed =
item.get { agree?.hasAgree == 1 } item.get { agree?.hasAgree == 1 }
@ -1053,7 +1057,7 @@ fun ThreadPage(
) )
) )
}, },
onClickContent = { onReplyClick = {
navigator.navigate( navigator.navigate(
ReplyPageDestination( ReplyPageDestination(
forumId = curForumId ?: 0, forumId = curForumId ?: 0,
@ -1080,6 +1084,37 @@ fun ThreadPage(
) )
} }
}, },
onMenuCopyClick = {
TiebaUtil.copyText(context, it.content.plainText)
},
onMenuReportClick = {
TiebaUtil.reportPost(context, it.id.toString())
},
onMenuFavoriteClick = {
val isPostCollected =
it.id == thread?.get { collectMarkPid.toLongOrNull() }
val fid = forum?.get { id } ?: forumId
val tbs = anti?.get { tbs }
if (fid != null) {
if (isPostCollected) {
viewModel.send(
ThreadUiIntent.RemoveFavorite(
threadId = threadId,
forumId = fid,
tbs = tbs
)
)
} else {
viewModel.send(
ThreadUiIntent.AddFavorite(
threadId = threadId,
postId = it.id,
floor = it.floor
)
)
}
}
}
) )
} }
if (data.isEmpty()) { if (data.isEmpty()) {
@ -1298,11 +1333,17 @@ fun PostCard(
subPostContents: ImmutableList<AnnotatedString> = persistentListOf(), subPostContents: ImmutableList<AnnotatedString> = persistentListOf(),
threadAuthorId: Long = 0L, threadAuthorId: Long = 0L,
blocked: Boolean = false, blocked: Boolean = false,
canDelete: (Post) -> Boolean = { false },
immersiveMode: Boolean = false, immersiveMode: Boolean = false,
isCollected: (Post) -> Boolean = { false },
showSubPosts: Boolean = true, showSubPosts: Boolean = true,
onAgree: () -> Unit = {}, onAgree: () -> Unit = {},
onClickContent: (Post) -> Unit = {}, onReplyClick: (Post) -> Unit = {},
onOpenSubPosts: (subPostId: Long) -> Unit = {}, onOpenSubPosts: (subPostId: Long) -> Unit = {},
onMenuCopyClick: ((Post) -> Unit)? = null,
onMenuFavoriteClick: ((Post) -> Unit)? = null,
onMenuReportClick: ((Post) -> Unit)? = null,
onMenuDeleteClick: ((Post) -> Unit)? = null,
) { ) {
val context = LocalContext.current val context = LocalContext.current
if (blocked && !immersiveMode) { if (blocked && !immersiveMode) {
@ -1325,7 +1366,7 @@ fun PostCard(
} }
return return
} }
val (post) = postHolder val post = remember(postHolder) { postHolder.get() }
val hasPadding = remember(key1 = postHolder, key2 = immersiveMode) { val hasPadding = remember(key1 = postHolder, key2 = immersiveMode) {
postHolder.get { floor > 1 } && !immersiveMode postHolder.get { floor > 1 } && !immersiveMode
} }
@ -1343,58 +1384,92 @@ fun PostCard(
val subPosts = remember(postHolder) { val subPosts = remember(postHolder) {
post.sub_post_list?.sub_post_list?.toImmutableList() ?: persistentListOf() post.sub_post_list?.sub_post_list?.toImmutableList() ?: persistentListOf()
} }
Card( LongClickMenu(
header = { indication = null,
if (!immersiveMode) { onClick = {
UserHeader( onReplyClick(post)
avatar = { },
Avatar( menuContent = {
data = StringUtil.getAvatarUrl(author.portrait), DropdownMenuItem(onClick = { onReplyClick(post) }) {
size = Sizes.Small, Text(text = stringResource(id = R.string.btn_reply))
contentDescription = null }
) if (onMenuCopyClick != null) {
}, DropdownMenuItem(onClick = { onMenuCopyClick(post) }) {
name = { Text(text = stringResource(id = R.string.menu_copy))
UserNameText( }
userName = StringUtil.getUsernameAnnotatedString( }
LocalContext.current, if (onMenuFavoriteClick != null) {
author.name, DropdownMenuItem(onClick = { onMenuFavoriteClick(post) }) {
author.nameShow if (isCollected(post)) {
), Text(text = stringResource(id = R.string.title_collect))
userLevel = author.level_id, } else {
isLz = author.id == threadAuthorId, Text(text = stringResource(id = R.string.title_collect_on))
bawuType = author.bawuType,
)
},
desc = {
Text(text = getDescText(post.time.toLong(), post.floor, author.ip_address))
},
onClick = {
UserActivity.launch(context, author.id.toString())
}
) {
if (post.floor > 1) {
PostAgreeBtn(
hasAgreed = hasAgreed,
agreeNum = agreeNum,
onClick = onAgree
)
} }
} }
} }
}, if (onMenuReportClick != null) {
content = { DropdownMenuItem(onClick = { onMenuReportClick(post) }) {
SelectionContainer { Text(text = stringResource(id = R.string.title_report))
}
}
if (canDelete(post) && onMenuDeleteClick != null) {
DropdownMenuItem(onClick = { onMenuDeleteClick(post) }) {
Text(text = stringResource(id = R.string.title_delete))
}
}
}
) {
Card(
header = {
if (!immersiveMode) {
UserHeader(
avatar = {
Avatar(
data = StringUtil.getAvatarUrl(author.portrait),
size = Sizes.Small,
contentDescription = null
)
},
name = {
UserNameText(
userName = StringUtil.getUsernameAnnotatedString(
LocalContext.current,
author.name,
author.nameShow
),
userLevel = author.level_id,
isLz = author.id == threadAuthorId,
bawuType = author.bawuType,
)
},
desc = {
Text(
text = getDescText(
post.time.toLong(),
post.floor,
author.ip_address
)
)
},
onClick = {
UserActivity.launch(context, author.id.toString())
}
) {
if (post.floor > 1) {
PostAgreeBtn(
hasAgreed = hasAgreed,
agreeNum = agreeNum,
onClick = onAgree
)
}
}
}
},
content = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = paddingModifier modifier = paddingModifier
.fillMaxWidth() .fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onClickContent(post)
}
) { ) {
if (showTitle) { if (showTitle) {
Text( Text(
@ -1406,58 +1481,58 @@ fun PostCard(
contentRenders.forEach { it.Render() } contentRenders.forEach { it.Render() }
} }
}
if (showSubPosts && post.sub_post_number > 0 && subPostContents.isNotEmpty() && !immersiveMode) { if (showSubPosts && post.sub_post_number > 0 && subPostContents.isNotEmpty() && !immersiveMode) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then(paddingModifier) .then(paddingModifier)
.clip(RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp))
.background(ExtendedTheme.colors.floorCard) .background(ExtendedTheme.colors.floorCard)
.padding(vertical = 12.dp), .padding(vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
subPostContents.forEachIndexed { index, text -> subPostContents.forEachIndexed { index, text ->
PbContentText( PbContentText(
text = text, text = text,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
onOpenSubPosts(subPosts[index].id) onOpenSubPosts(subPosts[index].id)
} }
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
color = ExtendedTheme.colors.text, color = ExtendedTheme.colors.text,
fontSize = 13.sp, fontSize = 13.sp,
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
emoticonSize = 0.9f, emoticonSize = 0.9f,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 4, maxLines = 4,
) )
} }
if (post.sub_post_number > subPostContents.size) { if (post.sub_post_number > subPostContents.size) {
Text( Text(
text = stringResource( text = stringResource(
id = R.string.open_all_sub_posts, id = R.string.open_all_sub_posts,
post.sub_post_number post.sub_post_number
), ),
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
fontSize = 13.sp, fontSize = 13.sp,
color = ExtendedTheme.colors.accent, color = ExtendedTheme.colors.accent,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 2.dp) .padding(top = 2.dp)
.clickable { .clickable {
onOpenSubPosts(0) onOpenSubPosts(0)
} }
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
) )
}
} }
} }
} }
} )
) }
} }
@Composable @Composable

View File

@ -2,6 +2,7 @@ package com.huanchengfly.tieba.post.ui.widgets.compose
import android.util.Log import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Indication
import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@ -24,7 +25,6 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -106,19 +106,19 @@ fun ClickMenu(
} }
} }
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LongClickMenu( fun LongClickMenu(
menuContent: @Composable() (ColumnScope.() -> Unit),
modifier: Modifier = Modifier,
menuState: MenuState = rememberMenuState(), menuState: MenuState = rememberMenuState(),
menuContent: @Composable ColumnScope.() -> Unit,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
shape: Shape = RoundedCornerShape(14.dp), shape: Shape = RoundedCornerShape(14.dp),
modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
indication: Indication? = LocalIndication.current,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }
val indication = LocalIndication.current
LaunchedEffect(key1 = null) { LaunchedEffect(key1 = null) {
coroutineScope.launch { coroutineScope.launch {
interactionSource.interactions interactionSource.interactions

View File

@ -78,7 +78,6 @@ fun AccountNavIcon(
val context = LocalContext.current val context = LocalContext.current
val menuState = rememberMenuState() val menuState = rememberMenuState()
LongClickMenu( LongClickMenu(
menuState = menuState,
menuContent = { menuContent = {
val allAccounts = AccountUtil.allAccounts val allAccounts = AccountUtil.allAccounts
allAccounts.forEach { allAccounts.forEach {
@ -129,6 +128,7 @@ fun AccountNavIcon(
Text(text = stringResource(id = R.string.title_new_account)) Text(text = stringResource(id = R.string.title_new_account))
} }
}, },
menuState = menuState,
onClick = onClick, onClick = onClick,
shape = CircleShape shape = CircleShape
) { ) {

View File

@ -696,4 +696,5 @@
<string name="title_image_watermark_user_name">显示用户名</string> <string name="title_image_watermark_user_name">显示用户名</string>
<string name="title_image_watermark_forum_name">显示吧名</string> <string name="title_image_watermark_forum_name">显示吧名</string>
<string name="title_modify_username">修改用户名</string> <string name="title_modify_username">修改用户名</string>
<string name="btn_reply">回复</string>
</resources> </resources>