33 Commits

Author SHA1 Message Date
742b8b25e8 创建群聊页面UI调整:修改创建群聊按钮正常状态下的颜色;新增禁用状态下点击时提示语 2025-12-01 18:45:08 +08:00
af3b7e7bb9 Merge pull request #95 from Kevinlinpr/zhong_1
修复BUG-我的pai coin积分、草稿箱和举报界面,上滑弹框,弹框会持续抖动
2025-12-01 10:44:58 +08:00
b11910d8a9 Merge pull request #96 from Kevinlinpr/nagisa
调整个人资料编辑简介框、修改短视频暂停图标、修复短视频界面点暂停锁屏后再解锁会自动播放视频
2025-12-01 10:44:20 +08:00
80ac0090bd 调整个人资料编辑简介框、修改短视频暂停图标、修复短视频界面点暂停锁屏后再解锁会自动播放视频 2025-11-28 18:48:43 +08:00
1e3e417035 Merge branch 'main' of github.com:Kevinlinpr/rider-pro-android-app into zhong_1 2025-11-28 18:45:40 +08:00
ca8b7b3c53 修复BUG-我的pai coin积分、草稿箱和举报界面,上滑弹框,弹框会持续抖动 2025-11-28 18:44:49 +08:00
e33c1e8aef Merge pull request #94 from Kevinlinpr/nagisa
文本
2025-11-28 17:47:20 +08:00
fcce1f863b 文本 2025-11-28 17:41:08 +08:00
04974b0a01 Merge pull request #92 from Kevinlinpr/nagisa
调整界面以及修复多个bug
2025-11-28 15:01:41 +08:00
672dc1859f Merge branch 'main' into nagisa 2025-11-28 14:59:47 +08:00
90e47fc3ce Merge pull request #93 from Kevinlinpr/zhong_1
通知界面UI调整
2025-11-28 14:51:30 +08:00
084eb7bb52 替换聊天界面发送按钮图标;
修复群聊天界面顶部不显示群头像以及组件异常显示;
修改群聊信息界面群记忆UI
2025-11-27 18:57:54 +08:00
8937ccbf56 修复多个bug、更改评论筛选
-修复搜索/动态/关注界面可以同时点赞/收藏同一个动态,使总点赞/收藏数增加
-修复个人主页上滑时动态/智能体/群聊图标会被遮挡
-评论显示不全,只显示50条内容(现在滑动到第50条评论后会出现加载更多按键)
-评论筛选改为全部和最新和热门
-修复评论筛选图标自动发生改变
2025-11-27 18:38:18 +08:00
fe78b5192b Merge branch 'main' of github.com:Kevinlinpr/rider-pro-android-app into zhong_1 2025-11-26 18:54:27 +08:00
cfe5ce8102 通知界面UI调整
修复BUG:通知界面系统栏空白区域调整合适长度,统一点赞/粉丝/评论3个页面的消息长宽和大小
2025-11-26 18:45:17 +08:00
05615ce5dc 调整界面以及修复多个bug
-调整mbti类型界面
-修复断网时,用户资料编辑保存出现闪退
-修复暂停短视频后进入后台并返回,视频会继续播放,暂停图标不会消失
-修复输入框为空时显示为英文,且个人简介布局靠上
-修复其他用户/智能体个人主页群聊列表显示我加入的群聊
-修复系统切换暗黑模式,应用会刷新,但模式不变
2025-11-26 18:44:07 +08:00
3d64cc5929 Merge pull request #90 from Kevinlinpr/zhong_1
优化代码
2025-11-26 10:18:12 +08:00
e59d4283ca Merge pull request #91 from Kevinlinpr/nagisa
界面调整、以及修复bug等
2025-11-26 10:17:51 +08:00
5b9487b60d ui调整、bug修复
-修复星座以及mbti类型选择后不需要保存就能更改成功
-mbti类型选择界面缺省图调整
-修复我的-侧边栏在滑动时会出现黑色遮罩
2025-11-25 18:12:30 +08:00
0a1601c16c 新增空状态组件文件
GalleryItem.kt、GroupChatEmptyContent.kt 、UserAgentsList.kt 3个文件都使用统一的 EmptyStateView 组件来显示空状态
消息页统一使用ChatEmptyStateView组件来显示空状态
2025-11-25 17:55:18 +08:00
c94fcd493e 界面调整、以及修复bug等
-收藏界面和动态界面添加了多图角标和视频角标
-短视频新增双击点赞和双击取消点赞功能
-修复帖子详情页的多图内容不能左右滑动图片,去掉帖子详情页多图下通过Next和Previous按钮来切换图片
-评论框界面调整
2025-11-24 18:35:51 +08:00
357790d794 优化代码
修改我的-群聊UI将群聊列表样改为网格样式
2025-11-24 18:28:42 +08:00
6d18e13826 Merge pull request #89 from Kevinlinpr/zhong_1
优化网络错误缺省图
2025-11-24 14:59:51 +08:00
9c592ee62b 优化网络错误缺省图
将缺省内容单独写成函数,替换对应标签页中的缺省内容,避免大量重复代码
2025-11-21 18:56:52 +08:00
f238a2e83f Merge pull request #87 from Kevinlinpr/nagisa
修复bug、暗色模式适配
2025-11-20 23:06:53 +08:00
882de043ee Merge pull request #88 from Kevinlinpr/zhong_1
优化通知页面、关于派派页面UI
2025-11-20 23:06:27 +08:00
1a0ed2da19 优化页面卡顿
将缺省内容单独写成函数,替换4个标签页中的缺省内容避免重复代码
2025-11-20 18:52:10 +08:00
958d1c16be 修复bug、暗色模式适配
-密码界面和删除账户界面暗色模式适配
-调整消息:所有Tab 分类下的缺省图和文案大小和位置
-修复给搜索到的帖子点赞或者收藏再退出搜索界面 去动态界面或者关注界面找到这个帖子点赞或者收藏后 点进这个帖子点赞或者收藏会变为双倍
2025-11-20 18:50:55 +08:00
e686bc3b52 优化通知页面UI
关闭列表中的顶部占位,用户名Text增加maxLines = 1和overflow = TextOverflow.Ellipsis,超长昵称会自动截断显示…。
关于派派UI调整
更换图标和文本
2025-11-20 18:47:30 +08:00
bd5079806b Merge pull request #85 from Kevinlinpr/nagisa
修复多个界面和权限相关问题
2025-11-19 23:50:17 +08:00
787c5dc574 Merge pull request #86 from Kevinlinpr/zhong_1
群聊消息页面、群聊成员页面UI调整
2025-11-19 23:49:38 +08:00
f89564e60a 群聊消息页面、群聊成员页面UI调整
进入群聊后判断当前用户是否为群主,若为群主则显示“添加成员、群资料设置、群可见性、删除成员”组件;替换组件图标
2025-11-19 18:37:43 +08:00
778c06342a 修复多个界面和权限相关问题
- 修复其他用户个人主页群聊列表显示问题
- 移除不必要的全部、公开、私有标签栏
- 修复搜索框切换类型闪退
- 修复摄像机权限被拒绝时的闪退问题
- 修复视频动态显示不全问题
- 移除标签栏触摸反馈
2025-11-19 18:30:07 +08:00
163 changed files with 2912 additions and 1934 deletions

View File

@@ -54,7 +54,7 @@
android:theme="@style/Theme.App.Starting"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:configChanges="fontScale|orientation|screenSize|keyboardHidden">
android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -73,6 +73,10 @@ class MainActivity : ComponentActivity() {
val config = Configuration(newConfig)
config.fontScale = 1.0f
super.onConfigurationChanged(config)
val isNightMode = (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
if (AppState.darkMode != isNightMode) {
syncDarkModeWithSystem(isNightMode)
}
}
// 请求通知权限
@@ -129,9 +133,7 @@ class MainActivity : ComponentActivity() {
JPushInterface.init(this)
if (AppState.darkMode) {
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
}
updateWindowBackground(AppState.darkMode)
enableEdgeToEdge()
scope.launch {
@@ -269,8 +271,22 @@ class MainActivity : ComponentActivity() {
notificationManager.createNotificationChannel(channel)
}
}
private fun syncDarkModeWithSystem(isNightMode: Boolean) {
AppState.darkMode = isNightMode
AppState.appTheme = if (isNightMode) DarkThemeColors() else LightThemeColors()
AppStore.saveDarkMode(isNightMode)
updateWindowBackground(isNightMode)
}
private fun updateWindowBackground(isDarkMode: Boolean) {
window.decorView.setBackgroundColor(
if (isDarkMode) android.graphics.Color.BLACK else android.graphics.Color.WHITE
)
}
}
val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController not provided")
}

View File

