Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 742b8b25e8 | |||
| af3b7e7bb9 | |||
| b11910d8a9 | |||
| 80ac0090bd | |||
| 1e3e417035 | |||
| ca8b7b3c53 | |||
| e33c1e8aef | |||
| fcce1f863b | |||
| 04974b0a01 | |||
| 672dc1859f | |||
| 90e47fc3ce | |||
| 084eb7bb52 | |||
| 8937ccbf56 | |||
| fe78b5192b | |||
| cfe5ce8102 | |||
| 05615ce5dc | |||
| 3d64cc5929 | |||
| e59d4283ca | |||
| 5b9487b60d | |||
| 0a1601c16c | |||
| c94fcd493e | |||
| 357790d794 | |||
| 6d18e13826 | |||
| 9c592ee62b | |||
| f238a2e83f | |||
| 882de043ee | |||
| 1a0ed2da19 | |||
| 958d1c16be | |||
| e686bc3b52 | |||
| bd5079806b | |||
| 787c5dc574 | |||
| f89564e60a | |||
| 778c06342a |
@@ -54,7 +54,7 @@
|
||||
android:theme="@style/Theme.App.Starting"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="fontScale|orientation|screenSize|keyboardHidden">
|
||||
android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
@@ -73,6 +73,10 @@ class MainActivity : ComponentActivity() {
|
||||
val config = Configuration(newConfig)
|
||||
config.fontScale = 1.0f
|
||||
super.onConfigurationChanged(config)
|
||||
val isNightMode = (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
if (AppState.darkMode != isNightMode) {
|
||||
syncDarkModeWithSystem(isNightMode)
|
||||
}
|
||||
}
|
||||
|
||||
// 请求通知权限
|
||||
@@ -129,9 +133,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
JPushInterface.init(this)
|
||||
|
||||
if (AppState.darkMode) {
|
||||
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
|
||||
}
|
||||
updateWindowBackground(AppState.darkMode)
|
||||
enableEdgeToEdge()
|
||||
|
||||
scope.launch {
|
||||
@@ -269,8 +271,22 @@ class MainActivity : ComponentActivity() {
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncDarkModeWithSystem(isNightMode: Boolean) {
|
||||
AppState.darkMode = isNightMode
|
||||
AppState.appTheme = if (isNightMode) DarkThemeColors() else LightThemeColors()
|
||||
AppStore.saveDarkMode(isNightMode)
|
||||
updateWindowBackground(isNightMode)
|
||||
}
|
||||
|
||||
private fun updateWindowBackground(isDarkMode: Boolean) {
|
||||
window.decorView.setBackgroundColor(
|
||||
if (isDarkMode) android.graphics.Color.BLACK else android.graphics.Color.WHITE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val LocalNavController = compositionLocalOf<NavHostController> {
|
||||
error("NavController not provided")
|
||||
}
|
||||
|
||||
@@ -545,7 +545,13 @@ class AccountServiceImpl : AccountService {
|
||||
val bannerField: MultipartBody.Part? = banner?.let {
|
||||
createMultipartBody(it.file, it.filename, "banner")
|
||||
}
|
||||
ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
|
||||
val resp = ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
|
||||
if (!resp.isSuccessful) {
|
||||
parseErrorResponse(resp.errorBody())?.let {
|
||||
throw it.toServiceException()
|
||||
}
|
||||
throw ServiceException("Failed to update profile")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun registerUserWithPassword(loginName: String, password: String) {
|
||||
|
||||
@@ -410,7 +410,16 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||
fun updateMomentLike(id: Int,isLike:Boolean) {
|
||||
this.list = this.list.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
|
||||
// 只有当状态发生变化时才更新计数,避免重复更新
|
||||
val countDelta = if (momentItem.liked != isLike) {
|
||||
if (isLike) 1 else -1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
momentItem.copy(
|
||||
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
|
||||
liked = isLike
|
||||
)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
@@ -421,7 +430,16 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
|
||||
this.list = this.list.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
|
||||
// 只有当状态发生变化时才更新计数,避免重复更新
|
||||
val countDelta = if (momentItem.isFavorite != isFavorite) {
|
||||
if (isFavorite) 1 else -1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
momentItem.copy(
|
||||
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
|
||||
isFavorite = isFavorite
|
||||
)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aiosman.ravenow.entity
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.aiosman.ravenow.data.ListContainer
|
||||
@@ -272,19 +273,41 @@ class RoomRemoteDataSource {
|
||||
pageSize: Int = 20,
|
||||
search: String
|
||||
): ListContainer<RoomEntity>? {
|
||||
val resp = ApiClient.api.getRooms(
|
||||
page = pageNumber,
|
||||
pageSize = pageSize,
|
||||
search = search,
|
||||
roomType = "public" // 搜索时只显示公有房间
|
||||
)
|
||||
val body = resp.body() ?: return null
|
||||
return ListContainer(
|
||||
total = body.total,
|
||||
page = pageNumber,
|
||||
pageSize = pageSize,
|
||||
list = body.list.map { it.toRoomtEntity() }
|
||||
)
|
||||
return try {
|
||||
val resp = ApiClient.api.getRooms(
|
||||
page = pageNumber,
|
||||
pageSize = pageSize,
|
||||
search = search,
|
||||
roomType = "public" // 搜索时只显示公有房间
|
||||
)
|
||||
if (!resp.isSuccessful) {
|
||||
// API 调用失败,返回 null
|
||||
return null
|
||||
}
|
||||
val body = resp.body() ?: return null
|
||||
|
||||
// 安全地转换数据,过滤掉转换失败的项目
|
||||
val roomList = body.list.mapNotNull { room ->
|
||||
try {
|
||||
room.toRoomtEntity()
|
||||
} catch (e: Exception) {
|
||||
// 如果某个房间数据转换失败,记录错误但继续处理其他房间
|
||||
Log.e("RoomRemoteDataSource", "Failed to convert room: ${room.id}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
ListContainer(
|
||||
total = body.total,
|
||||
page = pageNumber,
|
||||
pageSize = pageSize,
|
||||
list = roomList
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// 捕获所有异常,返回 null 让 PagingSource 处理
|
||||
Log.e("RoomRemoteDataSource", "searchRooms error", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,17 +326,31 @@ class RoomSearchPagingSource(
|
||||
pageSize = params.loadSize,
|
||||
search = keyword
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = rooms?.list ?: listOf(),
|
||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
|
||||
)
|
||||
} catch (exception: IOException) {
|
||||
if (rooms == null) {
|
||||
// API 调用失败,返回空列表
|
||||
LoadResult.Page(
|
||||
data = emptyList(),
|
||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||
nextKey = null
|
||||
)
|
||||
} else {
|
||||
LoadResult.Page(
|
||||
data = rooms.list,
|
||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||
nextKey = if (rooms.list.isNotEmpty() && rooms.list.size >= params.loadSize) currentPage + 1 else null
|
||||
)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
// 捕获所有异常,包括 IOException、ServiceException 等
|
||||
LoadResult.Error(exception)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
|
||||
return state.anchorPosition
|
||||
// 更健壮的实现:根据 anchorPosition 计算刷新键
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.aiosman.ravenow
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||
|
||||
/**
|
||||
@@ -24,11 +25,16 @@ object AppStore {
|
||||
.requestEmail()
|
||||
.build()
|
||||
googleSignInOptions = gso
|
||||
// apply dark mode
|
||||
if (sharedPreferences.getBoolean("darkMode", false)) {
|
||||
AppState.darkMode = true
|
||||
AppState.appTheme = DarkThemeColors()
|
||||
// apply dark mode - 如果用户未手动设置,优先跟随系统
|
||||
val hasUserPreference = sharedPreferences.contains("darkMode")
|
||||
val resolvedDarkMode = if (hasUserPreference) {
|
||||
sharedPreferences.getBoolean("darkMode", false)
|
||||
} else {
|
||||
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
currentNightMode == Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
AppState.darkMode = resolvedDarkMode
|
||||
AppState.appTheme = if (resolvedDarkMode) DarkThemeColors() else LightThemeColors()
|
||||
|
||||
// load chat background
|
||||
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.about
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -40,22 +41,21 @@ fun AboutScreen() {
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
title = stringResource(R.string.about_rave_now),
|
||||
title = stringResource(R.string.about_paipai),
|
||||
moreIcon = false
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
// app icon
|
||||
// app icondww
|
||||
Box {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
|
||||
painter = painterResource(id = R.mipmap.invalid_name),
|
||||
contentDescription = "app icon",
|
||||
modifier = Modifier.size(80.dp)
|
||||
)
|
||||
@@ -63,7 +63,7 @@ fun AboutScreen() {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
// app name
|
||||
Text(
|
||||
text = "Rave Now".uppercase(),
|
||||
text = stringResource(R.string.paipai),
|
||||
fontSize = 24.sp,
|
||||
color = appColors.text,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,59 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
|
||||
// MBTI类型列表
|
||||
val MBTI_TYPES = listOf(
|
||||
@@ -46,96 +63,326 @@ val MBTI_TYPES = listOf(
|
||||
"ISTP", "ISFP", "ESTP", "ESFP"
|
||||
)
|
||||
|
||||
fun getMbtiImageResId(mbti: String, isDarkMode: Boolean): Int {
|
||||
return when {
|
||||
isDarkMode && mbti == "ENTP" -> R.mipmap.anmbti_entp
|
||||
isDarkMode && mbti == "ESTP" -> R.mipmap.anmbti_estp
|
||||
isDarkMode && mbti == "ENTJ" -> R.mipmap.anmbti_entj
|
||||
else -> when (mbti) {
|
||||
"INTJ" -> R.mipmap.mbti_intj
|
||||
"INTP" -> R.mipmap.mbti_intp
|
||||
"ENTJ" -> R.mipmap.mbti_entj
|
||||
"ENTP" -> R.mipmap.mbti_entp
|
||||
"INFJ" -> R.mipmap.mbti_infj
|
||||
"INFP" -> R.mipmap.mbti_infp
|
||||
"ENFJ" -> R.mipmap.mbti_enfj
|
||||
"ENFP" -> R.mipmap.mbti_enfp
|
||||
"ISTJ" -> R.mipmap.mbti_istj
|
||||
"ISFJ" -> R.mipmap.mbti_isfj
|
||||
"ESTJ" -> R.mipmap.mbti_estj
|
||||
"ESFJ" -> R.mipmap.mbti_esfj
|
||||
"ISTP" -> R.mipmap.mbti_istp
|
||||
"ISFP" -> R.mipmap.mbti_isfp
|
||||
"ESTP" -> R.mipmap.mbti_estp
|
||||
"ESFP" -> R.mipmap.mbti_esfp
|
||||
else -> R.mipmap.xingzuo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MbtiSelectScreen() {
|
||||
val navController = LocalNavController.current
|
||||
fun MbtiSelectBottomSheet(
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val isDarkMode = AppState.darkMode
|
||||
val model = AccountEditViewModel
|
||||
val currentMbti = model.mbti
|
||||
val sheetBackgroundColor = if (isDarkMode) {
|
||||
appColors.secondaryBackground
|
||||
} else {
|
||||
Color(0xFFFFFFFF)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(appColors.profileBackground)
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
// 确保弹窗展开
|
||||
LaunchedEffect(Unit) {
|
||||
sheetState.expand()
|
||||
}
|
||||
|
||||
// 监听状态变化,确保弹窗始终展开(防止拖拽关闭和滑动)
|
||||
LaunchedEffect(sheetState.currentValue, sheetState.targetValue, sheetState.isVisible) {
|
||||
// 如果弹窗被拖拽关闭或位置发生变化,立即重新展开
|
||||
if (!sheetState.isVisible || sheetState.targetValue != androidx.compose.material3.SheetValue.Expanded) {
|
||||
kotlinx.coroutines.delay(10) // 短暂延迟确保状态更新
|
||||
sheetState.expand()
|
||||
}
|
||||
}
|
||||
|
||||
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onClose,
|
||||
sheetState = sheetState,
|
||||
containerColor = sheetBackgroundColor,
|
||||
dragHandle = null
|
||||
) {
|
||||
// 头部
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
title = stringResource(R.string.choose_mbti),
|
||||
moreIcon = false
|
||||
)
|
||||
}
|
||||
|
||||
// 列表
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
items(MBTI_TYPES) { mbti ->
|
||||
MBTIItem(
|
||||
mbti = mbti,
|
||||
isSelected = mbti == currentMbti,
|
||||
onClick = {
|
||||
model.mbti = mbti
|
||||
// 立即保存到本地存储,确保选择后立即生效
|
||||
AppState.UserId?.let { uid ->
|
||||
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
|
||||
}
|
||||
navController.navigateUp()
|
||||
}
|
||||
.fillMaxHeight(0.95f)
|
||||
.offset(y = offsetY)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
// 头部 - 使用 Box 实现绝对居中布局
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val cancelButtonGradientColors = if (isDarkMode) {
|
||||
listOf(
|
||||
Color(0xFF3A3A3C),
|
||||
Color(0xFF2C2C2E)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
Color(0xFFFFFFFF),
|
||||
Color(0xFFF8F8F8)
|
||||
)
|
||||
}
|
||||
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
|
||||
|
||||
// 左上角返回按钮
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.height(36.dp)
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = cancelButtonGradientColors
|
||||
)
|
||||
)
|
||||
.noRippleClickable { onClose() }
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 左箭头图标
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(17.dp),
|
||||
colorFilter = ColorFilter.tint(cancelButtonContentColor)
|
||||
)
|
||||
|
||||
// "取消" 文字
|
||||
Text(
|
||||
text = "取消",
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = cancelButtonContentColor,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
// 中间标题 - 绝对居中
|
||||
Text(
|
||||
text = stringResource(R.string.choose_mbti),
|
||||
color = appColors.text,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
// 不消费任何事件,让 LazyColumn 先处理
|
||||
return Offset.Zero
|
||||
}
|
||||
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
// 消费 LazyColumn 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
|
||||
return available
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
// 不消费惯性滚动,让 LazyColumn 先处理
|
||||
return Velocity.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
// 消费 LazyColumn 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
|
||||
return available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MBTI解释文字背景色
|
||||
val descriptionBackgroundColor = if (isDarkMode) {
|
||||
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
|
||||
} else {
|
||||
Color(0xFFFAF9FB)
|
||||
}
|
||||
|
||||
// 使用LazyColumn包裹解释文字和MBTI类型网格,使它们一起滚动
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
contentPadding = PaddingValues(
|
||||
start = 8.dp,
|
||||
top = 0.dp,
|
||||
end = 8.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// MBTI解释文字 - 作为第一个item
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(descriptionBackgroundColor)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.mbti_description),
|
||||
color = appColors.text,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MBTI类型网格 - 手动创建2列网格布局
|
||||
itemsIndexed(MBTI_TYPES.chunked(2)) { rowIndex, rowItems ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = if (rowIndex < MBTI_TYPES.chunked(2).size - 1) 10.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
rowItems.forEachIndexed { colIndex, mbti ->
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
MbtiItem(
|
||||
mbti = mbti,
|
||||
isSelected = mbti == currentMbti,
|
||||
onClick = {
|
||||
// 保存MBTI类型
|
||||
model.mbti = mbti
|
||||
onClose()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// 如果这一行只有1个item,添加一个空的Spacer来保持布局
|
||||
if (rowItems.size == 1) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MBTIItem(
|
||||
fun MbtiSelectScreen() {
|
||||
val navController = LocalNavController.current
|
||||
MbtiSelectBottomSheet(
|
||||
onClose = {
|
||||
navController.navigateUp()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MbtiItem(
|
||||
mbti: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val isDarkMode = AppState.darkMode
|
||||
|
||||
Box(
|
||||
// 卡片背景色
|
||||
val cardBackgroundColor = if (isDarkMode) {
|
||||
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
|
||||
} else {
|
||||
Color(0xFFFAF9FB)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
|
||||
.aspectRatio(1.1f)
|
||||
.shadow(
|
||||
elevation = if (isDarkMode) 8.dp else 2.dp,
|
||||
shape = RoundedCornerShape(21.dp),
|
||||
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
|
||||
)
|
||||
.clip(RoundedCornerShape(21.dp))
|
||||
.background(cardBackgroundColor)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick()
|
||||
}
|
||||
.padding(16.dp)
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
// MBTI图标 - 使用占位图片
|
||||
Box(
|
||||
modifier = Modifier.size(100.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = mbti,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (isSelected) appColors.main else appColors.text,
|
||||
modifier = Modifier.weight(1f)
|
||||
Image(
|
||||
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)),
|
||||
contentDescription = mbti,
|
||||
modifier = Modifier.size(100.dp)
|
||||
)
|
||||
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = appColors.main
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MBTI名称 - 使用负间距让文本向上移动,与图标更靠近
|
||||
Text(
|
||||
text = mbti,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = appColors.text,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.offset(y = (-20).dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,10 +293,6 @@ fun ZodiacSelectBottomSheet(
|
||||
onClick = {
|
||||
// 保存当前语言的星座文本
|
||||
model.zodiac = zodiacText
|
||||
// 立即保存到本地存储,确保选择后立即生效
|
||||
AppState.UserId?.let { uid ->
|
||||
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -16,9 +16,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
@@ -68,6 +70,19 @@ fun ChangePasswordScreen() {
|
||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||
val AppColors = LocalAppTheme.current
|
||||
// 暗色模式下的 hint 文本颜色
|
||||
val isDarkMode = AppState.darkMode
|
||||
val hintColor = if (isDarkMode) {
|
||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
||||
} else {
|
||||
null // 使用默认颜色
|
||||
}
|
||||
val labelColor = if (isDarkMode) {
|
||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
||||
} else {
|
||||
null // 使用默认颜色
|
||||
}
|
||||
|
||||
fun validate(): Boolean {
|
||||
// 使用通用密码校验器校验当前密码
|
||||
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
|
||||
@@ -112,7 +127,9 @@ fun ChangePasswordScreen() {
|
||||
password = true,
|
||||
label = stringResource(R.string.current_password),
|
||||
hint = stringResource(R.string.current_password_tip5),
|
||||
error = oldPasswordError
|
||||
error = oldPasswordError,
|
||||
customHintColor = hintColor,
|
||||
customLabelColor = labelColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
TextInputField(
|
||||
@@ -121,7 +138,9 @@ fun ChangePasswordScreen() {
|
||||
password = true,
|
||||
label = stringResource(R.string.new_password),
|
||||
hint = stringResource(R.string.new_password),
|
||||
error = passwordError
|
||||
error = passwordError,
|
||||
customHintColor = hintColor,
|
||||
customLabelColor = labelColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
TextInputField(
|
||||
@@ -130,7 +149,9 @@ fun ChangePasswordScreen() {
|
||||
password = true,
|
||||
label = stringResource(R.string.confirm_new_password_tip1),
|
||||
hint = stringResource(R.string.new_password_tip1),
|
||||
error = confirmPasswordError
|
||||
error = confirmPasswordError,
|
||||
customHintColor = hintColor,
|
||||
customLabelColor = labelColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
ActionButton(
|
||||
|
||||
@@ -70,6 +70,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
||||
import android.widget.Toast
|
||||
@@ -77,6 +79,8 @@ import java.io.File
|
||||
import androidx.activity.compose.BackHandler
|
||||
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
|
||||
import com.aiosman.ravenow.ui.account.ZodiacSheetManager
|
||||
import com.aiosman.ravenow.ui.account.MbtiBottomSheetHost
|
||||
import com.aiosman.ravenow.ui.account.MbtiSheetManager
|
||||
|
||||
/**
|
||||
* 编辑用户资料界面
|
||||
@@ -194,6 +198,8 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
||||
) {
|
||||
// 挂载星座选择弹窗
|
||||
ZodiacBottomSheetHost()
|
||||
// 挂载MBTI选择弹窗
|
||||
MbtiBottomSheetHost()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -392,7 +398,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
||||
ProfileInfoCard(
|
||||
label = stringResource(R.string.nickname),
|
||||
value = model.name,
|
||||
placeholder = "Value",
|
||||
placeholder = stringResource(R.string.nickname_placeholder),
|
||||
onValueChange = { onNicknameChange(it) },
|
||||
isMultiline = false
|
||||
)
|
||||
@@ -403,7 +409,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
||||
ProfileInfoCard(
|
||||
label = stringResource(R.string.personal_intro),
|
||||
value = model.bio,
|
||||
placeholder = "Welcome to my fantiac word i will show you something about magic",
|
||||
placeholder = "",
|
||||
onValueChange = { onBioChange(it) },
|
||||
isMultiline = true
|
||||
)
|
||||
@@ -425,9 +431,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
||||
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
||||
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
||||
onClick = {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.MbtiSelect.route)
|
||||
}
|
||||
MbtiSheetManager.open()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -500,12 +504,24 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
||||
// 验证通过,执行保存
|
||||
model.viewModelScope.launch {
|
||||
model.isUpdating = true
|
||||
model.updateUserProfile(context)
|
||||
model.viewModelScope.launch(Dispatchers.Main) {
|
||||
debouncedNavigation {
|
||||
navController.navigateUp()
|
||||
try {
|
||||
model.updateUserProfile(context)
|
||||
model.viewModelScope.launch(Dispatchers.Main) {
|
||||
debouncedNavigation {
|
||||
navController.navigateUp()
|
||||
}
|
||||
model.isUpdating = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 捕获所有异常,包括网络异常
|
||||
model.viewModelScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.network_error_check_network),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
model.isUpdating = false
|
||||
}
|
||||
model.isUpdating = false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -550,29 +566,45 @@ fun ProfileInfoCard(
|
||||
isMultiline: Boolean = false
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
var lineCount by remember { mutableStateOf(1) }
|
||||
|
||||
// 根据行数决定对齐方式:单行时居中,多行时顶部对齐
|
||||
val verticalAlignment = if (isMultiline) {
|
||||
if (lineCount <= 1) Alignment.CenterVertically else Alignment.Top
|
||||
} else {
|
||||
Alignment.CenterVertically
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp,个人简介66dp
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(appColors.secondaryBackground),
|
||||
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
|
||||
contentAlignment = if (isMultiline && lineCount > 1) Alignment.TopStart else Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(vertical = if (isMultiline) 11.dp else 0.dp),
|
||||
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
|
||||
.padding(vertical = if (isMultiline && lineCount > 1) 11.dp else 0.dp),
|
||||
verticalAlignment = verticalAlignment
|
||||
) {
|
||||
// 标签
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = appColors.text,
|
||||
modifier = Modifier.width(100.dp)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(if (isMultiline) 44.dp else 56.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = appColors.text
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
@@ -580,10 +612,26 @@ fun ProfileInfoCard(
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
// 对于个人简介(isMultiline = true),当值为空且没有焦点时显示图标
|
||||
if (value.isEmpty() && isMultiline && !isFocused) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(44.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = appColors.secondaryText
|
||||
)
|
||||
}
|
||||
} else if (value.isEmpty() && !isMultiline && placeholder.isNotEmpty()) {
|
||||
// 对于非多行输入框,仍然显示 placeholder 文字
|
||||
Text(
|
||||
text = placeholder,
|
||||
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = appColors.secondaryText,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -593,7 +641,11 @@ fun ProfileInfoCard(
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { focusState ->
|
||||
isFocused = focusState.isFocused
|
||||
},
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
@@ -601,7 +653,12 @@ fun ProfileInfoCard(
|
||||
),
|
||||
cursorBrush = SolidColor(appColors.text),
|
||||
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
|
||||
singleLine = !isMultiline
|
||||
singleLine = !isMultiline,
|
||||
onTextLayout = { textLayoutResult: TextLayoutResult ->
|
||||
if (isMultiline) {
|
||||
lineCount = textLayoutResult.lineCount
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -50,6 +51,14 @@ fun RemoveAccountScreen() {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
// 暗色模式下的 hint 文本颜色
|
||||
val isDarkMode = AppState.darkMode
|
||||
val hintColor = if (isDarkMode) {
|
||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
||||
} else {
|
||||
null // 使用默认颜色
|
||||
}
|
||||
|
||||
fun removeAccount(password: String) {
|
||||
// 使用通用密码校验器
|
||||
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
||||
@@ -132,7 +141,8 @@ fun RemoveAccountScreen() {
|
||||
},
|
||||
password = true,
|
||||
hint = stringResource(R.string.remove_account_password_hint),
|
||||
error = passwordError
|
||||
error = passwordError,
|
||||
customHintColor = hintColor
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
@@ -617,7 +617,7 @@ fun ChatAiInput(
|
||||
animationSpec = tween(300)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||
painter = painterResource(R.mipmap.btn),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.alpha(alpha)
|
||||
|
||||
@@ -650,7 +650,7 @@ fun ChatInput(
|
||||
animationSpec = tween(300)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||
painter = painterResource(R.mipmap.btn),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.alpha(alpha)
|
||||
|
||||
@@ -183,45 +183,21 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
if (viewModel.groupAvatar.isNotEmpty()) {
|
||||
if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = viewModel.groupAvatar,
|
||||
imageUrl = viewModel.groupInfo!!.groupAvatar,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.noRippleClickable {
|
||||
navController.navigateToGroupInfo(groupId)
|
||||
},
|
||||
.size(35.dp)
|
||||
.clip(RoundedCornerShape(15.dp)),
|
||||
contentDescription = "群聊头像"
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(AppColors.decentBackground)
|
||||
.noRippleClickable {
|
||||
navController.navigateToGroupInfo(groupId)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = viewModel.groupName,
|
||||
style = TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 18.sp,
|
||||
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow =TextOverflow.Ellipsis,
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
text = viewModel.groupName,
|
||||
style = TextStyle(
|
||||
@@ -229,24 +205,21 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
||||
fontSize = 18.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow =TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Box {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToGroupInfo(groupId)
|
||||
},
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToGroupInfo(groupId)
|
||||
},
|
||||
contentDescription = "更多",
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -677,7 +650,7 @@ fun GroupChatInput(
|
||||
animationSpec = tween(300)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||
painter = painterResource(R.mipmap.btn),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.alpha(alpha)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.aiosman.ravenow.ui.chat
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.data.api.ApiClient
|
||||
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
||||
import io.openim.android.sdk.enums.ConversationType
|
||||
@@ -50,17 +52,46 @@ class GroupChatViewModel(
|
||||
}
|
||||
|
||||
private suspend fun getGroupInfo() {
|
||||
// 简化群组信息获取,使用默认信息
|
||||
groupInfo = GroupInfo(
|
||||
groupId = groupId,
|
||||
groupName = name,
|
||||
groupAvatar = avatar,
|
||||
memberCount = 0,
|
||||
ownerId = ""
|
||||
)
|
||||
groupName = groupInfo?.groupName ?: ""
|
||||
groupAvatar = groupInfo?.groupAvatar ?: ""
|
||||
memberCount = groupInfo?.memberCount ?: 0
|
||||
try {
|
||||
val response = ApiClient.api.getRoomDetail(trtcId = groupId)
|
||||
val room = response.body()?.data
|
||||
groupInfo = room?.let {
|
||||
GroupInfo(
|
||||
groupId = groupId,
|
||||
groupName = it.name,
|
||||
groupAvatar = if (it.avatar.isNullOrEmpty()) {
|
||||
val groupIdBase64 = Base64.encodeToString(
|
||||
groupId.toByteArray(),
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${AppStore.token}"
|
||||
} else {
|
||||
"${ApiClient.BASE_API_URL}/outside${it.avatar}?token=${AppStore.token}"
|
||||
},
|
||||
memberCount = it.userCount,
|
||||
ownerId = it.creator.userId
|
||||
)
|
||||
} ?: GroupInfo(
|
||||
groupId = groupId,
|
||||
groupName = name,
|
||||
groupAvatar = avatar,
|
||||
memberCount = 0,
|
||||
ownerId = ""
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("GroupChatViewModel", "加载群信息失败: ${e.message}", e)
|
||||
groupInfo = GroupInfo(
|
||||
groupId = groupId,
|
||||
groupName = name,
|
||||
groupAvatar = avatar,
|
||||
memberCount = 0,
|
||||
ownerId = ""
|
||||
)
|
||||
} finally {
|
||||
groupName = groupInfo?.groupName ?: ""
|
||||
groupAvatar = groupInfo?.groupAvatar ?: ""
|
||||
memberCount = groupInfo?.memberCount ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -31,6 +32,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
@@ -79,8 +81,9 @@ class CommentModalViewModel(
|
||||
fun CommentModalContent(
|
||||
postId: Int? = null,
|
||||
commentCount: Int = 0,
|
||||
onCommentAdded: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
onDismiss: () -> Unit = {},
|
||||
showTitle: Boolean = true,
|
||||
onCommentAdded: () -> Unit = {}
|
||||
) {
|
||||
val model = viewModel<CommentModalViewModel>(
|
||||
key = "CommentModalViewModel_$postId",
|
||||
@@ -161,28 +164,42 @@ fun CommentModalContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
// 拖动手柄
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
|
||||
|
||||
.padding(top = 8.dp, bottom = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.comment),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(40.dp)
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(AppColors.divider)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = AppColors.divider
|
||||
)
|
||||
if (showTitle) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
|
||||
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.comment),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
@@ -191,7 +208,9 @@ fun CommentModalContent(
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
OrderSelectionComponent {
|
||||
OrderSelectionComponent(
|
||||
selectedOrder = commentViewModel.order
|
||||
) {
|
||||
commentViewModel.order = it
|
||||
commentViewModel.reloadComment()
|
||||
}
|
||||
|
||||
@@ -41,16 +41,15 @@ import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.CommentEntity
|
||||
import com.aiosman.ravenow.exp.timeAgo
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
import kotlinx.coroutines.launch
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
||||
@Composable
|
||||
fun CommentNoticeScreen() {
|
||||
fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
|
||||
val viewModel = viewModel<CommentNoticeListViewModel>(
|
||||
key = "CommentNotice",
|
||||
factory = object : ViewModelProvider.Factory {
|
||||
@@ -68,50 +67,22 @@ fun CommentNoticeScreen() {
|
||||
val navController = LocalNavController.current
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
|
||||
StatusBarMaskLayout(
|
||||
modifier = Modifier
|
||||
.background(color = AppColors.background)
|
||||
.padding(horizontal = 16.dp),
|
||||
darkIcons = !AppState.darkMode,
|
||||
maskBoxBackgroundColor = AppColors.background,
|
||||
includeStatusBarPadding = includeStatusBarPadding
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||
|
||||
if (!isNetworkAvailable) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 149.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
viewModel.initData(context, force = true)
|
||||
}
|
||||
)
|
||||
NetworkErrorContent(
|
||||
onReload = {
|
||||
viewModel.initData(context, force = true)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -141,7 +112,8 @@ fun CommentNoticeScreen() {
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize().padding(horizontal = 16.dp)
|
||||
.weight(1f)
|
||||
.background(color = AppColors.background)
|
||||
) {
|
||||
items(comments.itemCount) { index ->
|
||||
comments[index]?.let { comment ->
|
||||
@@ -215,53 +187,58 @@ fun CommentNoticeItem(
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val AppColors = LocalAppTheme.current
|
||||
val commentPrefix = stringResource(R.string.comment_notice)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
) {
|
||||
Box {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = commentItem.avatar,
|
||||
contentDescription = commentItem.name,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
commentItem.author.toString()
|
||||
)
|
||||
// 左侧头像区域
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = commentItem.avatar,
|
||||
contentDescription = commentItem.name,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
commentItem.author.toString()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// 右侧内容区域
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 12.dp)
|
||||
.padding(start = 8.dp)
|
||||
.noRippleClickable {
|
||||
onPostClick()
|
||||
}
|
||||
) {
|
||||
// 主要信息列
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = commentItem.name,
|
||||
fontSize = 18.sp,
|
||||
modifier = Modifier,
|
||||
color = AppColors.text
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// 评论内容行
|
||||
Row {
|
||||
var text = commentItem.comment
|
||||
if (commentItem.parentCommentId != null) {
|
||||
text = "Reply you: $text"
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
text = "$commentPrefix $text",
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
color = AppColors.secondaryText,
|
||||
@@ -275,25 +252,20 @@ fun CommentNoticeItem(
|
||||
color = AppColors.secondaryText,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
// 右侧帖子图片
|
||||
commentItem.post?.let {
|
||||
Box {
|
||||
Box(
|
||||
modifier = Modifier.padding(4.dp)
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = it.images[0].thumbnail,
|
||||
contentDescription = "Post Image",
|
||||
modifier = Modifier
|
||||
.size(48.dp).clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
// unread indicator
|
||||
|
||||
}
|
||||
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = it.images[0].thumbnail,
|
||||
contentDescription = "Post Image",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
// 未读指示器
|
||||
if (commentItem.unread) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -303,11 +275,7 @@ fun CommentNoticeItem(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ fun StatusBarMaskLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
darkIcons: Boolean = true,
|
||||
useNavigationBarMask: Boolean = true,
|
||||
includeStatusBarPadding: Boolean = true,
|
||||
maskBoxBackgroundColor: Color = Color.Transparent,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
@@ -50,13 +51,13 @@ fun StatusBarMaskLayout(
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(paddingValues.calculateTopPadding())
|
||||
.fillMaxWidth()
|
||||
.background(maskBoxBackgroundColor)
|
||||
) {
|
||||
|
||||
if (includeStatusBarPadding) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(paddingValues.calculateTopPadding())
|
||||
.fillMaxWidth()
|
||||
.background(maskBoxBackgroundColor)
|
||||
)
|
||||
}
|
||||
content()
|
||||
if (navigationBarPaddings > 24.dp && useNavigationBarMask) {
|
||||
|
||||
@@ -17,15 +17,19 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -42,7 +46,7 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@@ -84,43 +88,11 @@ fun FavouriteListPage() {
|
||||
var moments = dataFlow.collectAsLazyPagingItems()
|
||||
|
||||
if (!isNetworkAvailable) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top=149.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
model.refreshPager(force = true)
|
||||
}
|
||||
)
|
||||
NetworkErrorContent(
|
||||
onReload = {
|
||||
model.refreshPager(force = true)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if(moments.itemCount == 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -187,7 +159,10 @@ fun FavouriteListPage() {
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
context = context
|
||||
)
|
||||
if (momentItem.images.size > 1) {
|
||||
|
||||
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
|
||||
|
||||
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, end = 8.dp)
|
||||
@@ -200,6 +175,31 @@ fun FavouriteListPage() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isVideoMoment) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, end = 8.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.4f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||
import kotlinx.coroutines.launch
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@@ -76,43 +76,11 @@ fun FollowerListScreen(userId: Int) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||
|
||||
if (!isNetworkAvailable) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 149.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = appColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = appColors.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
model.loadData(userId, true)
|
||||
}
|
||||
)
|
||||
NetworkErrorContent(
|
||||
onReload = {
|
||||
model.loadData(userId, true)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (users.itemCount == 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -40,7 +41,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.FollowButton
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
||||
import kotlinx.coroutines.launch
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
|
||||
@@ -48,13 +49,14 @@ import com.aiosman.ravenow.utils.NetworkUtils
|
||||
* 关注消息列表
|
||||
*/
|
||||
@Composable
|
||||
fun FollowerNoticeScreen() {
|
||||
fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val AppColors = LocalAppTheme.current
|
||||
StatusBarMaskLayout(
|
||||
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
|
||||
darkIcons = !AppState.darkMode,
|
||||
maskBoxBackgroundColor = AppColors.background
|
||||
maskBoxBackgroundColor = AppColors.background,
|
||||
includeStatusBarPadding = includeStatusBarPadding
|
||||
) {
|
||||
val model = FollowerNoticeViewModel
|
||||
var dataFlow = model.followerItemsFlow
|
||||
@@ -66,43 +68,11 @@ fun FollowerNoticeScreen() {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||
|
||||
if (!isNetworkAvailable) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top=149.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
model.reload(force = true)
|
||||
}
|
||||
)
|
||||
NetworkErrorContent(
|
||||
onReload = {
|
||||
model.reload(force = true)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (followers.itemCount == 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -171,35 +141,57 @@ fun FollowItem(
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
Box(
|
||||
val followText = stringResource(R.string.followed_you)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
userId.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 左侧头像区域
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = avatar,
|
||||
contentDescription = nickname,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
userId.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
// 右侧内容区域
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = avatar,
|
||||
contentDescription = nickname,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
|
||||
Text(
|
||||
text = nickname,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = followText,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (!isFollowing && userId != AppState.UserId) {
|
||||
FollowButton(
|
||||
|
||||
@@ -40,7 +40,7 @@ import com.aiosman.ravenow.exp.viewModelFactory
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
||||
import kotlinx.coroutines.launch
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
|
||||
@@ -78,43 +78,11 @@ fun FollowingListScreen(userId: Int) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||
|
||||
if (!isNetworkAvailable) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top=149.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = appColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = appColors.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
model.loadData(userId, true)
|
||||
}
|
||||
)
|
||||
NetworkErrorContent(
|
||||
onReload = {
|
||||
model.loadData(userId, true)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if(users.itemCount == 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -99,6 +99,15 @@ fun CreateGroupChatScreen() {
|
||||
}
|
||||
|
||||
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
var showSelectTipsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// 自动隐藏“请选择群成员并输入群名称”提示弹窗
|
||||
LaunchedEffect(showSelectTipsDialog) {
|
||||
if (showSelectTipsDialog) {
|
||||
kotlinx.coroutines.delay(2000)
|
||||
showSelectTipsDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取费用和余额信息
|
||||
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
|
||||
@@ -495,63 +504,134 @@ fun CreateGroupChatScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// 创建群聊按钮 - 固定在底部
|
||||
Button(
|
||||
onClick = {
|
||||
// 创建群聊逻辑
|
||||
if (selectedMembers.isNotEmpty()) {
|
||||
// 检查是否超过上限
|
||||
if (selectedMembers.size > maxMemberLimit) {
|
||||
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
|
||||
return@Button
|
||||
}
|
||||
// 如果费用大于0,显示确认弹窗
|
||||
if (cost > 0) {
|
||||
CreateGroupChatViewModel.showConfirmDialog()
|
||||
// 创建群聊按钮 - 固定在底部(启用时使用渐变背景)
|
||||
val isCreateEnabled =
|
||||
groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = buttonTopPadding,
|
||||
bottom = navigationBarPadding + 16.dp
|
||||
)
|
||||
.let { baseModifier ->
|
||||
if (isCreateEnabled) {
|
||||
baseModifier.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0x997c45ed),
|
||||
Color(0x997c68ef),
|
||||
Color(0x997bd8f8)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
} else {
|
||||
// 费用为0,直接创建
|
||||
scope.launch {
|
||||
val success = CreateGroupChatViewModel.createGroupChat(
|
||||
groupName = groupName.text,
|
||||
selectedMembers = selectedMembers,
|
||||
context = context
|
||||
baseModifier
|
||||
}
|
||||
}
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
// 创建群聊逻辑
|
||||
if (selectedMembers.isNotEmpty()) {
|
||||
// 检查是否超过上限
|
||||
if (selectedMembers.size > maxMemberLimit) {
|
||||
CreateGroupChatViewModel.showError(
|
||||
context.getString(
|
||||
R.string.create_group_chat_exceed_limit,
|
||||
maxMemberLimit
|
||||
)
|
||||
)
|
||||
if (success) {
|
||||
navController.popBackStack()
|
||||
return@Button
|
||||
}
|
||||
// 如果费用大于0,显示确认弹窗
|
||||
if (cost > 0) {
|
||||
CreateGroupChatViewModel.showConfirmDialog()
|
||||
} else {
|
||||
// 费用为0,直接创建
|
||||
scope.launch {
|
||||
val success = CreateGroupChatViewModel.createGroupChat(
|
||||
groupName = groupName.text,
|
||||
selectedMembers = selectedMembers,
|
||||
context = context
|
||||
)
|
||||
if (success) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isCreateEnabled) Color.Transparent else AppColors.main,
|
||||
contentColor = AppColors.mainText,
|
||||
disabledContainerColor = AppColors.disabledBackground,
|
||||
disabledContentColor = AppColors.text
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
enabled = isCreateEnabled
|
||||
) {
|
||||
if (CreateGroupChatViewModel.isLoading) {
|
||||
Text(
|
||||
text = stringResource(R.string.agent_createing),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.create_group_chat),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = AppColors.main,
|
||||
contentColor = AppColors.mainText,
|
||||
disabledContainerColor = AppColors.disabledBackground,
|
||||
disabledContentColor = AppColors.text
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
|
||||
) {
|
||||
if (CreateGroupChatViewModel.isLoading) {
|
||||
Text(
|
||||
text = stringResource(R.string.agent_createing),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.create_group_chat),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
}
|
||||
|
||||
// 禁用状态下拦截点击并弹出提示
|
||||
if (!isCreateEnabled) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.noRippleClickable {
|
||||
showSelectTipsDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 请选择群成员并输入群名称 提示弹窗
|
||||
if (showSelectTipsDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = { showSelectTipsDialog = false },
|
||||
properties = DialogProperties(dismissOnClickOutside = true, dismissOnBackPress = true)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(color = AppColors.background, shape = RoundedCornerShape(16.dp))
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.create_group_chat_select_members_and_name),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Color(0xFF7C45ED),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 消费确认弹窗
|
||||
if (CreateGroupChatViewModel.showConfirmDialog) {
|
||||
CreateGroupChatConfirmDialog(
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.group
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -44,6 +45,7 @@ import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.index.NavItem
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToAddGroupMember
|
||||
import com.aiosman.ravenow.ui.navigateToGroupMembers
|
||||
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -173,7 +175,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "${viewModel.groupInfo?.memberCount ?: 0}人",
|
||||
text = "${viewModel.groupInfo?.memberCount ?: 0}${stringResource(R.string.people)}",
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text.copy(alpha = 0.7f),
|
||||
fontSize = 11.sp
|
||||
@@ -187,38 +189,42 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
// 添加其他人
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
// TODO: 实现添加其他人功能
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
if (viewModel.groupInfo?.isCreator == true) {
|
||||
// 添加其他人
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rider_pro_add_other),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rider_pro_add_other),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_add_member),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_add_member),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 通知设置
|
||||
@@ -258,11 +264,11 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
)
|
||||
}
|
||||
|
||||
// 退出群聊
|
||||
// 分享群聊
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
// TODO: 实现退出群聊功能
|
||||
// TODO: 实现分享功能
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
@@ -272,7 +278,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.group_info_edit),
|
||||
painter = painterResource(R.mipmap.icon_share),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
@@ -297,16 +303,16 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(1.dp, AppColors.decentBackground, RoundedCornerShape(12.dp))
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(AppColors.decentBackground.copy(alpha = 0.28f))
|
||||
.background(AppColors.background)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.group_info_edit),
|
||||
modifier = Modifier.size(16.dp),
|
||||
painter = painterResource(R.mipmap.icons_brain),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
@@ -315,7 +321,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
@@ -337,7 +342,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(AppColors.background)
|
||||
.background(AppColors.decentBackground)
|
||||
.padding(vertical = 8.dp)
|
||||
.noRippleClickable {
|
||||
showAddMemoryDialog = true
|
||||
@@ -356,7 +361,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(AppColors.background)
|
||||
.background(AppColors.decentBackground)
|
||||
.padding(vertical = 8.dp)
|
||||
.noRippleClickable {
|
||||
showMemoryManageDialog = true
|
||||
@@ -379,81 +384,84 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(13.dp))
|
||||
|
||||
// 群资料设置
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(12.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToGroupProfileSettings(groupId)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.group_info_edit),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_group_settings),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||
modifier = Modifier.size(16.dp),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
// 群可见性
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(12.dp)
|
||||
.noRippleClickable {
|
||||
showVisibilityDialog = true
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.rider_pro_change_password),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_group_visibility),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
// 未解锁时才显示"待解锁"
|
||||
if (viewModel.groupInfo?.privateFeePaid != true) {
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_locked),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text.copy(alpha = 0.5f),
|
||||
fontSize = 11.sp
|
||||
// 仅当当前用户是群聊创建者时显示以下组件
|
||||
if (viewModel.groupInfo?.isCreator == true) {
|
||||
// 群资料设置
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(12.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToGroupProfileSettings(groupId)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.fengm),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_group_settings),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||
modifier = Modifier.size(16.dp),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
// 群可见性
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(12.dp)
|
||||
.noRippleClickable {
|
||||
showVisibilityDialog = true
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.rider_pro_change_password),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_group_visibility),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
// 未解锁时才显示"待解锁"
|
||||
if (viewModel.groupInfo?.privateFeePaid != true) {
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_locked),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text.copy(alpha = 0.5f),
|
||||
fontSize = 11.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||
modifier = Modifier.size(16.dp),
|
||||
contentDescription = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||
modifier = Modifier.size(16.dp),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
// 成员管理
|
||||
@@ -468,15 +476,21 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.group_info_users),
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(R.mipmap.icons_users),
|
||||
modifier = Modifier.size(25.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_member_manage),
|
||||
text = stringResource(
|
||||
if (viewModel.groupInfo?.isCreator == true) {
|
||||
R.string.group_chat_info_member_manage
|
||||
} else {
|
||||
R.string.group_members_title
|
||||
}
|
||||
),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = AppColors.text,
|
||||
fontSize = 15.sp
|
||||
@@ -499,8 +513,8 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.group_info_edit),
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(R.mipmap.iconsgallery),
|
||||
modifier = Modifier.size(25.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
@@ -520,36 +534,70 @@ fun GroupChatInfoScreen(groupId: String) {
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
// 解散群聊
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(12.dp)
|
||||
.noRippleClickable { },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.group_info_exit),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_dissolve),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = Color(0xFFFF3B30),
|
||||
fontSize = 15.sp
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||
modifier = Modifier.size(18.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
||||
)
|
||||
if (viewModel.groupInfo?.isCreator == true) {
|
||||
// 解散群聊(仅群主)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(12.dp)
|
||||
.noRippleClickable { },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(3.dp))
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.iconslogout),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(11.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_dissolve),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = Color(0xFFFF3B30),
|
||||
fontSize = 15.sp
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||
modifier = Modifier.size(18.dp),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 退出群聊(非群主)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(12.dp)
|
||||
.noRippleClickable { },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(3.dp))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.group_info_exit),
|
||||
modifier = Modifier.size(20.dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(11.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.group_chat_info_quit),
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = Color(0xFFFF3B30),
|
||||
fontSize = 15.sp
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||
modifier = Modifier.size(18.dp),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,16 +107,16 @@ fun GroupMembersScreen(groupId: String) {
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(R.drawable.rider_pro_add_other),
|
||||
contentDescription = stringResource(R.string.group_chat_info_add_member),
|
||||
colorFilter = ColorFilter.tint(AppColors.text),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
||||
}
|
||||
)
|
||||
// androidx.compose.foundation.Image(
|
||||
// painter = painterResource(R.drawable.rider_pro_add_other),
|
||||
// contentDescription = stringResource(R.string.group_chat_info_add_member),
|
||||
// colorFilter = ColorFilter.tint(AppColors.text),
|
||||
// modifier = Modifier
|
||||
// .size(24.dp)
|
||||
// .noRippleClickable {
|
||||
// navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
||||
// }
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ private fun MemberItem(
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 菜单按钮
|
||||
|
||||
if (isAdmin) {
|
||||
IconButton(
|
||||
onClick = { onMenuClick(itemPosition, itemHeight) },
|
||||
modifier = Modifier.size(24.dp)
|
||||
@@ -404,6 +404,7 @@ private fun MemberItem(
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ package com.aiosman.ravenow.ui.index
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -140,6 +136,7 @@ fun IndexScreen() {
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = drawerState.isOpen,
|
||||
scrimColor = Color.Black.copy(alpha = 0.6f),
|
||||
drawerContent = {
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
SideMenuContent(
|
||||
@@ -525,8 +522,6 @@ fun SideMenuContent(
|
||||
} else {
|
||||
Color(0xFFFAF9FB) // 亮色模式:浅灰色
|
||||
}
|
||||
// 遮罩颜色 黑色透明度0.6
|
||||
val overlayColor = Color.Black.copy(alpha = 0.6f)
|
||||
// 卡片背景色 - 根据暗色模式适配
|
||||
val cardBackgroundColor = if (darkModeEnabled) {
|
||||
appColors.background // 暗色模式:深色背景
|
||||
@@ -546,24 +541,6 @@ fun SideMenuContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
// 左侧半透明遮罩(平滑淡入淡出)
|
||||
val overlayTransition = updateTransition(targetState = isDrawerOpen, label = "overlay")
|
||||
val overlayAlpha by overlayTransition.animateFloat(
|
||||
transitionSpec = {
|
||||
if (targetState) {
|
||||
tween(durationMillis = 400, easing = LinearOutSlowInEasing)
|
||||
} else {
|
||||
tween(durationMillis = 300, easing = FastOutLinearInEasing)
|
||||
}
|
||||
},
|
||||
label = "overlayAlpha"
|
||||
) { open -> if (open) 0.6f else 0f }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = overlayAlpha))
|
||||
)
|
||||
|
||||
// 右侧菜单面板
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -48,11 +49,12 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.LottieConstants
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
||||
|
||||
/**
|
||||
* 智能体聊天列表页面
|
||||
@@ -89,72 +91,28 @@ fun AgentChatListScreen() {
|
||||
.pullRefresh(state)
|
||||
) {
|
||||
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
|
||||
// 空状态
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
// 空状态
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.offset(y = (-40).dp)
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||
contentDescription = "null data",
|
||||
modifier = Modifier
|
||||
.width(181.dp)
|
||||
.height(153.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(9.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.no_one_knocked_yet),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
else {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier
|
||||
.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
AgentChatListViewModel.refreshPager(context = context)
|
||||
}
|
||||
)
|
||||
if (isNetworkAvailable) {
|
||||
ChatEmptyStateView()
|
||||
} else {
|
||||
NetworkErrorContentCompact(
|
||||
onReload = {
|
||||
AgentChatListViewModel.refreshPager(context = context)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -42,10 +42,11 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
||||
|
||||
data class CombinedConversation(
|
||||
val type: String, // "agent", "group", or "friend"
|
||||
@@ -217,73 +218,31 @@ fun AllChatListScreen() {
|
||||
.pullRefresh(state)
|
||||
) {
|
||||
if (allConversations.isEmpty() && !isLoading) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.offset(y = (-40).dp)
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||
contentDescription = "null data",
|
||||
modifier = Modifier
|
||||
.width(181.dp)
|
||||
.height(153.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(9.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.no_one_knocked_yet),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier
|
||||
.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
isLoading = true
|
||||
// 重新加载所有类型的数据
|
||||
AgentChatListViewModel.refreshPager(context = context)
|
||||
GroupChatListViewModel.refreshPager(context = context)
|
||||
FriendChatListViewModel.refreshPager(context = context)
|
||||
}
|
||||
)
|
||||
if (isNetworkAvailable) {
|
||||
ChatEmptyStateView()
|
||||
} else {
|
||||
NetworkErrorContentCompact(
|
||||
onReload = {
|
||||
isLoading = true
|
||||
// 重新加载所有类型的数据
|
||||
AgentChatListViewModel.refreshPager(context = context)
|
||||
GroupChatListViewModel.refreshPager(context = context)
|
||||
FriendChatListViewModel.refreshPager(context = context)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -33,7 +33,7 @@ import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||
import com.aiosman.ravenow.ui.index.tabs.search.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@@ -72,70 +73,27 @@ fun FriendChatListScreen() {
|
||||
.pullRefresh(state)
|
||||
) {
|
||||
if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
//verticalArrangement = Arrangement.Center
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.offset(y = (-40).dp)
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||
contentDescription = "null data",
|
||||
modifier = Modifier
|
||||
.width(181.dp)
|
||||
.height(153.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(9.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.no_one_knocked_yet),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}else {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier
|
||||
.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
||||
}
|
||||
)
|
||||
if (isNetworkAvailable) {
|
||||
ChatEmptyStateView()
|
||||
} else {
|
||||
NetworkErrorContentCompact(
|
||||
onReload = {
|
||||
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -307,43 +265,4 @@ fun FriendChatItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun ReloadButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val gradientBrush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7c45ed),
|
||||
Color(0xFF7c68ef),
|
||||
Color(0xFF7bd8f8)
|
||||
)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 120.dp)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = Color.Transparent
|
||||
),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(gradientBrush),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.Reload),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@@ -67,69 +68,27 @@ fun GroupChatListScreen() {
|
||||
.pullRefresh(state)
|
||||
) {
|
||||
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.offset(y = (-40).dp)
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_3),
|
||||
contentDescription = "null data",
|
||||
modifier = Modifier
|
||||
.width(181.dp)
|
||||
.height(153.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(9.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.no_one_knocked_yet),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}else {
|
||||
Spacer(modifier = Modifier.height(39.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier
|
||||
.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
GroupChatListViewModel.refreshPager(context = context)
|
||||
}
|
||||
)
|
||||
if (isNetworkAvailable) {
|
||||
ChatEmptyStateView()
|
||||
} else {
|
||||
NetworkErrorContentCompact(
|
||||
onReload = {
|
||||
GroupChatListViewModel.refreshPager(context = context)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -64,12 +64,36 @@ open class BaseMomentModel :ViewModel(){
|
||||
momentLoader.updateMomentLike(event.postId, event.isLike)
|
||||
}
|
||||
suspend fun likeMoment(id: Int) {
|
||||
// 获取当前动态信息,用于计算新的点赞数
|
||||
val currentMoment = momentLoader.list.find { it.id == id }
|
||||
val newLikeCount = (currentMoment?.likeCount ?: 0) + 1
|
||||
|
||||
momentService.likeMoment(id)
|
||||
momentLoader.updateMomentLike(id, true)
|
||||
|
||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
||||
EventBus.getDefault().post(
|
||||
MomentLikeChangeEvent(
|
||||
postId = id,
|
||||
likeCount = newLikeCount,
|
||||
isLike = true
|
||||
)
|
||||
)
|
||||
}
|
||||
suspend fun dislikeMoment(id: Int) {
|
||||
// 获取当前动态信息,用于计算新的点赞数
|
||||
val currentMoment = momentLoader.list.find { it.id == id }
|
||||
val newLikeCount = ((currentMoment?.likeCount ?: 0) - 1).coerceAtLeast(0)
|
||||
|
||||
momentService.dislikeMoment(id)
|
||||
momentLoader.updateMomentLike(id, false)
|
||||
|
||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
||||
EventBus.getDefault().post(
|
||||
MomentLikeChangeEvent(
|
||||
postId = id,
|
||||
likeCount = newLikeCount,
|
||||
isLike = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -90,14 +114,27 @@ open class BaseMomentModel :ViewModel(){
|
||||
|
||||
suspend fun favoriteMoment(id: Int) {
|
||||
momentService.favoriteMoment(id)
|
||||
momentLoader.updateFavoriteCount(id, true)
|
||||
|
||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
||||
EventBus.getDefault().post(
|
||||
MomentFavouriteChangeEvent(
|
||||
postId = id,
|
||||
isFavourite = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
suspend fun unfavoriteMoment(id: Int) {
|
||||
momentService.unfavoriteMoment(id)
|
||||
momentLoader.updateFavoriteCount(id, false)
|
||||
|
||||
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
|
||||
EventBus.getDefault().post(
|
||||
MomentFavouriteChangeEvent(
|
||||
postId = id,
|
||||
isFavourite = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -226,7 +226,9 @@ fun NewsCommentModal(
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
OrderSelectionComponent {
|
||||
OrderSelectionComponent(
|
||||
selectedOrder = commentViewModel.order
|
||||
) {
|
||||
commentViewModel.order = it
|
||||
commentViewModel.reloadComment()
|
||||
}
|
||||
|
||||
@@ -16,10 +16,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
@@ -33,8 +30,11 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
@@ -97,6 +97,10 @@ fun VideoRecommendationItem(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
var pauseIconVisibleState by remember { mutableStateOf(false) }
|
||||
var shouldResumeAfterLifecyclePause by remember { mutableStateOf(false) }
|
||||
// 防抖:记录上次双击时间,防止快速重复双击
|
||||
val lastDoubleTapTime = remember { mutableStateOf(0L) }
|
||||
val doubleTapDebounceTime = 500L // 500ms 防抖时间
|
||||
|
||||
val exoPlayer = remember(videoUrl) {
|
||||
ExoPlayer.Builder(context)
|
||||
@@ -167,29 +171,43 @@ fun VideoRecommendationItem(
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.noRippleClickable {
|
||||
pauseIconVisibleState = true
|
||||
exoPlayer.pause()
|
||||
scope.launch {
|
||||
delay(100)
|
||||
if (exoPlayer.isPlaying) {
|
||||
.pointerInput(videoUrl, moment.liked) {
|
||||
detectTapGestures(
|
||||
onDoubleTap = { offset ->
|
||||
// 双击点赞/取消点赞
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
|
||||
lastDoubleTapTime.value = currentTime
|
||||
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
|
||||
onLikeClick?.invoke(moment)
|
||||
}
|
||||
},
|
||||
onTap = {
|
||||
// 单击播放/暂停
|
||||
pauseIconVisibleState = true
|
||||
exoPlayer.pause()
|
||||
} else {
|
||||
pauseIconVisibleState = false
|
||||
exoPlayer.play()
|
||||
scope.launch {
|
||||
delay(100)
|
||||
if (exoPlayer.isPlaying) {
|
||||
exoPlayer.pause()
|
||||
} else {
|
||||
pauseIconVisibleState = false
|
||||
exoPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (pauseIconVisibleState) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(80.dp),
|
||||
tint = Color.White
|
||||
colorFilter = ColorFilter.tint(Color.White)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -300,7 +318,9 @@ fun VideoRecommendationItem(
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showCommentModal = false },
|
||||
containerColor = Color.White,
|
||||
sheetState = sheetState
|
||||
sheetState = sheetState,
|
||||
dragHandle = {},
|
||||
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
||||
) {
|
||||
CommentModalContent(postId = moment.id) {
|
||||
// 评论添加后的回调
|
||||
@@ -320,10 +340,12 @@ fun VideoRecommendationItem(
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
shouldResumeAfterLifecyclePause = exoPlayer.isPlaying && !pauseIconVisibleState
|
||||
exoPlayer.pause()
|
||||
}
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
if (isVisible) {
|
||||
if (isVisible && shouldResumeAfterLifecyclePause) {
|
||||
pauseIconVisibleState = false
|
||||
exoPlayer.play()
|
||||
}
|
||||
}
|
||||
@@ -399,3 +421,4 @@ private fun VideoBtn(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||
import kotlinx.coroutines.launch
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
|
||||
|
||||
/**
|
||||
* 动态列表
|
||||
@@ -90,38 +91,11 @@ fun TimelineMomentsList() {
|
||||
.padding(top = 188.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val exploreDebouncer = rememberDebouncer()
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ExploreButton(
|
||||
onClick = {
|
||||
exploreDebouncer {
|
||||
/* TODO: 添加点击事件处理 */
|
||||
} }
|
||||
)
|
||||
}
|
||||
NetworkErrorContentInline(
|
||||
onReload = {
|
||||
model.refreshPager(pullRefresh = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (moments.isEmpty()) {
|
||||
Box(
|
||||
|
||||
@@ -33,6 +33,8 @@ import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
@@ -308,14 +310,16 @@ object MyProfileViewModel : ViewModel() {
|
||||
* 加载房间列表
|
||||
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
||||
* @param pullRefresh 是否下拉刷新
|
||||
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为null,则显示当前用户创建或加入的房间
|
||||
*/
|
||||
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false) {
|
||||
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false, ownerSessionId: String? = null) {
|
||||
// 游客模式下不加载房间列表
|
||||
if (AppStore.isGuest) {
|
||||
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
|
||||
return
|
||||
}
|
||||
|
||||
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
|
||||
if (roomsLoading && !pullRefresh) return
|
||||
|
||||
viewModelScope.launch {
|
||||
@@ -331,60 +335,69 @@ object MyProfileViewModel : ViewModel() {
|
||||
roomsCurrentPage
|
||||
}
|
||||
|
||||
val response = when (filterType) {
|
||||
0 -> {
|
||||
// 全部:显示自己创建或加入的所有房间
|
||||
apiClient.getRooms(
|
||||
page = currentPage,
|
||||
pageSize = roomsPageSize,
|
||||
showCreated = true,
|
||||
showJoined = true
|
||||
)
|
||||
}
|
||||
1 -> {
|
||||
// 公开:显示公开房间中自己创建或加入的
|
||||
apiClient.getRooms(
|
||||
page = currentPage,
|
||||
pageSize = roomsPageSize,
|
||||
roomType = "public",
|
||||
showCreated = true,
|
||||
showJoined = true
|
||||
)
|
||||
}
|
||||
2 -> {
|
||||
// 私有:显示自己创建或加入的私有房间
|
||||
apiClient.getRooms(
|
||||
page = currentPage,
|
||||
pageSize = roomsPageSize,
|
||||
roomType = "private"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
apiClient.getRooms(
|
||||
page = currentPage,
|
||||
pageSize = roomsPageSize,
|
||||
showCreated = true,
|
||||
showJoined = true
|
||||
)
|
||||
}
|
||||
// 根据filterType确定roomType
|
||||
val roomType = when (filterType) {
|
||||
1 -> "public"
|
||||
2 -> "private"
|
||||
else -> null
|
||||
}
|
||||
val effectiveRoomType = if (normalizedOwnerId != null) "public" else roomType
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val roomList = response.body()?.list ?: emptyList()
|
||||
val total = response.body()?.total ?: 0L
|
||||
// 构建API调用参数
|
||||
if (normalizedOwnerId != null) {
|
||||
// 查看其他用户的房间:仅显示该用户创建的公开房间
|
||||
val createdResponse = apiClient.getRooms(
|
||||
page = currentPage,
|
||||
pageSize = roomsPageSize,
|
||||
roomType = effectiveRoomType,
|
||||
ownerSessionId = normalizedOwnerId,
|
||||
includeUsers = false
|
||||
)
|
||||
|
||||
val createdRooms = if (createdResponse.isSuccessful) {
|
||||
createdResponse.body()?.list?.map { it.toRoomtEntity() } ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
if (pullRefresh || currentPage == 1) {
|
||||
rooms = roomList.map { it.toRoomtEntity() }
|
||||
rooms = createdRooms
|
||||
} else {
|
||||
rooms = rooms + roomList.map { it.toRoomtEntity() }
|
||||
rooms = rooms + createdRooms
|
||||
}
|
||||
|
||||
val total = createdResponse.body()?.total ?: 0L
|
||||
roomsHasMore = rooms.size < total
|
||||
if (roomsHasMore && !pullRefresh) {
|
||||
roomsCurrentPage++
|
||||
}
|
||||
} else {
|
||||
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
|
||||
// 查看自己的房间:显示创建或加入的房间
|
||||
val response = apiClient.getRooms(
|
||||
page = currentPage,
|
||||
pageSize = roomsPageSize,
|
||||
roomType = effectiveRoomType,
|
||||
showCreated = true,
|
||||
showJoined = if (filterType == 2) null else true // 私有房间不显示加入的
|
||||
)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val roomList = response.body()?.list ?: emptyList()
|
||||
val total = response.body()?.total ?: 0L
|
||||
|
||||
if (pullRefresh || currentPage == 1) {
|
||||
rooms = roomList.map { it.toRoomtEntity() }
|
||||
} else {
|
||||
rooms = rooms + roomList.map { it.toRoomtEntity() }
|
||||
}
|
||||
|
||||
roomsHasMore = rooms.size < total
|
||||
if (roomsHasMore && !pullRefresh) {
|
||||
roomsCurrentPage++
|
||||
}
|
||||
} else {
|
||||
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MyProfileViewModel", "loadRooms error: ", e)
|
||||
@@ -398,20 +411,29 @@ object MyProfileViewModel : ViewModel() {
|
||||
/**
|
||||
* 加载更多房间
|
||||
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
||||
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间
|
||||
*/
|
||||
fun loadMoreRooms(filterType: Int = 0) {
|
||||
fun loadMoreRooms(filterType: Int = 0, ownerSessionId: String? = null) {
|
||||
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
|
||||
if (roomsLoading || !roomsHasMore) return
|
||||
loadRooms(filterType = filterType, pullRefresh = false)
|
||||
loadRooms(filterType = filterType, pullRefresh = false, ownerSessionId = normalizedOwnerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新房间列表
|
||||
* @param filterType 筛选类型:0=全部,1=公开,2=私有
|
||||
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间
|
||||
*/
|
||||
fun refreshRooms(filterType: Int = 0) {
|
||||
fun refreshRooms(filterType: Int = 0, ownerSessionId: String? = null) {
|
||||
rooms = emptyList()
|
||||
roomsCurrentPage = 1
|
||||
roomsHasMore = true
|
||||
loadRooms(filterType = filterType, pullRefresh = true)
|
||||
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
|
||||
loadRooms(filterType = filterType, pullRefresh = true, ownerSessionId = normalizedOwnerId)
|
||||
}
|
||||
|
||||
private fun normalizeOwnerSessionId(ownerSessionId: String?): String? {
|
||||
val trimmed = ownerSessionId?.trim()
|
||||
return if (trimmed.isNullOrEmpty()) null else trimmed
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -59,6 +60,8 @@ import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -85,6 +88,8 @@ import com.aiosman.ravenow.ui.index.IndexViewModel
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SegmentedControl
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.AgentSegmentedControl
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
|
||||
@@ -168,6 +173,41 @@ fun ProfileV3(
|
||||
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
|
||||
)
|
||||
val scrollState = rememberScrollState(model.profileScrollOffset)
|
||||
var tabIndicatorContentOffset by remember { mutableStateOf<Float?>(null) }
|
||||
var tabIndicatorHeightPx by remember { mutableStateOf(0) }
|
||||
val topNavigationBarHeightPx = with(density) { (statusBarPaddingValues.calculateTopPadding() + 56.dp).toPx() }
|
||||
val stickyTopPadding = statusBarPaddingValues.calculateTopPadding() + 56.dp
|
||||
var agentSegmentOffset by remember { mutableStateOf<Float?>(null) }
|
||||
var agentSegmentHeightPx by remember { mutableStateOf(0) }
|
||||
var groupSegmentOffset by remember { mutableStateOf<Float?>(null) }
|
||||
var groupSegmentHeightPx by remember { mutableStateOf(0) }
|
||||
var agentSegmentSelected by remember { mutableStateOf(0) }
|
||||
var groupSegmentSelected by remember { mutableStateOf(0) }
|
||||
val tabIndicatorHeightDp = with(density) { tabIndicatorHeightPx.toDp() }
|
||||
val tabBarBottomPx = topNavigationBarHeightPx + tabIndicatorHeightPx
|
||||
val tabBarBottomPadding = stickyTopPadding + tabIndicatorHeightDp
|
||||
val tabStickyThreshold = remember(tabIndicatorContentOffset, topNavigationBarHeightPx) {
|
||||
tabIndicatorContentOffset?.minus(topNavigationBarHeightPx)
|
||||
}
|
||||
val agentSegmentThreshold = remember(agentSegmentOffset, tabBarBottomPx) {
|
||||
agentSegmentOffset?.minus(tabBarBottomPx)
|
||||
}
|
||||
val groupSegmentThreshold = remember(groupSegmentOffset, tabBarBottomPx) {
|
||||
groupSegmentOffset?.minus(tabBarBottomPx)
|
||||
}
|
||||
val agentTabIndex = if (isAiAccount) -1 else 1
|
||||
val groupTabIndex = if (isAiAccount) 1 else 2
|
||||
val shouldStickTabBar = tabStickyThreshold?.let { scrollState.value >= it } ?: false
|
||||
val shouldStickAgentSegments = isSelf && !isAiAccount && agentSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == agentTabIndex
|
||||
val shouldStickGroupSegments = isSelf && groupSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == groupTabIndex
|
||||
val externalOwnerSessionId = remember(isSelf, profile?.chatAIId, profile?.trtcUserId) {
|
||||
if (isSelf) {
|
||||
null
|
||||
} else {
|
||||
profile?.chatAIId?.takeIf { it.isNotBlank() }
|
||||
?: profile?.trtcUserId?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
@@ -492,10 +532,19 @@ fun ProfileV3(
|
||||
.background(AppColors.profileBackground)
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
UserContentPageIndicator(
|
||||
pagerState = pagerState,
|
||||
showAgentTab = !isAiAccount
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
tabIndicatorHeightPx = coordinates.size.height
|
||||
tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value
|
||||
}
|
||||
.alpha(if (shouldStickTabBar) 0f else 1f)
|
||||
) {
|
||||
UserContentPageIndicator(
|
||||
pagerState = pagerState,
|
||||
showAgentTab = !isAiAccount
|
||||
)
|
||||
}
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.height(650.dp) // 固定滚动高度
|
||||
@@ -533,22 +582,53 @@ fun ProfileV3(
|
||||
showNoMoreText = isSelf,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
nestedScrollConnection = nestedScrollConnection
|
||||
nestedScrollConnection = nestedScrollConnection,
|
||||
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
|
||||
segmentSelectedIndex = agentSegmentSelected,
|
||||
onSegmentSelected = { agentSegmentSelected = it },
|
||||
onSegmentMeasured = { offset, height ->
|
||||
agentSegmentOffset = offset
|
||||
agentSegmentHeightPx = height
|
||||
},
|
||||
isSegmentSticky = shouldStickAgentSegments,
|
||||
parentScrollProvider = { scrollState.value }
|
||||
)
|
||||
} else {
|
||||
// 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊;查看自己的主页时传递null
|
||||
GroupChatPlaceholder(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
listState = groupChatListState,
|
||||
nestedScrollConnection = nestedScrollConnection
|
||||
nestedScrollConnection = nestedScrollConnection,
|
||||
ownerSessionId = externalOwnerSessionId,
|
||||
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
|
||||
selectedSegmentIndex = groupSegmentSelected,
|
||||
onSegmentSelected = { groupSegmentSelected = it },
|
||||
onSegmentMeasured = { offset, height ->
|
||||
groupSegmentOffset = offset
|
||||
groupSegmentHeightPx = height
|
||||
},
|
||||
isSegmentSticky = shouldStickGroupSegments,
|
||||
parentScrollProvider = { scrollState.value }
|
||||
)
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
if (!isAiAccount) {
|
||||
// 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊;查看自己的主页时传递null
|
||||
GroupChatPlaceholder(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
listState = groupChatListState,
|
||||
nestedScrollConnection = nestedScrollConnection
|
||||
nestedScrollConnection = nestedScrollConnection,
|
||||
ownerSessionId = externalOwnerSessionId,
|
||||
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
|
||||
selectedSegmentIndex = groupSegmentSelected,
|
||||
onSegmentSelected = { groupSegmentSelected = it },
|
||||
onSegmentMeasured = { offset, height ->
|
||||
groupSegmentOffset = offset
|
||||
groupSegmentHeightPx = height
|
||||
},
|
||||
isSegmentSticky = shouldStickGroupSegments,
|
||||
parentScrollProvider = { scrollState.value }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -560,6 +640,55 @@ fun ProfileV3(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (shouldStickTabBar && tabIndicatorHeightPx > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = stickyTopPadding)
|
||||
.background(AppColors.profileBackground)
|
||||
) {
|
||||
UserContentPageIndicator(
|
||||
pagerState = pagerState,
|
||||
showAgentTab = !isAiAccount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStickAgentSegments && agentSegmentHeightPx > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = tabBarBottomPadding)
|
||||
.background(AppColors.profileBackground)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
AgentSegmentedControl(
|
||||
selectedIndex = agentSegmentSelected,
|
||||
onSegmentSelected = { agentSegmentSelected = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStickGroupSegments && groupSegmentHeightPx > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = tabBarBottomPadding)
|
||||
.background(AppColors.profileBackground)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
SegmentedControl(
|
||||
selectedIndex = groupSegmentSelected,
|
||||
onSegmentSelected = { groupSegmentSelected = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部导航栏
|
||||
TopNavigationBar(
|
||||
isMain = isMain,
|
||||
@@ -656,12 +785,26 @@ fun ProfileV3(
|
||||
private fun GroupChatPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
listState: androidx.compose.foundation.lazy.LazyListState,
|
||||
nestedScrollConnection: NestedScrollConnection? = null
|
||||
nestedScrollConnection: NestedScrollConnection? = null,
|
||||
ownerSessionId: String? = null, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为null,则显示当前用户创建或加入的房间
|
||||
showSegments: Boolean = true,
|
||||
selectedSegmentIndex: Int = 0,
|
||||
onSegmentSelected: (Int) -> Unit = {},
|
||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
||||
isSegmentSticky: Boolean = false,
|
||||
parentScrollProvider: () -> Int = { 0 }
|
||||
) {
|
||||
GroupChatEmptyContent(
|
||||
modifier = modifier,
|
||||
listState = listState,
|
||||
nestedScrollConnection = nestedScrollConnection
|
||||
nestedScrollConnection = nestedScrollConnection,
|
||||
ownerSessionId = ownerSessionId,
|
||||
showSegments = showSegments,
|
||||
selectedSegmentIndex = selectedSegmentIndex,
|
||||
onSegmentSelected = onSegmentSelected,
|
||||
onSegmentMeasured = onSegmentMeasured,
|
||||
isSegmentSticky = isSegmentSticky,
|
||||
parentScrollProvider = parentScrollProvider
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,50 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.aiosman.ravenow.AppState
|
||||
@@ -155,7 +158,8 @@ fun GalleryGrid(
|
||||
modifier = baseModifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 60.dp),
|
||||
.padding(vertical = 60.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
@@ -198,24 +202,8 @@ fun GalleryGrid(
|
||||
.padding(vertical = 60.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.l_empty_img),
|
||||
contentDescription = "暂无图片",
|
||||
modifier = Modifier
|
||||
.size(width = 181.dp, height = 153.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(9.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.cosmos_awaits),
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.text,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
EmptyStateView(
|
||||
contentDescription = "暂无图片"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -227,8 +215,20 @@ fun GalleryGrid(
|
||||
.padding(bottom = 8.dp),
|
||||
) {
|
||||
itemsIndexed(moments) { idx, moment ->
|
||||
if (moment != null && moment.images.isNotEmpty()) {
|
||||
moment?.let { momentItem ->
|
||||
val itemDebouncer = rememberDebouncer()
|
||||
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
|
||||
val previewUrl = when {
|
||||
momentItem.images.isNotEmpty() -> momentItem.images[0].thumbnail
|
||||
isVideoMoment -> {
|
||||
val firstVideo = momentItem.videos!!.first()
|
||||
firstVideo.thumbnailDirectUrl
|
||||
?: firstVideo.thumbnailUrl
|
||||
?: firstVideo.directUrl
|
||||
?: firstVideo.url
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -237,20 +237,32 @@ fun GalleryGrid(
|
||||
.noRippleClickable {
|
||||
itemDebouncer {
|
||||
navController.navigateToPost(
|
||||
id = moment.id,
|
||||
id = momentItem.id,
|
||||
highlightCommentId = 0,
|
||||
initImagePagerIndex = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = moment.images[0].thumbnail,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
context = LocalContext.current
|
||||
)
|
||||
if (moment.images.size > 1) {
|
||||
if (previewUrl != null) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = previewUrl,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
context = LocalContext.current
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
color = AppColors.basicMain.copy(alpha = 0.2f),
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, end = 8.dp)
|
||||
@@ -263,6 +275,31 @@ fun GalleryGrid(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isVideoMoment) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, end = 8.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.4f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,16 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.LottieConstants
|
||||
@@ -40,6 +41,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -63,37 +66,47 @@ import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.data.api.ApiClient
|
||||
import android.util.Base64
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.utils.NetworkUtils.isNetworkAvailable
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun GroupChatEmptyContent(
|
||||
modifier: Modifier = Modifier,
|
||||
listState: LazyListState,
|
||||
nestedScrollConnection: NestedScrollConnection? = null
|
||||
nestedScrollConnection: NestedScrollConnection? = null,
|
||||
ownerSessionId: String? = null, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为null,则显示当前用户创建或加入的房间
|
||||
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
|
||||
selectedSegmentIndex: Int = 0,
|
||||
onSegmentSelected: (Int) -> Unit = {},
|
||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
||||
isSegmentSticky: Boolean = false,
|
||||
parentScrollProvider: () -> Int = { 0 }
|
||||
) {
|
||||
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val viewModel = MyProfileViewModel
|
||||
val normalizedOwnerSessionId = ownerSessionId?.takeIf { it.isNotBlank() }
|
||||
val canLoadRooms = showSegments || normalizedOwnerSessionId != null
|
||||
val networkAvailable = isNetworkAvailable(context)
|
||||
|
||||
// 如果查看其他用户的房间,固定使用全部类型(filterType = 0)
|
||||
val filterType = if (showSegments) selectedSegmentIndex else 0
|
||||
|
||||
val state = rememberPullRefreshState(
|
||||
refreshing = viewModel.roomsRefreshing,
|
||||
refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false,
|
||||
onRefresh = {
|
||||
viewModel.refreshRooms(filterType = selectedSegment)
|
||||
if (canLoadRooms) {
|
||||
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 当分段改变时,重新加载数据
|
||||
LaunchedEffect(selectedSegment) {
|
||||
// 切换分段时重新加载
|
||||
viewModel.refreshRooms(filterType = selectedSegment)
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
|
||||
viewModel.loadRooms(filterType = selectedSegment)
|
||||
// 当分段或用户ID改变时,重新加载数据
|
||||
LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) {
|
||||
if (canLoadRooms) {
|
||||
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,72 +123,90 @@ fun GroupChatEmptyContent(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 分段控制器
|
||||
SegmentedControl(
|
||||
selectedIndex = selectedSegment,
|
||||
onSegmentSelected = {
|
||||
selectedSegment = it
|
||||
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
// 只在查看自己的房间时显示分段控制器
|
||||
if (showSegments) {
|
||||
SegmentedControl(
|
||||
selectedIndex = selectedSegmentIndex,
|
||||
onSegmentSelected = onSegmentSelected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onGloballyPositioned { coordinates ->
|
||||
onSegmentMeasured?.invoke(
|
||||
coordinates.positionInRoot().y + parentScrollProvider(),
|
||||
coordinates.size.height
|
||||
)
|
||||
}
|
||||
.alpha(if (isSegmentSticky) 0f else 1f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = nestedScrollModifier
|
||||
.fillMaxSize()
|
||||
.pullRefresh(state)
|
||||
) {
|
||||
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
|
||||
if (!canLoadRooms) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
|
||||
// 空状态内容(居中)
|
||||
Column(
|
||||
modifier = nestedScrollModifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 空状态插图
|
||||
EmptyStateIllustration()
|
||||
EmptyStateIllustration(
|
||||
isNetworkAvailable = networkAvailable,
|
||||
onReload = {
|
||||
MyProfileViewModel.ResetModel()
|
||||
MyProfileViewModel.loadProfile(pullRefresh = true)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(9.dp))
|
||||
|
||||
// 空状态文本
|
||||
Text(
|
||||
text = stringResource(R.string.cosmos_awaits),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = AppColors.text,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = nestedScrollModifier.fillMaxSize()
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = viewModel.rooms,
|
||||
key = { _, item -> item.id }
|
||||
) { index, room ->
|
||||
RoomItem(
|
||||
room = room,
|
||||
onRoomClick = { roomEntity ->
|
||||
// 导航到群聊聊天界面
|
||||
navController.navigateToGroupChat(
|
||||
id = roomEntity.trtcRoomId,
|
||||
name = roomEntity.name,
|
||||
avatar = roomEntity.avatar
|
||||
// 网格布局:每行显示2个房间卡片
|
||||
items(
|
||||
items = viewModel.rooms.chunked(2),
|
||||
key = { rowRooms -> rowRooms.firstOrNull()?.id?.toString() ?: "" }
|
||||
) { rowRooms ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
rowRooms.forEach { room ->
|
||||
RoomCard(
|
||||
room = room,
|
||||
onRoomClick = { roomEntity ->
|
||||
// 导航到群聊聊天界面
|
||||
navController.navigateToGroupChat(
|
||||
id = roomEntity.trtcRoomId,
|
||||
name = roomEntity.name,
|
||||
avatar = roomEntity.avatar
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (index < viewModel.rooms.size - 1) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
color = AppColors.divider
|
||||
)
|
||||
// 如果这一行只有一个房间,添加一个空的占位符
|
||||
if (rowRooms.size == 1) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,22 +232,141 @@ fun GroupChatEmptyContent(
|
||||
if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
|
||||
item {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadMoreRooms(filterType = selectedSegment)
|
||||
viewModel.loadMoreRooms(
|
||||
filterType = filterType,
|
||||
ownerSessionId = normalizedOwnerSessionId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewModel.roomsRefreshing,
|
||||
state = state,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
if (canLoadRooms) {
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewModel.roomsRefreshing,
|
||||
state = state,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun 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
|
||||
fun RoomItem(
|
||||
room: RoomEntity,
|
||||
@@ -230,7 +380,6 @@ fun RoomItem(
|
||||
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
|
||||
@@ -307,7 +456,7 @@ fun RoomItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SegmentedControl(
|
||||
fun SegmentedControl(
|
||||
selectedIndex: Int,
|
||||
onSegmentSelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -372,7 +521,7 @@ private fun SegmentButton(
|
||||
},
|
||||
shape = RoundedCornerShape(1000.dp)
|
||||
)
|
||||
.clickable(onClick = onClick),
|
||||
.noRippleClickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
@@ -389,14 +538,43 @@ private fun SegmentButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyStateIllustration() {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.l_empty_img),
|
||||
contentDescription = "空状态",
|
||||
modifier = Modifier
|
||||
.width(181.dp)
|
||||
.height(153.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
private fun EmptyStateIllustration(
|
||||
isNetworkAvailable: Boolean,
|
||||
onReload: () -> Unit
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
if (isNetworkAvailable) {
|
||||
EmptyStateView(
|
||||
contentDescription = "空状态",
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(181.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.text,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(onClick = onReload)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -52,6 +55,7 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.entity.AgentEntity
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.utils.DebounceUtils
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
@@ -66,7 +70,13 @@ fun UserAgentsList(
|
||||
showNoMoreText: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyListState,
|
||||
nestedScrollConnection: NestedScrollConnection? = null
|
||||
nestedScrollConnection: NestedScrollConnection? = null,
|
||||
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
|
||||
segmentSelectedIndex: Int = 0,
|
||||
onSegmentSelected: (Int) -> Unit = {},
|
||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
||||
isSegmentSticky: Boolean = false,
|
||||
parentScrollProvider: () -> Int = { 0 }
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val listModifier = if (nestedScrollConnection != null) {
|
||||
@@ -80,7 +90,14 @@ fun UserAgentsList(
|
||||
Box(
|
||||
modifier = listModifier.fillMaxSize()
|
||||
) {
|
||||
AgentEmptyContentWithSegments()
|
||||
AgentEmptyContentWithSegments(
|
||||
showSegments = showSegments,
|
||||
segmentSelectedIndex = segmentSelectedIndex,
|
||||
onSegmentSelected = onSegmentSelected,
|
||||
onSegmentMeasured = onSegmentMeasured,
|
||||
isSegmentSticky = isSegmentSticky,
|
||||
parentScrollProvider = parentScrollProvider
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
@@ -248,8 +265,14 @@ fun UserAgentCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AgentEmptyContentWithSegments() {
|
||||
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
|
||||
fun AgentEmptyContentWithSegments(
|
||||
showSegments: Boolean = true,
|
||||
segmentSelectedIndex: Int = 0,
|
||||
onSegmentSelected: (Int) -> Unit = {},
|
||||
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
|
||||
isSegmentSticky: Boolean = false,
|
||||
parentScrollProvider: () -> Int = { 0 }
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||
|
||||
@@ -260,14 +283,24 @@ fun AgentEmptyContentWithSegments() {
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 分段控制器
|
||||
AgentSegmentedControl(
|
||||
selectedIndex = selectedSegment,
|
||||
onSegmentSelected = { selectedSegment = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
// 只在查看自己的智能体时显示分段控制器
|
||||
if (showSegments) {
|
||||
AgentSegmentedControl(
|
||||
selectedIndex = segmentSelectedIndex,
|
||||
onSegmentSelected = onSegmentSelected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onGloballyPositioned { coordinates ->
|
||||
onSegmentMeasured?.invoke(
|
||||
coordinates.positionInRoot().y + parentScrollProvider(),
|
||||
coordinates.size.height
|
||||
)
|
||||
}
|
||||
.alpha(if (isSegmentSticky) 0f else 1f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// 空状态内容(与动态、群聊保持一致)
|
||||
Column(
|
||||
@@ -275,25 +308,8 @@ fun AgentEmptyContentWithSegments() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (isNetworkAvailable) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.l_empty_img),
|
||||
contentDescription = "暂无Agent",
|
||||
modifier = Modifier
|
||||
.size(width = 181.dp, height = 153.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(9.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.cosmos_awaits),
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.text,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
EmptyStateView(
|
||||
contentDescription = "暂无Agent"
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
@@ -332,7 +348,7 @@ fun AgentEmptyContentWithSegments() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AgentSegmentedControl(
|
||||
fun AgentSegmentedControl(
|
||||
selectedIndex: Int,
|
||||
onSegmentSelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -397,7 +413,7 @@ private fun AgentSegmentButton(
|
||||
},
|
||||
shape = RoundedCornerShape(1000.dp)
|
||||
)
|
||||
.clickable(onClick = onClick),
|
||||
.noRippleClickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
|
||||
@@ -84,6 +84,7 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -415,64 +416,17 @@ fun MomentResultTab() {
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
if (moments.itemCount == 0 && model.showResult) {
|
||||
Column(
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
SearchPlaceholderContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(
|
||||
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||
else R.mipmap.invalid_name_1),
|
||||
contentDescription = "No Comment",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = "咦,什么都没找到...",
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "换个关键词试试吧,也许会有新发现!",
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
} else {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
)
|
||||
isNetworkAvailable = isNetworkAvailable,
|
||||
onReload = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -566,64 +520,17 @@ fun UserResultTab() {
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
if (users.itemCount == 0 && model.showResult) {
|
||||
Column(
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
SearchPlaceholderContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(
|
||||
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||
else R.mipmap.invalid_name_1),
|
||||
contentDescription = "No Comment",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = "咦,什么都没找到...",
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "换个关键词试试吧,也许会有新发现!",
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
} else {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
)
|
||||
isNetworkAvailable = isNetworkAvailable,
|
||||
onReload = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -734,64 +641,17 @@ fun AiResultTab() {
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
if (agents.itemCount == 0 && model.showResult) {
|
||||
Column(
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
SearchPlaceholderContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
if (isNetworkAvailable) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(
|
||||
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||
else R.mipmap.invalid_name_1
|
||||
),
|
||||
contentDescription = "No Result",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = "咦,什么都没找到...",
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "换个关键词试试吧,也许会有新发现!",
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
} else {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
)
|
||||
isNetworkAvailable = isNetworkAvailable,
|
||||
onReload = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -863,65 +723,17 @@ fun RoomResultTab() {
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
if (rooms.itemCount == 0 && model.showResult) {
|
||||
Column(
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
SearchPlaceholderContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(
|
||||
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||
else R.mipmap.invalid_name_1
|
||||
),
|
||||
contentDescription = "No Result",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = "咦,什么都没找到...",
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "换个关键词试试吧,也许会有新发现!",
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
} else {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
)
|
||||
isNetworkAvailable = isNetworkAvailable,
|
||||
onReload = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -945,41 +757,32 @@ fun RoomResultTab() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReloadButton(
|
||||
onClick: () -> Unit
|
||||
fun SearchPlaceholderContent(
|
||||
modifier: Modifier = Modifier,
|
||||
isNetworkAvailable: Boolean,
|
||||
onReload: () -> Unit
|
||||
) {
|
||||
val gradientBrush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7c45ed),
|
||||
Color(0xFF7c68ef),
|
||||
Color(0xFF7bd8f8)
|
||||
)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 120.dp)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = Color.Transparent
|
||||
),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
val appColors = LocalAppTheme.current
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(gradientBrush),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.Reload),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
if (isNetworkAvailable) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(id = R.mipmap.empty_img),
|
||||
contentDescription = "No Comment",
|
||||
modifier = Modifier.size(168.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.null_search),
|
||||
color = appColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
} else {
|
||||
NetworkErrorContentInline(onReload = onReload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||
import com.aiosman.ravenow.entity.AgentEntity
|
||||
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
||||
@@ -31,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
object SearchViewModel : ViewModel() {
|
||||
var searchText by mutableStateOf("")
|
||||
@@ -204,7 +207,14 @@ object SearchViewModel : ViewModel() {
|
||||
suspend fun likeMoment(id: Int) {
|
||||
try {
|
||||
momentService.likeMoment(id)
|
||||
updateMomentLike(id, true)
|
||||
val likeCount = updateMomentLike(id, true)
|
||||
EventBus.getDefault().post(
|
||||
MomentLikeChangeEvent(
|
||||
postId = id,
|
||||
likeCount = likeCount,
|
||||
isLike = true
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -213,7 +223,14 @@ object SearchViewModel : ViewModel() {
|
||||
suspend fun dislikeMoment(id: Int) {
|
||||
try {
|
||||
momentService.dislikeMoment(id)
|
||||
updateMomentLike(id, false)
|
||||
val likeCount = updateMomentLike(id, false)
|
||||
EventBus.getDefault().post(
|
||||
MomentLikeChangeEvent(
|
||||
postId = id,
|
||||
likeCount = likeCount,
|
||||
isLike = false
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -223,6 +240,12 @@ object SearchViewModel : ViewModel() {
|
||||
try {
|
||||
momentService.favoriteMoment(id)
|
||||
updateMomentFavorite(id, true)
|
||||
EventBus.getDefault().post(
|
||||
MomentFavouriteChangeEvent(
|
||||
postId = id,
|
||||
isFavourite = true
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -232,6 +255,12 @@ object SearchViewModel : ViewModel() {
|
||||
try {
|
||||
momentService.unfavoriteMoment(id)
|
||||
updateMomentFavorite(id, false)
|
||||
EventBus.getDefault().post(
|
||||
MomentFavouriteChangeEvent(
|
||||
postId = id,
|
||||
isFavourite = false
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -241,19 +270,23 @@ object SearchViewModel : ViewModel() {
|
||||
updateMomentCommentCount(id, 1)
|
||||
}
|
||||
|
||||
private fun updateMomentLike(id: Int, isLike: Boolean) {
|
||||
private fun updateMomentLike(id: Int, isLike: Boolean): Int? {
|
||||
var latestLikeCount: Int? = null
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
val nextCount = (momentItem.likeCount + if (isLike) 1 else -1).coerceAtLeast(0)
|
||||
latestLikeCount = nextCount
|
||||
momentItem.copy(
|
||||
liked = isLike,
|
||||
likeCount = momentItem.likeCount + if (isLike) 1 else -1
|
||||
likeCount = nextCount
|
||||
)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
return latestLikeCount
|
||||
}
|
||||
|
||||
private fun updateMomentFavorite(id: Int, isFavorite: Boolean) {
|
||||
@@ -262,7 +295,7 @@ object SearchViewModel : ViewModel() {
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(
|
||||
isFavorite = isFavorite,
|
||||
favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1
|
||||
favoriteCount = (momentItem.favoriteCount + if (isFavorite) 1 else -1).coerceAtLeast(0)
|
||||
)
|
||||
} else {
|
||||
momentItem
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -25,10 +26,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
@@ -56,6 +54,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -319,9 +318,9 @@ private fun SingleVideoItemContent(
|
||||
isPageVisible: Boolean = true
|
||||
) {
|
||||
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
|
||||
val pauseIconVisibleState = remember(pager) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val pauseIconVisibleState = remember(pager) { mutableStateOf(false) }
|
||||
// 记录进入后台前是否在播放,用于决定是否需要自动恢复播放
|
||||
val shouldResumeAfterLifecyclePause = remember(pager) { mutableStateOf(false) }
|
||||
|
||||
// 当页面切换时,重置暂停状态
|
||||
LaunchedEffect(pager, pagerState.currentPage) {
|
||||
@@ -341,6 +340,7 @@ private fun SingleVideoItemContent(
|
||||
pagerState = pagerState,
|
||||
pager = pager,
|
||||
pauseIconVisibleState = pauseIconVisibleState,
|
||||
shouldResumeAfterLifecyclePause = shouldResumeAfterLifecyclePause,
|
||||
onLikeClick = onLikeClick,
|
||||
onCommentClick = onCommentClick,
|
||||
onCommentAdded = onCommentAdded,
|
||||
@@ -373,6 +373,7 @@ fun VideoPlayer(
|
||||
pagerState: PagerState,
|
||||
pager: Int,
|
||||
pauseIconVisibleState: MutableState<Boolean>,
|
||||
shouldResumeAfterLifecyclePause: MutableState<Boolean>,
|
||||
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentAdded: ((MomentEntity) -> Unit)? = null,
|
||||
@@ -467,6 +468,9 @@ fun VideoPlayer(
|
||||
.clip(RectangleShape)
|
||||
) {
|
||||
var playerView by remember { mutableStateOf<PlayerView?>(null) }
|
||||
// 防抖:记录上次双击时间,防止快速重复双击
|
||||
val lastDoubleTapTime = remember { mutableStateOf(0L) }
|
||||
val doubleTapDebounceTime = 500L // 500ms 防抖时间
|
||||
|
||||
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
|
||||
androidx.compose.runtime.key(videoUrl) {
|
||||
@@ -479,15 +483,31 @@ fun VideoPlayer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RectangleShape)
|
||||
.noRippleClickable {
|
||||
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
|
||||
.pointerInput(videoUrl, moment?.liked) {
|
||||
detectTapGestures(
|
||||
onDoubleTap = { offset ->
|
||||
// 双击点赞/取消点赞
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
|
||||
lastDoubleTapTime.value = currentTime
|
||||
moment?.let {
|
||||
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
|
||||
onLikeClick?.invoke(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
onTap = {
|
||||
// 单击播放/暂停
|
||||
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (pauseIconVisibleState.value) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
@@ -514,15 +534,21 @@ fun VideoPlayer(
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
// 应用进入后台时暂停
|
||||
shouldResumeAfterLifecyclePause.value = exoPlayer.isPlaying && !pauseIconVisibleState.value
|
||||
exoPlayer.playWhenReady = false
|
||||
exoPlayer.pause()
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
// 返回前台且为当前页面时恢复播放
|
||||
if (pager == pagerState.currentPage) {
|
||||
if (
|
||||
pager == pagerState.currentPage &&
|
||||
isPageVisible &&
|
||||
shouldResumeAfterLifecyclePause.value
|
||||
) {
|
||||
exoPlayer.playWhenReady = true
|
||||
exoPlayer.play()
|
||||
pauseIconVisibleState.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,7 +686,8 @@ fun VideoPlayer(
|
||||
},
|
||||
containerColor = AppColors.background,
|
||||
sheetState = sheetState,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||
dragHandle = {},
|
||||
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -670,6 +697,7 @@ fun VideoPlayer(
|
||||
CommentModalContent(
|
||||
postId = moment.id,
|
||||
commentCount = moment.commentCount,
|
||||
showTitle = false,
|
||||
onCommentAdded = {
|
||||
onCommentAdded?.invoke(moment)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -46,10 +47,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
import java.util.Date
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
||||
@Preview
|
||||
@Composable
|
||||
fun LikeNoticeScreen() {
|
||||
fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
||||
val model = LikeNoticeViewModel
|
||||
val listState = rememberLazyListState()
|
||||
var dataFlow = model.likeItemsFlow
|
||||
@@ -63,7 +64,8 @@ fun LikeNoticeScreen() {
|
||||
|
||||
StatusBarMaskLayout(
|
||||
darkIcons = !AppState.darkMode,
|
||||
maskBoxBackgroundColor = AppColors.background
|
||||
maskBoxBackgroundColor = AppColors.background,
|
||||
includeStatusBarPadding = includeStatusBarPadding
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -75,42 +77,11 @@ fun LikeNoticeScreen() {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||
|
||||
if (!isNetworkAvailable) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.padding(top=149.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(181.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = AppColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = AppColors.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
LikeNoticeViewModel.reload(force = true)
|
||||
}
|
||||
)
|
||||
NetworkErrorContent(
|
||||
onReload = {
|
||||
LikeNoticeViewModel.reload(force = true)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (likes.itemCount == 0) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -184,57 +155,83 @@ fun ActionPostNoticeItem(
|
||||
val navController = LocalNavController.current
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
val actionLabel = when (action) {
|
||||
"favourite" -> stringResource(R.string.favourite_your_post)
|
||||
else -> stringResource(R.string.like_your_post)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp, horizontal = 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = avatar,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
userId.toString()
|
||||
)
|
||||
)
|
||||
},
|
||||
contentDescription = action,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToPost(
|
||||
id = postId,
|
||||
highlightCommentId = 0,
|
||||
initImagePagerIndex = 0
|
||||
)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
imageUrl = avatar,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
userId.toString()
|
||||
)
|
||||
)
|
||||
},
|
||||
contentDescription = action,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.noRippleClickable {
|
||||
navController.navigateToPost(
|
||||
id = postId,
|
||||
highlightCommentId = 0,
|
||||
initImagePagerIndex = 0
|
||||
)
|
||||
}
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
when (action) {
|
||||
"like" -> Text(stringResource(R.string.like_your_post), color = AppColors.text)
|
||||
"favourite" -> Text(stringResource(R.string.favourite_your_post), color = AppColors.text)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row {
|
||||
Text(likeTime.timeAgo(context), fontSize = 12.sp, color = AppColors.secondaryText)
|
||||
Text(
|
||||
text = nickName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = actionLabel,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = likeTime.timeAgo(context),
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
context = context,
|
||||
imageUrl = thumbnail,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentDescription = action,
|
||||
)
|
||||
@@ -249,10 +246,11 @@ fun LikeCommentNoticeItem(
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val AppColors = LocalAppTheme.current
|
||||
val previewPost = item.comment?.replyComment?.post ?: item.comment?.post
|
||||
|
||||
Box(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.padding(vertical = 12.dp)
|
||||
.noRippleClickable {
|
||||
item.comment?.postId.let {
|
||||
navController.navigateToPost(
|
||||
@@ -263,105 +261,103 @@ fun LikeCommentNoticeItem(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = item.user.avatar,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape),
|
||||
contentDescription = stringResource(R.string.like_your_comment)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = item.user.nickName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.like_your_comment),
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = item.likeTime.timeAgo(context),
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
previewPost?.let {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = it.images[0].thumbnail,
|
||||
contentDescription = "Post Thumbnail",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 48.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = AppState.profile?.avatar ?: "",
|
||||
contentDescription = "Comment Profile Picture",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(RoundedCornerShape(24.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = item.user.avatar,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape),
|
||||
contentDescription = stringResource(R.string.like_your_comment)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(item.user.nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(stringResource(R.string.like_your_comment), color = AppColors.text)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row {
|
||||
Text(
|
||||
item.likeTime.timeAgo(context),
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 48.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = AppState.profile?.avatar ?: "",
|
||||
contentDescription = "Comment Profile Picture",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(RoundedCornerShape(24.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = AppState.profile?.nickName ?: "",
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
Text(
|
||||
text = item.comment?.content ?: "",
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText,
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = AppState.profile?.nickName ?: "",
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
Text(
|
||||
text = item.comment?.content ?: "",
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText,
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
if (item.comment?.replyComment?.post != null) {
|
||||
item.comment.replyComment.post.let {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = it.images[0].thumbnail,
|
||||
contentDescription = "Post Thumbnail",
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item.comment?.post?.let {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = it.images[0].thumbnail,
|
||||
contentDescription = "Post Thumbnail",
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
153
app/src/main/java/com/aiosman/ravenow/ui/network/Networkerror.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -89,7 +90,7 @@ fun NotificationScreen() {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
|
||||
.padding(start = 16.dp, top = 8.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
@@ -106,7 +107,7 @@ fun NotificationScreen() {
|
||||
TabSpacer()
|
||||
|
||||
TabItem(
|
||||
text = stringResource(R.string.followers_upper),
|
||||
text = stringResource(R.string.follow_upper),
|
||||
isSelected = pagerState.currentPage == 1,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
@@ -135,9 +136,9 @@ fun NotificationScreen() {
|
||||
.weight(1f)
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> LikeNoticeScreen()
|
||||
1 -> FollowerNoticeScreen()
|
||||
2 -> CommentNoticeScreen()
|
||||
0 -> LikeNoticeScreen(includeStatusBarPadding = false)
|
||||
1 -> FollowerNoticeScreen(includeStatusBarPadding = false)
|
||||
2 -> CommentNoticeScreen(includeStatusBarPadding = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
@@ -56,7 +54,6 @@ import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
@@ -106,33 +103,37 @@ fun PointsBottomSheet(
|
||||
}
|
||||
}
|
||||
|
||||
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮)
|
||||
sheetState = sheetState,
|
||||
containerColor = AppColors.background,
|
||||
dragHandle = null // 移除拖动手柄
|
||||
containerColor = Color.Transparent,
|
||||
dragHandle = null, // 移除拖动手柄
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.95f)
|
||||
.offset(y = offsetY)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp
|
||||
)
|
||||
.padding(top = 10.dp)
|
||||
) {
|
||||
Column(
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
||||
color = AppColors.background,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp
|
||||
)
|
||||
) {
|
||||
// 头部 - 使用 Box 实现绝对居中布局
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -340,6 +341,7 @@ fun PointsBottomSheet(
|
||||
} else {
|
||||
HowToEarnList(onRecharge = onRecharge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,37 +13,30 @@ import kotlinx.coroutines.launch
|
||||
class CommentsViewModel(
|
||||
var postId: String = 0.toString(),
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
private const val ORDER_ALL = "all"
|
||||
private const val COMMENTS_PAGE_SIZE = 50
|
||||
}
|
||||
|
||||
var commentService: CommentService = CommentServiceImpl()
|
||||
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||
var order: String by mutableStateOf("like")
|
||||
var order: String by mutableStateOf(ORDER_ALL)
|
||||
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
|
||||
var highlightCommentId by mutableStateOf<Int?>(null)
|
||||
var highlightComment by mutableStateOf<CommentEntity?>(null)
|
||||
var isLoading by mutableStateOf(false)
|
||||
var hasError by mutableStateOf(false)
|
||||
var isLoadingMore by mutableStateOf(false)
|
||||
var hasMore by mutableStateOf(false)
|
||||
private var currentPage by mutableStateOf(0)
|
||||
private var totalComments by mutableStateOf(0)
|
||||
|
||||
/**
|
||||
* 预加载,在跳转到 PostScreen 之前设置好内容
|
||||
*/
|
||||
fun preTransit() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
isLoading = true
|
||||
val response = commentService.getComments(
|
||||
pageNumber = 1,
|
||||
postId = postId.toInt(),
|
||||
pageSize = 10
|
||||
)
|
||||
commentsList = response.list
|
||||
hasError = false
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
hasError = true
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
reloadComment()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,25 +44,61 @@ class CommentsViewModel(
|
||||
*/
|
||||
fun reloadComment() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
loadComments(page = 1, reset = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMoreComments() {
|
||||
if (isLoading || isLoadingMore || !hasMore) {
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
loadComments(page = currentPage + 1, reset = false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadComments(page: Int, reset: Boolean) {
|
||||
try {
|
||||
if (reset) {
|
||||
isLoading = true
|
||||
val response = commentService.getComments(
|
||||
pageNumber = 1,
|
||||
postId = postId.toInt(),
|
||||
order = order,
|
||||
pageSize = 50
|
||||
)
|
||||
commentsList = response.list
|
||||
hasError = false
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} else {
|
||||
isLoadingMore = true
|
||||
}
|
||||
val response = commentService.getComments(
|
||||
pageNumber = page,
|
||||
postId = postId.toInt(),
|
||||
order = normalizeOrder(order),
|
||||
pageSize = COMMENTS_PAGE_SIZE
|
||||
)
|
||||
val total = response.total.coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
|
||||
totalComments = total
|
||||
currentPage = response.page
|
||||
commentsList = if (reset) {
|
||||
response.list
|
||||
} else {
|
||||
commentsList + response.list
|
||||
}
|
||||
hasMore = commentsList.size < totalComments
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
if (reset) {
|
||||
hasError = true
|
||||
} finally {
|
||||
commentsList = emptyList()
|
||||
}
|
||||
} finally {
|
||||
if (reset) {
|
||||
isLoading = false
|
||||
} else {
|
||||
isLoadingMore = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeOrder(currentOrder: String): String? {
|
||||
return currentOrder.takeUnless { it.equals(ORDER_ALL, ignoreCase = true) }
|
||||
}
|
||||
|
||||
|
||||
suspend fun highlightComment(commentId: Int) {
|
||||
highlightCommentId = commentId
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -34,6 +35,7 @@ import android.graphics.BitmapFactory
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -78,92 +80,108 @@ fun DraftBoxBottomSheet(
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = AppColors.background,
|
||||
containerColor = Color.Transparent,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
dragHandle = {}
|
||||
) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.9f)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
// 标题
|
||||
Box(
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
||||
color = AppColors.background,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.drafts),
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
|
||||
// 草稿列表
|
||||
if (drafts.isEmpty()) {
|
||||
Box(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_drafts),
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
itemsIndexed(drafts) { index, draft ->
|
||||
DraftItem(
|
||||
draft = draft,
|
||||
dateFormat = dateFormat,
|
||||
onEditClick = {
|
||||
model.viewModelScope.launch {
|
||||
model.loadDraft(context, draft)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
onDeleteClick = {
|
||||
draftStore.deleteDraft(index)
|
||||
drafts = draftStore.getAllDrafts()
|
||||
},
|
||||
AppColors = AppColors,
|
||||
context = context
|
||||
// 标题
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.drafts),
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
// 在草稿项之间添加分割线(最后一个不添加)
|
||||
if (index < drafts.size - 1) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider(
|
||||
color = AppColors.secondaryText.copy(alpha = 0.2f),
|
||||
thickness = 0.5.dp,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
}
|
||||
|
||||
// 草稿列表
|
||||
if (drafts.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_drafts),
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
itemsIndexed(drafts) { index, draft ->
|
||||
DraftItem(
|
||||
draft = draft,
|
||||
dateFormat = dateFormat,
|
||||
onEditClick = {
|
||||
model.viewModelScope.launch {
|
||||
model.loadDraft(context, draft)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
onDeleteClick = {
|
||||
draftStore.deleteDraft(index)
|
||||
drafts = draftStore.getAllDrafts()
|
||||
},
|
||||
AppColors = AppColors,
|
||||
context = context
|
||||
)
|
||||
// 在草稿项之间添加分割线(最后一个不添加)
|
||||
if (index < drafts.size - 1) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider(
|
||||
color = AppColors.secondaryText.copy(alpha = 0.2f),
|
||||
thickness = 0.5.dp,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部提示
|
||||
Text(
|
||||
text = stringResource(R.string.only_save_the_last_5_drafts),
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 底部提示
|
||||
Text(
|
||||
text = stringResource(R.string.only_save_the_last_5_drafts),
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
@@ -588,6 +591,34 @@ fun AddImageGrid() {
|
||||
}
|
||||
}
|
||||
|
||||
// 摄像头权限请求
|
||||
val requestCameraPermissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
// 权限已授予,打开相机
|
||||
if (model.imageList.size < 9) {
|
||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
||||
val photoUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
photoFile
|
||||
)
|
||||
model.currentPhotoUri = photoUri
|
||||
takePictureLauncher.launch(photoUri)
|
||||
} else {
|
||||
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
// 权限被拒绝,提示用户
|
||||
Toast.makeText(
|
||||
context,
|
||||
"需要摄像头权限才能拍摄照片,请在设置中开启",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
val addImageDebouncer = rememberDebouncer()
|
||||
val canAddMoreImages = model.imageList.size < 9
|
||||
|
||||
@@ -642,14 +673,27 @@ fun AddImageGrid() {
|
||||
.background(Color(0xFFFAF9FB))
|
||||
.noRippleClickable {
|
||||
if (model.imageList.size < 9) {
|
||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
||||
val photoUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
photoFile
|
||||
)
|
||||
model.currentPhotoUri = photoUri
|
||||
takePictureLauncher.launch(photoUri)
|
||||
// 检查摄像头权限
|
||||
when {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED -> {
|
||||
// 已有权限,直接打开相机
|
||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
||||
val photoUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
photoFile
|
||||
)
|
||||
model.currentPhotoUri = photoUri
|
||||
takePictureLauncher.launch(photoUri)
|
||||
}
|
||||
else -> {
|
||||
// 没有权限,请求权限
|
||||
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
@@ -24,6 +26,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -44,6 +47,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -300,20 +304,35 @@ fun PostScreen(
|
||||
onDismissRequest = {
|
||||
showReportDialog = false
|
||||
},
|
||||
containerColor = AppColors.background,
|
||||
containerColor = Color.Transparent,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
dragHandle = {},
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
|
||||
) {
|
||||
ReportModal(
|
||||
momentId = viewModel.moment!!.id,
|
||||
onClose = {
|
||||
showReportDialog = false
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 7.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
color = AppColors.background,
|
||||
contentColor = AppColors.text
|
||||
) {
|
||||
ReportModal(
|
||||
momentId = viewModel.moment!!.id,
|
||||
onClose = {
|
||||
showReportDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
@@ -493,7 +512,9 @@ fun PostScreen(
|
||||
color = AppColors.nonActiveText
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
OrderSelectionComponent() {
|
||||
OrderSelectionComponent(
|
||||
selectedOrder = commentsViewModel.order
|
||||
) {
|
||||
commentsViewModel.order = it
|
||||
viewModel.reloadComment()
|
||||
}
|
||||
@@ -740,6 +761,33 @@ fun CommentContent(
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.isLoadingMore || viewModel.hasMore) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (viewModel.isLoadingMore) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = AppColors.main,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(id = R.string.load_more),
|
||||
color = AppColors.main,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
viewModel.loadMoreComments()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态处理
|
||||
if (viewModel.isLoading) {
|
||||
Box(
|
||||
@@ -1159,14 +1207,26 @@ fun ImageViewerDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PostImageView(
|
||||
images: List<MomentImageEntity>,
|
||||
initialPage: Int? = 0
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var isImageViewerDialog by remember { mutableStateOf(false) }
|
||||
var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) }
|
||||
val initialPageIndex = initialPage ?: 0
|
||||
val pagerState = rememberPagerState(
|
||||
pageCount = { images.size },
|
||||
initialPage = initialPageIndex.coerceIn(0, maxOf(0, images.size - 1))
|
||||
)
|
||||
var currentImageIndex by remember { mutableStateOf(pagerState.currentPage) }
|
||||
|
||||
// 同步 pagerState 的当前页面到 currentImageIndex
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
currentImageIndex = pagerState.currentPage
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
@@ -1187,23 +1247,31 @@ fun PostImageView(
|
||||
modifier = Modifier
|
||||
) {
|
||||
if (images.isNotEmpty()) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
images[currentImageIndex].thumbnail,
|
||||
contentDescription = "Image",
|
||||
contentScale = ContentScale.Crop,
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
isImageViewerDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
)
|
||||
) { page ->
|
||||
val image = images[page]
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
image.thumbnail,
|
||||
contentDescription = "Image",
|
||||
blurHash = image.blurHash,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
isImageViewerDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 图片导航控件
|
||||
@@ -1212,56 +1280,26 @@ fun PostImageView(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Previous button
|
||||
Text(
|
||||
text = "Previous",
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.noRippleClickable {
|
||||
if (currentImageIndex > 0) {
|
||||
currentImageIndex--
|
||||
}
|
||||
},
|
||||
color = if (currentImageIndex > 0) Color.Blue else Color.Gray
|
||||
)
|
||||
|
||||
// Indicators
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
images.forEachIndexed { index, _ ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
|
||||
alpha = 0.5f
|
||||
)
|
||||
images.forEachIndexed { index, _ ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
|
||||
alpha = 0.5f
|
||||
)
|
||||
.padding(4.dp)
|
||||
)
|
||||
if (index < images.size - 1) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
)
|
||||
.padding(4.dp)
|
||||
)
|
||||
if (index < images.size - 1) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Next button
|
||||
Text(
|
||||
text = "Next",
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.noRippleClickable {
|
||||
if (currentImageIndex < images.size - 1) {
|
||||
currentImageIndex++
|
||||
}
|
||||
},
|
||||
color = if (currentImageIndex < images.size - 1) Color.Blue else Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1917,15 +1955,15 @@ fun CommentMenuModal(
|
||||
|
||||
@Composable
|
||||
fun OrderSelectionComponent(
|
||||
selectedOrder: String,
|
||||
onSelected: (String) -> Unit = {}
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
var selectedOrder by remember { mutableStateOf("like") }
|
||||
val orders = listOf(
|
||||
"like" to stringResource(R.string.order_comment_default),
|
||||
"earliest" to stringResource(R.string.order_comment_earliest),
|
||||
"latest" to stringResource(R.string.order_comment_latest)
|
||||
"all" to stringResource(R.string.order_comment_default),
|
||||
"latest" to stringResource(R.string.order_comment_latest),
|
||||
"like" to stringResource(R.string.order_comment_hot)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -1943,8 +1981,9 @@ fun OrderSelectionComponent(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.noRippleClickable {
|
||||
selectedOrder = order.first
|
||||
onSelected(order.first)
|
||||
if (selectedOrder != order.first) {
|
||||
onSelected(order.first)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
if (
|
||||
@@ -2011,8 +2050,8 @@ fun ReportModal(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.background)
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp)
|
||||
.fillMaxHeight()
|
||||
.padding(start = 24.dp, end = 24.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp),
|
||||
|
||||
BIN
app/src/main/res/mipmap-hdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 1006 B |
BIN
app/src/main/res/mipmap-hdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 866 B |
BIN
app/src/main/res/mipmap-hdpi/dt_ts_sp_bf_btn.png
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
app/src/main/res/mipmap-hdpi/mbti_enfj.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_enfp.png
Normal file
|
After Width: | Height: | Size: 935 B |
BIN
app/src/main/res/mipmap-hdpi/mbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_entp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_esfj.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_esfp.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_estj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_estp.png
Normal file
|
After Width: | Height: | Size: 886 B |
BIN
app/src/main/res/mipmap-hdpi/mbti_infj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_infp.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_intj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_intp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_isfj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_isfp.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_istj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_istp.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 829 B |
BIN
app/src/main/res/mipmap-mdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
app/src/main/res/mipmap-mdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 656 B |
BIN
app/src/main/res/mipmap-mdpi/dt_ts_sp_bf_btn.png
Normal file
|
After Width: | Height: | Size: 618 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_enfj.png
Normal file
|
After Width: | Height: | Size: 916 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_enfp.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_entj.png
Normal file
|
After Width: | Height: | Size: 882 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_entp.png
Normal file
|
After Width: | Height: | Size: 800 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_esfj.png
Normal file
|
After Width: | Height: | Size: 860 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_esfp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/mbti_estj.png
Normal file
|
After Width: | Height: | Size: 864 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_estp.png
Normal file
|
After Width: | Height: | Size: 675 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_infj.png
Normal file
|
After Width: | Height: | Size: 919 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_infp.png
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_intj.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_intp.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_isfj.png
Normal file
|
After Width: | Height: | Size: 852 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_isfp.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/mbti_istj.png
Normal file
|
After Width: | Height: | Size: 793 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_istp.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
app/src/main/res/mipmap-xhdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/mipmap-xhdpi/dt_ts_sp_bf_btn.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/mipmap-xhdpi/iconsgallery.png
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
app/src/main/res/mipmap-xhdpi/iconslogout.png
Normal file
|
After Width: | Height: | Size: 259 B |