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

View File

@@ -73,6 +73,10 @@ class MainActivity : ComponentActivity() {
val config = Configuration(newConfig) val config = Configuration(newConfig)
config.fontScale = 1.0f config.fontScale = 1.0f
super.onConfigurationChanged(config) 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) JPushInterface.init(this)
if (AppState.darkMode) { updateWindowBackground(AppState.darkMode)
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
}
enableEdgeToEdge() enableEdgeToEdge()
scope.launch { scope.launch {
@@ -269,8 +271,22 @@ class MainActivity : ComponentActivity() {
notificationManager.createNotificationChannel(channel) 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> { val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController not provided") error("NavController not provided")
} }

View File

@@ -545,7 +545,13 @@ class AccountServiceImpl : AccountService {
val bannerField: MultipartBody.Part? = banner?.let { val bannerField: MultipartBody.Part? = banner?.let {
createMultipartBody(it.file, it.filename, "banner") 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) { 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) { fun updateMomentLike(id: Int,isLike:Boolean) {
this.list = this.list.map { momentItem -> this.list = this.list.map { momentItem ->
if (momentItem.id == id) { 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 { } else {
momentItem momentItem
} }
@@ -421,7 +430,16 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateFavoriteCount(id: Int,isFavorite:Boolean) { fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
this.list = this.list.map { momentItem -> this.list = this.list.map { momentItem ->
if (momentItem.id == id) { 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 { } else {
momentItem momentItem
} }

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.entity package com.aiosman.ravenow.entity
import android.util.Log
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
@@ -272,19 +273,41 @@ class RoomRemoteDataSource {
pageSize: Int = 20, pageSize: Int = 20,
search: String search: String
): ListContainer<RoomEntity>? { ): ListContainer<RoomEntity>? {
return try {
val resp = ApiClient.api.getRooms( val resp = ApiClient.api.getRooms(
page = pageNumber, page = pageNumber,
pageSize = pageSize, pageSize = pageSize,
search = search, search = search,
roomType = "public" // 搜索时只显示公有房间 roomType = "public" // 搜索时只显示公有房间
) )
if (!resp.isSuccessful) {
// API 调用失败,返回 null
return null
}
val body = resp.body() ?: return null val body = resp.body() ?: return null
return ListContainer(
// 安全地转换数据,过滤掉转换失败的项目
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, total = body.total,
page = pageNumber, page = pageNumber,
pageSize = pageSize, pageSize = pageSize,
list = body.list.map { it.toRoomtEntity() } list = roomList
) )
} catch (e: Exception) {
// 捕获所有异常,返回 null 让 PagingSource 处理
Log.e("RoomRemoteDataSource", "searchRooms error", e)
null
}
} }
} }
@@ -303,17 +326,31 @@ class RoomSearchPagingSource(
pageSize = params.loadSize, pageSize = params.loadSize,
search = keyword search = keyword
) )
if (rooms == null) {
// API 调用失败,返回空列表
LoadResult.Page( LoadResult.Page(
data = rooms?.list ?: listOf(), data = emptyList(),
prevKey = if (currentPage == 1) null else currentPage - 1, prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null nextKey = null
) )
} catch (exception: IOException) { } 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) LoadResult.Error(exception)
} }
} }
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? { 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.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
/** /**
@@ -24,11 +25,16 @@ object AppStore {
.requestEmail() .requestEmail()
.build() .build()
googleSignInOptions = gso googleSignInOptions = gso
// apply dark mode // apply dark mode - 如果用户未手动设置,优先跟随系统
if (sharedPreferences.getBoolean("darkMode", false)) { val hasUserPreference = sharedPreferences.contains("darkMode")
AppState.darkMode = true val resolvedDarkMode = if (hasUserPreference) {
AppState.appTheme = DarkThemeColors() 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 // load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null) 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -40,22 +41,21 @@ fun AboutScreen() {
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) { ) {
NoticeScreenHeader( NoticeScreenHeader(
title = stringResource(R.string.about_rave_now), title = stringResource(R.string.about_paipai),
moreIcon = false moreIcon = false
) )
} }
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth() .fillMaxWidth(),
.padding(start = 24.dp), horizontalAlignment = Alignment.CenterHorizontally,
horizontalAlignment = Alignment.CenterHorizontally verticalArrangement = Arrangement.Center
) { ) {
Spacer(modifier = Modifier.height(48.dp)) // app icondww
// app icon
Box { Box {
Image( Image(
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next), painter = painterResource(id = R.mipmap.invalid_name),
contentDescription = "app icon", contentDescription = "app icon",
modifier = Modifier.size(80.dp) modifier = Modifier.size(80.dp)
) )
@@ -63,7 +63,7 @@ fun AboutScreen() {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// app name // app name
Text( Text(
text = "Rave Now".uppercase(), text = stringResource(R.string.paipai),
fontSize = 24.sp, fontSize = 24.sp,
color = appColors.text, color = appColors.text,
fontWeight = FontWeight.ExtraBold 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 package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter 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.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp 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.AppState
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.modifiers.noRippleClickable
// MBTI类型列表 // MBTI类型列表
val MBTI_TYPES = listOf( val MBTI_TYPES = listOf(
@@ -46,96 +63,326 @@ val MBTI_TYPES = listOf(
"ISTP", "ISFP", "ESTP", "ESFP" "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 @Composable
fun MbtiSelectScreen() { fun MbtiSelectBottomSheet(
val navController = LocalNavController.current onClose: () -> Unit
) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
val model = AccountEditViewModel val model = AccountEditViewModel
val currentMbti = model.mbti val currentMbti = model.mbti
val sheetBackgroundColor = if (isDarkMode) {
appColors.secondaryBackground
} else {
Color(0xFFFFFFFF)
}
Column( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
modifier = Modifier
.fillMaxSize() // 确保弹窗展开
.background(appColors.profileBackground) 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()
}
}
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( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 16.dp) .fillMaxHeight(0.95f)
.offset(y = offsetY)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
) { ) {
NoticeScreenHeader( Column(
title = stringResource(R.string.choose_mbti), modifier = Modifier
moreIcon = false .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( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp) .fillMaxWidth()
.weight(1f)
.nestedScroll(nestedScrollConnection),
contentPadding = PaddingValues(
start = 8.dp,
top = 0.dp,
end = 8.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
items(MBTI_TYPES) { mbti -> // MBTI解释文字 - 作为第一个item
MBTIItem( 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, mbti = mbti,
isSelected = mbti == currentMbti, isSelected = mbti == currentMbti,
onClick = { onClick = {
// 保存MBTI类型
model.mbti = mbti model.mbti = mbti
// 立即保存到本地存储,确保选择后立即生效 onClose()
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
}
navController.navigateUp()
} }
) )
Spacer(modifier = Modifier.height(8.dp)) }
}
// 如果这一行只有1个item添加一个空的Spacer来保持布局
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
} }
} }
} }
} }
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MBTIItem( fun MbtiSelectScreen() {
val navController = LocalNavController.current
MbtiSelectBottomSheet(
onClose = {
navController.navigateUp()
}
)
}
@Composable
fun MbtiItem(
mbti: String, mbti: String,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
Box( // 卡片背景色
val cardBackgroundColor = if (isDarkMode) {
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
} else {
Color(0xFFFAF9FB)
}
Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .aspectRatio(1.1f)
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White) .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( .clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
onClick() onClick()
} }
.padding(16.dp) .padding(horizontal = 24.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ) {
Row( // MBTI图标 - 使用占位图片
modifier = Modifier.fillMaxWidth(), Box(
verticalAlignment = Alignment.CenterVertically modifier = Modifier.size(100.dp),
contentAlignment = Alignment.Center
) { ) {
Image(
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)),
contentDescription = mbti,
modifier = Modifier.size(100.dp)
)
}
// MBTI名称 - 使用负间距让文本向上移动,与图标更靠近
Text( Text(
text = mbti, text = mbti,
fontSize = 17.sp, fontSize = 14.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Medium,
color = if (isSelected) appColors.main else appColors.text, color = appColors.text,
modifier = Modifier.weight(1f) textAlign = TextAlign.Center,
modifier = Modifier.offset(y = (-20).dp)
) )
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
} }
} }

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 = { onClick = {
// 保存当前语言的星座文本 // 保存当前语言的星座文本
model.zodiac = zodiacText model.zodiac = zodiacText
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
}
onClose() onClose()
} }
) )

View File

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

View File

@@ -70,6 +70,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.SolidColor 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.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import android.widget.Toast import android.widget.Toast
@@ -77,6 +79,8 @@ import java.io.File
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
import com.aiosman.ravenow.ui.account.ZodiacSheetManager 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() ZodiacBottomSheetHost()
// 挂载MBTI选择弹窗
MbtiBottomSheetHost()
Box( Box(
modifier = Modifier modifier = Modifier
@@ -392,7 +398,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard( ProfileInfoCard(
label = stringResource(R.string.nickname), label = stringResource(R.string.nickname),
value = model.name, value = model.name,
placeholder = "Value", placeholder = stringResource(R.string.nickname_placeholder),
onValueChange = { onNicknameChange(it) }, onValueChange = { onNicknameChange(it) },
isMultiline = false isMultiline = false
) )
@@ -403,7 +409,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard( ProfileInfoCard(
label = stringResource(R.string.personal_intro), label = stringResource(R.string.personal_intro),
value = model.bio, value = model.bio,
placeholder = "Welcome to my fantiac word i will show you something about magic", placeholder = "",
onValueChange = { onBioChange(it) }, onValueChange = { onBioChange(it) },
isMultiline = true isMultiline = true
) )
@@ -425,9 +431,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
iconResDark = null, // TODO: 添加MBTI暗色模式图标 iconResDark = null, // TODO: 添加MBTI暗色模式图标
iconResLight = null, // TODO: 添加MBTI亮色模式图标 iconResLight = null, // TODO: 添加MBTI亮色模式图标
onClick = { onClick = {
debouncedNavigation { MbtiSheetManager.open()
navController.navigate(NavigationRoute.MbtiSelect.route)
}
} }
) )
@@ -500,6 +504,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 验证通过,执行保存 // 验证通过,执行保存
model.viewModelScope.launch { model.viewModelScope.launch {
model.isUpdating = true model.isUpdating = true
try {
model.updateUserProfile(context) model.updateUserProfile(context)
model.viewModelScope.launch(Dispatchers.Main) { model.viewModelScope.launch(Dispatchers.Main) {
debouncedNavigation { debouncedNavigation {
@@ -507,6 +512,17 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
} }
model.isUpdating = false 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
}
}
} }
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -550,29 +566,45 @@ fun ProfileInfoCard(
isMultiline: Boolean = false isMultiline: Boolean = false
) { ) {
val appColors = LocalAppTheme.current 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( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp个人简介66dp .height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp个人简介66dp
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(appColors.secondaryBackground), .background(appColors.secondaryBackground),
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart contentAlignment = if (isMultiline && lineCount > 1) Alignment.TopStart else Alignment.CenterStart
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(vertical = if (isMultiline) 11.dp else 0.dp), .padding(vertical = if (isMultiline && lineCount > 1) 11.dp else 0.dp),
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically verticalAlignment = verticalAlignment
) { ) {
// 标签 // 标签
Box(
modifier = Modifier
.width(100.dp)
.height(if (isMultiline) 44.dp else 56.dp),
contentAlignment = Alignment.CenterStart
) {
Text( Text(
text = label, text = label,
fontSize = 17.sp, fontSize = 17.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = appColors.text, color = appColors.text
modifier = Modifier.width(100.dp)
) )
}
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -580,10 +612,26 @@ fun ProfileInfoCard(
Box( Box(
modifier = Modifier.weight(1f) 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(
text = placeholder, text = placeholder,
fontSize = if (isMultiline) 15.sp else 17.sp, fontSize = 17.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = appColors.secondaryText, color = appColors.secondaryText,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -593,7 +641,11 @@ fun ProfileInfoCard(
BasicTextField( BasicTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
},
textStyle = androidx.compose.ui.text.TextStyle( textStyle = androidx.compose.ui.text.TextStyle(
fontSize = if (isMultiline) 15.sp else 17.sp, fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
@@ -601,7 +653,12 @@ fun ProfileInfoCard(
), ),
cursorBrush = SolidColor(appColors.text), cursorBrush = SolidColor(appColors.text),
maxLines = if (isMultiline) Int.MAX_VALUE else 1, 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.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -50,6 +51,14 @@ fun RemoveAccountScreen() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current 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) { fun removeAccount(password: String) {
// 使用通用密码校验器 // 使用通用密码校验器
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context) val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
@@ -132,7 +141,8 @@ fun RemoveAccountScreen() {
}, },
password = true, password = true,
hint = stringResource(R.string.remove_account_password_hint), hint = stringResource(R.string.remove_account_password_hint),
error = passwordError error = passwordError,
customHintColor = hintColor
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))

View File

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

View File

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

View File

@@ -183,45 +183,21 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
if (viewModel.groupAvatar.isNotEmpty()) { if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = viewModel.groupAvatar, imageUrl = viewModel.groupInfo!!.groupAvatar,
modifier = Modifier modifier = Modifier
.size(32.dp) .size(35.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(15.dp)),
.noRippleClickable {
navController.navigateToGroupInfo(groupId)
},
contentDescription = "群聊头像" 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)) Spacer(modifier = Modifier.width(8.dp))
Column { Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.Start
) {
Text( Text(
text = viewModel.groupName, text = viewModel.groupName,
style = TextStyle( style = TextStyle(
@@ -230,12 +206,10 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
), ),
maxLines = 1, maxLines = 1,
overflow =TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
) )
} }
Spacer(modifier = Modifier.weight(1f))
Box {
Image( Image(
painter = painterResource(R.drawable.rider_pro_more_horizon), painter = painterResource(R.drawable.rider_pro_more_horizon),
modifier = Modifier modifier = Modifier
@@ -243,12 +217,11 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
.noRippleClickable { .noRippleClickable {
navController.navigateToGroupInfo(groupId) navController.navigateToGroupInfo(groupId)
}, },
contentDescription = null, contentDescription = "更多",
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
} }
} }
}
}, },
bottomBar = { bottomBar = {
Column( Column(
@@ -677,7 +650,7 @@ fun GroupChatInput(
animationSpec = tween(300) animationSpec = tween(300)
) )
Image( Image(
painter = painterResource(R.mipmap.rider_pro_im_send), painter = painterResource(R.mipmap.btn),
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.alpha(alpha) .alpha(alpha)

View File

@@ -1,11 +1,13 @@
package com.aiosman.ravenow.ui.chat package com.aiosman.ravenow.ui.chat
import android.content.Context import android.content.Context
import android.util.Base64
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.SendChatAiRequestBody import com.aiosman.ravenow.data.api.SendChatAiRequestBody
import io.openim.android.sdk.enums.ConversationType import io.openim.android.sdk.enums.ConversationType
@@ -50,7 +52,34 @@ class GroupChatViewModel(
} }
private suspend fun getGroupInfo() { private suspend fun getGroupInfo() {
// 简化群组信息获取,使用默认信息 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( groupInfo = GroupInfo(
groupId = groupId, groupId = groupId,
groupName = name, groupName = name,
@@ -58,10 +87,12 @@ class GroupChatViewModel(
memberCount = 0, memberCount = 0,
ownerId = "" ownerId = ""
) )
} finally {
groupName = groupInfo?.groupName ?: "" groupName = groupInfo?.groupName ?: ""
groupAvatar = groupInfo?.groupAvatar ?: "" groupAvatar = groupInfo?.groupAvatar ?: ""
memberCount = groupInfo?.memberCount ?: 0 memberCount = groupInfo?.memberCount ?: 0
} }
}
override fun getConversationParams(): Triple<String, Int, Boolean> { override fun getConversationParams(): Triple<String, Int, Boolean> {
// 根据群组类型决定ConversationType这里假设是普通群聊 // 根据群组类型决定ConversationType这里假设是普通群聊

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ fun StatusBarMaskLayout(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
darkIcons: Boolean = true, darkIcons: Boolean = true,
useNavigationBarMask: Boolean = true, useNavigationBarMask: Boolean = true,
includeStatusBarPadding: Boolean = true,
maskBoxBackgroundColor: Color = Color.Transparent, maskBoxBackgroundColor: Color = Color.Transparent,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
@@ -50,13 +51,13 @@ fun StatusBarMaskLayout(
Column( Column(
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) { ) {
if (includeStatusBarPadding) {
Box( Box(
modifier = Modifier modifier = Modifier
.height(paddingValues.calculateTopPadding()) .height(paddingValues.calculateTopPadding())
.fillMaxWidth() .fillMaxWidth()
.background(maskBoxBackgroundColor) .background(maskBoxBackgroundColor)
) { )
} }
content() content()
if (navigationBarPaddings > 24.dp && useNavigationBarMask) { 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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi 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.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.favourite.FavouriteListViewModel.refreshPager
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost 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 import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@@ -84,43 +88,11 @@ fun FavouriteListPage() {
var moments = dataFlow.collectAsLazyPagingItems() var moments = dataFlow.collectAsLazyPagingItems()
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
Box( NetworkErrorContent(
modifier = Modifier onReload = {
.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) model.refreshPager(force = true)
} }
) )
}
}
} else if(moments.itemCount == 0) { } else if(moments.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -187,7 +159,10 @@ fun FavouriteListPage() {
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
context = context 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( Box(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp, end = 8.dp) .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 com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton import com.aiosman.ravenow.ui.network.NetworkErrorContent
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -76,43 +76,11 @@ fun FollowerListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
Box( NetworkErrorContent(
modifier = Modifier onReload = {
.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) model.loadData(userId, true)
} }
) )
}
}
} else if (users.itemCount == 0) { } else if (users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.FollowButton
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.modifiers.noRippleClickable 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 kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -48,13 +49,14 @@ import com.aiosman.ravenow.utils.NetworkUtils
* 关注消息列表 * 关注消息列表
*/ */
@Composable @Composable
fun FollowerNoticeScreen() { fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
StatusBarMaskLayout( StatusBarMaskLayout(
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp), modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode, darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
) { ) {
val model = FollowerNoticeViewModel val model = FollowerNoticeViewModel
var dataFlow = model.followerItemsFlow var dataFlow = model.followerItemsFlow
@@ -66,43 +68,11 @@ fun FollowerNoticeScreen() {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
Box( NetworkErrorContent(
modifier = Modifier onReload = {
.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) model.reload(force = true)
} }
) )
}
}
} else if (followers.itemCount == 0) { } else if (followers.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -171,9 +141,22 @@ fun FollowItem(
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
Box( val followText = stringResource(R.string.followed_you)
Row(
modifier = Modifier modifier = Modifier
.padding(vertical = 16.dp) .fillMaxWidth()
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 左侧头像区域
CustomAsyncImage(
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.noRippleClickable { .noRippleClickable {
navController.navigate( navController.navigate(
NavigationRoute.AccountProfile.route.replace( NavigationRoute.AccountProfile.route.replace(
@@ -182,24 +165,33 @@ fun FollowItem(
) )
) )
} }
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
CustomAsyncImage(
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
) )
Spacer(modifier = Modifier.width(12.dp)) // 右侧内容区域
Row(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column( Column(
modifier = Modifier.weight(1f) 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) { if (!isFollowing && userId != AppState.UserId) {
FollowButton( FollowButton(

View File

@@ -40,7 +40,7 @@ import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout 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 kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -78,43 +78,11 @@ fun FollowingListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
Box( NetworkErrorContent(
modifier = Modifier onReload = {
.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) model.loadData(userId, true)
} }
) )
}
}
} else if(users.itemCount == 0) { } else if(users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -99,6 +99,15 @@ fun CreateGroupChatScreen() {
} }
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() 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) val pointsRules by PointService.pointsRules.collectAsState(initial = null)
@@ -495,14 +504,48 @@ fun CreateGroupChatScreen() {
} }
} }
// 创建群聊按钮 - 固定在底部 // 创建群聊按钮 - 固定在底部(启用时使用渐变背景)
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 {
baseModifier
}
}
) {
Button( Button(
onClick = { onClick = {
// 创建群聊逻辑 // 创建群聊逻辑
if (selectedMembers.isNotEmpty()) { if (selectedMembers.isNotEmpty()) {
// 检查是否超过上限 // 检查是否超过上限
if (selectedMembers.size > maxMemberLimit) { if (selectedMembers.size > maxMemberLimit) {
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit)) CreateGroupChatViewModel.showError(
context.getString(
R.string.create_group_chat_exceed_limit,
maxMemberLimit
)
)
return@Button return@Button
} }
// 如果费用大于0显示确认弹窗 // 如果费用大于0显示确认弹窗
@@ -524,16 +567,15 @@ fun CreateGroupChatScreen() {
} }
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main, containerColor = if (isCreateEnabled) Color.Transparent else AppColors.main,
contentColor = AppColors.mainText, contentColor = AppColors.mainText,
disabledContainerColor = AppColors.disabledBackground, disabledContainerColor = AppColors.disabledBackground,
disabledContentColor = AppColors.text disabledContentColor = AppColors.text
), ),
shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading enabled = isCreateEnabled
) { ) {
if (CreateGroupChatViewModel.isLoading) { if (CreateGroupChatViewModel.isLoading) {
Text( Text(
@@ -550,6 +592,44 @@ fun CreateGroupChatScreen() {
} }
} }
// 禁用状态下拦截点击并弹出提示
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
)
}
}
}
} }
// 消费确认弹窗 // 消费确认弹窗

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.index.NavItem import com.aiosman.ravenow.ui.index.NavItem
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToAddGroupMember
import com.aiosman.ravenow.ui.navigateToGroupMembers import com.aiosman.ravenow.ui.navigateToGroupMembers
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -173,7 +175,7 @@ fun GroupChatInfoScreen(groupId: String) {
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = "${viewModel.groupInfo?.memberCount ?: 0}", text = "${viewModel.groupInfo?.memberCount ?: 0}${stringResource(R.string.people)}",
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text.copy(alpha = 0.7f), color = AppColors.text.copy(alpha = 0.7f),
fontSize = 11.sp fontSize = 11.sp
@@ -187,14 +189,17 @@ fun GroupChatInfoScreen(groupId: String) {
item { item {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
if (viewModel.groupInfo?.isCreator == true) {
// 添加其他人 // 添加其他人
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
// TODO: 实现添加其他人功能 navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
} }
) { ) {
Box( Box(
@@ -220,6 +225,7 @@ fun GroupChatInfoScreen(groupId: String) {
) )
) )
} }
}
// 通知设置 // 通知设置
Column( Column(
@@ -258,11 +264,11 @@ fun GroupChatInfoScreen(groupId: String) {
) )
} }
// 退出群聊 // 分享群聊
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
// TODO: 实现退出群聊功能 // TODO: 实现分享功能
} }
) { ) {
Box( Box(
@@ -272,7 +278,7 @@ fun GroupChatInfoScreen(groupId: String) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image( Image(
painter = painterResource(R.drawable.group_info_edit), painter = painterResource(R.mipmap.icon_share),
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
@@ -297,16 +303,16 @@ fun GroupChatInfoScreen(groupId: String) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.border(1.dp, AppColors.decentBackground, RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(AppColors.decentBackground.copy(alpha = 0.28f)) .background(AppColors.background)
.padding(12.dp) .padding(12.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Image( Image(
painter = painterResource(R.drawable.group_info_edit), painter = painterResource(R.mipmap.icons_brain),
modifier = Modifier.size(16.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@@ -315,7 +321,6 @@ fun GroupChatInfoScreen(groupId: String) {
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text, color = AppColors.text,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.Bold
) )
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
@@ -337,7 +342,7 @@ fun GroupChatInfoScreen(groupId: String) {
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background(AppColors.background) .background(AppColors.decentBackground)
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.noRippleClickable { .noRippleClickable {
showAddMemoryDialog = true showAddMemoryDialog = true
@@ -356,7 +361,7 @@ fun GroupChatInfoScreen(groupId: String) {
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background(AppColors.background) .background(AppColors.decentBackground)
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.noRippleClickable { .noRippleClickable {
showMemoryManageDialog = true showMemoryManageDialog = true
@@ -379,6 +384,8 @@ fun GroupChatInfoScreen(groupId: String) {
item { item {
Spacer(modifier = Modifier.height(13.dp)) Spacer(modifier = Modifier.height(13.dp))
// 仅当当前用户是群聊创建者时显示以下组件
if (viewModel.groupInfo?.isCreator == true) {
// 群资料设置 // 群资料设置
Row( Row(
modifier = Modifier modifier = Modifier
@@ -391,7 +398,7 @@ fun GroupChatInfoScreen(groupId: String) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Image(
painter = painterResource(R.drawable.group_info_edit), painter = painterResource(R.mipmap.fengm),
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
@@ -455,6 +462,7 @@ fun GroupChatInfoScreen(groupId: String) {
contentDescription = null, contentDescription = null,
) )
} }
}
// 成员管理 // 成员管理
Row( Row(
@@ -468,15 +476,21 @@ fun GroupChatInfoScreen(groupId: String) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Image(
painter = painterResource(R.drawable.group_info_users), painter = painterResource(R.mipmap.icons_users),
modifier = Modifier.size(20.dp), modifier = Modifier.size(25.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
AppColors.text) AppColors.text)
) )
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text( 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( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text, color = AppColors.text,
fontSize = 15.sp fontSize = 15.sp
@@ -499,8 +513,8 @@ fun GroupChatInfoScreen(groupId: String) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Image(
painter = painterResource(R.drawable.group_info_edit), painter = painterResource(R.mipmap.iconsgallery),
modifier = Modifier.size(20.dp), modifier = Modifier.size(25.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
AppColors.text) AppColors.text)
@@ -520,7 +534,8 @@ fun GroupChatInfoScreen(groupId: String) {
contentDescription = null, contentDescription = null,
) )
} }
// 解散群聊 if (viewModel.groupInfo?.isCreator == true) {
// 解散群聊(仅群主)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -529,13 +544,14 @@ fun GroupChatInfoScreen(groupId: String) {
.noRippleClickable { }, .noRippleClickable { },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Spacer(modifier = Modifier.width(3.dp))
Image( Image(
painter = painterResource(R.drawable.group_info_exit), painter = painterResource(R.mipmap.iconslogout),
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFF3B30)) colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
) )
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(11.dp))
Text( Text(
text = stringResource(R.string.group_chat_info_dissolve), text = stringResource(R.string.group_chat_info_dissolve),
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
@@ -547,9 +563,41 @@ fun GroupChatInfoScreen(groupId: String) {
Image( Image(
painter = painterResource(R.drawable.rave_now_nav_right), painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(18.dp), 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, contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFF3B30)) 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 textAlign = TextAlign.Center
) )
androidx.compose.foundation.Image( // androidx.compose.foundation.Image(
painter = painterResource(R.drawable.rider_pro_add_other), // painter = painterResource(R.drawable.rider_pro_add_other),
contentDescription = stringResource(R.string.group_chat_info_add_member), // contentDescription = stringResource(R.string.group_chat_info_add_member),
colorFilter = ColorFilter.tint(AppColors.text), // colorFilter = ColorFilter.tint(AppColors.text),
modifier = Modifier // modifier = Modifier
.size(24.dp) // .size(24.dp)
.noRippleClickable { // .noRippleClickable {
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName) // navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
} // }
) // )
} }
} }
@@ -391,7 +391,7 @@ private fun MemberItem(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 菜单按钮 // 菜单按钮
if (isAdmin) {
IconButton( IconButton(
onClick = { onMenuClick(itemPosition, itemHeight) }, onClick = { onMenuClick(itemPosition, itemHeight) },
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
@@ -404,6 +404,7 @@ private fun MemberItem(
.size(24.dp) .size(24.dp)
) )
} }
}
Spacer(modifier = Modifier.width(8.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.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState 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.FastOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@@ -140,6 +136,7 @@ fun IndexScreen() {
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
gesturesEnabled = drawerState.isOpen, gesturesEnabled = drawerState.isOpen,
scrimColor = Color.Black.copy(alpha = 0.6f),
drawerContent = { drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
SideMenuContent( SideMenuContent(
@@ -525,8 +522,6 @@ fun SideMenuContent(
} else { } else {
Color(0xFFFAF9FB) // 亮色模式:浅灰色 Color(0xFFFAF9FB) // 亮色模式:浅灰色
} }
// 遮罩颜色 黑色透明度0.6
val overlayColor = Color.Black.copy(alpha = 0.6f)
// 卡片背景色 - 根据暗色模式适配 // 卡片背景色 - 根据暗色模式适配
val cardBackgroundColor = if (darkModeEnabled) { val cardBackgroundColor = if (darkModeEnabled) {
appColors.background // 暗色模式:深色背景 appColors.background // 暗色模式:深色背景
@@ -546,24 +541,6 @@ fun SideMenuContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .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( Box(
modifier = Modifier 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils 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.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.lottie.compose.rememberLottieComposition
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
/** /**
* 智能体聊天列表页面 * 智能体聊天列表页面
@@ -89,74 +91,30 @@ fun AgentChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) { if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
// 空状态 Box(
Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
// 空状态
Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) ChatEmptyStateView()
Image( } else {
painter = painterResource(id = R.mipmap.invalid_name_3), NetworkErrorContentCompact(
contentDescription = "null data", onReload = {
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) AgentChatListViewModel.refreshPager(context = context)
} }
) )
} }
} }
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

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.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils 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.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
data class CombinedConversation( data class CombinedConversation(
val type: String, // "agent", "group", or "friend" val type: String, // "agent", "group", or "friend"
@@ -217,66 +218,23 @@ fun AllChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (allConversations.isEmpty() && !isLoading) { if (allConversations.isEmpty() && !isLoading) {
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) ChatEmptyStateView()
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 { } else {
Spacer(modifier = Modifier.height(39.dp)) NetworkErrorContentCompact(
Image( onReload = {
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 isLoading = true
// 重新加载所有类型的数据 // 重新加载所有类型的数据
AgentChatListViewModel.refreshPager(context = context) AgentChatListViewModel.refreshPager(context = context)
@@ -286,6 +244,7 @@ fun AllChatListScreen() {
) )
} }
} }
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

@@ -33,7 +33,7 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer 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.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.text.style.TextAlign 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.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -72,72 +73,29 @@ fun FriendChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) { if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) {
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
//verticalArrangement = Arrangement.Center modifier = Modifier.offset(y = (-40).dp)
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) ChatEmptyStateView()
Image( } else {
painter = painterResource(id = R.mipmap.invalid_name_3), NetworkErrorContentCompact(
contentDescription = "null data", onReload = {
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) FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
} }
) )
} }
} }
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -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.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils 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) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -67,71 +68,29 @@ fun GroupChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) { if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) ChatEmptyStateView()
Image( } else {
painter = painterResource(id = R.mipmap.invalid_name_3), NetworkErrorContentCompact(
contentDescription = "null data", onReload = {
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) GroupChatListViewModel.refreshPager(context = context)
} }
) )
} }
} }
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

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

View File

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

View File

@@ -16,10 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
@@ -33,8 +30,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
@@ -97,6 +97,10 @@ fun VideoRecommendationItem(
skipPartiallyExpanded = true skipPartiallyExpanded = true
) )
var pauseIconVisibleState by remember { mutableStateOf(false) } 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) { val exoPlayer = remember(videoUrl) {
ExoPlayer.Builder(context) ExoPlayer.Builder(context)
@@ -167,7 +171,19 @@ fun VideoRecommendationItem(
}, },
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.noRippleClickable { .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 pauseIconVisibleState = true
exoPlayer.pause() exoPlayer.pause()
scope.launch { scope.launch {
@@ -181,15 +197,17 @@ fun VideoRecommendationItem(
} }
} }
) )
}
)
if (pauseIconVisibleState) { if (pauseIconVisibleState) {
Icon( Image(
imageVector = Icons.Default.PlayArrow, painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
.size(80.dp), .size(80.dp),
tint = Color.White colorFilter = ColorFilter.tint(Color.White)
) )
} }
} }
@@ -300,7 +318,9 @@ fun VideoRecommendationItem(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showCommentModal = false }, onDismissRequest = { showCommentModal = false },
containerColor = Color.White, containerColor = Color.White,
sheetState = sheetState sheetState = sheetState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) { ) {
CommentModalContent(postId = moment.id) { CommentModalContent(postId = moment.id) {
// 评论添加后的回调 // 评论添加后的回调
@@ -320,10 +340,12 @@ fun VideoRecommendationItem(
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
shouldResumeAfterLifecyclePause = exoPlayer.isPlaying && !pauseIconVisibleState
exoPlayer.pause() exoPlayer.pause()
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
if (isVisible) { if (isVisible && shouldResumeAfterLifecyclePause) {
pauseIconVisibleState = false
exoPlayer.play() 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 kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
/** /**
* 动态列表 * 动态列表
@@ -90,38 +91,11 @@ fun TimelineMomentsList() {
.padding(top = 188.dp), .padding(top = 188.dp),
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
Column( NetworkErrorContentInline(
horizontalAlignment = Alignment.CenterHorizontally, onReload = {
modifier = Modifier.fillMaxWidth() model.refreshPager(pullRefresh = true)
) {
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: 添加点击事件处理 */
} }
)
} }
)
} }
} else if (moments.isEmpty()) { } else if (moments.isEmpty()) {
Box( Box(

View File

@@ -33,6 +33,8 @@ import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent import com.aiosman.ravenow.event.MomentRemoveEvent
import com.aiosman.ravenow.data.PointService import com.aiosman.ravenow.data.PointService
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@@ -308,14 +310,16 @@ object MyProfileViewModel : ViewModel() {
* 加载房间列表 * 加载房间列表
* @param filterType 筛选类型0=全部1=公开2=私有 * @param filterType 筛选类型0=全部1=公开2=私有
* @param pullRefresh 是否下拉刷新 * @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) { if (AppStore.isGuest) {
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表") Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
return return
} }
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
if (roomsLoading && !pullRefresh) return if (roomsLoading && !pullRefresh) return
viewModelScope.launch { viewModelScope.launch {
@@ -331,43 +335,51 @@ object MyProfileViewModel : ViewModel() {
roomsCurrentPage roomsCurrentPage
} }
val response = when (filterType) { // 根据filterType确定roomType
0 -> { val roomType = when (filterType) {
// 全部:显示自己创建或加入的所有房间 1 -> "public"
apiClient.getRooms( 2 -> "private"
else -> null
}
val effectiveRoomType = if (normalizedOwnerId != null) "public" else roomType
// 构建API调用参数
if (normalizedOwnerId != null) {
// 查看其他用户的房间:仅显示该用户创建的公开房间
val createdResponse = apiClient.getRooms(
page = currentPage, page = currentPage,
pageSize = roomsPageSize, 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 = createdRooms
} else {
rooms = rooms + createdRooms
}
val total = createdResponse.body()?.total ?: 0L
roomsHasMore = rooms.size < total
if (roomsHasMore && !pullRefresh) {
roomsCurrentPage++
}
} else {
// 查看自己的房间:显示创建或加入的房间
val response = apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = effectiveRoomType,
showCreated = true, showCreated = true,
showJoined = true showJoined = if (filterType == 2) null else 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
)
}
}
if (response.isSuccessful) { if (response.isSuccessful) {
val roomList = response.body()?.list ?: emptyList() val roomList = response.body()?.list ?: emptyList()
@@ -386,6 +398,7 @@ object MyProfileViewModel : ViewModel() {
} else { } else {
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}") Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
} }
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MyProfileViewModel", "loadRooms error: ", e) Log.e("MyProfileViewModel", "loadRooms error: ", e)
} finally { } finally {
@@ -398,20 +411,29 @@ object MyProfileViewModel : ViewModel() {
/** /**
* 加载更多房间 * 加载更多房间
* @param filterType 筛选类型0=全部1=公开2=私有 * @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 if (roomsLoading || !roomsHasMore) return
loadRooms(filterType = filterType, pullRefresh = false) loadRooms(filterType = filterType, pullRefresh = false, ownerSessionId = normalizedOwnerId)
} }
/** /**
* 刷新房间列表 * 刷新房间列表
* @param filterType 筛选类型0=全部1=公开2=私有 * @param filterType 筛选类型0=全部1=公开2=私有
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
*/ */
fun refreshRooms(filterType: Int = 0) { fun refreshRooms(filterType: Int = 0, ownerSessionId: String? = null) {
rooms = emptyList() rooms = emptyList()
roomsCurrentPage = 1 roomsCurrentPage = 1
roomsHasMore = true 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color 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.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.ContentScale 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.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource 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.GalleryGrid
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent 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.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.UserAgentsList
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
@@ -168,6 +173,41 @@ fun ProfileV3(
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
) )
val scrollState = rememberScrollState(model.profileScrollOffset) 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) { val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@@ -491,11 +531,20 @@ fun ProfileV3(
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
.padding(top = 8.dp) .padding(top = 8.dp)
) {
Box(
modifier = Modifier
.onGloballyPositioned { coordinates ->
tabIndicatorHeightPx = coordinates.size.height
tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value
}
.alpha(if (shouldStickTabBar) 0f else 1f)
) { ) {
UserContentPageIndicator( UserContentPageIndicator(
pagerState = pagerState, pagerState = pagerState,
showAgentTab = !isAiAccount showAgentTab = !isAiAccount
) )
}
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.height(650.dp) // 固定滚动高度 modifier = Modifier.height(650.dp) // 固定滚动高度
@@ -533,22 +582,53 @@ fun ProfileV3(
showNoMoreText = isSelf, showNoMoreText = isSelf,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, 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 { } else {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder( GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
listState = groupChatListState, 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 -> { 2 -> {
if (!isAiAccount) { if (!isAiAccount) {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder( GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
listState = groupChatListState, 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)) 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( TopNavigationBar(
isMain = isMain, isMain = isMain,
@@ -656,12 +785,26 @@ fun ProfileV3(
private fun GroupChatPlaceholder( private fun GroupChatPlaceholder(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
listState: androidx.compose.foundation.lazy.LazyListState, 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( GroupChatEmptyContent(
modifier = modifier, modifier = modifier,
listState = listState, 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 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Alignment import androidx.compose.foundation.layout.size
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.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
@@ -155,7 +158,8 @@ fun GalleryGrid(
modifier = baseModifier modifier = baseModifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(vertical = 60.dp), .padding(vertical = 60.dp)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( Image(
@@ -198,24 +202,8 @@ fun GalleryGrid(
.padding(vertical = 60.dp), .padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( EmptyStateView(
painter = painterResource(id = R.mipmap.l_empty_img), contentDescription = "暂无图片"
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
) )
} }
} else { } else {
@@ -227,8 +215,20 @@ fun GalleryGrid(
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
) { ) {
itemsIndexed(moments) { idx, moment -> itemsIndexed(moments) { idx, moment ->
if (moment != null && moment.images.isNotEmpty()) { moment?.let { momentItem ->
val itemDebouncer = rememberDebouncer() 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( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -237,20 +237,32 @@ fun GalleryGrid(
.noRippleClickable { .noRippleClickable {
itemDebouncer { itemDebouncer {
navController.navigateToPost( navController.navigateToPost(
id = moment.id, id = momentItem.id,
highlightCommentId = 0, highlightCommentId = 0,
initImagePagerIndex = 0 initImagePagerIndex = 0
) )
} }
} }
) { ) {
if (previewUrl != null) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = moment.images[0].thumbnail, imageUrl = previewUrl,
contentDescription = "", contentDescription = "",
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
context = LocalContext.current context = LocalContext.current
) )
if (moment.images.size > 1) { } 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( Box(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp, end = 8.dp) .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.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text 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.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants 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.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale 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.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import android.util.Base64 import android.util.Base64
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils.isNetworkAvailable
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun GroupChatEmptyContent( fun GroupChatEmptyContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
listState: LazyListState, 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 AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val viewModel = MyProfileViewModel 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( val state = rememberPullRefreshState(
refreshing = viewModel.roomsRefreshing, refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false,
onRefresh = { onRefresh = {
viewModel.refreshRooms(filterType = selectedSegment) if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
} }
) )
// 当分段改变时,重新加载数据 // 当分段或用户ID改变时,重新加载数据
LaunchedEffect(selectedSegment) { LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) {
// 切换分段时重新加载 if (canLoadRooms) {
viewModel.refreshRooms(filterType = selectedSegment) viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
// 初始加载
LaunchedEffect(Unit) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
viewModel.loadRooms(filterType = selectedSegment)
} }
} }
@@ -110,56 +123,74 @@ fun GroupChatEmptyContent(
) { ) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 分段控制器 // 只在查看自己的房间时显示分段控制器
if (showSegments) {
SegmentedControl( SegmentedControl(
selectedIndex = selectedSegment, selectedIndex = selectedSegmentIndex,
onSegmentSelected = { onSegmentSelected = onSegmentSelected,
selectedSegment = it modifier = Modifier
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新 .fillMaxWidth()
}, .onGloballyPositioned { coordinates ->
modifier = Modifier.fillMaxWidth() onSegmentMeasured?.invoke(
coordinates.positionInRoot().y + parentScrollProvider(),
coordinates.size.height
)
}
.alpha(if (isSegmentSticky) 0f else 1f)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
}
Box( Box(
modifier = nestedScrollModifier modifier = nestedScrollModifier
.fillMaxSize() .fillMaxSize()
.pullRefresh(state) .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( Column(
modifier = nestedScrollModifier.fillMaxWidth(), modifier = nestedScrollModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 空状态插图 // 空状态插图
EmptyStateIllustration() EmptyStateIllustration(
isNetworkAvailable = networkAvailable,
onReload = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
Spacer(modifier = Modifier.height(9.dp)) 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 { } else {
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = nestedScrollModifier.fillMaxSize() modifier = nestedScrollModifier.fillMaxSize()
) { ) {
itemsIndexed( // 网格布局每行显示2个房间卡片
items = viewModel.rooms, items(
key = { _, item -> item.id } items = viewModel.rooms.chunked(2),
) { index, room -> key = { rowRooms -> rowRooms.firstOrNull()?.id?.toString() ?: "" }
RoomItem( ) { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { room ->
RoomCard(
room = room, room = room,
onRoomClick = { roomEntity -> onRoomClick = { roomEntity ->
// 导航到群聊聊天界面 // 导航到群聊聊天界面
@@ -168,14 +199,14 @@ fun GroupChatEmptyContent(
name = roomEntity.name, name = roomEntity.name,
avatar = roomEntity.avatar avatar = roomEntity.avatar
) )
},
modifier = Modifier.weight(1f)
)
}
// 如果这一行只有一个房间,添加一个空的占位符
if (rowRooms.size == 1) {
Spacer(modifier = Modifier.weight(1f))
} }
)
if (index < viewModel.rooms.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
} }
} }
@@ -201,13 +232,17 @@ fun GroupChatEmptyContent(
if (viewModel.roomsHasMore && !viewModel.roomsLoading) { if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
item { item {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.loadMoreRooms(filterType = selectedSegment) viewModel.loadMoreRooms(
filterType = filterType,
ownerSessionId = normalizedOwnerSessionId
)
} }
} }
} }
} }
} }
if (canLoadRooms) {
PullRefreshIndicator( PullRefreshIndicator(
refreshing = viewModel.roomsRefreshing, refreshing = viewModel.roomsRefreshing,
state = state, state = state,
@@ -215,8 +250,123 @@ fun GroupChatEmptyContent(
) )
} }
} }
}
} }
@Composable
fun RoomCard(
room: RoomEntity,
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()) {
"${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}"
}
// 优先显示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 @Composable
fun RoomItem( fun RoomItem(
room: RoomEntity, room: RoomEntity,
@@ -230,7 +380,6 @@ fun RoomItem(
val avatarUrl = if (room.avatar.isNotEmpty()) { val avatarUrl = if (room.avatar.isNotEmpty()) {
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}" "${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
} else { } else {
// 如果头像为空,使用群头像接口
val groupIdBase64 = Base64.encodeToString( val groupIdBase64 = Base64.encodeToString(
room.trtcType.toByteArray(), room.trtcType.toByteArray(),
Base64.NO_WRAP Base64.NO_WRAP
@@ -307,7 +456,7 @@ fun RoomItem(
} }
@Composable @Composable
private fun SegmentedControl( fun SegmentedControl(
selectedIndex: Int, selectedIndex: Int,
onSegmentSelected: (Int) -> Unit, onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -372,7 +521,7 @@ private fun SegmentButton(
}, },
shape = RoundedCornerShape(1000.dp) shape = RoundedCornerShape(1000.dp)
) )
.clickable(onClick = onClick), .noRippleClickable { onClick() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
@@ -389,14 +538,43 @@ private fun SegmentButton(
} }
@Composable @Composable
private fun EmptyStateIllustration() { private fun EmptyStateIllustration(
Image( isNetworkAvailable: Boolean,
painter = painterResource(id = R.mipmap.l_empty_img), onReload: () -> Unit
) {
val AppColors = LocalAppTheme.current
if (isNetworkAvailable) {
EmptyStateView(
contentDescription = "空状态", contentDescription = "空状态",
modifier = Modifier fontWeight = FontWeight.SemiBold
.width(181.dp)
.height(153.dp),
contentScale = ContentScale.Fit
) )
} 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale 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.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.entity.AgentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel 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.ui.network.ReloadButton
import com.aiosman.ravenow.utils.DebounceUtils import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -66,7 +70,13 @@ fun UserAgentsList(
showNoMoreText: Boolean = false, showNoMoreText: Boolean = false,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
state: LazyListState, 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 AppColors = LocalAppTheme.current
val listModifier = if (nestedScrollConnection != null) { val listModifier = if (nestedScrollConnection != null) {
@@ -80,7 +90,14 @@ fun UserAgentsList(
Box( Box(
modifier = listModifier.fillMaxSize() modifier = listModifier.fillMaxSize()
) { ) {
AgentEmptyContentWithSegments() AgentEmptyContentWithSegments(
showSegments = showSegments,
segmentSelectedIndex = segmentSelectedIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
)
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -248,8 +265,14 @@ fun UserAgentCard(
} }
@Composable @Composable
fun AgentEmptyContentWithSegments() { fun AgentEmptyContentWithSegments(
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有 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 AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
@@ -260,14 +283,24 @@ fun AgentEmptyContentWithSegments() {
) { ) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 分段控制器 // 只在查看自己的智能体时显示分段控制器
if (showSegments) {
AgentSegmentedControl( AgentSegmentedControl(
selectedIndex = selectedSegment, selectedIndex = segmentSelectedIndex,
onSegmentSelected = { selectedSegment = it }, onSegmentSelected = onSegmentSelected,
modifier = Modifier.fillMaxWidth() 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)) Spacer(modifier = Modifier.height(8.dp))
}
// 空状态内容(与动态、群聊保持一致) // 空状态内容(与动态、群聊保持一致)
Column( Column(
@@ -275,25 +308,8 @@ fun AgentEmptyContentWithSegments() {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (isNetworkAvailable) { if (isNetworkAvailable) {
Image( EmptyStateView(
painter = painterResource(id = R.mipmap.l_empty_img), contentDescription = "暂无Agent"
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
) )
} else { } else {
Image( Image(
@@ -332,7 +348,7 @@ fun AgentEmptyContentWithSegments() {
} }
@Composable @Composable
private fun AgentSegmentedControl( fun AgentSegmentedControl(
selectedIndex: Int, selectedIndex: Int,
onSegmentSelected: (Int) -> Unit, onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -397,7 +413,7 @@ private fun AgentSegmentButton(
}, },
shape = RoundedCornerShape(1000.dp) shape = RoundedCornerShape(1000.dp)
) )
.clickable(onClick = onClick), .noRippleClickable { onClick() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(

View File

@@ -84,6 +84,7 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -415,64 +416,17 @@ fun MomentResultTab() {
.background(AppColors.background) .background(AppColors.background)
) { ) {
if (moments.itemCount == 0 && model.showResult) { if (moments.itemCount == 0 && model.showResult) {
Column( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, isNetworkAvailable = isNetworkAvailable,
verticalArrangement = Arrangement.Center onReload = {
) {
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.ResetModel()
SearchViewModel.search() SearchViewModel.search()
} }
) )
}
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -566,64 +520,17 @@ fun UserResultTab() {
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
if (users.itemCount == 0 && model.showResult) { if (users.itemCount == 0 && model.showResult) {
Column( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, isNetworkAvailable = isNetworkAvailable,
verticalArrangement = Arrangement.Center onReload = {
) {
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.ResetModel()
SearchViewModel.search() SearchViewModel.search()
} }
) )
}
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -734,64 +641,17 @@ fun AiResultTab() {
.background(AppColors.background) .background(AppColors.background)
) { ) {
if (agents.itemCount == 0 && model.showResult) { if (agents.itemCount == 0 && model.showResult) {
Column( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, isNetworkAvailable = isNetworkAvailable,
verticalArrangement = Arrangement.Center onReload = {
) {
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.ResetModel()
SearchViewModel.search() SearchViewModel.search()
} }
) )
}
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -863,65 +723,17 @@ fun RoomResultTab() {
.background(AppColors.background) .background(AppColors.background)
) { ) {
if (rooms.itemCount == 0 && model.showResult) { if (rooms.itemCount == 0 && model.showResult) {
Column( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, isNetworkAvailable = isNetworkAvailable,
verticalArrangement = Arrangement.Center onReload = {
) {
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.ResetModel()
SearchViewModel.search() SearchViewModel.search()
} }
) )
}
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -945,41 +757,32 @@ fun RoomResultTab() {
} }
@Composable @Composable
fun ReloadButton( fun SearchPlaceholderContent(
onClick: () -> Unit modifier: Modifier = Modifier,
isNetworkAvailable: Boolean,
onReload: () -> Unit
) { ) {
val gradientBrush = Brush.linearGradient( val appColors = LocalAppTheme.current
colors = listOf( Column(
Color(0xFF7c45ed), modifier = modifier,
Color(0xFF7c68ef), horizontalAlignment = Alignment.CenterHorizontally,
Color(0xFF7bd8f8) verticalArrangement = Arrangement.Center
)
)
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
) { ) {
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.empty_img),
contentDescription = "No Comment",
modifier = Modifier.size(168.dp)
)
Text( Text(
text = stringResource(R.string.Reload), text = stringResource(R.string.null_search),
color = appColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.W600
color = Color.White,
) )
} 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.MomentPagingSource
import com.aiosman.ravenow.entity.MomentRemoteDataSource import com.aiosman.ravenow.entity.MomentRemoteDataSource
import com.aiosman.ravenow.entity.MomentServiceImpl 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.AgentEntity
import com.aiosman.ravenow.entity.AgentRemoteDataSource import com.aiosman.ravenow.entity.AgentRemoteDataSource
import com.aiosman.ravenow.entity.AgentSearchPagingSource import com.aiosman.ravenow.entity.AgentSearchPagingSource
@@ -31,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
object SearchViewModel : ViewModel() { object SearchViewModel : ViewModel() {
var searchText by mutableStateOf("") var searchText by mutableStateOf("")
@@ -204,7 +207,14 @@ object SearchViewModel : ViewModel() {
suspend fun likeMoment(id: Int) { suspend fun likeMoment(id: Int) {
try { try {
momentService.likeMoment(id) momentService.likeMoment(id)
updateMomentLike(id, true) val likeCount = updateMomentLike(id, true)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = true
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -213,7 +223,14 @@ object SearchViewModel : ViewModel() {
suspend fun dislikeMoment(id: Int) { suspend fun dislikeMoment(id: Int) {
try { try {
momentService.dislikeMoment(id) momentService.dislikeMoment(id)
updateMomentLike(id, false) val likeCount = updateMomentLike(id, false)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = false
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -223,6 +240,12 @@ object SearchViewModel : ViewModel() {
try { try {
momentService.favoriteMoment(id) momentService.favoriteMoment(id)
updateMomentFavorite(id, true) updateMomentFavorite(id, true)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = true
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -232,6 +255,12 @@ object SearchViewModel : ViewModel() {
try { try {
momentService.unfavoriteMoment(id) momentService.unfavoriteMoment(id)
updateMomentFavorite(id, false) updateMomentFavorite(id, false)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = false
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -241,19 +270,23 @@ object SearchViewModel : ViewModel() {
updateMomentCommentCount(id, 1) 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 currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem -> val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) { if (momentItem.id == id) {
val nextCount = (momentItem.likeCount + if (isLike) 1 else -1).coerceAtLeast(0)
latestLikeCount = nextCount
momentItem.copy( momentItem.copy(
liked = isLike, liked = isLike,
likeCount = momentItem.likeCount + if (isLike) 1 else -1 likeCount = nextCount
) )
} else { } else {
momentItem momentItem
} }
} }
_momentsFlow.value = updatedPagingData _momentsFlow.value = updatedPagingData
return latestLikeCount
} }
private fun updateMomentFavorite(id: Int, isFavorite: Boolean) { private fun updateMomentFavorite(id: Int, isFavorite: Boolean) {
@@ -262,7 +295,7 @@ object SearchViewModel : ViewModel() {
if (momentItem.id == id) { if (momentItem.id == id) {
momentItem.copy( momentItem.copy(
isFavorite = isFavorite, isFavorite = isFavorite,
favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1 favoriteCount = (momentItem.favoriteCount + if (isFavorite) 1 else -1).coerceAtLeast(0)
) )
} else { } else {
momentItem momentItem

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.RectangleShape 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@@ -319,9 +318,9 @@ private fun SingleVideoItemContent(
isPageVisible: Boolean = true isPageVisible: Boolean = true
) { ) {
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置 // 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
val pauseIconVisibleState = remember(pager) { val pauseIconVisibleState = remember(pager) { mutableStateOf(false) }
mutableStateOf(false) // 记录进入后台前是否在播放,用于决定是否需要自动恢复播放
} val shouldResumeAfterLifecyclePause = remember(pager) { mutableStateOf(false) }
// 当页面切换时,重置暂停状态 // 当页面切换时,重置暂停状态
LaunchedEffect(pager, pagerState.currentPage) { LaunchedEffect(pager, pagerState.currentPage) {
@@ -341,6 +340,7 @@ private fun SingleVideoItemContent(
pagerState = pagerState, pagerState = pagerState,
pager = pager, pager = pager,
pauseIconVisibleState = pauseIconVisibleState, pauseIconVisibleState = pauseIconVisibleState,
shouldResumeAfterLifecyclePause = shouldResumeAfterLifecyclePause,
onLikeClick = onLikeClick, onLikeClick = onLikeClick,
onCommentClick = onCommentClick, onCommentClick = onCommentClick,
onCommentAdded = onCommentAdded, onCommentAdded = onCommentAdded,
@@ -373,6 +373,7 @@ fun VideoPlayer(
pagerState: PagerState, pagerState: PagerState,
pager: Int, pager: Int,
pauseIconVisibleState: MutableState<Boolean>, pauseIconVisibleState: MutableState<Boolean>,
shouldResumeAfterLifecyclePause: MutableState<Boolean>,
onLikeClick: ((MomentEntity) -> Unit)? = null, onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null, onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null, onCommentAdded: ((MomentEntity) -> Unit)? = null,
@@ -467,6 +468,9 @@ fun VideoPlayer(
.clip(RectangleShape) .clip(RectangleShape)
) { ) {
var playerView by remember { mutableStateOf<PlayerView?>(null) } var playerView by remember { mutableStateOf<PlayerView?>(null) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留 // 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
androidx.compose.runtime.key(videoUrl) { androidx.compose.runtime.key(videoUrl) {
@@ -479,15 +483,31 @@ fun VideoPlayer(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RectangleShape) .clip(RectangleShape)
.noRippleClickable { .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) handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
} }
) )
} }
)
}
if (pauseIconVisibleState.value) { if (pauseIconVisibleState.value) {
Icon( Image(
imageVector = Icons.Default.PlayArrow, painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
@@ -514,15 +534,21 @@ fun VideoPlayer(
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
// 应用进入后台时暂停 // 应用进入后台时暂停
shouldResumeAfterLifecyclePause.value = exoPlayer.isPlaying && !pauseIconVisibleState.value
exoPlayer.playWhenReady = false exoPlayer.playWhenReady = false
exoPlayer.pause() exoPlayer.pause()
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
// 返回前台且为当前页面时恢复播放 // 返回前台且为当前页面时恢复播放
if (pager == pagerState.currentPage) { if (
pager == pagerState.currentPage &&
isPageVisible &&
shouldResumeAfterLifecyclePause.value
) {
exoPlayer.playWhenReady = true exoPlayer.playWhenReady = true
exoPlayer.play() exoPlayer.play()
pauseIconVisibleState.value = false
} }
} }
@@ -660,7 +686,8 @@ fun VideoPlayer(
}, },
containerColor = AppColors.background, containerColor = AppColors.background,
sheetState = sheetState, sheetState = sheetState,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -670,6 +697,7 @@ fun VideoPlayer(
CommentModalContent( CommentModalContent(
postId = moment.id, postId = moment.id,
commentCount = moment.commentCount, commentCount = moment.commentCount,
showTitle = false,
onCommentAdded = { onCommentAdded = {
onCommentAdded?.invoke(moment) 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -46,10 +47,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import java.util.Date import java.util.Date
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton import com.aiosman.ravenow.ui.network.NetworkErrorContent
@Preview @Preview
@Composable @Composable
fun LikeNoticeScreen() { fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
val model = LikeNoticeViewModel val model = LikeNoticeViewModel
val listState = rememberLazyListState() val listState = rememberLazyListState()
var dataFlow = model.likeItemsFlow var dataFlow = model.likeItemsFlow
@@ -63,7 +64,8 @@ fun LikeNoticeScreen() {
StatusBarMaskLayout( StatusBarMaskLayout(
darkIcons = !AppState.darkMode, darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -75,42 +77,11 @@ fun LikeNoticeScreen() {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
Box( NetworkErrorContent(
modifier = Modifier.fillMaxSize() onReload = {
.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) LikeNoticeViewModel.reload(force = true)
} }
) )
}
}
} else if (likes.itemCount == 0) { } else if (likes.itemCount == 0) {
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -184,18 +155,22 @@ fun ActionPostNoticeItem(
val navController = LocalNavController.current val navController = LocalNavController.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
Box( val actionLabel = when (action) {
modifier = Modifier.padding(vertical = 16.dp) "favourite" -> stringResource(R.string.favourite_your_post)
) { else -> stringResource(R.string.like_your_post)
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
verticalAlignment = Alignment.Top, .fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 0.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
CustomAsyncImage( CustomAsyncImage(
context, context = context,
imageUrl = avatar, imageUrl = avatar,
modifier = Modifier modifier = Modifier
.size(48.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.noRippleClickable { .noRippleClickable {
navController.navigate( navController.navigate(
@@ -207,34 +182,56 @@ fun ActionPostNoticeItem(
}, },
contentDescription = action, contentDescription = action,
) )
Spacer(modifier = Modifier.width(12.dp)) Row(
Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 8.dp)
.noRippleClickable { .noRippleClickable {
navController.navigateToPost( navController.navigateToPost(
id = postId, id = postId,
highlightCommentId = 0, highlightCommentId = 0,
initImagePagerIndex = 0 initImagePagerIndex = 0
) )
} },
verticalAlignment = Alignment.CenterVertically
) { ) {
Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text) Column(
Spacer(modifier = Modifier.height(2.dp)) modifier = Modifier.weight(1f)
when (action) { ) {
"like" -> Text(stringResource(R.string.like_your_post), color = AppColors.text) Text(
"favourite" -> Text(stringResource(R.string.favourite_your_post), color = AppColors.text) text = nickName,
} fontWeight = FontWeight.Bold,
Spacer(modifier = Modifier.height(2.dp)) fontSize = 14.sp,
Row { color = AppColors.text,
Text(likeTime.timeAgo(context), fontSize = 12.sp, color = AppColors.secondaryText) 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( CustomAsyncImage(
context, context = context,
imageUrl = thumbnail, imageUrl = thumbnail,
modifier = Modifier modifier = Modifier
.size(48.dp) .size(40.dp)
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
contentDescription = action, contentDescription = action,
) )
@@ -249,10 +246,11 @@ fun LikeCommentNoticeItem(
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val previewPost = item.comment?.replyComment?.post ?: item.comment?.post
Box( Column(
modifier = Modifier modifier = Modifier
.padding(vertical = 16.dp) .padding(vertical = 12.dp)
.noRippleClickable { .noRippleClickable {
item.comment?.postId.let { item.comment?.postId.let {
navController.navigateToPost( navController.navigateToPost(
@@ -262,41 +260,69 @@ fun LikeCommentNoticeItem(
) )
} }
} }
) {
Row {
Column(
modifier = Modifier.weight(1f)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.Top,
) { ) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = item.user.avatar, imageUrl = item.user.avatar,
modifier = Modifier modifier = Modifier
.size(48.dp) .size(40.dp)
.clip(CircleShape), .clip(CircleShape),
contentDescription = stringResource(R.string.like_your_comment) contentDescription = stringResource(R.string.like_your_comment)
) )
Spacer(modifier = Modifier.width(12.dp)) Row(
Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
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( Text(
item.likeTime.timeAgo(context), text = item.user.nickName,
fontSize = 12.sp, 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 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(12.dp)) }
}
Spacer(modifier = Modifier.height(8.dp))
Row( Row(
modifier = Modifier.padding(start = 48.dp) modifier = Modifier.padding(start = 48.dp)
) { ) {
@@ -334,34 +360,4 @@ fun LikeCommentNoticeItem(
} }
} }
} }
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.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -89,7 +90,7 @@ fun NotificationScreen() {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp), .padding(start = 16.dp, top = 8.dp),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
@@ -106,7 +107,7 @@ fun NotificationScreen() {
TabSpacer() TabSpacer()
TabItem( TabItem(
text = stringResource(R.string.followers_upper), text = stringResource(R.string.follow_upper),
isSelected = pagerState.currentPage == 1, isSelected = pagerState.currentPage == 1,
onClick = { onClick = {
scope.launch { scope.launch {
@@ -135,9 +136,9 @@ fun NotificationScreen() {
.weight(1f) .weight(1f)
) { page -> ) { page ->
when (page) { when (page) {
0 -> LikeNoticeScreen() 0 -> LikeNoticeScreen(includeStatusBarPadding = false)
1 -> FollowerNoticeScreen() 1 -> FollowerNoticeScreen(includeStatusBarPadding = false)
2 -> CommentNoticeScreen() 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.clickable
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.offset 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.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.animation.core.animateDpAsState 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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@@ -106,32 +103,36 @@ fun PointsBottomSheet(
} }
} }
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮) onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮)
sheetState = sheetState, sheetState = sheetState,
containerColor = AppColors.background, containerColor = Color.Transparent,
dragHandle = null // 移除拖动手柄 dragHandle = null, // 移除拖动手柄
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(0.95f) .padding(top = 10.dp)
.offset(y = offsetY) ) {
.padding( Surface(
start = 16.dp, modifier = Modifier
end = 16.dp, .fillMaxWidth()
bottom = 8.dp .fillMaxHeight()
) .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = AppColors.background,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
) { ) {
// 头部 - 使用 Box 实现绝对居中布局 // 头部 - 使用 Box 实现绝对居中布局
Box( Box(
@@ -343,6 +344,7 @@ fun PointsBottomSheet(
} }
} }
} }
}
} }
@Composable @Composable

View File

@@ -13,37 +13,30 @@ import kotlinx.coroutines.launch
class CommentsViewModel( class CommentsViewModel(
var postId: String = 0.toString(), var postId: String = 0.toString(),
) : ViewModel() { ) : ViewModel() {
companion object {
private const val ORDER_ALL = "all"
private const val COMMENTS_PAGE_SIZE = 50
}
var commentService: CommentService = CommentServiceImpl() var commentService: CommentService = CommentServiceImpl()
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList()) 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 addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>()) var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
var highlightCommentId by mutableStateOf<Int?>(null) var highlightCommentId by mutableStateOf<Int?>(null)
var highlightComment by mutableStateOf<CommentEntity?>(null) var highlightComment by mutableStateOf<CommentEntity?>(null)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var hasError 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 之前设置好内容 * 预加载,在跳转到 PostScreen 之前设置好内容
*/ */
fun preTransit() { fun preTransit() {
viewModelScope.launch { reloadComment()
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
}
}
} }
/** /**
@@ -51,25 +44,61 @@ class CommentsViewModel(
*/ */
fun reloadComment() { fun reloadComment() {
viewModelScope.launch { viewModelScope.launch {
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 { try {
if (reset) {
isLoading = true isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
order = order,
pageSize = 50
)
commentsList = response.list
hasError = false hasError = false
} 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) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
if (reset) {
hasError = true hasError = true
commentsList = emptyList()
}
} finally { } finally {
if (reset) {
isLoading = false 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) { suspend fun highlightComment(commentId: Int) {
highlightCommentId = commentId highlightCommentId = commentId

View File

@@ -22,6 +22,7 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -34,6 +35,7 @@ import android.graphics.BitmapFactory
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -78,14 +80,28 @@ fun DraftBoxBottomSheet(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
containerColor = AppColors.background, containerColor = Color.Transparent,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {} dragHandle = {}
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = AppColors.background,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(0.9f) .fillMaxHeight()
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {
// 标题 // 标题
@@ -167,6 +183,8 @@ fun DraftBoxBottomSheet(
} }
} }
} }
}
}
} }
@Composable @Composable

View File

@@ -1,9 +1,12 @@
package com.aiosman.ravenow.ui.post package com.aiosman.ravenow.ui.post
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloatAsState 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 addImageDebouncer = rememberDebouncer()
val canAddMoreImages = model.imageList.size < 9 val canAddMoreImages = model.imageList.size < 9
@@ -642,6 +673,13 @@ fun AddImageGrid() {
.background(Color(0xFFFAF9FB)) .background(Color(0xFFFAF9FB))
.noRippleClickable { .noRippleClickable {
if (model.imageList.size < 9) { if (model.imageList.size < 9) {
// 检查摄像头权限
when {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
// 已有权限,直接打开相机
val photoFile = File(context.cacheDir, "photo.jpg") val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile( val photoUri: Uri = FileProvider.getUriForFile(
context, context,
@@ -650,6 +688,12 @@ fun AddImageGrid() {
) )
model.currentPhotoUri = photoUri model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri) takePictureLauncher.launch(photoUri)
}
else -> {
// 没有权限,请求权限
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
} else { } else {
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show() 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.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image 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.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures 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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -44,6 +47,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -300,13 +304,26 @@ fun PostScreen(
onDismissRequest = { onDismissRequest = {
showReportDialog = false showReportDialog = false
}, },
containerColor = AppColors.background, containerColor = Color.Transparent,
sheetState = rememberModalBottomSheetState( sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true skipPartiallyExpanded = true
), ),
dragHandle = {}, dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
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( ReportModal(
momentId = viewModel.moment!!.id, momentId = viewModel.moment!!.id,
@@ -316,6 +333,8 @@ fun PostScreen(
) )
} }
} }
}
}
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { bottomBar = {
@@ -493,7 +512,9 @@ fun PostScreen(
color = AppColors.nonActiveText color = AppColors.nonActiveText
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
OrderSelectionComponent() { OrderSelectionComponent(
selectedOrder = commentsViewModel.order
) {
commentsViewModel.order = it commentsViewModel.order = it
viewModel.reloadComment() 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) { if (viewModel.isLoading) {
Box( Box(
@@ -1159,14 +1207,26 @@ fun ImageViewerDialog(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PostImageView( fun PostImageView(
images: List<MomentImageEntity>, images: List<MomentImageEntity>,
initialPage: Int? = 0 initialPage: Int? = 0
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
var isImageViewerDialog by remember { mutableStateOf(false) } 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) { DisposableEffect(Unit) {
onDispose { onDispose {
@@ -1187,14 +1247,21 @@ fun PostImageView(
modifier = Modifier modifier = Modifier
) { ) {
if (images.isNotEmpty()) { if (images.isNotEmpty()) {
CustomAsyncImage( HorizontalPager(
context, state = pagerState,
images[currentImageIndex].thumbnail,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth() .fillMaxWidth()
) { page ->
val image = images[page]
CustomAsyncImage(
context,
image.thumbnail,
contentDescription = "Image",
blurHash = image.blurHash,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { onTap = {
@@ -1205,6 +1272,7 @@ fun PostImageView(
.background(Color.Gray.copy(alpha = 0.1f)) .background(Color.Gray.copy(alpha = 0.1f))
) )
} }
}
// 图片导航控件 // 图片导航控件
if (images.size > 1) { if (images.size > 1) {
@@ -1212,33 +1280,17 @@ fun PostImageView(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically 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 // Indicators
Row(
horizontalArrangement = Arrangement.Center
) {
images.forEachIndexed { index, _ -> images.forEachIndexed { index, _ ->
Box( Box(
modifier = Modifier modifier = Modifier
.size(4.dp) .size(4.dp)
.clip(CircleShape) .clip(CircleShape)
.background( .background(
if (currentImageIndex == index) Color.Red else Color.Gray.copy( if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
alpha = 0.5f alpha = 0.5f
) )
) )
@@ -1249,20 +1301,6 @@ fun PostImageView(
} }
} }
} }
// 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 @Composable
fun OrderSelectionComponent( fun OrderSelectionComponent(
selectedOrder: String,
onSelected: (String) -> Unit = {} onSelected: (String) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
var selectedOrder by remember { mutableStateOf("like") }
val orders = listOf( val orders = listOf(
"like" to stringResource(R.string.order_comment_default), "all" to stringResource(R.string.order_comment_default),
"earliest" to stringResource(R.string.order_comment_earliest), "latest" to stringResource(R.string.order_comment_latest),
"latest" to stringResource(R.string.order_comment_latest) "like" to stringResource(R.string.order_comment_hot)
) )
Box( Box(
modifier = Modifier modifier = Modifier
@@ -1943,9 +1981,10 @@ fun OrderSelectionComponent(
Box( Box(
modifier = Modifier modifier = Modifier
.noRippleClickable { .noRippleClickable {
selectedOrder = order.first if (selectedOrder != order.first) {
onSelected(order.first) onSelected(order.first)
} }
}
.background( .background(
if ( if (
selectedOrder == order.first selectedOrder == order.first
@@ -2011,8 +2050,8 @@ fun ReportModal(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.background) .fillMaxHeight()
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp) .padding(start = 24.dp, end = 24.dp)
) { ) {
Box( Box(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), 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