@@ -545,7 +545,13 @@ class AccountServiceImpl : AccountService {
val bannerField: MultipartBody.Part? = banner?.let {
createMultipartBody(it.file, it.filename, "banner")
}
ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
val resp = ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to update profile")
}
}
override suspend fun registerUserWithPassword(loginName: String, password: String) {

View File

@@ -410,7 +410,16 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateMomentLike(id: Int,isLike:Boolean) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
// 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.liked != isLike) {
if (isLike) 1 else -1
} else {
0
}
momentItem.copy(
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
liked = isLike
)
} else {
momentItem
}
@@ -421,7 +430,16 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
// 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.isFavorite != isFavorite) {
if (isFavorite) 1 else -1
} else {
0
}
momentItem.copy(
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
isFavorite = isFavorite
)
} else {
momentItem
}

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.entity
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer
@@ -272,19 +273,41 @@ class RoomRemoteDataSource {
pageSize: Int = 20,
search: String
): ListContainer<RoomEntity>? {
val resp = ApiClient.api.getRooms(
page = pageNumber,
pageSize = pageSize,
search = search,
roomType = "public" // 搜索时只显示公有房间
)
val body = resp.body() ?: return null
return ListContainer(
total = body.total,
page = pageNumber,
pageSize = pageSize,
list = body.list.map { it.toRoomtEntity() }
)
return try {
val resp = ApiClient.api.getRooms(
page = pageNumber,
pageSize = pageSize,
search = search,
roomType = "public" // 搜索时只显示公有房间
)
if (!resp.isSuccessful) {
// API 调用失败,返回 null
return null
}
val body = resp.body() ?: return null
// 安全地转换数据,过滤掉转换失败的项目
val roomList = body.list.mapNotNull { room ->
try {
room.toRoomtEntity()
} catch (e: Exception) {
// 如果某个房间数据转换失败,记录错误但继续处理其他房间
Log.e("RoomRemoteDataSource", "Failed to convert room: ${room.id}", e)
null
}
}
ListContainer(
total = body.total,
page = pageNumber,
pageSize = pageSize,
list = roomList
)
} catch (e: Exception) {
// 捕获所有异常,返回 null 让 PagingSource 处理
Log.e("RoomRemoteDataSource", "searchRooms error", e)
null
}
}
}
@@ -303,17 +326,31 @@ class RoomSearchPagingSource(
pageSize = params.loadSize,
search = keyword
)
LoadResult.Page(
data = rooms?.list ?: listOf(),
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
)
} catch (exception: IOException) {
if (rooms == null) {
// API 调用失败,返回空列表
LoadResult.Page(
data = emptyList(),
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = null
)
} else {
LoadResult.Page(
data = rooms.list,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (rooms.list.isNotEmpty() && rooms.list.size >= params.loadSize) currentPage + 1 else null
)
}
} catch (exception: Exception) {
// 捕获所有异常,包括 IOException、ServiceException 等
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
return state.anchorPosition
// 更健壮的实现:根据 anchorPosition 计算刷新键
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
/**
@@ -24,11 +25,16 @@ object AppStore {
.requestEmail()
.build()
googleSignInOptions = gso
// apply dark mode
if (sharedPreferences.getBoolean("darkMode", false)) {
AppState.darkMode = true
AppState.appTheme = DarkThemeColors()
// apply dark mode - 如果用户未手动设置,优先跟随系统
val hasUserPreference = sharedPreferences.contains("darkMode")
val resolvedDarkMode = if (hasUserPreference) {
sharedPreferences.getBoolean("darkMode", false)
} else {
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
currentNightMode == Configuration.UI_MODE_NIGHT_YES
}
AppState.darkMode = resolvedDarkMode
AppState.appTheme = if (resolvedDarkMode) DarkThemeColors() else LightThemeColors()
// load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.about
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -40,22 +41,21 @@ fun AboutScreen() {
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.about_rave_now),
title = stringResource(R.string.about_paipai),
moreIcon = false
)
}
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(start = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.height(48.dp))
// app icon
// app icondww
Box {
Image(
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
painter = painterResource(id = R.mipmap.invalid_name),
contentDescription = "app icon",
modifier = Modifier.size(80.dp)
)
@@ -63,7 +63,7 @@ fun AboutScreen() {
Spacer(modifier = Modifier.height(24.dp))
// app name
Text(
text = "Rave Now".uppercase(),
text = stringResource(R.string.paipai),
fontSize = 24.sp,
color = appColors.text,
fontWeight = FontWeight.ExtraBold

View File

@@ -0,0 +1,15 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@Composable
fun MbtiBottomSheetHost() {
val show = MbtiSheetManager.visible.collectAsState(false).value
if (show) {
MbtiSelectBottomSheet(
onClose = { MbtiSheetManager.close() }
)
}
}

View File

@@ -1,42 +1,59 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
// MBTI类型列表
val MBTI_TYPES = listOf(
@@ -46,96 +63,326 @@ val MBTI_TYPES = listOf(
"ISTP", "ISFP", "ESTP", "ESFP"
)
fun getMbtiImageResId(mbti: String, isDarkMode: Boolean): Int {
return when {
isDarkMode && mbti == "ENTP" -> R.mipmap.anmbti_entp
isDarkMode && mbti == "ESTP" -> R.mipmap.anmbti_estp
isDarkMode && mbti == "ENTJ" -> R.mipmap.anmbti_entj
else -> when (mbti) {
"INTJ" -> R.mipmap.mbti_intj
"INTP" -> R.mipmap.mbti_intp
"ENTJ" -> R.mipmap.mbti_entj
"ENTP" -> R.mipmap.mbti_entp
"INFJ" -> R.mipmap.mbti_infj
"INFP" -> R.mipmap.mbti_infp
"ENFJ" -> R.mipmap.mbti_enfj
"ENFP" -> R.mipmap.mbti_enfp
"ISTJ" -> R.mipmap.mbti_istj
"ISFJ" -> R.mipmap.mbti_isfj
"ESTJ" -> R.mipmap.mbti_estj
"ESFJ" -> R.mipmap.mbti_esfj
"ISTP" -> R.mipmap.mbti_istp
"ISFP" -> R.mipmap.mbti_isfp
"ESTP" -> R.mipmap.mbti_estp
"ESFP" -> R.mipmap.mbti_esfp
else -> R.mipmap.xingzuo
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MbtiSelectScreen() {
val navController = LocalNavController.current
fun MbtiSelectBottomSheet(
onClose: () -> Unit
) {
val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
val model = AccountEditViewModel
val currentMbti = model.mbti
val sheetBackgroundColor = if (isDarkMode) {
appColors.secondaryBackground
} else {
Color(0xFFFFFFFF)
}
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// 确保弹窗展开
LaunchedEffect(Unit) {
sheetState.expand()
}
// 监听状态变化,确保弹窗始终展开(防止拖拽关闭和滑动)
LaunchedEffect(sheetState.currentValue, sheetState.targetValue, sheetState.isVisible) {
// 如果弹窗被拖拽关闭或位置发生变化,立即重新展开
if (!sheetState.isVisible || sheetState.targetValue != androidx.compose.material3.SheetValue.Expanded) {
kotlinx.coroutines.delay(10) // 短暂延迟确保状态更新
sheetState.expand()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
ModalBottomSheet(
onDismissRequest = onClose,
sheetState = sheetState,
containerColor = sheetBackgroundColor,
dragHandle = null
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.choose_mbti),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(MBTI_TYPES) { mbti ->
MBTIItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
}
navController.navigateUp()
}
.fillMaxHeight(0.95f)
.offset(y = offsetY)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
Spacer(modifier = Modifier.height(8.dp))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
// 头部 - 使用 Box 实现绝对居中布局
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
contentAlignment = Alignment.Center
) {
val cancelButtonGradientColors = if (isDarkMode) {
listOf(
Color(0xFF3A3A3C),
Color(0xFF2C2C2E)
)
} else {
listOf(
Color(0xFFFFFFFF),
Color(0xFFF8F8F8)
)
}
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
// 左上角返回按钮
Row(
modifier = Modifier
.align(Alignment.CenterStart)
.height(36.dp)
.clip(RoundedCornerShape(18.dp))
.background(
brush = Brush.linearGradient(
colors = cancelButtonGradientColors
)
)
.noRippleClickable { onClose() }
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
// 左箭头图标
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = null,
modifier = Modifier.size(17.dp),
colorFilter = ColorFilter.tint(cancelButtonContentColor)
)
// "取消" 文字
Text(
text = "取消",
fontSize = 17.sp,
fontWeight = FontWeight.Medium,
color = cancelButtonContentColor,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
// 中间标题 - 绝对居中
Text(
text = stringResource(R.string.choose_mbti),
color = appColors.text,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
Spacer(Modifier.height(12.dp))
// 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 不消费任何事件,让 LazyColumn 先处理
return Offset.Zero
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
// 消费 LazyColumn 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
return available
}
override suspend fun onPreFling(available: Velocity): Velocity {
// 不消费惯性滚动,让 LazyColumn 先处理
return Velocity.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
// 消费 LazyColumn 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
return available
}
}
}
// MBTI解释文字背景色
val descriptionBackgroundColor = if (isDarkMode) {
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
} else {
Color(0xFFFAF9FB)
}
// 使用LazyColumn包裹解释文字和MBTI类型网格使它们一起滚动
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.nestedScroll(nestedScrollConnection),
contentPadding = PaddingValues(
start = 8.dp,
top = 0.dp,
end = 8.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// MBTI解释文字 - 作为第一个item
item {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(descriptionBackgroundColor)
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
text = stringResource(R.string.mbti_description),
color = appColors.text,
fontSize = 14.sp,
lineHeight = 20.sp
)
}
}
// MBTI类型网格 - 手动创建2列网格布局
itemsIndexed(MBTI_TYPES.chunked(2)) { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = if (rowIndex < MBTI_TYPES.chunked(2).size - 1) 10.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
rowItems.forEachIndexed { colIndex, mbti ->
Box(
modifier = Modifier.weight(1f)
) {
MbtiItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
// 保存MBTI类型
model.mbti = mbti
onClose()
}
)
}
}
// 如果这一行只有1个item添加一个空的Spacer来保持布局
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
}
}
}
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MBTIItem(
fun MbtiSelectScreen() {
val navController = LocalNavController.current
MbtiSelectBottomSheet(
onClose = {
navController.navigateUp()
}
)
}
@Composable
fun MbtiItem(
mbti: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
Box(
// 卡片背景色
val cardBackgroundColor = if (isDarkMode) {
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
} else {
Color(0xFFFAF9FB)
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.aspectRatio(1.1f)
.shadow(
elevation = if (isDarkMode) 8.dp else 2.dp,
shape = RoundedCornerShape(21.dp),
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
)
.clip(RoundedCornerShape(21.dp))
.background(cardBackgroundColor)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(16.dp)
.padding(horizontal = 24.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
// MBTI图标 - 使用占位图片
Box(
modifier = Modifier.size(100.dp),
contentAlignment = Alignment.Center
) {
Text(
text = mbti,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
Image(
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)),
contentDescription = mbti,
modifier = Modifier.size(100.dp)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
// MBTI名称 - 使用负间距让文本向上移动,与图标更靠近
Text(
text = mbti,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
textAlign = TextAlign.Center,
modifier = Modifier.offset(y = (-20).dp)
)
}
}

View File

@@ -0,0 +1,19 @@
package com.aiosman.ravenow.ui.account
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
object MbtiSheetManager {
private val _visible = MutableStateFlow(false)
val visible: StateFlow<Boolean> = _visible.asStateFlow()
fun open() {
_visible.value = true
}
fun close() {
_visible.value = false
}
}

View File

@@ -293,10 +293,6 @@ fun ZodiacSelectBottomSheet(
onClick = {
// 保存当前语言的星座文本
model.zodiac = zodiacText
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
}
onClose()
}
)

View File

@@ -16,9 +16,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
@@ -68,6 +70,19 @@ fun ChangePasswordScreen() {
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var passwordError by remember { mutableStateOf<String?>(null) }
val AppColors = LocalAppTheme.current
// 暗色模式下的 hint 文本颜色
val isDarkMode = AppState.darkMode
val hintColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f)
} else {
null // 使用默认颜色
}
val labelColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f)
} else {
null // 使用默认颜色
}
fun validate(): Boolean {
// 使用通用密码校验器校验当前密码
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
@@ -112,7 +127,9 @@ fun ChangePasswordScreen() {
password = true,
label = stringResource(R.string.current_password),
hint = stringResource(R.string.current_password_tip5),
error = oldPasswordError
error = oldPasswordError,
customHintColor = hintColor,
customLabelColor = labelColor
)
Spacer(modifier = Modifier.height(4.dp))
TextInputField(
@@ -121,7 +138,9 @@ fun ChangePasswordScreen() {
password = true,
label = stringResource(R.string.new_password),
hint = stringResource(R.string.new_password),
error = passwordError
error = passwordError,
customHintColor = hintColor,
customLabelColor = labelColor
)
Spacer(modifier = Modifier.height(4.dp))
TextInputField(
@@ -130,7 +149,9 @@ fun ChangePasswordScreen() {
password = true,
label = stringResource(R.string.confirm_new_password_tip1),
hint = stringResource(R.string.new_password_tip1),
error = confirmPasswordError
error = confirmPasswordError,
customHintColor = hintColor,
customLabelColor = labelColor
)
Spacer(modifier = Modifier.height(50.dp))
ActionButton(

View File

@@ -70,6 +70,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.TextLayoutResult
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import android.widget.Toast
@@ -77,6 +79,8 @@ import java.io.File
import androidx.activity.compose.BackHandler
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
import com.aiosman.ravenow.ui.account.ZodiacSheetManager
import com.aiosman.ravenow.ui.account.MbtiBottomSheetHost
import com.aiosman.ravenow.ui.account.MbtiSheetManager
/**
* 编辑用户资料界面
@@ -194,6 +198,8 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
) {
// 挂载星座选择弹窗
ZodiacBottomSheetHost()
// 挂载MBTI选择弹窗
MbtiBottomSheetHost()
Box(
modifier = Modifier
@@ -392,7 +398,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard(
label = stringResource(R.string.nickname),
value = model.name,
placeholder = "Value",
placeholder = stringResource(R.string.nickname_placeholder),
onValueChange = { onNicknameChange(it) },
isMultiline = false
)
@@ -403,7 +409,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard(
label = stringResource(R.string.personal_intro),
value = model.bio,
placeholder = "Welcome to my fantiac word i will show you something about magic",
placeholder = "",
onValueChange = { onBioChange(it) },
isMultiline = true
)
@@ -425,9 +431,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
iconResDark = null, // TODO: 添加MBTI暗色模式图标
iconResLight = null, // TODO: 添加MBTI亮色模式图标
onClick = {
debouncedNavigation {
navController.navigate(NavigationRoute.MbtiSelect.route)
}
MbtiSheetManager.open()
}
)
@@ -500,12 +504,24 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 验证通过,执行保存
model.viewModelScope.launch {
model.isUpdating = true
model.updateUserProfile(context)
model.viewModelScope.launch(Dispatchers.Main) {
debouncedNavigation {
navController.navigateUp()
try {
model.updateUserProfile(context)
model.viewModelScope.launch(Dispatchers.Main) {
debouncedNavigation {
navController.navigateUp()
}
model.isUpdating = false
}
} catch (e: Exception) {
// 捕获所有异常,包括网络异常
model.viewModelScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.network_error_check_network),
Toast.LENGTH_SHORT
).show()
model.isUpdating = false
}
model.isUpdating = false
}
}
},
@@ -550,29 +566,45 @@ fun ProfileInfoCard(
isMultiline: Boolean = false
) {
val appColors = LocalAppTheme.current
var isFocused by remember { mutableStateOf(false) }
var lineCount by remember { mutableStateOf(1) }
// 根据行数决定对齐方式:单行时居中,多行时顶部对齐
val verticalAlignment = if (isMultiline) {
if (lineCount <= 1) Alignment.CenterVertically else Alignment.Top
} else {
Alignment.CenterVertically
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp个人简介66dp
.clip(RoundedCornerShape(16.dp))
.background(appColors.secondaryBackground),
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
contentAlignment = if (isMultiline && lineCount > 1) Alignment.TopStart else Alignment.CenterStart
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(vertical = if (isMultiline) 11.dp else 0.dp),
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
.padding(vertical = if (isMultiline && lineCount > 1) 11.dp else 0.dp),
verticalAlignment = verticalAlignment
) {
// 标签
Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text,
modifier = Modifier.width(100.dp)
)
Box(
modifier = Modifier
.width(100.dp)
.height(if (isMultiline) 44.dp else 56.dp),
contentAlignment = Alignment.CenterStart
) {
Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text
)
}
Spacer(modifier = Modifier.width(16.dp))
@@ -580,10 +612,26 @@ fun ProfileInfoCard(
Box(
modifier = Modifier.weight(1f)
) {
if (value.isEmpty()) {
// 对于个人简介isMultiline = true当值为空且没有焦点时显示图标
if (value.isEmpty() && isMultiline && !isFocused) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp),
contentAlignment = Alignment.CenterStart
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
}
} else if (value.isEmpty() && !isMultiline && placeholder.isNotEmpty()) {
// 对于非多行输入框,仍然显示 placeholder 文字
Text(
text = placeholder,
fontSize = if (isMultiline) 15.sp else 17.sp,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.secondaryText,
modifier = Modifier.fillMaxWidth()
@@ -593,7 +641,11 @@ fun ProfileInfoCard(
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
},
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal,
@@ -601,7 +653,12 @@ fun ProfileInfoCard(
),
cursorBrush = SolidColor(appColors.text),
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
singleLine = !isMultiline
singleLine = !isMultiline,
onTextLayout = { textLayoutResult: TextLayoutResult ->
if (isMultiline) {
lineCount = textLayoutResult.lineCount
}
}
)
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -49,6 +50,14 @@ fun RemoveAccountScreen() {
var passwordError by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
val context = LocalContext.current
// 暗色模式下的 hint 文本颜色
val isDarkMode = AppState.darkMode
val hintColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f)
} else {
null // 使用默认颜色
}
fun removeAccount(password: String) {
// 使用通用密码校验器
@@ -132,7 +141,8 @@ fun RemoveAccountScreen() {
},
password = true,
hint = stringResource(R.string.remove_account_password_hint),
error = passwordError
error = passwordError,
customHintColor = hintColor
)
Spacer(modifier = Modifier.weight(1f))

View File

@@ -617,7 +617,7 @@ fun ChatAiInput(
animationSpec = tween(300)
)
Image(
painter = painterResource(R.mipmap.rider_pro_im_send),
painter = painterResource(R.mipmap.btn),
modifier = Modifier
.size(24.dp)
.alpha(alpha)

View File

@@ -650,7 +650,7 @@ fun ChatInput(
animationSpec = tween(300)
)
Image(
painter = painterResource(R.mipmap.rider_pro_im_send),
painter = painterResource(R.mipmap.btn),
modifier = Modifier
.size(24.dp)
.alpha(alpha)

View File

@@ -182,46 +182,22 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(16.dp))
if (viewModel.groupAvatar.isNotEmpty()) {
if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) {
CustomAsyncImage(
imageUrl = viewModel.groupAvatar,
imageUrl = viewModel.groupInfo!!.groupAvatar,
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
.noRippleClickable {
navController.navigateToGroupInfo(groupId)
},
.size(35.dp)
.clip(RoundedCornerShape(15.dp)),
contentDescription = "群聊头像"
)
} else {
Box(
modifier = Modifier
.size(24.dp)
.clip(RoundedCornerShape(8.dp))
.background(AppColors.decentBackground)
.noRippleClickable {
navController.navigateToGroupInfo(groupId)
},
contentAlignment = Alignment.Center
) {
Text(
text = viewModel.groupName,
style = TextStyle(
color = AppColors.text,
fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
),
maxLines = 1,
overflow =TextOverflow.Ellipsis,
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Column {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.Start
) {
Text(
text = viewModel.groupName,
style = TextStyle(
@@ -229,24 +205,21 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
),
maxLines = 1,
overflow =TextOverflow.Ellipsis,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.weight(1f))
Box {
Image(
painter = painterResource(R.drawable.rider_pro_more_horizon),
modifier = Modifier
.size(28.dp)
.noRippleClickable {
navController.navigateToGroupInfo(groupId)
},
contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
)
}
Image(
painter = painterResource(R.drawable.rider_pro_more_horizon),
modifier = Modifier
.size(28.dp)
.noRippleClickable {
navController.navigateToGroupInfo(groupId)
},
contentDescription = "更多",
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}
},
@@ -677,7 +650,7 @@ fun GroupChatInput(
animationSpec = tween(300)
)
Image(
painter = painterResource(R.mipmap.rider_pro_im_send),
painter = painterResource(R.mipmap.btn),
modifier = Modifier
.size(24.dp)
.alpha(alpha)

View File

@@ -1,11 +1,13 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
import io.openim.android.sdk.enums.ConversationType
@@ -50,17 +52,46 @@ class GroupChatViewModel(
}
private suspend fun getGroupInfo() {
// 简化群组信息获取,使用默认信息
groupInfo = GroupInfo(
groupId = groupId,
groupName = name,
groupAvatar = avatar,
memberCount = 0,
ownerId = ""
)
groupName = groupInfo?.groupName ?: ""
groupAvatar = groupInfo?.groupAvatar ?: ""
memberCount = groupInfo?.memberCount ?: 0
try {
val response = ApiClient.api.getRoomDetail(trtcId = groupId)
val room = response.body()?.data
groupInfo = room?.let {
GroupInfo(
groupId = groupId,
groupName = it.name,
groupAvatar = if (it.avatar.isNullOrEmpty()) {
val groupIdBase64 = Base64.encodeToString(
groupId.toByteArray(),
Base64.NO_WRAP
)
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${AppStore.token}"
} else {
"${ApiClient.BASE_API_URL}/outside${it.avatar}?token=${AppStore.token}"
},
memberCount = it.userCount,
ownerId = it.creator.userId
)
} ?: GroupInfo(
groupId = groupId,
groupName = name,
groupAvatar = avatar,
memberCount = 0,
ownerId = ""
)
} catch (e: Exception) {
Log.e("GroupChatViewModel", "加载群信息失败: ${e.message}", e)
groupInfo = GroupInfo(
groupId = groupId,
groupName = name,
groupAvatar = avatar,
memberCount = 0,
ownerId = ""
)
} finally {
groupName = groupInfo?.groupName ?: ""
groupAvatar = groupInfo?.groupAvatar ?: ""
memberCount = groupInfo?.memberCount ?: 0
}
}
override fun getConversationParams(): Triple<String, Int, Boolean> {

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,6 +32,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -79,8 +81,9 @@ class CommentModalViewModel(
fun CommentModalContent(
postId: Int? = null,
commentCount: Int = 0,
onCommentAdded: () -> Unit = {},
onDismiss: () -> Unit = {}
onDismiss: () -> Unit = {},
showTitle: Boolean = true,
onCommentAdded: () -> Unit = {}
) {
val model = viewModel<CommentModalViewModel>(
key = "CommentModalViewModel_$postId",
@@ -161,28 +164,42 @@ fun CommentModalContent(
modifier = Modifier
.fillMaxSize()
) {
// 拖动手柄
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
.padding(top = 8.dp, bottom = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.comment),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.align(Alignment.Center)
Box(
modifier = Modifier
.width(40.dp)
.height(4.dp)
.clip(RoundedCornerShape(50))
.background(AppColors.divider)
)
}
HorizontalDivider(
color = AppColors.divider
)
if (showTitle) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
) {
Text(
stringResource(R.string.comment),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.align(Alignment.Center)
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -191,7 +208,9 @@ fun CommentModalContent(
fontSize = 14.sp,
color = AppColors.secondaryText
)
OrderSelectionComponent {
OrderSelectionComponent(
selectedOrder = commentViewModel.order
) {
commentViewModel.order = it
commentViewModel.reloadComment()
}

View File

@@ -41,16 +41,15 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContent
@Composable
fun CommentNoticeScreen() {
fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
val viewModel = viewModel<CommentNoticeListViewModel>(
key = "CommentNotice",
factory = object : ViewModelProvider.Factory {
@@ -68,50 +67,22 @@ fun CommentNoticeScreen() {
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
StatusBarMaskLayout(
modifier = Modifier
.background(color = AppColors.background)
.padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
) {
StatusBarSpacer()
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
viewModel.initData(context, force = true)
}
)
NetworkErrorContent(
onReload = {
viewModel.initData(context, force = true)
}
}
)
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
Box(
modifier = Modifier
@@ -141,7 +112,8 @@ fun CommentNoticeScreen() {
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize().padding(horizontal = 16.dp)
.weight(1f)
.background(color = AppColors.background)
) {
items(comments.itemCount) { index ->
comments[index]?.let { comment ->
@@ -215,53 +187,58 @@ fun CommentNoticeItem(
val navController = LocalNavController.current
val context = LocalContext.current
val AppColors = LocalAppTheme.current
val commentPrefix = stringResource(R.string.comment_notice)
Row(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
modifier = Modifier.padding(vertical = 12.dp)
) {
Box {
CustomAsyncImage(
context = context,
imageUrl = commentItem.avatar,
contentDescription = commentItem.name,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
commentItem.author.toString()
)
// 左侧头像区域
CustomAsyncImage(
context = context,
imageUrl = commentItem.avatar,
contentDescription = commentItem.name,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
commentItem.author.toString()
)
}
)
}
)
}
)
// 右侧内容区域
Row(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.padding(start = 8.dp)
.noRippleClickable {
onPostClick()
}
) {
// 主要信息列
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = commentItem.name,
fontSize = 18.sp,
modifier = Modifier,
color = AppColors.text
fontSize = 14.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
// 评论内容行
Row {
var text = commentItem.comment
if (commentItem.parentCommentId != null) {
text = "Reply you: $text"
}
Text(
text = text,
text = "$commentPrefix $text",
fontSize = 14.sp,
maxLines = 1,
color = AppColors.secondaryText,
@@ -275,25 +252,20 @@ fun CommentNoticeItem(
color = AppColors.secondaryText,
)
}
}
Spacer(modifier = Modifier.width(24.dp))
Spacer(modifier = Modifier.width(4.dp))
// 右侧帖子图片
commentItem.post?.let {
Box {
Box(
modifier = Modifier.padding(4.dp)
) {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Image",
modifier = Modifier
.size(48.dp).clip(RoundedCornerShape(8.dp))
)
// unread indicator
}
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Image",
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
)
// 未读指示器
if (commentItem.unread) {
Box(
modifier = Modifier
@@ -303,11 +275,7 @@ fun CommentNoticeItem(
)
}
}
}
}
}
}

View File

@@ -36,6 +36,7 @@ fun StatusBarMaskLayout(
modifier: Modifier = Modifier,
darkIcons: Boolean = true,
useNavigationBarMask: Boolean = true,
includeStatusBarPadding: Boolean = true,
maskBoxBackgroundColor: Color = Color.Transparent,
content: @Composable ColumnScope.() -> Unit
) {
@@ -50,13 +51,13 @@ fun StatusBarMaskLayout(
Column(
modifier = modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.height(paddingValues.calculateTopPadding())
.fillMaxWidth()
.background(maskBoxBackgroundColor)
) {
if (includeStatusBarPadding) {
Box(
modifier = Modifier
.height(paddingValues.calculateTopPadding())
.fillMaxWidth()
.background(maskBoxBackgroundColor)
)
}
content()
if (navigationBarPaddings > 24.dp && useNavigationBarMask) {

View File

@@ -17,15 +17,19 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -42,7 +46,7 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContent
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class)
@@ -84,43 +88,11 @@ fun FavouriteListPage() {
var moments = dataFlow.collectAsLazyPagingItems()
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.refreshPager(force = true)
}
)
NetworkErrorContent(
onReload = {
model.refreshPager(force = true)
}
}
)
} else if(moments.itemCount == 0) {
Box(
modifier = Modifier
@@ -187,7 +159,10 @@ fun FavouriteListPage() {
.clip(RoundedCornerShape(8.dp)),
context = context
)
if (momentItem.images.size > 1) {
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
@@ -200,6 +175,31 @@ fun FavouriteListPage() {
)
}
}
if (isVideoMoment) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "",
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
}

View File

@@ -42,7 +42,7 @@ import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContent
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -76,43 +76,11 @@ fun FollowerListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
NetworkErrorContent(
onReload = {
model.loadData(userId, true)
}
}
)
} else if (users.itemCount == 0) {
Box(
modifier = Modifier

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -40,7 +41,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.FollowButton
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContent
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
@@ -48,13 +49,14 @@ import com.aiosman.ravenow.utils.NetworkUtils
* 关注消息列表
*/
@Composable
fun FollowerNoticeScreen() {
fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
val scope = rememberCoroutineScope()
val AppColors = LocalAppTheme.current
StatusBarMaskLayout(
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background
maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
) {
val model = FollowerNoticeViewModel
var dataFlow = model.followerItemsFlow
@@ -66,43 +68,11 @@ fun FollowerNoticeScreen() {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.reload(force = true)
}
)
NetworkErrorContent(
onReload = {
model.reload(force = true)
}
}
)
} else if (followers.itemCount == 0) {
Box(
modifier = Modifier
@@ -171,35 +141,57 @@ fun FollowItem(
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
Box(
val followText = stringResource(R.string.followed_you)
Row(
modifier = Modifier
.padding(vertical = 16.dp)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
}
.fillMaxWidth()
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 左侧头像区域
CustomAsyncImage(
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
}
)
// 右侧内容区域
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
Text(
text = nickname,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = followText,
fontSize = 14.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (!isFollowing && userId != AppState.UserId) {
FollowButton(

View File

@@ -40,7 +40,7 @@ import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContent
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
@@ -78,43 +78,11 @@ fun FollowingListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
NetworkErrorContent(
onReload = {
model.loadData(userId, true)
}
}
)
} else if(users.itemCount == 0) {
Box(
modifier = Modifier

View File

@@ -99,6 +99,15 @@ fun CreateGroupChatScreen() {
}
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
var showSelectTipsDialog by remember { mutableStateOf(false) }
// 自动隐藏“请选择群成员并输入群名称”提示弹窗
LaunchedEffect(showSelectTipsDialog) {
if (showSelectTipsDialog) {
kotlinx.coroutines.delay(2000)
showSelectTipsDialog = false
}
}
// 获取费用和余额信息
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
@@ -495,63 +504,134 @@ fun CreateGroupChatScreen() {
}
}
// 创建群聊按钮 - 固定在底部
Button(
onClick = {
// 创建群聊逻辑
if (selectedMembers.isNotEmpty()) {
// 检查是否超过上限
if (selectedMembers.size > maxMemberLimit) {
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
return@Button
}
// 如果费用大于0显示确认弹窗
if (cost > 0) {
CreateGroupChatViewModel.showConfirmDialog()
// 创建群聊按钮 - 固定在底部(启用时使用渐变背景)
val isCreateEnabled =
groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
Box(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
end = 16.dp,
top = buttonTopPadding,
bottom = navigationBarPadding + 16.dp
)
.let { baseModifier ->
if (isCreateEnabled) {
baseModifier.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x997c45ed),
Color(0x997c68ef),
Color(0x997bd8f8)
)
),
shape = RoundedCornerShape(24.dp)
)
} else {
// 费用为0直接创建
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
baseModifier
}
}
) {
Button(
onClick = {
// 创建群聊逻辑
if (selectedMembers.isNotEmpty()) {
// 检查是否超过上限
if (selectedMembers.size > maxMemberLimit) {
CreateGroupChatViewModel.showError(
context.getString(
R.string.create_group_chat_exceed_limit,
maxMemberLimit
)
)
if (success) {
navController.popBackStack()
return@Button
}
// 如果费用大于0显示确认弹窗
if (cost > 0) {
CreateGroupChatViewModel.showConfirmDialog()
} else {
// 费用为0直接创建
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
)
if (success) {
navController.popBackStack()
}
}
}
}
},
modifier = Modifier
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = if (isCreateEnabled) Color.Transparent else AppColors.main,
contentColor = AppColors.mainText,
disabledContainerColor = AppColors.disabledBackground,
disabledContentColor = AppColors.text
),
shape = RoundedCornerShape(24.dp),
enabled = isCreateEnabled
) {
if (CreateGroupChatViewModel.isLoading) {
Text(
text = stringResource(R.string.agent_createing),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} else {
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main,
contentColor = AppColors.mainText,
disabledContainerColor = AppColors.disabledBackground,
disabledContentColor = AppColors.text
),
shape = RoundedCornerShape(24.dp),
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
) {
if (CreateGroupChatViewModel.isLoading) {
Text(
text = stringResource(R.string.agent_createing),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} else {
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 16.sp,
fontWeight = FontWeight.W600
}
// 禁用状态下拦截点击并弹出提示
if (!isCreateEnabled) {
Box(
modifier = Modifier
.matchParentSize()
.noRippleClickable {
showSelectTipsDialog = true
}
)
}
}
}
// 请选择群成员并输入群名称 提示弹窗
if (showSelectTipsDialog) {
Dialog(
onDismissRequest = { showSelectTipsDialog = false },
properties = DialogProperties(dismissOnClickOutside = true, dismissOnBackPress = true)
) {
Box(
modifier = Modifier
.background(color = AppColors.background, shape = RoundedCornerShape(16.dp))
.padding(horizontal = 20.dp, vertical = 16.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.create_group_chat_select_members_and_name),
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color(0xFF7C45ED),
textAlign = TextAlign.Center
)
}
}
}
}
// 消费确认弹窗
if (CreateGroupChatViewModel.showConfirmDialog) {
CreateGroupChatConfirmDialog(

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -44,6 +45,7 @@ import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.index.NavItem
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToAddGroupMember
import com.aiosman.ravenow.ui.navigateToGroupMembers
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
import kotlinx.coroutines.launch
@@ -173,7 +175,7 @@ fun GroupChatInfoScreen(groupId: String) {
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${viewModel.groupInfo?.memberCount ?: 0}",
text = "${viewModel.groupInfo?.memberCount ?: 0}${stringResource(R.string.people)}",
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text.copy(alpha = 0.7f),
fontSize = 11.sp
@@ -187,38 +189,42 @@ fun GroupChatInfoScreen(groupId: String) {
item {
Spacer(modifier = Modifier.height(10.dp))
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 添加其他人
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable {
// TODO: 实现添加其他人功能
}
) {
Box(
modifier = Modifier
.size(30.dp)
.clip(CircleShape),
contentAlignment = Alignment.Center
if (viewModel.groupInfo?.isCreator == true) {
// 添加其他人
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable {
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
}
) {
Image(
painter = painterResource(R.drawable.rider_pro_add_other),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
Box(
modifier = Modifier
.size(30.dp)
.clip(CircleShape),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.rider_pro_add_other),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
}
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.group_chat_info_add_member),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 11.sp
)
)
}
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.group_chat_info_add_member),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 11.sp
)
)
}
// 通知设置
@@ -258,11 +264,11 @@ fun GroupChatInfoScreen(groupId: String) {
)
}
// 退出群聊
// 分享群聊
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable {
// TODO: 实现退出群聊功能
// TODO: 实现分享功能
}
) {
Box(
@@ -272,7 +278,7 @@ fun GroupChatInfoScreen(groupId: String) {
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.group_info_edit),
painter = painterResource(R.mipmap.icon_share),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
@@ -297,16 +303,16 @@ fun GroupChatInfoScreen(groupId: String) {
Column(
modifier = Modifier
.fillMaxWidth()
.border(1.dp, AppColors.decentBackground, RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(12.dp))
.background(AppColors.decentBackground.copy(alpha = 0.28f))
.background(AppColors.background)
.padding(12.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(R.drawable.group_info_edit),
modifier = Modifier.size(16.dp),
painter = painterResource(R.mipmap.icons_brain),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(6.dp))
Column(modifier = Modifier.weight(1f)) {
@@ -315,7 +321,6 @@ fun GroupChatInfoScreen(groupId: String) {
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 15.sp,
fontWeight = FontWeight.Bold
)
)
Spacer(modifier = Modifier.height(2.dp))
@@ -337,7 +342,7 @@ fun GroupChatInfoScreen(groupId: String) {
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(20.dp))
.background(AppColors.background)
.background(AppColors.decentBackground)
.padding(vertical = 8.dp)
.noRippleClickable {
showAddMemoryDialog = true
@@ -356,7 +361,7 @@ fun GroupChatInfoScreen(groupId: String) {
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(20.dp))
.background(AppColors.background)
.background(AppColors.decentBackground)
.padding(vertical = 8.dp)
.noRippleClickable {
showMemoryManageDialog = true
@@ -379,81 +384,84 @@ fun GroupChatInfoScreen(groupId: String) {
item {
Spacer(modifier = Modifier.height(13.dp))
// 群资料设置
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable {
navController.navigateToGroupProfileSettings(groupId)
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.group_info_edit),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = stringResource(R.string.group_chat_info_group_settings),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(16.dp),
contentDescription = null,
)
}
// 群可见性
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable {
showVisibilityDialog = true
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.rider_pro_change_password),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = stringResource(R.string.group_chat_info_group_visibility),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
// 未解锁时才显示"待解锁"
if (viewModel.groupInfo?.privateFeePaid != true) {
Text(
text = stringResource(R.string.group_chat_info_locked),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text.copy(alpha = 0.5f),
fontSize = 11.sp
// 仅当当前用户是群聊创建者时显示以下组件
if (viewModel.groupInfo?.isCreator == true) {
// 群资料设置
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable {
navController.navigateToGroupProfileSettings(groupId)
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.fengm),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = stringResource(R.string.group_chat_info_group_settings),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(16.dp),
contentDescription = null,
)
}
// 群可见性
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable {
showVisibilityDialog = true
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.rider_pro_change_password),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = stringResource(R.string.group_chat_info_group_visibility),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
// 未解锁时才显示"待解锁"
if (viewModel.groupInfo?.privateFeePaid != true) {
Text(
text = stringResource(R.string.group_chat_info_locked),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text.copy(alpha = 0.5f),
fontSize = 11.sp
)
)
}
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(16.dp),
contentDescription = null,
)
)
}
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(16.dp),
contentDescription = null,
)
}
// 成员管理
@@ -468,15 +476,21 @@ fun GroupChatInfoScreen(groupId: String) {
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.group_info_users),
modifier = Modifier.size(20.dp),
painter = painterResource(R.mipmap.icons_users),
modifier = Modifier.size(25.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = stringResource(R.string.group_chat_info_member_manage),
text = stringResource(
if (viewModel.groupInfo?.isCreator == true) {
R.string.group_chat_info_member_manage
} else {
R.string.group_members_title
}
),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 15.sp
@@ -499,8 +513,8 @@ fun GroupChatInfoScreen(groupId: String) {
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.group_info_edit),
modifier = Modifier.size(20.dp),
painter = painterResource(R.mipmap.iconsgallery),
modifier = Modifier.size(25.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
@@ -520,36 +534,70 @@ fun GroupChatInfoScreen(groupId: String) {
contentDescription = null,
)
}
// 解散群聊
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable { },
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.group_info_exit),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = stringResource(R.string.group_chat_info_dissolve),
style = androidx.compose.ui.text.TextStyle(
color = Color(0xFFFF3B30),
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(18.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
)
if (viewModel.groupInfo?.isCreator == true) {
// 解散群聊(仅群主)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable { },
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(3.dp))
Image(
painter = painterResource(R.mipmap.iconslogout),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
)
Spacer(modifier = Modifier.width(11.dp))
Text(
text = stringResource(R.string.group_chat_info_dissolve),
style = androidx.compose.ui.text.TextStyle(
color = Color(0xFFFF3B30),
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(18.dp),
contentDescription = null
)
}
} else {
// 退出群聊(非群主)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable { },
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(3.dp))
Image(
painter = painterResource(R.drawable.group_info_exit),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
)
Spacer(modifier = Modifier.width(11.dp))
Text(
text = stringResource(R.string.group_chat_info_quit),
style = androidx.compose.ui.text.TextStyle(
color = Color(0xFFFF3B30),
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(18.dp),
contentDescription = null
)
}
}
}
}

View File

@@ -107,16 +107,16 @@ fun GroupMembersScreen(groupId: String) {
textAlign = TextAlign.Center
)
androidx.compose.foundation.Image(
painter = painterResource(R.drawable.rider_pro_add_other),
contentDescription = stringResource(R.string.group_chat_info_add_member),
colorFilter = ColorFilter.tint(AppColors.text),
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
}
)
// androidx.compose.foundation.Image(
// painter = painterResource(R.drawable.rider_pro_add_other),
// contentDescription = stringResource(R.string.group_chat_info_add_member),
// colorFilter = ColorFilter.tint(AppColors.text),
// modifier = Modifier
// .size(24.dp)
// .noRippleClickable {
// navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
// }
// )
}
}
@@ -391,7 +391,7 @@ private fun MemberItem(
Spacer(modifier = Modifier.weight(1f))
// 菜单按钮
if (isAdmin) {
IconButton(
onClick = { onMenuClick(itemPosition, itemHeight) },
modifier = Modifier.size(24.dp)
@@ -404,6 +404,7 @@ private fun MemberItem(
.size(24.dp)
)
}
}
Spacer(modifier = Modifier.width(8.dp))

View File

@@ -2,10 +2,6 @@ package com.aiosman.ravenow.ui.index
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -140,6 +136,7 @@ fun IndexScreen() {
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = drawerState.isOpen,
scrimColor = Color.Black.copy(alpha = 0.6f),
drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
SideMenuContent(
@@ -525,8 +522,6 @@ fun SideMenuContent(
} else {
Color(0xFFFAF9FB) // 亮色模式:浅灰色
}
// 遮罩颜色 黑色透明度0.6
val overlayColor = Color.Black.copy(alpha = 0.6f)
// 卡片背景色 - 根据暗色模式适配
val cardBackgroundColor = if (darkModeEnabled) {
appColors.background // 暗色模式:深色背景
@@ -546,24 +541,6 @@ fun SideMenuContent(
modifier = Modifier
.fillMaxSize()
) {
// 左侧半透明遮罩(平滑淡入淡出)
val overlayTransition = updateTransition(targetState = isDrawerOpen, label = "overlay")
val overlayAlpha by overlayTransition.animateFloat(
transitionSpec = {
if (targetState) {
tween(durationMillis = 400, easing = LinearOutSlowInEasing)
} else {
tween(durationMillis = 300, easing = FastOutLinearInEasing)
}
},
label = "overlayAlpha"
) { open -> if (open) 0.6f else 0f }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = overlayAlpha))
)
// 右侧菜单面板
Box(
modifier = Modifier

View File

@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -48,11 +49,12 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
/**
* 智能体聊天列表页面
@@ -89,72 +91,28 @@ fun AgentChatListScreen() {
.pullRefresh(state)
) {
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
// 空状态
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
// 空状态
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = "null data",
modifier = Modifier
.width(181.dp)
.height(153.dp)
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.no_one_knocked_yet),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
AgentChatListViewModel.refreshPager(context = context)
}
)
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
AgentChatListViewModel.refreshPager(context = context)
}
)
}
}
}
} else {

View File

@@ -42,10 +42,11 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.ui.text.font.FontFamily
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
data class CombinedConversation(
val type: String, // "agent", "group", or "friend"
@@ -217,73 +218,31 @@ fun AllChatListScreen() {
.pullRefresh(state)
) {
if (allConversations.isEmpty() && !isLoading) {
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = "null data",
modifier = Modifier
.width(181.dp)
.height(153.dp)
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.no_one_knocked_yet),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
isLoading = true
// 重新加载所有类型的数据
AgentChatListViewModel.refreshPager(context = context)
GroupChatListViewModel.refreshPager(context = context)
FriendChatListViewModel.refreshPager(context = context)
}
)
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
isLoading = true
// 重新加载所有类型的数据
AgentChatListViewModel.refreshPager(context = context)
GroupChatListViewModel.refreshPager(context = context)
FriendChatListViewModel.refreshPager(context = context)
}
)
}
}
}
} else {

View File

@@ -33,7 +33,7 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.search.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.text.style.TextAlign
@@ -41,6 +41,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.foundation.layout.PaddingValues
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -72,70 +73,27 @@ fun FriendChatListScreen() {
.pullRefresh(state)
) {
if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) {
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
//verticalArrangement = Arrangement.Center
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = "null data",
modifier = Modifier
.width(181.dp)
.height(153.dp)
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.no_one_knocked_yet),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
}
}
}
} else {
@@ -307,43 +265,4 @@ fun FriendChatItem(
}
}
}
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -35,7 +35,8 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -67,69 +68,27 @@ fun GroupChatListScreen() {
.pullRefresh(state)
) {
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = "null data",
modifier = Modifier
.width(181.dp)
.height(153.dp)
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.no_one_knocked_yet),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
GroupChatListViewModel.refreshPager(context = context)
}
)
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
GroupChatListViewModel.refreshPager(context = context)
}
)
}
}
}
} else {

View File

@@ -64,12 +64,36 @@ open class BaseMomentModel :ViewModel(){
momentLoader.updateMomentLike(event.postId, event.isLike)
}
suspend fun likeMoment(id: Int) {
// 获取当前动态信息,用于计算新的点赞数
val currentMoment = momentLoader.list.find { it.id == id }
val newLikeCount = (currentMoment?.likeCount ?: 0) + 1
momentService.likeMoment(id)
momentLoader.updateMomentLike(id, true)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = newLikeCount,
isLike = true
)
)
}
suspend fun dislikeMoment(id: Int) {
// 获取当前动态信息,用于计算新的点赞数
val currentMoment = momentLoader.list.find { it.id == id }
val newLikeCount = ((currentMoment?.likeCount ?: 0) - 1).coerceAtLeast(0)
momentService.dislikeMoment(id)
momentLoader.updateMomentLike(id, false)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = newLikeCount,
isLike = false
)
)
}
@@ -90,14 +114,27 @@ open class BaseMomentModel :ViewModel(){
suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = true
)
)
}
suspend fun unfavoriteMoment(id: Int) {
momentService.unfavoriteMoment(id)
momentLoader.updateFavoriteCount(id, false)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = false
)
)
}
@Subscribe

View File

@@ -226,7 +226,9 @@ fun NewsCommentModal(
.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
OrderSelectionComponent {
OrderSelectionComponent(
selectedOrder = commentViewModel.order
) {
commentViewModel.order = it
commentViewModel.reloadComment()
}

View File

@@ -16,10 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
@@ -33,8 +30,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
@@ -97,6 +97,10 @@ fun VideoRecommendationItem(
skipPartiallyExpanded = true
)
var pauseIconVisibleState by remember { mutableStateOf(false) }
var shouldResumeAfterLifecyclePause by remember { mutableStateOf(false) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
val exoPlayer = remember(videoUrl) {
ExoPlayer.Builder(context)
@@ -167,29 +171,43 @@ fun VideoRecommendationItem(
},
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
pauseIconVisibleState = true
exoPlayer.pause()
scope.launch {
delay(100)
if (exoPlayer.isPlaying) {
.pointerInput(videoUrl, moment.liked) {
detectTapGestures(
onDoubleTap = { offset ->
// 双击点赞/取消点赞
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
lastDoubleTapTime.value = currentTime
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(moment)
}
},
onTap = {
// 单击播放/暂停
pauseIconVisibleState = true
exoPlayer.pause()
} else {
pauseIconVisibleState = false
exoPlayer.play()
scope.launch {
delay(100)
if (exoPlayer.isPlaying) {
exoPlayer.pause()
} else {
pauseIconVisibleState = false
exoPlayer.play()
}
}
}
}
)
}
)
if (pauseIconVisibleState) {
Icon(
imageVector = Icons.Default.PlayArrow,
Image(
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
.size(80.dp),
tint = Color.White
colorFilter = ColorFilter.tint(Color.White)
)
}
}
@@ -300,7 +318,9 @@ fun VideoRecommendationItem(
ModalBottomSheet(
onDismissRequest = { showCommentModal = false },
containerColor = Color.White,
sheetState = sheetState
sheetState = sheetState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) {
CommentModalContent(postId = moment.id) {
// 评论添加后的回调
@@ -320,10 +340,12 @@ fun VideoRecommendationItem(
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> {
shouldResumeAfterLifecyclePause = exoPlayer.isPlaying && !pauseIconVisibleState
exoPlayer.pause()
}
Lifecycle.Event.ON_RESUME -> {
if (isVisible) {
if (isVisible && shouldResumeAfterLifecyclePause) {
pauseIconVisibleState = false
exoPlayer.play()
}
}
@@ -399,3 +421,4 @@ private fun VideoBtn(
)
}
}

View File

@@ -47,6 +47,7 @@ import com.aiosman.ravenow.ui.composables.rememberDebouncer
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.platform.LocalContext
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
/**
* 动态列表
@@ -90,38 +91,11 @@ fun TimelineMomentsList() {
.padding(top = 188.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
val exploreDebouncer = rememberDebouncer()
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ExploreButton(
onClick = {
exploreDebouncer {
/* TODO: 添加点击事件处理 */
} }
)
}
NetworkErrorContentInline(
onReload = {
model.refreshPager(pullRefresh = true)
}
)
}
} else if (moments.isEmpty()) {
Box(

View File

@@ -33,6 +33,8 @@ import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent
import com.aiosman.ravenow.data.PointService
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@@ -308,14 +310,16 @@ object MyProfileViewModel : ViewModel() {
* 加载房间列表
* @param filterType 筛选类型0=全部1=公开2=私有
* @param pullRefresh 是否下拉刷新
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
*/
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false) {
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false, ownerSessionId: String? = null) {
// 游客模式下不加载房间列表
if (AppStore.isGuest) {
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
return
}
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
if (roomsLoading && !pullRefresh) return
viewModelScope.launch {
@@ -331,60 +335,69 @@ object MyProfileViewModel : ViewModel() {
roomsCurrentPage
}
val response = when (filterType) {
0 -> {
// 全部:显示自己创建或加入的所有房间
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
showCreated = true,
showJoined = true
)
}
1 -> {
// 公开:显示公开房间中自己创建或加入的
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = "public",
showCreated = true,
showJoined = true
)
}
2 -> {
// 私有:显示自己创建或加入的私有房间
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = "private"
)
}
else -> {
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
showCreated = true,
showJoined = true
)
}
// 根据filterType确定roomType
val roomType = when (filterType) {
1 -> "public"
2 -> "private"
else -> null
}
val effectiveRoomType = if (normalizedOwnerId != null) "public" else roomType
if (response.isSuccessful) {
val roomList = response.body()?.list ?: emptyList()
val total = response.body()?.total ?: 0L
// 构建API调用参数
if (normalizedOwnerId != null) {
// 查看其他用户的房间:仅显示该用户创建的公开房间
val createdResponse = apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = effectiveRoomType,
ownerSessionId = normalizedOwnerId,
includeUsers = false
)
val createdRooms = if (createdResponse.isSuccessful) {
createdResponse.body()?.list?.map { it.toRoomtEntity() } ?: emptyList()
} else {
emptyList()
}
if (pullRefresh || currentPage == 1) {
rooms = roomList.map { it.toRoomtEntity() }
rooms = createdRooms
} else {
rooms = rooms + roomList.map { it.toRoomtEntity() }
rooms = rooms + createdRooms
}
val total = createdResponse.body()?.total ?: 0L
roomsHasMore = rooms.size < total
if (roomsHasMore && !pullRefresh) {
roomsCurrentPage++
}
} else {
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
// 查看自己的房间:显示创建或加入的房间
val response = apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = effectiveRoomType,
showCreated = true,
showJoined = if (filterType == 2) null else true // 私有房间不显示加入的
)
if (response.isSuccessful) {
val roomList = response.body()?.list ?: emptyList()
val total = response.body()?.total ?: 0L
if (pullRefresh || currentPage == 1) {
rooms = roomList.map { it.toRoomtEntity() }
} else {
rooms = rooms + roomList.map { it.toRoomtEntity() }
}
roomsHasMore = rooms.size < total
if (roomsHasMore && !pullRefresh) {
roomsCurrentPage++
}
} else {
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
}
}
} catch (e: Exception) {
Log.e("MyProfileViewModel", "loadRooms error: ", e)
@@ -398,20 +411,29 @@ object MyProfileViewModel : ViewModel() {
/**
* 加载更多房间
* @param filterType 筛选类型0=全部1=公开2=私有
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
*/
fun loadMoreRooms(filterType: Int = 0) {
fun loadMoreRooms(filterType: Int = 0, ownerSessionId: String? = null) {
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
if (roomsLoading || !roomsHasMore) return
loadRooms(filterType = filterType, pullRefresh = false)
loadRooms(filterType = filterType, pullRefresh = false, ownerSessionId = normalizedOwnerId)
}
/**
* 刷新房间列表
* @param filterType 筛选类型0=全部1=公开2=私有
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
*/
fun refreshRooms(filterType: Int = 0) {
fun refreshRooms(filterType: Int = 0, ownerSessionId: String? = null) {
rooms = emptyList()
roomsCurrentPage = 1
roomsHasMore = true
loadRooms(filterType = filterType, pullRefresh = true)
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
loadRooms(filterType = filterType, pullRefresh = true, ownerSessionId = normalizedOwnerId)
}
private fun normalizeOwnerSessionId(ownerSessionId: String?): String? {
val trimmed = ownerSessionId?.trim()
return if (trimmed.isNullOrEmpty()) null else trimmed
}
}

View File

@@ -52,6 +52,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@@ -59,6 +60,8 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
@@ -85,6 +88,8 @@ import com.aiosman.ravenow.ui.index.IndexViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SegmentedControl
import com.aiosman.ravenow.ui.index.tabs.profile.composable.AgentSegmentedControl
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
@@ -168,6 +173,41 @@ fun ProfileV3(
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
)
val scrollState = rememberScrollState(model.profileScrollOffset)
var tabIndicatorContentOffset by remember { mutableStateOf<Float?>(null) }
var tabIndicatorHeightPx by remember { mutableStateOf(0) }
val topNavigationBarHeightPx = with(density) { (statusBarPaddingValues.calculateTopPadding() + 56.dp).toPx() }
val stickyTopPadding = statusBarPaddingValues.calculateTopPadding() + 56.dp
var agentSegmentOffset by remember { mutableStateOf<Float?>(null) }
var agentSegmentHeightPx by remember { mutableStateOf(0) }
var groupSegmentOffset by remember { mutableStateOf<Float?>(null) }
var groupSegmentHeightPx by remember { mutableStateOf(0) }
var agentSegmentSelected by remember { mutableStateOf(0) }
var groupSegmentSelected by remember { mutableStateOf(0) }
val tabIndicatorHeightDp = with(density) { tabIndicatorHeightPx.toDp() }
val tabBarBottomPx = topNavigationBarHeightPx + tabIndicatorHeightPx
val tabBarBottomPadding = stickyTopPadding + tabIndicatorHeightDp
val tabStickyThreshold = remember(tabIndicatorContentOffset, topNavigationBarHeightPx) {
tabIndicatorContentOffset?.minus(topNavigationBarHeightPx)
}
val agentSegmentThreshold = remember(agentSegmentOffset, tabBarBottomPx) {
agentSegmentOffset?.minus(tabBarBottomPx)
}
val groupSegmentThreshold = remember(groupSegmentOffset, tabBarBottomPx) {
groupSegmentOffset?.minus(tabBarBottomPx)
}
val agentTabIndex = if (isAiAccount) -1 else 1
val groupTabIndex = if (isAiAccount) 1 else 2
val shouldStickTabBar = tabStickyThreshold?.let { scrollState.value >= it } ?: false
val shouldStickAgentSegments = isSelf && !isAiAccount && agentSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == agentTabIndex
val shouldStickGroupSegments = isSelf && groupSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == groupTabIndex
val externalOwnerSessionId = remember(isSelf, profile?.chatAIId, profile?.trtcUserId) {
if (isSelf) {
null
} else {
profile?.chatAIId?.takeIf { it.isNotBlank() }
?: profile?.trtcUserId?.takeIf { it.isNotBlank() }
}
}
val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@@ -492,10 +532,19 @@ fun ProfileV3(
.background(AppColors.profileBackground)
.padding(top = 8.dp)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
Box(
modifier = Modifier
.onGloballyPositioned { coordinates ->
tabIndicatorHeightPx = coordinates.size.height
tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value
}
.alpha(if (shouldStickTabBar) 0f else 1f)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
}
HorizontalPager(
state = pagerState,
modifier = Modifier.height(650.dp) // 固定滚动高度
@@ -533,22 +582,53 @@ fun ProfileV3(
showNoMoreText = isSelf,
modifier = Modifier.fillMaxSize(),
state = listState,
nestedScrollConnection = nestedScrollConnection
nestedScrollConnection = nestedScrollConnection,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
segmentSelectedIndex = agentSegmentSelected,
onSegmentSelected = { agentSegmentSelected = it },
onSegmentMeasured = { offset, height ->
agentSegmentOffset = offset
agentSegmentHeightPx = height
},
isSegmentSticky = shouldStickAgentSegments,
parentScrollProvider = { scrollState.value }
)
} else {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(),
listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection
nestedScrollConnection = nestedScrollConnection,
ownerSessionId = externalOwnerSessionId,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
)
}
}
2 -> {
if (!isAiAccount) {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(),
listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection
nestedScrollConnection = nestedScrollConnection,
ownerSessionId = externalOwnerSessionId,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
)
}
}
@@ -560,6 +640,55 @@ fun ProfileV3(
Spacer(modifier = Modifier.height(16.dp))
}
if (shouldStickTabBar && tabIndicatorHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = stickyTopPadding)
.background(AppColors.profileBackground)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
}
}
if (shouldStickAgentSegments && agentSegmentHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = tabBarBottomPadding)
.background(AppColors.profileBackground)
.padding(horizontal = 16.dp)
) {
AgentSegmentedControl(
selectedIndex = agentSegmentSelected,
onSegmentSelected = { agentSegmentSelected = it },
modifier = Modifier.fillMaxWidth()
)
}
}
if (shouldStickGroupSegments && groupSegmentHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = tabBarBottomPadding)
.background(AppColors.profileBackground)
.padding(horizontal = 16.dp)
) {
SegmentedControl(
selectedIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
modifier = Modifier.fillMaxWidth()
)
}
}
// 顶部导航栏
TopNavigationBar(
isMain = isMain,
@@ -656,12 +785,26 @@ fun ProfileV3(
private fun GroupChatPlaceholder(
modifier: Modifier = Modifier,
listState: androidx.compose.foundation.lazy.LazyListState,
nestedScrollConnection: NestedScrollConnection? = null
nestedScrollConnection: NestedScrollConnection? = null,
ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
showSegments: Boolean = true,
selectedSegmentIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) {
GroupChatEmptyContent(
modifier = modifier,
listState = listState,
nestedScrollConnection = nestedScrollConnection
nestedScrollConnection = nestedScrollConnection,
ownerSessionId = ownerSessionId,
showSegments = showSegments,
selectedSegmentIndex = selectedSegmentIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
)
}

View File

@@ -0,0 +1,102 @@
package com.aiosman.ravenow.ui.index.tabs.profile.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
/**
* 空状态缺省图组件
* 用于显示我的-动态、智能体、群聊无内容时的缺省图片和提示文本
*/
@Composable
fun EmptyStateView(
modifier: Modifier = Modifier,
contentDescription: String = "暂无内容",
text: String = stringResource(R.string.cosmos_awaits),
fontWeight: FontWeight = FontWeight.W600
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = contentDescription,
modifier = Modifier
.size(width = 181.dp, height = 153.dp),
contentScale = ContentScale.Fit
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = text,
fontSize = 16.sp,
color = AppColors.text,
fontWeight = fontWeight,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
/**
* 消息页统一空状态
*/
@Composable
fun ChatEmptyStateView(
modifier: Modifier = Modifier,
contentDescription: String = "暂无会话",
text: String = stringResource(R.string.no_one_knocked_yet)
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = contentDescription,
modifier = Modifier.size(120.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = text,
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -1,47 +1,50 @@
package com.aiosman.ravenow.ui.index.tabs.profile.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.aiosman.ravenow.AppState
@@ -155,7 +158,8 @@ fun GalleryGrid(
modifier = baseModifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 60.dp),
.padding(vertical = 60.dp)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
@@ -198,24 +202,8 @@ fun GalleryGrid(
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = "暂无图片",
modifier = Modifier
.size(width = 181.dp, height = 153.dp),
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.cosmos_awaits),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
EmptyStateView(
contentDescription = "暂无图片"
)
}
} else {
@@ -227,8 +215,20 @@ fun GalleryGrid(
.padding(bottom = 8.dp),
) {
itemsIndexed(moments) { idx, moment ->
if (moment != null && moment.images.isNotEmpty()) {
moment?.let { momentItem ->
val itemDebouncer = rememberDebouncer()
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
val previewUrl = when {
momentItem.images.isNotEmpty() -> momentItem.images[0].thumbnail
isVideoMoment -> {
val firstVideo = momentItem.videos!!.first()
firstVideo.thumbnailDirectUrl
?: firstVideo.thumbnailUrl
?: firstVideo.directUrl
?: firstVideo.url
}
else -> null
}
Box(
modifier = Modifier
.fillMaxWidth()
@@ -237,20 +237,32 @@ fun GalleryGrid(
.noRippleClickable {
itemDebouncer {
navController.navigateToPost(
id = moment.id,
id = momentItem.id,
highlightCommentId = 0,
initImagePagerIndex = 0
)
}
}
) {
CustomAsyncImage(
imageUrl = moment.images[0].thumbnail,
contentDescription = "",
modifier = Modifier.fillMaxSize(),
context = LocalContext.current
)
if (moment.images.size > 1) {
if (previewUrl != null) {
CustomAsyncImage(
imageUrl = previewUrl,
contentDescription = "",
modifier = Modifier.fillMaxSize(),
context = LocalContext.current
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(
color = AppColors.basicMain.copy(alpha = 0.2f),
shape = RoundedCornerShape(10.dp)
)
)
}
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
@@ -263,6 +275,31 @@ fun GalleryGrid(
)
}
}
if (isVideoMoment) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "",
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
}

View File

@@ -16,15 +16,16 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.draw.alpha
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
@@ -40,6 +41,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -63,37 +66,47 @@ import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import android.util.Base64
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils.isNetworkAvailable
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun GroupChatEmptyContent(
modifier: Modifier = Modifier,
listState: LazyListState,
nestedScrollConnection: NestedScrollConnection? = null
nestedScrollConnection: NestedScrollConnection? = null,
ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
selectedSegmentIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
val viewModel = MyProfileViewModel
val normalizedOwnerSessionId = ownerSessionId?.takeIf { it.isNotBlank() }
val canLoadRooms = showSegments || normalizedOwnerSessionId != null
val networkAvailable = isNetworkAvailable(context)
// 如果查看其他用户的房间固定使用全部类型filterType = 0
val filterType = if (showSegments) selectedSegmentIndex else 0
val state = rememberPullRefreshState(
refreshing = viewModel.roomsRefreshing,
refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false,
onRefresh = {
viewModel.refreshRooms(filterType = selectedSegment)
if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
}
)
// 当分段改变时,重新加载数据
LaunchedEffect(selectedSegment) {
// 切换分段时重新加载
viewModel.refreshRooms(filterType = selectedSegment)
}
// 初始加载
LaunchedEffect(Unit) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
viewModel.loadRooms(filterType = selectedSegment)
// 当分段或用户ID改变时,重新加载数据
LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) {
if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
}
@@ -110,72 +123,90 @@ fun GroupChatEmptyContent(
) {
Spacer(modifier = Modifier.height(16.dp))
// 分段控制器
SegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = {
selectedSegment = it
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// 只在查看自己的房间时显示分段控制器
if (showSegments) {
SegmentedControl(
selectedIndex = selectedSegmentIndex,
onSegmentSelected = onSegmentSelected,
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
onSegmentMeasured?.invoke(
coordinates.positionInRoot().y + parentScrollProvider(),
coordinates.size.height
)
}
.alpha(if (isSegmentSticky) 0f else 1f)
)
Spacer(modifier = Modifier.height(8.dp))
}
Box(
modifier = nestedScrollModifier
.fillMaxSize()
.pullRefresh(state)
) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
if (!canLoadRooms) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
// 空状态内容(居中)
Column(
modifier = nestedScrollModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 空状态插图
EmptyStateIllustration()
EmptyStateIllustration(
isNetworkAvailable = networkAvailable,
onReload = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
Spacer(modifier = Modifier.height(9.dp))
// 空状态文本
Text(
text = stringResource(R.string.cosmos_awaits),
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = AppColors.text,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
} else {
LazyColumn(
state = listState,
modifier = nestedScrollModifier.fillMaxSize()
) {
itemsIndexed(
items = viewModel.rooms,
key = { _, item -> item.id }
) { index, room ->
RoomItem(
room = room,
onRoomClick = { roomEntity ->
// 导航到群聊聊天界面
navController.navigateToGroupChat(
id = roomEntity.trtcRoomId,
name = roomEntity.name,
avatar = roomEntity.avatar
// 网格布局每行显示2个房间卡片
items(
items = viewModel.rooms.chunked(2),
key = { rowRooms -> rowRooms.firstOrNull()?.id?.toString() ?: "" }
) { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { room ->
RoomCard(
room = room,
onRoomClick = { roomEntity ->
// 导航到群聊聊天界面
navController.navigateToGroupChat(
id = roomEntity.trtcRoomId,
name = roomEntity.name,
avatar = roomEntity.avatar
)
},
modifier = Modifier.weight(1f)
)
}
)
if (index < viewModel.rooms.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
// 如果这一行只有一个房间,添加一个空的占位符
if (rowRooms.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
@@ -201,30 +232,37 @@ fun GroupChatEmptyContent(
if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
item {
LaunchedEffect(Unit) {
viewModel.loadMoreRooms(filterType = selectedSegment)
viewModel.loadMoreRooms(
filterType = filterType,
ownerSessionId = normalizedOwnerSessionId
)
}
}
}
}
}
PullRefreshIndicator(
refreshing = viewModel.roomsRefreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
if (canLoadRooms) {
PullRefreshIndicator(
refreshing = viewModel.roomsRefreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}
}
@Composable
fun RoomItem(
fun RoomCard(
room: RoomEntity,
onRoomClick: (RoomEntity) -> Unit = {}
onRoomClick: (RoomEntity) -> Unit = {},
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val roomDebouncer = rememberDebouncer()
val cardSize = 180.dp
// 构建头像URL
val avatarUrl = if (room.avatar.isNotEmpty()) {
@@ -238,6 +276,117 @@ fun RoomItem(
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=${groupIdBase64}&token=${AppStore.token}"
}
// 优先显示cover如果没有cover则显示recommendBanner最后显示avatar
val imageUrl = when {
room.cover.isNotEmpty() -> "${ConstVars.BASE_SERVER}/api/v1/outside/${room.cover}?token=${AppStore.token}"
room.recommendBanner.isNotEmpty() -> "${ConstVars.BASE_SERVER}/api/v1/outside/${room.recommendBanner}?token=${AppStore.token}"
else -> avatarUrl
}
// 正方形卡片,文字重叠在底部
Box(
modifier = modifier
.size(cardSize)
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.noRippleClickable {
roomDebouncer {
onRoomClick(room)
}
}
) {
CustomAsyncImage(
context = context,
imageUrl = imageUrl,
contentDescription = room.name,
modifier = Modifier
.width(cardSize)
.height(120.dp)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
contentScale = ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
)
// 房间名称,重叠在底部
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 32.dp, start = 10.dp, end = 10.dp)
.clip(RoundedCornerShape(12.dp))
) {
Text(
text = room.name,
fontSize = 14.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Left
)
}
// 显示人数
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 10.dp, start = 10.dp, end = 10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(R.drawable.rider_pro_nav_profile),
contentDescription = "chat",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${room.userCount} ${stringResource(R.string.chatting_now)}",
fontSize = 12.sp,
modifier = Modifier
.alpha(0.6f)
.weight(1f),
color = AppColors.text,
fontWeight = FontWeight.W500,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
/**
* 列表样式的房间项,供搜索等场景复用
*/
@Composable
fun RoomItem(
room: RoomEntity,
onRoomClick: (RoomEntity) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val roomDebouncer = rememberDebouncer()
// 构建头像URL
val avatarUrl = if (room.avatar.isNotEmpty()) {
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
} else {
val groupIdBase64 = Base64.encodeToString(
room.trtcType.toByteArray(),
Base64.NO_WRAP
)
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=${groupIdBase64}&token=${AppStore.token}"
}
Row(
modifier = Modifier
.fillMaxWidth()
@@ -258,7 +407,7 @@ fun RoomItem(
.clip(RoundedCornerShape(12.dp))
)
}
Column(
modifier = Modifier
.weight(1f)
@@ -278,9 +427,9 @@ fun RoomItem(
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
@@ -293,9 +442,9 @@ fun RoomItem(
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${room.userCount}",
fontSize = 12.sp,
@@ -307,7 +456,7 @@ fun RoomItem(
}
@Composable
private fun SegmentedControl(
fun SegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
@@ -372,7 +521,7 @@ private fun SegmentButton(
},
shape = RoundedCornerShape(1000.dp)
)
.clickable(onClick = onClick),
.noRippleClickable { onClick() },
contentAlignment = Alignment.Center
) {
Text(
@@ -389,14 +538,43 @@ private fun SegmentButton(
}
@Composable
private fun EmptyStateIllustration() {
Image(
painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = "空状态",
modifier = Modifier
.width(181.dp)
.height(153.dp),
contentScale = ContentScale.Fit
)
private fun EmptyStateIllustration(
isNetworkAvailable: Boolean,
onReload: () -> Unit
) {
val AppColors = LocalAppTheme.current
if (isNetworkAvailable) {
EmptyStateView(
contentDescription = "空状态",
fontWeight = FontWeight.SemiBold
)
} else {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(onClick = onReload)
}
}

View File

@@ -29,9 +29,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -52,6 +55,7 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils
@@ -66,7 +70,13 @@ fun UserAgentsList(
showNoMoreText: Boolean = false,
modifier: Modifier = Modifier,
state: LazyListState,
nestedScrollConnection: NestedScrollConnection? = null
nestedScrollConnection: NestedScrollConnection? = null,
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) {
val AppColors = LocalAppTheme.current
val listModifier = if (nestedScrollConnection != null) {
@@ -80,7 +90,14 @@ fun UserAgentsList(
Box(
modifier = listModifier.fillMaxSize()
) {
AgentEmptyContentWithSegments()
AgentEmptyContentWithSegments(
showSegments = showSegments,
segmentSelectedIndex = segmentSelectedIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
)
}
} else {
LazyColumn(
@@ -248,8 +265,14 @@ fun UserAgentCard(
}
@Composable
fun AgentEmptyContentWithSegments() {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
fun AgentEmptyContentWithSegments(
showSegments: Boolean = true,
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) {
val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
@@ -260,14 +283,24 @@ fun AgentEmptyContentWithSegments() {
) {
Spacer(modifier = Modifier.height(16.dp))
// 分段控制器
AgentSegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = { selectedSegment = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// 只在查看自己的智能体时显示分段控制器
if (showSegments) {
AgentSegmentedControl(
selectedIndex = segmentSelectedIndex,
onSegmentSelected = onSegmentSelected,
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
onSegmentMeasured?.invoke(
coordinates.positionInRoot().y + parentScrollProvider(),
coordinates.size.height
)
}
.alpha(if (isSegmentSticky) 0f else 1f)
)
Spacer(modifier = Modifier.height(8.dp))
}
// 空状态内容(与动态、群聊保持一致)
Column(
@@ -275,25 +308,8 @@ fun AgentEmptyContentWithSegments() {
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isNetworkAvailable) {
Image(
painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = "暂无Agent",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.cosmos_awaits),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
EmptyStateView(
contentDescription = "暂无Agent"
)
} else {
Image(
@@ -332,7 +348,7 @@ fun AgentEmptyContentWithSegments() {
}
@Composable
private fun AgentSegmentedControl(
fun AgentSegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
@@ -397,7 +413,7 @@ private fun AgentSegmentButton(
},
shape = RoundedCornerShape(1000.dp)
)
.clickable(onClick = onClick),
.noRippleClickable { onClick() },
contentAlignment = Alignment.Center
) {
Text(

View File

@@ -84,6 +84,7 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
@OptIn(ExperimentalFoundationApi::class)
@@ -415,64 +416,17 @@ fun MomentResultTab() {
.background(AppColors.background)
) {
if (moments.itemCount == 0 && model.showResult) {
Column(
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1),
contentDescription = "No Comment",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
}
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -566,64 +520,17 @@ fun UserResultTab() {
modifier = Modifier.fillMaxSize()
) {
if (users.itemCount == 0 && model.showResult) {
Column(
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1),
contentDescription = "No Comment",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
}
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -734,64 +641,17 @@ fun AiResultTab() {
.background(AppColors.background)
) {
if (agents.itemCount == 0 && model.showResult) {
Column(
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1
),
contentDescription = "No Result",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
}
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -863,65 +723,17 @@ fun RoomResultTab() {
.background(AppColors.background)
) {
if (rooms.itemCount == 0 && model.showResult) {
Column(
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1
),
contentDescription = "No Result",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
}
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -945,41 +757,32 @@ fun RoomResultTab() {
}
@Composable
fun ReloadButton(
onClick: () -> Unit
fun SearchPlaceholderContent(
modifier: Modifier = Modifier,
isNetworkAvailable: Boolean,
onReload: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
val appColors = LocalAppTheme.current
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.empty_img),
contentDescription = "No Comment",
modifier = Modifier.size(168.dp)
)
Text(
text = stringResource(R.string.null_search),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} else {
NetworkErrorContentInline(onReload = onReload)
}
}
}
}

View File

@@ -19,6 +19,8 @@ import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentPagingSource
import com.aiosman.ravenow.entity.MomentRemoteDataSource
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.AgentRemoteDataSource
import com.aiosman.ravenow.entity.AgentSearchPagingSource
@@ -31,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
object SearchViewModel : ViewModel() {
var searchText by mutableStateOf("")
@@ -204,7 +207,14 @@ object SearchViewModel : ViewModel() {
suspend fun likeMoment(id: Int) {
try {
momentService.likeMoment(id)
updateMomentLike(id, true)
val likeCount = updateMomentLike(id, true)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = true
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -213,7 +223,14 @@ object SearchViewModel : ViewModel() {
suspend fun dislikeMoment(id: Int) {
try {
momentService.dislikeMoment(id)
updateMomentLike(id, false)
val likeCount = updateMomentLike(id, false)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = false
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -223,6 +240,12 @@ object SearchViewModel : ViewModel() {
try {
momentService.favoriteMoment(id)
updateMomentFavorite(id, true)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = true
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -232,6 +255,12 @@ object SearchViewModel : ViewModel() {
try {
momentService.unfavoriteMoment(id)
updateMomentFavorite(id, false)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = false
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -241,19 +270,23 @@ object SearchViewModel : ViewModel() {
updateMomentCommentCount(id, 1)
}
private fun updateMomentLike(id: Int, isLike: Boolean) {
private fun updateMomentLike(id: Int, isLike: Boolean): Int? {
var latestLikeCount: Int? = null
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
val nextCount = (momentItem.likeCount + if (isLike) 1 else -1).coerceAtLeast(0)
latestLikeCount = nextCount
momentItem.copy(
liked = isLike,
likeCount = momentItem.likeCount + if (isLike) 1 else -1
likeCount = nextCount
)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
return latestLikeCount
}
private fun updateMomentFavorite(id: Int, isFavorite: Boolean) {
@@ -262,7 +295,7 @@ object SearchViewModel : ViewModel() {
if (momentItem.id == id) {
momentItem.copy(
isFavorite = isFavorite,
favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1
favoriteCount = (momentItem.favoriteCount + if (isFavorite) 1 else -1).coerceAtLeast(0)
)
} else {
momentItem

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -25,10 +26,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
@@ -56,6 +54,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -319,9 +318,9 @@ private fun SingleVideoItemContent(
isPageVisible: Boolean = true
) {
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
val pauseIconVisibleState = remember(pager) {
mutableStateOf(false)
}
val pauseIconVisibleState = remember(pager) { mutableStateOf(false) }
// 记录进入后台前是否在播放,用于决定是否需要自动恢复播放
val shouldResumeAfterLifecyclePause = remember(pager) { mutableStateOf(false) }
// 当页面切换时,重置暂停状态
LaunchedEffect(pager, pagerState.currentPage) {
@@ -341,6 +340,7 @@ private fun SingleVideoItemContent(
pagerState = pagerState,
pager = pager,
pauseIconVisibleState = pauseIconVisibleState,
shouldResumeAfterLifecyclePause = shouldResumeAfterLifecyclePause,
onLikeClick = onLikeClick,
onCommentClick = onCommentClick,
onCommentAdded = onCommentAdded,
@@ -373,6 +373,7 @@ fun VideoPlayer(
pagerState: PagerState,
pager: Int,
pauseIconVisibleState: MutableState<Boolean>,
shouldResumeAfterLifecyclePause: MutableState<Boolean>,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
@@ -467,6 +468,9 @@ fun VideoPlayer(
.clip(RectangleShape)
) {
var playerView by remember { mutableStateOf<PlayerView?>(null) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
androidx.compose.runtime.key(videoUrl) {
@@ -479,15 +483,31 @@ fun VideoPlayer(
modifier = Modifier
.fillMaxSize()
.clip(RectangleShape)
.noRippleClickable {
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
.pointerInput(videoUrl, moment?.liked) {
detectTapGestures(
onDoubleTap = { offset ->
// 双击点赞/取消点赞
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
lastDoubleTapTime.value = currentTime
moment?.let {
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(it)
}
}
},
onTap = {
// 单击播放/暂停
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
}
)
}
)
}
if (pauseIconVisibleState.value) {
Icon(
imageVector = Icons.Default.PlayArrow,
Image(
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
@@ -514,15 +534,21 @@ fun VideoPlayer(
when (event) {
Lifecycle.Event.ON_PAUSE -> {
// 应用进入后台时暂停
shouldResumeAfterLifecyclePause.value = exoPlayer.isPlaying && !pauseIconVisibleState.value
exoPlayer.playWhenReady = false
exoPlayer.pause()
}
Lifecycle.Event.ON_RESUME -> {
// 返回前台且为当前页面时恢复播放
if (pager == pagerState.currentPage) {
if (
pager == pagerState.currentPage &&
isPageVisible &&
shouldResumeAfterLifecyclePause.value
) {
exoPlayer.playWhenReady = true
exoPlayer.play()
pauseIconVisibleState.value = false
}
}
@@ -660,7 +686,8 @@ fun VideoPlayer(
},
containerColor = AppColors.background,
sheetState = sheetState,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) {
Box(
modifier = Modifier
@@ -670,6 +697,7 @@ fun VideoPlayer(
CommentModalContent(
postId = moment.id,
commentCount = moment.commentCount,
showTitle = false,
onCommentAdded = {
onCommentAdded?.invoke(moment)
}

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -46,10 +47,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import java.util.Date
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.network.NetworkErrorContent
@Preview
@Composable
fun LikeNoticeScreen() {
fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
val model = LikeNoticeViewModel
val listState = rememberLazyListState()
var dataFlow = model.likeItemsFlow
@@ -63,7 +64,8 @@ fun LikeNoticeScreen() {
StatusBarMaskLayout(
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background
maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
) {
Column(
modifier = Modifier
@@ -75,42 +77,11 @@ fun LikeNoticeScreen() {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
LikeNoticeViewModel.reload(force = true)
}
)
NetworkErrorContent(
onReload = {
LikeNoticeViewModel.reload(force = true)
}
}
)
} else if (likes.itemCount == 0) {
Box(
modifier = Modifier.fillMaxSize()
@@ -184,57 +155,83 @@ fun ActionPostNoticeItem(
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
Box(
modifier = Modifier.padding(vertical = 16.dp)
val actionLabel = when (action) {
"favourite" -> stringResource(R.string.favourite_your_post)
else -> stringResource(R.string.like_your_post)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 0.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
context = context,
imageUrl = avatar,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
},
contentDescription = action,
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
.noRippleClickable {
navController.navigateToPost(
id = postId,
highlightCommentId = 0,
initImagePagerIndex = 0
)
},
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
context,
imageUrl = avatar,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
},
contentDescription = action,
)
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier
.weight(1f)
.noRippleClickable {
navController.navigateToPost(
id = postId,
highlightCommentId = 0,
initImagePagerIndex = 0
)
}
modifier = Modifier.weight(1f)
) {
Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
Spacer(modifier = Modifier.height(2.dp))
when (action) {
"like" -> Text(stringResource(R.string.like_your_post), color = AppColors.text)
"favourite" -> Text(stringResource(R.string.favourite_your_post), color = AppColors.text)
}
Spacer(modifier = Modifier.height(2.dp))
Row {
Text(likeTime.timeAgo(context), fontSize = 12.sp, color = AppColors.secondaryText)
Text(
text = nickName,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = actionLabel,
fontSize = 14.sp,
color = AppColors.secondaryText,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = likeTime.timeAgo(context),
fontSize = 14.sp,
color = AppColors.secondaryText
)
}
}
Spacer(modifier = Modifier.width(4.dp))
CustomAsyncImage(
context,
context = context,
imageUrl = thumbnail,
modifier = Modifier
.size(48.dp)
.size(40.dp)
.clip(RoundedCornerShape(8.dp)),
contentDescription = action,
)
@@ -249,10 +246,11 @@ fun LikeCommentNoticeItem(
val navController = LocalNavController.current
val context = LocalContext.current
val AppColors = LocalAppTheme.current
val previewPost = item.comment?.replyComment?.post ?: item.comment?.post
Box(
Column(
modifier = Modifier
.padding(vertical = 16.dp)
.padding(vertical = 12.dp)
.noRippleClickable {
item.comment?.postId.let {
navController.navigateToPost(
@@ -263,105 +261,103 @@ fun LikeCommentNoticeItem(
}
}
) {
Row {
Row(
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
imageUrl = item.user.avatar,
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
contentDescription = stringResource(R.string.like_your_comment)
)
Row(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = item.user.nickName,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.like_your_comment),
fontSize = 14.sp,
color = AppColors.secondaryText,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = item.likeTime.timeAgo(context),
fontSize = 14.sp,
color = AppColors.secondaryText
)
}
}
Spacer(modifier = Modifier.width(4.dp))
previewPost?.let {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Thumbnail",
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.padding(start = 48.dp)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(Color.Gray.copy(alpha = 0.1f))
) {
CustomAsyncImage(
context = context,
imageUrl = AppState.profile?.avatar ?: "",
contentDescription = "Comment Profile Picture",
modifier = Modifier
.size(24.dp)
.clip(RoundedCornerShape(24.dp)),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier.weight(1f)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
) {
CustomAsyncImage(
imageUrl = item.user.avatar,
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
contentDescription = stringResource(R.string.like_your_comment)
)
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier
.weight(1f)
) {
Text(item.user.nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
Spacer(modifier = Modifier.height(2.dp))
Text(stringResource(R.string.like_your_comment), color = AppColors.text)
Spacer(modifier = Modifier.height(2.dp))
Row {
Text(
item.likeTime.timeAgo(context),
fontSize = 12.sp,
color = AppColors.secondaryText
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.padding(start = 48.dp)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(Color.Gray.copy(alpha = 0.1f))
) {
CustomAsyncImage(
context = context,
imageUrl = AppState.profile?.avatar ?: "",
contentDescription = "Comment Profile Picture",
modifier = Modifier
.size(24.dp)
.clip(RoundedCornerShape(24.dp)),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = AppState.profile?.nickName ?: "",
fontWeight = FontWeight.W600,
fontSize = 14.sp,
color = AppColors.text
)
Text(
text = item.comment?.content ?: "",
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 2
)
}
}
Text(
text = AppState.profile?.nickName ?: "",
fontWeight = FontWeight.W600,
fontSize = 14.sp,
color = AppColors.text
)
Text(
text = item.comment?.content ?: "",
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 2
)
}
Spacer(modifier = Modifier.width(16.dp))
if (item.comment?.replyComment?.post != null) {
item.comment.replyComment.post.let {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Thumbnail",
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
} else {
item.comment?.post?.let {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Thumbnail",
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
}
}
}
}

View File

@@ -0,0 +1,153 @@
package com.aiosman.ravenow.ui.network
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
//关注、粉丝、通知、like界面网络缺省图
@Composable
fun NetworkErrorContent(
onReload: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Box(
modifier = modifier
.fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = onReload
)
}
}
}
@Composable
fun NetworkErrorContentInline(
onReload: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = onReload
)
}
}
//消息界面网络缺省图
@Composable
fun NetworkErrorContentCompact(
onReload: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(120.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = onReload
)
}
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -89,7 +90,7 @@ fun NotificationScreen() {
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
.padding(start = 16.dp, top = 8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
@@ -106,7 +107,7 @@ fun NotificationScreen() {
TabSpacer()
TabItem(
text = stringResource(R.string.followers_upper),
text = stringResource(R.string.follow_upper),
isSelected = pagerState.currentPage == 1,
onClick = {
scope.launch {
@@ -135,9 +136,9 @@ fun NotificationScreen() {
.weight(1f)
) { page ->
when (page) {
0 -> LikeNoticeScreen()
1 -> FollowerNoticeScreen()
2 -> CommentNoticeScreen()
0 -> LikeNoticeScreen(includeStatusBarPadding = false)
1 -> FollowerNoticeScreen(includeStatusBarPadding = false)
2 -> CommentNoticeScreen(includeStatusBarPadding = false)
}
}
}

View File

@@ -23,12 +23,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.animation.core.animateDpAsState
@@ -56,7 +54,6 @@ import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
@@ -106,33 +103,37 @@ fun PointsBottomSheet(
}
}
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
ModalBottomSheet(
onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮)
sheetState = sheetState,
containerColor = AppColors.background,
dragHandle = null // 移除拖动手柄
containerColor = Color.Transparent,
dragHandle = null, // 移除拖动手柄
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.95f)
.offset(y = offsetY)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
.padding(top = 10.dp)
) {
Column(
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = AppColors.background,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
) {
// 头部 - 使用 Box 实现绝对居中布局
Box(
modifier = Modifier
@@ -340,6 +341,7 @@ fun PointsBottomSheet(
} else {
HowToEarnList(onRecharge = onRecharge)
}
}
}
}
}

View File

@@ -13,37 +13,30 @@ import kotlinx.coroutines.launch
class CommentsViewModel(
var postId: String = 0.toString(),
) : ViewModel() {
companion object {
private const val ORDER_ALL = "all"
private const val COMMENTS_PAGE_SIZE = 50
}
var commentService: CommentService = CommentServiceImpl()
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList())
var order: String by mutableStateOf("like")
var order: String by mutableStateOf(ORDER_ALL)
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
var highlightCommentId by mutableStateOf<Int?>(null)
var highlightComment by mutableStateOf<CommentEntity?>(null)
var isLoading by mutableStateOf(false)
var hasError by mutableStateOf(false)
var isLoadingMore by mutableStateOf(false)
var hasMore by mutableStateOf(false)
private var currentPage by mutableStateOf(0)
private var totalComments by mutableStateOf(0)
/**
* 预加载,在跳转到 PostScreen 之前设置好内容
*/
fun preTransit() {
viewModelScope.launch {
try {
isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
pageSize = 10
)
commentsList = response.list
hasError = false
} catch (e: Exception) {
e.printStackTrace()
hasError = true
} finally {
isLoading = false
}
}
reloadComment()
}
/**
@@ -51,25 +44,61 @@ class CommentsViewModel(
*/
fun reloadComment() {
viewModelScope.launch {
try {
loadComments(page = 1, reset = true)
}
}
fun loadMoreComments() {
if (isLoading || isLoadingMore || !hasMore) {
return
}
viewModelScope.launch {
loadComments(page = currentPage + 1, reset = false)
}
}
private suspend fun loadComments(page: Int, reset: Boolean) {
try {
if (reset) {
isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
order = order,
pageSize = 50
)
commentsList = response.list
hasError = false
} catch (e: Exception) {
e.printStackTrace()
} else {
isLoadingMore = true
}
val response = commentService.getComments(
pageNumber = page,
postId = postId.toInt(),
order = normalizeOrder(order),
pageSize = COMMENTS_PAGE_SIZE
)
val total = response.total.coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
totalComments = total
currentPage = response.page
commentsList = if (reset) {
response.list
} else {
commentsList + response.list
}
hasMore = commentsList.size < totalComments
} catch (e: Exception) {
e.printStackTrace()
if (reset) {
hasError = true
} finally {
commentsList = emptyList()
}
} finally {
if (reset) {
isLoading = false
} else {
isLoadingMore = false
}
}
}
private fun normalizeOrder(currentOrder: String): String? {
return currentOrder.takeUnless { it.equals(ORDER_ALL, ignoreCase = true) }
}
suspend fun highlightComment(commentId: Int) {
highlightCommentId = commentId

View File

@@ -22,6 +22,7 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@@ -34,6 +35,7 @@ import android.graphics.BitmapFactory
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -78,92 +80,108 @@ fun DraftBoxBottomSheet(
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = AppColors.background,
containerColor = Color.Transparent,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {}
) {
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.9f)
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(top = 8.dp)
) {
// 标题
Box(
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
.fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = AppColors.background,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) {
Text(
text = stringResource(R.string.drafts),
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
// 草稿列表
if (drafts.isEmpty()) {
Box(
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
contentAlignment = Alignment.Center
.fillMaxHeight()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = stringResource(R.string.no_drafts),
fontSize = 16.sp,
color = AppColors.secondaryText
)
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
itemsIndexed(drafts) { index, draft ->
DraftItem(
draft = draft,
dateFormat = dateFormat,
onEditClick = {
model.viewModelScope.launch {
model.loadDraft(context, draft)
onDismiss()
}
},
onDeleteClick = {
draftStore.deleteDraft(index)
drafts = draftStore.getAllDrafts()
},
AppColors = AppColors,
context = context
// 标题
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.drafts),
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
// 在草稿项之间添加分割线(最后一个不添加)
if (index < drafts.size - 1) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
color = AppColors.secondaryText.copy(alpha = 0.2f),
thickness = 0.5.dp,
modifier = Modifier.padding(horizontal = 16.dp)
}
// 草稿列表
if (drafts.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.no_drafts),
fontSize = 16.sp,
color = AppColors.secondaryText
)
Spacer(modifier = Modifier.height(12.dp))
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
itemsIndexed(drafts) { index, draft ->
DraftItem(
draft = draft,
dateFormat = dateFormat,
onEditClick = {
model.viewModelScope.launch {
model.loadDraft(context, draft)
onDismiss()
}
},
onDeleteClick = {
draftStore.deleteDraft(index)
drafts = draftStore.getAllDrafts()
},
AppColors = AppColors,
context = context
)
// 在草稿项之间添加分割线(最后一个不添加)
if (index < drafts.size - 1) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
color = AppColors.secondaryText.copy(alpha = 0.2f),
thickness = 0.5.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(12.dp))
}
}
}
// 底部提示
Text(
text = stringResource(R.string.only_save_the_last_5_drafts),
fontSize = 12.sp,
color = AppColors.secondaryText,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}
// 底部提示
Text(
text = stringResource(R.string.only_save_the_last_5_drafts),
fontSize = 12.sp,
color = AppColors.secondaryText,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}
}

View File

@@ -1,9 +1,12 @@
package com.aiosman.ravenow.ui.post
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloatAsState
@@ -588,6 +591,34 @@ fun AddImageGrid() {
}
}
// 摄像头权限请求
val requestCameraPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// 权限已授予,打开相机
if (model.imageList.size < 9) {
val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
photoFile
)
model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri)
} else {
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
}
} else {
// 权限被拒绝,提示用户
Toast.makeText(
context,
"需要摄像头权限才能拍摄照片,请在设置中开启",
Toast.LENGTH_LONG
).show()
}
}
val addImageDebouncer = rememberDebouncer()
val canAddMoreImages = model.imageList.size < 9
@@ -642,14 +673,27 @@ fun AddImageGrid() {
.background(Color(0xFFFAF9FB))
.noRippleClickable {
if (model.imageList.size < 9) {
val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
photoFile
)
model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri)
// 检查摄像头权限
when {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
// 已有权限,直接打开相机
val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
photoFile
)
model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri)
}
else -> {
// 没有权限,请求权限
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
} else {
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
}

View File

@@ -12,6 +12,8 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
@@ -24,6 +26,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -44,6 +47,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@@ -300,20 +304,35 @@ fun PostScreen(
onDismissRequest = {
showReportDialog = false
},
containerColor = AppColors.background,
containerColor = Color.Transparent,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
ReportModal(
momentId = viewModel.moment!!.id,
onClose = {
showReportDialog = false
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 7.dp)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
tonalElevation = 0.dp,
shadowElevation = 0.dp,
color = AppColors.background,
contentColor = AppColors.text
) {
ReportModal(
momentId = viewModel.moment!!.id,
onClose = {
showReportDialog = false
}
)
}
)
}
}
}
Scaffold(
@@ -493,7 +512,9 @@ fun PostScreen(
color = AppColors.nonActiveText
)
Spacer(modifier = Modifier.weight(1f))
OrderSelectionComponent() {
OrderSelectionComponent(
selectedOrder = commentsViewModel.order
) {
commentsViewModel.order = it
viewModel.reloadComment()
}
@@ -740,6 +761,33 @@ fun CommentContent(
}
}
if (viewModel.isLoadingMore || viewModel.hasMore) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
if (viewModel.isLoadingMore) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main,
strokeWidth = 2.dp
)
} else {
Text(
text = stringResource(id = R.string.load_more),
color = AppColors.main,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.noRippleClickable {
viewModel.loadMoreComments()
}
)
}
}
}
// 加载状态处理
if (viewModel.isLoading) {
Box(
@@ -1159,14 +1207,26 @@ fun ImageViewerDialog(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostImageView(
images: List<MomentImageEntity>,
initialPage: Int? = 0
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var isImageViewerDialog by remember { mutableStateOf(false) }
var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) }
val initialPageIndex = initialPage ?: 0
val pagerState = rememberPagerState(
pageCount = { images.size },
initialPage = initialPageIndex.coerceIn(0, maxOf(0, images.size - 1))
)
var currentImageIndex by remember { mutableStateOf(pagerState.currentPage) }
// 同步 pagerState 的当前页面到 currentImageIndex
LaunchedEffect(pagerState.currentPage) {
currentImageIndex = pagerState.currentPage
}
DisposableEffect(Unit) {
onDispose {
@@ -1187,23 +1247,31 @@ fun PostImageView(
modifier = Modifier
) {
if (images.isNotEmpty()) {
CustomAsyncImage(
context,
images[currentImageIndex].thumbnail,
contentDescription = "Image",
contentScale = ContentScale.Crop,
HorizontalPager(
state = pagerState,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
isImageViewerDialog = true
}
)
}
.background(Color.Gray.copy(alpha = 0.1f))
)
) { page ->
val image = images[page]
CustomAsyncImage(
context,
image.thumbnail,
contentDescription = "Image",
blurHash = image.blurHash,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
isImageViewerDialog = true
}
)
}
.background(Color.Gray.copy(alpha = 0.1f))
)
}
}
// 图片导航控件
@@ -1212,56 +1280,26 @@ fun PostImageView(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
// Previous button
Text(
text = "Previous",
modifier = Modifier
.padding(8.dp)
.noRippleClickable {
if (currentImageIndex > 0) {
currentImageIndex--
}
},
color = if (currentImageIndex > 0) Color.Blue else Color.Gray
)
// Indicators
Row(
horizontalArrangement = Arrangement.Center
) {
images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
.size(4.dp)
.clip(CircleShape)
.background(
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
.size(4.dp)
.clip(CircleShape)
.background(
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
.padding(4.dp)
)
if (index < images.size - 1) {
Spacer(modifier = Modifier.width(8.dp))
}
)
.padding(4.dp)
)
if (index < images.size - 1) {
Spacer(modifier = Modifier.width(8.dp))
}
}
// Next button
Text(
text = "Next",
modifier = Modifier
.padding(8.dp)
.noRippleClickable {
if (currentImageIndex < images.size - 1) {
currentImageIndex++
}
},
color = if (currentImageIndex < images.size - 1) Color.Blue else Color.Gray
)
}
}
}
@@ -1917,15 +1955,15 @@ fun CommentMenuModal(
@Composable
fun OrderSelectionComponent(
selectedOrder: String,
onSelected: (String) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
var selectedOrder by remember { mutableStateOf("like") }
val orders = listOf(
"like" to stringResource(R.string.order_comment_default),
"earliest" to stringResource(R.string.order_comment_earliest),
"latest" to stringResource(R.string.order_comment_latest)
"all" to stringResource(R.string.order_comment_default),
"latest" to stringResource(R.string.order_comment_latest),
"like" to stringResource(R.string.order_comment_hot)
)
Box(
modifier = Modifier
@@ -1943,8 +1981,9 @@ fun OrderSelectionComponent(
Box(
modifier = Modifier
.noRippleClickable {
selectedOrder = order.first
onSelected(order.first)
if (selectedOrder != order.first) {
onSelected(order.first)
}
}
.background(
if (
@@ -2011,8 +2050,8 @@ fun ReportModal(
Column(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp)
.fillMaxHeight()
.padding(start = 24.dp, end = 24.dp)
) {
Box(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp),

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Some files were not shown because too many files have changed in this diff Show More