Merge branch 'main' into zhong_1

This commit is contained in:
2025-11-11 18:49:50 +08:00
34 changed files with 594 additions and 152 deletions

View File

@@ -1,21 +1,22 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.compose.compiler)
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("com.google.firebase.firebase-perf")
id("org.jetbrains.kotlin.kapt")
id("com.google.devtools.ksp") version "1.9.10-1.0.13"
alias(libs.plugins.ksp)
}
android {
namespace = "com.aiosman.ravenow"
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "com.aiosman.ravenow"
minSdk = 24
targetSdk = 34
targetSdk = 35
versionCode = 1000019
versionName = "1.0.000.19"
@@ -46,19 +47,16 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -99,11 +97,13 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.androidx.animation)
implementation(libs.coil.compose)
implementation(libs.coil)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
implementation(libs.play.services.auth)
implementation(libs.kotlin.faker)
implementation(libs.androidx.material)
implementation(libs.androidx.material.icons.extended)
implementation(libs.zoomable)
implementation(libs.retrofit)
implementation(libs.converter.gson)

View File

@@ -7,11 +7,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil3.ImageLoader
import coil3.compose.rememberAsyncImagePainter
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.request.ImageRequest
import coil3.request.crossfade
import okio.Path.Companion.toPath
data class ImageItem(val url: String)
@@ -53,14 +55,15 @@ fun ImageItem(item: ImageItem, imageLoader: ImageLoader, context: Context) { //
fun getImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(context)
.memoryCache {
MemoryCache.Builder(context)
.maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25%
MemoryCache.Builder()
.maxSizePercent(context,0.25) // 设置内存缓存大小为可用内存的 25%
.build()
}
.diskCache {
val cacheDir = context.cacheDir.resolve("image_cache")
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2%
.directory(cacheDir.absolutePath.toPath())
.maxSizeBytes(250L * 1024 * 1024) // 250MB
.build()
}
.build()

View File

@@ -73,7 +73,7 @@ data class Profile(
@SerializedName("nickname")
val nickname: String,
@SerializedName("trtcUserId")
val trtcUserId: String,
val trtcUserId: String? = null,
@SerializedName("username")
val username: String
){
@@ -85,7 +85,7 @@ data class Profile(
avatar = "${ApiClient.BASE_SERVER}$avatar",
bio = bio,
banner = "${ApiClient.BASE_SERVER}$banner",
trtcUserId = trtcUserId,
trtcUserId = trtcUserId ?: "",
chatAIId = chatAIId,
aiAccount = aiAccount
)

View File

@@ -26,6 +26,22 @@ import com.aiosman.ravenow.entity.RoomRuleQuotaEntity
import com.aiosman.ravenow.entity.UsersEntity
import com.google.gson.annotations.SerializedName
/**
* 房间内的智能体信息PromptTemplate
*/
data class PromptTemplate(
@SerializedName("id")
val id: Int,
@SerializedName("openId")
val openId: String,
@SerializedName("title")
val title: String,
@SerializedName("desc")
val desc: String,
@SerializedName("avatar")
val avatar: String
)
data class Room(
@SerializedName("id")
val id: Int,
@@ -51,12 +67,26 @@ data class Room(
val creator: Creator,
@SerializedName("userCount")
val userCount: Int,
@SerializedName("totalMemberCount")
val totalMemberCount: Int? = null,
@SerializedName("maxMemberLimit")
val maxMemberLimit: Int,
@SerializedName("maxTotal")
val maxTotal: Int? = null,
@SerializedName("systemMaxTotal")
val systemMaxTotal: Int? = null,
@SerializedName("canJoin")
val canJoin: Boolean,
@SerializedName("canJoinCode")
val canJoinCode: Int,
@SerializedName("privateFeePaid")
val privateFeePaid: Boolean? = null,
@SerializedName("prompts")
val prompts: List<PromptTemplate>? = null,
@SerializedName("createdAt")
val createdAt: String? = null,
@SerializedName("updatedAt")
val updatedAt: String? = null,
@SerializedName("users")
val users: List<Users>
@@ -75,9 +105,24 @@ data class Room(
allowInHot = allowInHot,
creator = creator.toCreatorEntity(),
userCount = userCount,
totalMemberCount = totalMemberCount,
maxMemberLimit = maxMemberLimit,
maxTotal = maxTotal,
systemMaxTotal = systemMaxTotal,
canJoin = canJoin,
canJoinCode = canJoinCode,
privateFeePaid = privateFeePaid ?: false,
prompts = prompts?.map {
com.aiosman.ravenow.entity.PromptTemplateEntity(
id = it.id,
openId = it.openId,
title = it.title,
desc = it.desc,
avatar = it.avatar
)
} ?: emptyList(),
createdAt = createdAt,
updatedAt = updatedAt,
users = users.map { it.toUsersEntity() }
)
}
@@ -90,7 +135,7 @@ data class Creator(
@SerializedName("userId")
val userId: String,
@SerializedName("trtcUserId")
val trtcUserId: String,
val trtcUserId: String? = null,
@SerializedName("profile")
val profile: Profile
){
@@ -98,7 +143,7 @@ data class Creator(
return CreatorEntity(
id = id,
userId = userId,
trtcUserId = trtcUserId,
trtcUserId = trtcUserId ?: "",
profile = profile.toProfileEntity()
)
}
@@ -110,7 +155,7 @@ data class Users(
@SerializedName("userId")
val userId: String,
@SerializedName("trtcUserId")
val trtcUserId: String,
val trtcUserId: String? = null,
@SerializedName("profile")
val profile: Profile
){

View File

@@ -1423,11 +1423,39 @@ interface RaveNowAPI {
@POST("outside/rooms")
suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response<DataContainer<Unit>>
/**
* 获取房间列表
*
* 支持游客和认证用户访问,根据用户类型返回不同的房间数据。
* 游客模式返回公开推荐房间列表,认证用户模式返回用户可访问的群聊列表。
*
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 20游客模式最大50
* @param roomId 房间ID用于精确查询特定房间仅认证用户
* @param includeUsers 是否包含用户列表默认false仅认证用户
* @param isRecommended 是否推荐过滤器1=推荐0=非推荐null=不过滤(仅认证用户)
* @param roomType 房间类型过滤all=公有私有都显示, public=只显示公有, private=只显示私有, created=只显示自己创建的, joined=只显示自己加入的(仅认证用户)
* @param search 搜索关键字,支持房间名称、描述、智能体名称模糊匹配
* @param random 是否随机排序字符串长度不为0则为true传任意非空字符串即可
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
* @param showPublic 是否显示公有房间只有明确设置为true时才生效优先级高于roomType仅认证用户
* @param showCreated 是否显示自己创建的房间只有明确设置为true时才生效优先级高于roomType仅认证用户
* @param showJoined 是否显示自己加入的房间只有明确设置为true时才生效优先级高于roomType仅认证用户
*/
@GET("outside/rooms")
suspend fun getRooms(@Query("page") page: Int = 1,
suspend fun getRooms(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("isRecommended") isRecommended: Int = 1,
@Query("random") random: Int? = null,
@Query("roomId") roomId: Long? = null,
@Query("includeUsers") includeUsers: Boolean? = null,
@Query("isRecommended") isRecommended: Int? = null,
@Query("roomType") roomType: String? = null,
@Query("search") search: String? = null,
@Query("random") random: String? = null,
@Query("ownerSessionId") ownerSessionId: String? = null,
@Query("showPublic") showPublic: Boolean? = null,
@Query("showCreated") showCreated: Boolean? = null,
@Query("showJoined") showJoined: Boolean? = null,
): Response<ListContainer<Room>>
@GET("outside/rooms/detail")

View File

@@ -8,6 +8,17 @@ import com.aiosman.ravenow.data.api.ApiClient
* 群聊房间
*/
/**
* 房间内的智能体信息实体
*/
data class PromptTemplateEntity(
val id: Int,
val openId: String,
val title: String,
val desc: String,
val avatar: String
)
data class RoomEntity(
val id: Int,
val name: String,
@@ -21,9 +32,16 @@ data class RoomEntity(
val allowInHot: Boolean,
val creator: CreatorEntity,
val userCount: Int,
val totalMemberCount: Int? = null,
val maxMemberLimit: Int,
val maxTotal: Int? = null,
val systemMaxTotal: Int? = null,
val canJoin: Boolean,
val canJoinCode: Int,
val privateFeePaid: Boolean = false,
val prompts: List<PromptTemplateEntity> = emptyList(),
val createdAt: String? = null,
val updatedAt: String? = null,
val users: List<UsersEntity>,
)

View File

@@ -71,7 +71,7 @@ fun AboutScreen() {
Spacer(modifier = Modifier.height(16.dp))
// app version
Text(
text = stringResource(R.string.version_text, versionText),
text = stringResource(R.string.version_text, versionText ?: ""),
fontSize = 16.sp,
color = appColors.secondaryText,
fontWeight = FontWeight.Normal

View File

@@ -118,8 +118,7 @@ fun CommentModalContent(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
CommentMenuModal(
onDeleteClick = {

View File

@@ -8,9 +8,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toDrawable
import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.request.fallback
import coil3.request.placeholder
import com.aiosman.ravenow.utils.BlurHashDecoder
import com.aiosman.ravenow.utils.Utils.getImageLoader

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@@ -68,7 +69,6 @@ fun Modifier.debouncedClickableWithRipple(
clickable(
enabled = enabled && isClickable,
interactionSource = remember { MutableInteractionSource() },
indication = androidx.compose.material.ripple.rememberRipple()
) {
if (isClickable) {
isClickable = false

View File

@@ -123,7 +123,7 @@ fun LazyGridItemScope.DraggableItem(
translationY = dragDropState.previousItemOffset.value.y
}
} else {
Modifier.animateItemPlacement()
Modifier
}
Box(modifier = modifier.then(draggingModifier).clip(RoundedCornerShape(8.dp)), propagateMinConstraints = true) {
content(dragging)

View File

@@ -16,11 +16,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil3.ImageLoader
import coil3.asDrawable
import coil3.asImage
import coil3.compose.AsyncImage
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.crossfade
import com.aiosman.ravenow.utils.Utils.getImageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -59,7 +64,11 @@ fun rememberImageBitmap(imageUrl: String, imageLoader: ImageLoader): Bitmap? {
.build()
val result = withContext(Dispatchers.IO) {
(imageLoader.execute(request) as? SuccessResult)?.drawable?.toBitmap()
val successResult = imageLoader.execute(request) as? SuccessResult
successResult?.let {
val drawable = it.image.asDrawable(context.resources)
drawable.toBitmap()
}
}
bitmap = result
@@ -138,25 +147,33 @@ fun CustomAsyncImage(
}
// 处理字符串URL
val ctx = context ?: localContext
val placeholderImage = remember(placeholderRes, ctx) {
placeholderRes?.let { resId ->
ContextCompat.getDrawable(ctx, resId)?.asImage()
}
}
val errorImage = remember(errorRes, ctx) {
errorRes?.let { resId ->
ContextCompat.getDrawable(ctx, resId)?.asImage()
}
}
if (showShimmer) {
var isLoading by remember { mutableStateOf(true) }
Box(modifier = modifier) {
AsyncImage(
model = ImageRequest.Builder(context ?: localContext)
model = ImageRequest.Builder(ctx)
.data(imageUrl)
.crossfade(200)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.apply {
// 设置占位符图片
if (placeholderRes != null) {
placeholder(placeholderRes)
}
placeholderImage?.let { placeholder(it) }
// 设置错误时显示的图片
if (errorRes != null) {
error(errorRes)
}
errorImage?.let { error(it) }
}
.build(),
contentDescription = contentDescription,
@@ -177,20 +194,16 @@ fun CustomAsyncImage(
}
} else {
AsyncImage(
model = ImageRequest.Builder(context ?: localContext)
model = ImageRequest.Builder(ctx)
.data(imageUrl)
.crossfade(200)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.apply {
// 设置占位符图片
if (placeholderRes != null) {
placeholder(placeholderRes)
}
placeholderImage?.let { placeholder(it) }
// 设置错误时显示的图片
if (errorRes != null) {
error(errorRes)
}
errorImage?.let { error(it) }
}
.build(),
contentDescription = contentDescription,

View File

@@ -515,7 +515,6 @@ fun MomentBottomOperateRowGroup(
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
windowInsets = WindowInsets(0),
dragHandle = {
Box(
modifier = Modifier

View File

@@ -71,7 +71,6 @@ fun PolicyCheckbox(
showModal = false
},
sheetState = modalSheetState,
windowInsets = WindowInsets(0),
containerColor = Color.White,
) {
WebViewDisplay(

View File

@@ -47,7 +47,6 @@ fun CreateBottomSheet(
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
windowInsets = BottomSheetDefaults.windowInsets,
containerColor = appColors.background,
dragHandle = null,
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)

View File

@@ -265,7 +265,6 @@ fun IndexScreen() {
modifier = Modifier
.background(AppColors.background)
.padding(0.dp),
beyondBoundsPageCount = 4,
userScrollEnabled = false
) { page ->
when (page) {

View File

@@ -54,7 +54,6 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.GuestLoginCheckOut
@@ -115,7 +114,7 @@ fun Agent() {
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
val viewModel: AgentViewModel = viewModel()
val viewModel: AgentViewModel = AgentViewModel
// 确保推荐Agent数据已加载
LaunchedEffect(Unit) {
@@ -183,7 +182,6 @@ fun Agent() {
colors = TopAppBarDefaults.topAppBarColors(
containerColor = AppColors.background
),
windowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
.padding(top = statusBarPaddingValues.calculateTopPadding())
@@ -902,7 +900,7 @@ fun ChatRoomCard(
) {
val AppColors = LocalAppTheme.current
val cardSize = 180.dp
val viewModel: AgentViewModel = viewModel()
val viewModel: AgentViewModel = AgentViewModel
val context = LocalContext.current
// 防抖状态

View File

@@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() {
page = 1,
pageSize = 20,
isRecommended = 1,
random = 1
random = "1"
)
if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList()

View File

@@ -156,10 +156,10 @@ object HotAgentViewModel : ViewModel() {
try {
// 预加载头像图片到缓存
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
coil3.request.ImageRequest.Builder(context)
.data(agent.avatar)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.build()
)
preloadedImageIds.add(agent.id)

View File

@@ -841,20 +841,20 @@ fun Explore() {
if (bannerItem.backgroundImageUrl.isNotEmpty()) {
// 预加载背景图片
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
coil3.request.ImageRequest.Builder(context)
.data(bannerItem.backgroundImageUrl)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.build()
)
}
if (bannerItem.imageUrl.isNotEmpty()) {
// 预加载头像图片
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
coil3.request.ImageRequest.Builder(context)
.data(bannerItem.imageUrl)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.build()
)
}

View File

@@ -59,7 +59,6 @@ fun FullArticleModal(
.height(sheetHeight),
containerColor = appColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
Column(
modifier = Modifier

View File

@@ -154,7 +154,6 @@ fun NewsCommentModal(
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
CommentMenuModal(
onDeleteClick = {

View File

@@ -194,7 +194,6 @@ fun NewsScreen() {
.height(sheetHeight),
containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
NewsCommentModal(
postId = selectedMoment?.id,

View File

@@ -23,6 +23,10 @@ import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentLoader
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.event.FollowChangeEvent
import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
@@ -40,6 +44,14 @@ object MyProfileViewModel : ViewModel() {
var profile by mutableStateOf<AccountProfileEntity?>(null)
var moments by mutableStateOf<List<MomentEntity>>(emptyList())
var agents by mutableStateOf<List<AgentEntity>>(emptyList())
var rooms by mutableStateOf<List<RoomEntity>>(emptyList())
var roomsLoading by mutableStateOf(false)
var roomsRefreshing by mutableStateOf(false)
var roomsCurrentPage by mutableStateOf(1)
var roomsHasMore by mutableStateOf(true)
private val roomsPageSize = 20
private val apiClient: RaveNowAPI = ApiClient.api
val momentLoader: MomentLoader = MomentLoader().apply {
pageSize = 20 // 设置与后端一致的页面大小
onListChanged = {
@@ -254,4 +266,115 @@ object MyProfileViewModel : ViewModel() {
fun onFollowChangeEvent(event: FollowChangeEvent) {
momentLoader.updateFollowStatus(event.userId, event.isFollow)
}
/**
* 加载房间列表
* @param filterType 筛选类型0=全部1=公开2=私有
* @param pullRefresh 是否下拉刷新
*/
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false) {
// 游客模式下不加载房间列表
if (AppStore.isGuest) {
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
return
}
if (roomsLoading && !pullRefresh) return
viewModelScope.launch {
try {
roomsLoading = true
roomsRefreshing = pullRefresh
val currentPage = if (pullRefresh) {
roomsCurrentPage = 1
roomsHasMore = true
1
} else {
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
)
}
}
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)
} finally {
roomsLoading = false
roomsRefreshing = false
}
}
}
/**
* 加载更多房间
* @param filterType 筛选类型0=全部1=公开2=私有
*/
fun loadMoreRooms(filterType: Int = 0) {
if (roomsLoading || !roomsHasMore) return
loadRooms(filterType = filterType, pullRefresh = false)
}
/**
* 刷新房间列表
* @param filterType 筛选类型0=全部1=公开2=私有
*/
fun refreshRooms(filterType: Int = 0) {
rooms = emptyList()
roomsCurrentPage = 1
roomsHasMore = true
loadRooms(filterType = filterType, pullRefresh = true)
}
}

View File

@@ -258,7 +258,6 @@ fun ProfileV3(
sheetState = agentMenuModalState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
AgentMenuModal(
agent = contextAgent,
@@ -535,7 +534,7 @@ fun ProfileV3(
containerColor = Color.Transparent, // 设置容器背景透明
contentColor = Color.Transparent, // 设置内容背景透明
dragHandle = null, // 移除拖拽手柄
windowInsets = androidx.compose.foundation.layout.WindowInsets(0) // 移除窗口边距
contentWindowInsets = {androidx.compose.foundation.layout.WindowInsets(0)},
) {
Box(
modifier = Modifier
@@ -567,7 +566,7 @@ fun ProfileV3(
containerColor = Color.Transparent,
contentColor = Color.Transparent,
dragHandle = null,
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
contentWindowInsets = { androidx.compose.foundation.layout.WindowInsets(0) }
) {
Box(
modifier = Modifier

View File

@@ -14,17 +14,28 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
@@ -32,13 +43,46 @@ 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.ConstVars
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.AppStore
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun GroupChatEmptyContent() {
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 state = rememberPullRefreshState(
refreshing = viewModel.roomsRefreshing,
onRefresh = {
viewModel.refreshRooms(filterType = selectedSegment)
}
)
// 当分段改变时,重新加载数据
LaunchedEffect(selectedSegment) {
// 切换分段时重新加载
viewModel.refreshRooms(filterType = selectedSegment)
}
// 初始加载
LaunchedEffect(Unit) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
viewModel.loadRooms(filterType = selectedSegment)
}
}
Column(
modifier = Modifier
@@ -50,12 +94,21 @@ fun GroupChatEmptyContent() {
// 分段控制器
SegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = { selectedSegment = it },
onSegmentSelected = {
selectedSegment = it
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state)
) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
// 空状态内容(居中)
Column(
modifier = Modifier.fillMaxWidth(),
@@ -78,6 +131,152 @@ fun GroupChatEmptyContent() {
overflow = TextOverflow.Ellipsis
)
}
} else {
LazyColumn(
modifier = Modifier.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
)
}
)
if (index < viewModel.rooms.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
}
}
// 加载更多指示器
if (viewModel.roomsLoading && viewModel.rooms.isNotEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main
)
}
}
}
// 加载更多触发
if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
item {
LaunchedEffect(Unit) {
viewModel.loadMoreRooms(filterType = selectedSegment)
}
}
}
}
}
PullRefreshIndicator(
refreshing = viewModel.roomsRefreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}
@Composable
fun RoomItem(
room: RoomEntity,
onRoomClick: (RoomEntity) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val roomDebouncer = rememberDebouncer()
// 构建头像URL
val avatarUrl = if (room.avatar.isNotEmpty()) {
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
} else {
""
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.noRippleClickable {
roomDebouncer {
onRoomClick(room)
}
}
) {
Box {
CustomAsyncImage(
context = context,
imageUrl = avatarUrl,
contentDescription = room.name,
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(12.dp))
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = room.name,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = room.description.ifEmpty { "暂无描述" },
fontSize = 14.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${room.userCount}",
fontSize = 12.sp,
color = AppColors.secondaryText
)
}
}
}
}

View File

@@ -114,11 +114,14 @@ fun getFeedItems(): List<FeedItem> {
@Composable
fun LocationDetailScreen(x: Float, y: Float) {
val scope = rememberCoroutineScope()
val density = LocalDensity.current
val scaffoldState = rememberBottomSheetScaffoldState(
SheetState(
bottomSheetState = SheetState(
skipPartiallyExpanded = false,
density = LocalDensity.current, initialValue = SheetValue.PartiallyExpanded,
skipHiddenState = true
initialValue = SheetValue.PartiallyExpanded,
skipHiddenState = true,
positionalThreshold = { 0.5f },
velocityThreshold = { with(density) { 125.dp.toPx() } }
)
)
val configuration = LocalConfiguration.current

View File

@@ -22,6 +22,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@@ -30,7 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -67,7 +68,7 @@ fun NewPostImageGridScreen() {
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "back",
modifier = Modifier
.size(24.dp)
@@ -84,7 +85,7 @@ fun NewPostImageGridScreen() {
fontSize = 18.sp,
)
Icon(
Icons.Default.Delete,
Icons.Filled.Delete,
contentDescription = "delete",
modifier = Modifier
.size(24.dp)

View File

@@ -173,7 +173,6 @@ fun PostScreen(
sheetState = commentModalState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
CommentMenuModal(
onDeleteClick = {
@@ -262,7 +261,6 @@ fun PostScreen(
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
EditCommentBottomModal(replyComment) {
viewModel.viewModelScope.launch {
@@ -849,7 +847,6 @@ fun Header(
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
PostMenuModal(

View File

@@ -4,14 +4,16 @@ import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import coil.request.ImageRequest
import coil.request.SuccessResult
import androidx.core.graphics.drawable.toBitmap
import coil3.asDrawable
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.allowHardware
import com.aiosman.ravenow.utils.Utils.getImageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -30,8 +32,9 @@ object FileUtil {
.allowHardware(false) // Disable hardware bitmaps.
.build()
val result = (loader.execute(request) as SuccessResult).drawable
val bitmap = (result as BitmapDrawable).bitmap
val result = loader.execute(request) as? SuccessResult ?: return
val drawable = result.image.asDrawable(context.resources)
val bitmap = drawable.toBitmap()
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "image_${System.currentTimeMillis()}.jpg")

View File

@@ -4,8 +4,9 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import coil.ImageLoader
import coil.request.CachePolicy
import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.CachePolicy
import com.aiosman.ravenow.data.api.AuthInterceptor
import com.aiosman.ravenow.data.api.getSafeOkHttpClient
import java.io.File
@@ -32,7 +33,15 @@ object Utils {
val okHttpClient = getSafeOkHttpClient(authInterceptor = AuthInterceptor())
val loader = ImageLoader.Builder(appContext)
.okHttpClient(okHttpClient)
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = {
okHttpClient
}
)
)
}
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build()

View File

@@ -2,6 +2,7 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.compose.compiler) apply false
id("com.google.gms.google-services") version "4.4.2" apply false
id("com.google.firebase.crashlytics") version "3.0.2" apply false
id("com.google.firebase.firebase-perf") version "1.4.2" apply false

View File

@@ -1,48 +1,51 @@
[versions]
accompanistSystemuicontroller = "0.27.0"
agp = "8.4.0"
animation = "1.7.0-beta05"
coil = "2.7.0"
accompanistSystemuicontroller = "0.34.0"
agp = "8.9.0"
animation = "1.7.6"
coil = "3.3.0"
composeImageBlurhash = "3.0.2"
converterGson = "2.11.0"
imSdk = "3.8.3.2"
imcoreSdk = "3.8.3-patch10"
coreSplashscreen = "1.0.1"
credentialsPlayServicesAuth = "1.2.2"
coreSplashscreen = "1.2.0"
credentialsPlayServicesAuth = "1.0.0-alpha05"
eventbus = "3.3.1"
firebaseBom = "33.2.0"
firebaseBom = "33.7.0"
gson = "2.12.1"
imagecropview = "3.0.1"
jpushGoogle = "5.4.0"
jwtdecode = "2.0.2"
kotlin = "1.9.10"
coreKtx = "1.10.1"
kotlin = "2.2.21"
ksp = "2.2.21-2.0.4"
composeCompiler = "2.2.21"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
kotlinFaker = "2.0.0-rc.5"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.06.00"
lifecycleRuntimeKtxVersion = "2.6.2"
mapsCompose = "4.3.3"
material = "1.6.8"
material3Android = "1.2.1"
media3Exoplayer = "1.3.1"
navigationCompose = "2.7.7"
pagingRuntime = "3.3.0"
activityKtx = "1.9.0"
lifecycleCommonJvm = "2.8.2"
places = "3.3.0"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
kotlinFaker = "2.0.0-rc.11"
lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2"
composeBom = "2025.11.00"
lifecycleRuntimeKtxVersion = "2.8.6"
mapsCompose = "6.1.0"
material = "1.7.6"
materialIconsExtended = "1.7.6"
material3Android = "1.3.1"
media3Exoplayer = "1.4.1"
navigationCompose = "2.8.6"
pagingRuntime = "3.3.6"
activityKtx = "1.9.2"
lifecycleCommonJvm = "2.8.6"
places = "3.4.0"
googleid = "1.1.1"
identityCredential = "20231002"
lifecycleProcess = "2.8.4"
lifecycleProcess = "2.8.6"
playServicesAuth = "21.4.0"
rendering = "1.17.1"
zoomable = "1.6.1"
camerax = "1.3.4"
camerax = "1.4.0"
mlkitBarcode = "17.3.0"
room = "2.6.1"
room = "2.8.3"
[libraries]
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
@@ -53,6 +56,7 @@ androidx-credentials = { module = "androidx.credentials:credentials", version.re
androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" }
androidx-lifecycle-runtime-ktx-v262 = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtxVersion" }
androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Exoplayer" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Exoplayer" }
@@ -62,8 +66,9 @@ androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.r
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
compose-image-blurhash = { module = "com.github.orlando-dev-code:compose-image-blurhash", version.ref = "composeImageBlurhash" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
@@ -79,7 +84,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version = "1.9.0" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
@@ -104,7 +109,7 @@ play-services-auth = { module = "com.google.android.gms:play-services-auth", ver
rendering = { group = "com.google.ar.sceneform", name = "rendering", version.ref = "rendering" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "converterGson" }
zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
lottie = { module="com.airbnb.android:lottie-compose", version="6.6.10"}
lottie = { module="com.airbnb.android:lottie-compose", version="6.7.0"}
mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkitBarcode" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
@@ -112,3 +117,5 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "composeCompiler" }

View File

@@ -1,6 +1,6 @@
#Fri Jun 14 03:23:01 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists