Compare commits
27 Commits
atm
...
feat/pr-20
| Author | SHA1 | Date | |
|---|---|---|---|
| 234587afc9 | |||
| b855acd8d3 | |||
| 391f841f45 | |||
| 2ed5639cbc | |||
| 2cbd2a975f | |||
| 6590b09300 | |||
| 30563a75f3 | |||
| aac3220a69 | |||
| e2de134180 | |||
| 7f1be94896 | |||
| 1c048fd9c0 | |||
| 2613d2e801 | |||
| c100a8ceef | |||
| f86b5e1d39 | |||
| 75eb38b188 | |||
| 4f588483c0 | |||
| 0bc442762d | |||
| 397ac6a9ee | |||
| 784f87dc39 | |||
| e714f567b9 | |||
| 703beb8d43 | |||
| 2b30beb367 | |||
| 6fffa0447e | |||
| 2a9d6a2f6b | |||
| cc12a08472 | |||
| 513897499d | |||
| baa6f284bd |
@@ -19,6 +19,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.RaveNow"
|
android:theme="@style/Theme.RaveNow"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.geo.API_KEY"
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.entity.MomentImageEntity
|
import com.aiosman.ravenow.entity.MomentImageEntity
|
||||||
|
import com.aiosman.ravenow.entity.MomentVideoEntity
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -12,8 +13,12 @@ data class Moment(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
@SerializedName("textContent")
|
@SerializedName("textContent")
|
||||||
val textContent: String,
|
val textContent: String,
|
||||||
|
@SerializedName("url")
|
||||||
|
val url: String? = null,
|
||||||
@SerializedName("images")
|
@SerializedName("images")
|
||||||
val images: List<Image>,
|
val images: List<Image>? = null,
|
||||||
|
@SerializedName("videos")
|
||||||
|
val videos: List<Video>? = null,
|
||||||
@SerializedName("user")
|
@SerializedName("user")
|
||||||
val user: User,
|
val user: User,
|
||||||
@SerializedName("likeCount")
|
@SerializedName("likeCount")
|
||||||
@@ -24,7 +29,7 @@ data class Moment(
|
|||||||
val favoriteCount: Long,
|
val favoriteCount: Long,
|
||||||
@SerializedName("isFavorite")
|
@SerializedName("isFavorite")
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
@SerializedName("shareCount")
|
@SerializedName("isCommented")
|
||||||
val isCommented: Boolean,
|
val isCommented: Boolean,
|
||||||
@SerializedName("commentCount")
|
@SerializedName("commentCount")
|
||||||
val commentCount: Long,
|
val commentCount: Long,
|
||||||
@@ -47,6 +52,14 @@ data class Moment(
|
|||||||
val newsLanguage: String? = null,
|
val newsLanguage: String? = null,
|
||||||
@SerializedName("newsContent")
|
@SerializedName("newsContent")
|
||||||
val newsContent: String? = null,
|
val newsContent: String? = null,
|
||||||
|
@SerializedName("hasFullText")
|
||||||
|
val hasFullText: Boolean = false,
|
||||||
|
@SerializedName("summary")
|
||||||
|
val summary: String? = null,
|
||||||
|
@SerializedName("publishedAt")
|
||||||
|
val publishedAt: String? = null,
|
||||||
|
@SerializedName("imageCached")
|
||||||
|
val imageCached: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun toMomentItem(): MomentEntity {
|
fun toMomentItem(): MomentEntity {
|
||||||
return MomentEntity(
|
return MomentEntity(
|
||||||
@@ -62,7 +75,7 @@ data class Moment(
|
|||||||
commentCount = commentCount.toInt(),
|
commentCount = commentCount.toInt(),
|
||||||
shareCount = 0,
|
shareCount = 0,
|
||||||
favoriteCount = favoriteCount.toInt(),
|
favoriteCount = favoriteCount.toInt(),
|
||||||
images = images.map {
|
images = images?.map {
|
||||||
MomentImageEntity(
|
MomentImageEntity(
|
||||||
url = "${ApiClient.BASE_SERVER}${it.url}",
|
url = "${ApiClient.BASE_SERVER}${it.url}",
|
||||||
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
|
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
|
||||||
@@ -71,10 +84,28 @@ data class Moment(
|
|||||||
width = it.width,
|
width = it.width,
|
||||||
height = it.height
|
height = it.height
|
||||||
)
|
)
|
||||||
},
|
} ?: emptyList(),
|
||||||
authorId = user.id.toInt(),
|
authorId = user.id.toInt(),
|
||||||
liked = isLiked,
|
liked = isLiked,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
url = url,
|
||||||
|
videos = videos?.map {
|
||||||
|
MomentVideoEntity(
|
||||||
|
id = it.id,
|
||||||
|
url = "${ApiClient.BASE_SERVER}${it.url}",
|
||||||
|
originalUrl = it.originalUrl,
|
||||||
|
directUrl = it.directUrl,
|
||||||
|
thumbnailUrl = it.thumbnailUrl?.let { thumb -> "${ApiClient.BASE_SERVER}$thumb" },
|
||||||
|
thumbnailDirectUrl = it.thumbnailDirectUrl,
|
||||||
|
duration = it.duration,
|
||||||
|
width = it.width,
|
||||||
|
height = it.height,
|
||||||
|
size = it.size,
|
||||||
|
format = it.format,
|
||||||
|
bitrate = it.bitrate,
|
||||||
|
frameRate = it.frameRate
|
||||||
|
)
|
||||||
|
},
|
||||||
// 新闻相关字段
|
// 新闻相关字段
|
||||||
isNews = isNews,
|
isNews = isNews,
|
||||||
newsTitle = newsTitle ?: "",
|
newsTitle = newsTitle ?: "",
|
||||||
@@ -82,7 +113,11 @@ data class Moment(
|
|||||||
newsSource = newsSource ?: "",
|
newsSource = newsSource ?: "",
|
||||||
newsCategory = newsCategory ?: "",
|
newsCategory = newsCategory ?: "",
|
||||||
newsLanguage = newsLanguage ?: "",
|
newsLanguage = newsLanguage ?: "",
|
||||||
newsContent = newsContent ?: ""
|
newsContent = newsContent ?: "",
|
||||||
|
hasFullText = hasFullText,
|
||||||
|
summary = summary,
|
||||||
|
publishedAt = publishedAt,
|
||||||
|
imageCached = imageCached
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +127,26 @@ data class Image(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
@SerializedName("url")
|
@SerializedName("url")
|
||||||
val url: String,
|
val url: String,
|
||||||
|
@SerializedName("original_url")
|
||||||
|
val originalUrl: String? = null,
|
||||||
|
@SerializedName("directUrl")
|
||||||
|
val directUrl: String? = null,
|
||||||
@SerializedName("thumbnail")
|
@SerializedName("thumbnail")
|
||||||
val thumbnail: String,
|
val thumbnail: String,
|
||||||
|
@SerializedName("thumbnailDirectUrl")
|
||||||
|
val thumbnailDirectUrl: String? = null,
|
||||||
|
@SerializedName("small")
|
||||||
|
val small: String? = null,
|
||||||
|
@SerializedName("smallDirectUrl")
|
||||||
|
val smallDirectUrl: String? = null,
|
||||||
|
@SerializedName("medium")
|
||||||
|
val medium: String? = null,
|
||||||
|
@SerializedName("mediumDirectUrl")
|
||||||
|
val mediumDirectUrl: String? = null,
|
||||||
|
@SerializedName("large")
|
||||||
|
val large: String? = null,
|
||||||
|
@SerializedName("largeDirectUrl")
|
||||||
|
val largeDirectUrl: String? = null,
|
||||||
@SerializedName("blurHash")
|
@SerializedName("blurHash")
|
||||||
val blurHash: String?,
|
val blurHash: String?,
|
||||||
@SerializedName("width")
|
@SerializedName("width")
|
||||||
@@ -102,13 +155,68 @@ data class Image(
|
|||||||
val height: Int?
|
val height: Int?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class Video(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Long,
|
||||||
|
@SerializedName("url")
|
||||||
|
val url: String,
|
||||||
|
@SerializedName("original_url")
|
||||||
|
val originalUrl: String? = null,
|
||||||
|
@SerializedName("directUrl")
|
||||||
|
val directUrl: String? = null,
|
||||||
|
@SerializedName("thumbnailUrl")
|
||||||
|
val thumbnailUrl: String? = null,
|
||||||
|
@SerializedName("thumbnailDirectUrl")
|
||||||
|
val thumbnailDirectUrl: String? = null,
|
||||||
|
@SerializedName("duration")
|
||||||
|
val duration: Int? = null,
|
||||||
|
@SerializedName("width")
|
||||||
|
val width: Int? = null,
|
||||||
|
@SerializedName("height")
|
||||||
|
val height: Int? = null,
|
||||||
|
@SerializedName("size")
|
||||||
|
val size: Long? = null,
|
||||||
|
@SerializedName("format")
|
||||||
|
val format: String? = null,
|
||||||
|
@SerializedName("bitrate")
|
||||||
|
val bitrate: Int? = null,
|
||||||
|
@SerializedName("frameRate")
|
||||||
|
val frameRate: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class User(
|
data class User(
|
||||||
@SerializedName("id")
|
@SerializedName("id")
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@SerializedName("nickName")
|
@SerializedName("nickName")
|
||||||
val nickName: String,
|
val nickName: String,
|
||||||
@SerializedName("avatar")
|
@SerializedName("avatar")
|
||||||
val avatar: String
|
val avatar: String,
|
||||||
|
@SerializedName("avatarMedium")
|
||||||
|
val avatarMedium: String? = null,
|
||||||
|
@SerializedName("avatarLarge")
|
||||||
|
val avatarLarge: String? = null,
|
||||||
|
@SerializedName("originAvatar")
|
||||||
|
val originAvatar: String? = null,
|
||||||
|
@SerializedName("avatarDirectUrl")
|
||||||
|
val avatarDirectUrl: String? = null,
|
||||||
|
@SerializedName("avatarMediumDirectUrl")
|
||||||
|
val avatarMediumDirectUrl: String? = null,
|
||||||
|
@SerializedName("avatarLargeDirectUrl")
|
||||||
|
val avatarLargeDirectUrl: String? = null,
|
||||||
|
@SerializedName("aiAccount")
|
||||||
|
val aiAccount: Boolean = false,
|
||||||
|
@SerializedName("aiRoleAvatar")
|
||||||
|
val aiRoleAvatar: String? = null,
|
||||||
|
@SerializedName("aiRoleAvatarMedium")
|
||||||
|
val aiRoleAvatarMedium: String? = null,
|
||||||
|
@SerializedName("aiRoleAvatarLarge")
|
||||||
|
val aiRoleAvatarLarge: String? = null,
|
||||||
|
@SerializedName("aiRoleAvatarDirectUrl")
|
||||||
|
val aiRoleAvatarDirectUrl: String? = null,
|
||||||
|
@SerializedName("aiRoleAvatarMediumDirectUrl")
|
||||||
|
val aiRoleAvatarMediumDirectUrl: String? = null,
|
||||||
|
@SerializedName("aiRoleAvatarLargeDirectUrl")
|
||||||
|
val aiRoleAvatarLargeDirectUrl: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class UploadImage(
|
data class UploadImage(
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ class UserServiceImpl : UserService {
|
|||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
search = nickname,
|
search = nickname,
|
||||||
followerId = followerId,
|
followerId = followerId,
|
||||||
followingId = followingId
|
followingId = followingId,
|
||||||
|
includeAI = true
|
||||||
)
|
)
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
return ListContainer<AccountProfileEntity>(
|
return ListContainer<AccountProfileEntity>(
|
||||||
|
|||||||
@@ -800,6 +800,7 @@ interface RaveNowAPI {
|
|||||||
@Query("favouriteUserId") favouriteUserId: Int? = null,
|
@Query("favouriteUserId") favouriteUserId: Int? = null,
|
||||||
@Query("explore") explore: String? = null,
|
@Query("explore") explore: String? = null,
|
||||||
@Query("newsFilter") newsFilter: String? = null,
|
@Query("newsFilter") newsFilter: String? = null,
|
||||||
|
@Query("videoFilter") videoFilter: String? = null,
|
||||||
): Response<ListContainer<Moment>>
|
): Response<ListContainer<Moment>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -954,7 +955,7 @@ interface RaveNowAPI {
|
|||||||
@Query("nickname") search: String? = null,
|
@Query("nickname") search: String? = null,
|
||||||
@Query("followerId") followerId: Int? = null,
|
@Query("followerId") followerId: Int? = null,
|
||||||
@Query("followingId") followingId: Int? = null,
|
@Query("followingId") followingId: Int? = null,
|
||||||
@Query("includeAI") includeAI: Boolean? = false,
|
@Query("includeAI") includeAI: Boolean? = true,
|
||||||
@Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true,
|
@Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true,
|
||||||
): Response<ListContainer<AccountProfile>>
|
): Response<ListContainer<AccountProfile>>
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,38 @@ data class MomentImageEntity(
|
|||||||
var height: Int? = null
|
var height: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态视频
|
||||||
|
*/
|
||||||
|
data class MomentVideoEntity(
|
||||||
|
// 视频ID
|
||||||
|
val id: Long,
|
||||||
|
// 视频URL
|
||||||
|
val url: String,
|
||||||
|
// 原始文件名
|
||||||
|
val originalUrl: String? = null,
|
||||||
|
// 直接访问URL
|
||||||
|
val directUrl: String? = null,
|
||||||
|
// 视频缩略图URL
|
||||||
|
val thumbnailUrl: String? = null,
|
||||||
|
// 视频缩略图直接访问URL
|
||||||
|
val thumbnailDirectUrl: String? = null,
|
||||||
|
// 视频时长(秒)
|
||||||
|
val duration: Int? = null,
|
||||||
|
// 宽度
|
||||||
|
val width: Int? = null,
|
||||||
|
// 高度
|
||||||
|
val height: Int? = null,
|
||||||
|
// 文件大小(字节)
|
||||||
|
val size: Long? = null,
|
||||||
|
// 视频格式
|
||||||
|
val format: String? = null,
|
||||||
|
// 视频比特率(kbps)
|
||||||
|
val bitrate: Int? = null,
|
||||||
|
// 帧率
|
||||||
|
val frameRate: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态
|
* 动态
|
||||||
*/
|
*/
|
||||||
@@ -300,6 +332,10 @@ data class MomentEntity(
|
|||||||
var relMoment: MomentEntity? = null,
|
var relMoment: MomentEntity? = null,
|
||||||
// 是否收藏
|
// 是否收藏
|
||||||
var isFavorite: Boolean = false,
|
var isFavorite: Boolean = false,
|
||||||
|
// 外部链接
|
||||||
|
val url: String? = null,
|
||||||
|
// 动态视频列表
|
||||||
|
val videos: List<MomentVideoEntity>? = null,
|
||||||
// 新闻相关字段
|
// 新闻相关字段
|
||||||
val isNews: Boolean = false,
|
val isNews: Boolean = false,
|
||||||
val newsTitle: String = "",
|
val newsTitle: String = "",
|
||||||
@@ -307,13 +343,22 @@ data class MomentEntity(
|
|||||||
val newsSource: String = "",
|
val newsSource: String = "",
|
||||||
val newsCategory: String = "",
|
val newsCategory: String = "",
|
||||||
val newsLanguage: String = "",
|
val newsLanguage: String = "",
|
||||||
val newsContent: String = ""
|
val newsContent: String = "",
|
||||||
|
// 是否已获取完整正文
|
||||||
|
val hasFullText: Boolean = false,
|
||||||
|
// 新闻摘要
|
||||||
|
val summary: String? = null,
|
||||||
|
// 新闻发布时间
|
||||||
|
val publishedAt: String? = null,
|
||||||
|
// 是否已缓存图片
|
||||||
|
val imageCached: Boolean = false
|
||||||
)
|
)
|
||||||
class MomentLoaderExtraArgs(
|
class MomentLoaderExtraArgs(
|
||||||
val explore: Boolean? = false,
|
val explore: Boolean? = false,
|
||||||
val timelineId: Int? = null,
|
val timelineId: Int? = null,
|
||||||
val authorId : Int? = null,
|
val authorId : Int? = null,
|
||||||
val newsOnly: Boolean? = null
|
val newsOnly: Boolean? = null,
|
||||||
|
val videoOnly: Boolean? = null
|
||||||
)
|
)
|
||||||
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||||
override suspend fun fetchData(
|
override suspend fun fetchData(
|
||||||
@@ -327,7 +372,8 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
explore = if (extra.explore == true) "true" else "",
|
explore = if (extra.explore == true) "true" else "",
|
||||||
timelineId = extra.timelineId,
|
timelineId = extra.timelineId,
|
||||||
authorId = extra.authorId,
|
authorId = extra.authorId,
|
||||||
newsFilter = if (extra.newsOnly == true) "news_only" else ""
|
newsFilter = if (extra.newsOnly == true) "news_only" else "",
|
||||||
|
videoFilter = if (extra.videoOnly == true) "video_only" else ""
|
||||||
)
|
)
|
||||||
val data = result.body()?.let {
|
val data = result.body()?.let {
|
||||||
ListContainer(
|
ListContainer(
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
var name by mutableStateOf("")
|
var name by mutableStateOf("")
|
||||||
var bio by mutableStateOf("")
|
var bio by mutableStateOf("")
|
||||||
var imageUrl by mutableStateOf<Uri?>(null)
|
var imageUrl by mutableStateOf<Uri?>(null)
|
||||||
|
var bannerImageUrl by mutableStateOf<Uri?>(null)
|
||||||
|
var bannerFile by mutableStateOf<File?>(null)
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||||
@@ -82,6 +84,30 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||||
UploadImage(file, "avatar.jpg", "", "jpg")
|
UploadImage(file, "avatar.jpg", "", "jpg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理背景图更新
|
||||||
|
val newBanner = bannerImageUrl?.let { uri ->
|
||||||
|
bannerFile?.let { file ->
|
||||||
|
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||||
|
var uploadBanner: UploadImage? = null
|
||||||
|
cursor?.use { cur ->
|
||||||
|
val columnIndex = cur.getColumnIndex("_display_name")
|
||||||
|
if (cur.moveToFirst() && columnIndex != -1) {
|
||||||
|
val displayName = cur.getString(columnIndex)
|
||||||
|
val extension = displayName.substringAfterLast(".")
|
||||||
|
Log.d("AccountEditViewModel", "Banner file name: $displayName, extension: $extension")
|
||||||
|
uploadBanner = UploadImage(file, displayName, uri.toString(), extension)
|
||||||
|
} else {
|
||||||
|
// 如果无法获取文件名,使用默认值
|
||||||
|
val displayName = "banner.jpg"
|
||||||
|
val extension = "jpg"
|
||||||
|
uploadBanner = UploadImage(file, displayName, uri.toString(), extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uploadBanner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 去除换行符,确保昵称和个人简介不包含换行
|
// 去除换行符,确保昵称和个人简介不包含换行
|
||||||
val cleanName = name.trim().replace("\n", "").replace("\r", "")
|
val cleanName = name.trim().replace("\n", "").replace("\r", "")
|
||||||
val cleanBio = bio.trim().replace("\n", "").replace("\r", "")
|
val cleanBio = bio.trim().replace("\n", "").replace("\r", "")
|
||||||
@@ -89,7 +115,7 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
val newName = if (cleanName == profile?.nickName) null else cleanName
|
val newName = if (cleanName == profile?.nickName) null else cleanName
|
||||||
accountService.updateProfile(
|
accountService.updateProfile(
|
||||||
avatar = newAvatar,
|
avatar = newAvatar,
|
||||||
banner = null,
|
banner = newBanner,
|
||||||
nickName = newName,
|
nickName = newName,
|
||||||
bio = cleanBio
|
bio = cleanBio
|
||||||
)
|
)
|
||||||
@@ -100,6 +126,9 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
|
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
|
// 清除背景图状态
|
||||||
|
bannerImageUrl = null
|
||||||
|
bannerFile = null
|
||||||
// 刷新用户资料
|
// 刷新用户资料
|
||||||
reloadProfile()
|
reloadProfile()
|
||||||
// 刷新个人资料页面的用户资料
|
// 刷新个人资料页面的用户资料
|
||||||
@@ -116,6 +145,8 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
name = ""
|
name = ""
|
||||||
bio = ""
|
bio = ""
|
||||||
imageUrl = null
|
imageUrl = null
|
||||||
|
bannerImageUrl = null
|
||||||
|
bannerFile = null
|
||||||
croppedBitmap = null
|
croppedBitmap = null
|
||||||
isUpdating = false
|
isUpdating = false
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|||||||
@@ -1,117 +1,177 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
import android.widget.Toast
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.ravenow.AppState
|
|
||||||
import com.aiosman.ravenow.AppStore
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.Messaging
|
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
|
||||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.ravenow.ui.index.NavItem
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
private object AccountSettingConstants {
|
||||||
|
const val BACK_BUTTON_SIZE = 36
|
||||||
|
const val BACK_BUTTON_ICON_SIZE = 24
|
||||||
|
const val BACK_BUTTON_START_PADDING = 19
|
||||||
|
const val OPTION_ITEM_HEIGHT = 56
|
||||||
|
const val OPTION_ITEM_ICON_SIZE = 24
|
||||||
|
const val OPTION_ITEM_HORIZONTAL_PADDING = 16
|
||||||
|
const val OPTION_ITEM_ICON_TEXT_SPACING = 12
|
||||||
|
const val OPTION_ITEM_TEXT_SIZE = 17
|
||||||
|
const val HEADER_VERTICAL_PADDING = 16
|
||||||
|
const val TITLE_OFFSET_X = 19
|
||||||
|
const val CARD_CORNER_RADIUS = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CircularBackButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(AccountSettingConstants.BACK_BUTTON_SIZE.dp)
|
||||||
|
.background(
|
||||||
|
color = appColors.secondaryBackground,
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.noRippleClickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "返回",
|
||||||
|
modifier = Modifier.size(AccountSettingConstants.BACK_BUTTON_ICON_SIZE.dp),
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SecurityOptionItem(
|
||||||
|
iconRes: Int,
|
||||||
|
label: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
applyColorFilter: Boolean = true
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(AccountSettingConstants.OPTION_ITEM_HEIGHT.dp)
|
||||||
|
.padding(horizontal = AccountSettingConstants.OPTION_ITEM_HORIZONTAL_PADDING.dp)
|
||||||
|
.noRippleClickable { onClick() },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(AccountSettingConstants.OPTION_ITEM_ICON_SIZE.dp),
|
||||||
|
colorFilter = if (applyColorFilter) ColorFilter.tint(appColors.text) else null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = AccountSettingConstants.OPTION_ITEM_ICON_TEXT_SPACING.dp)
|
||||||
|
.weight(1f),
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = AccountSettingConstants.OPTION_ITEM_TEXT_SIZE.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rave_now_nav_right),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(AccountSettingConstants.OPTION_ITEM_ICON_SIZE.dp),
|
||||||
|
colorFilter = ColorFilter.tint(appColors.secondaryText)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountSetting() {
|
fun AccountSetting() {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val context = LocalContext.current
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(appColors.background),
|
.background(appColors.background),
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
// 顶部标题栏
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = AccountSettingConstants.HEADER_VERTICAL_PADDING.dp)
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(
|
CircularBackButton(
|
||||||
title = stringResource(R.string.account_and_security),
|
onClick = { navController.navigateUp() },
|
||||||
moreIcon = false
|
modifier = Modifier.padding(start = AccountSettingConstants.BACK_BUTTON_START_PADDING.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.account_and_security),
|
||||||
|
fontWeight = FontWeight.W800,
|
||||||
|
fontSize = AccountSettingConstants.OPTION_ITEM_TEXT_SIZE.sp,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.offset(x = AccountSettingConstants.TITLE_OFFSET_X.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 安全选项卡片
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(start = 24.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp)
|
.background(
|
||||||
|
color = appColors.background,
|
||||||
|
shape = RoundedCornerShape(AccountSettingConstants.CARD_CORNER_RADIUS.dp)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
NavItem(
|
SecurityOptionItem(
|
||||||
iconRes = R.mipmap.rider_pro_change_password,
|
iconRes = R.mipmap.icons_padlock,
|
||||||
label = stringResource(R.string.change_password),
|
label = stringResource(R.string.change_password),
|
||||||
modifier = Modifier.noRippleClickable {
|
onClick = { navController.navigate(NavigationRoute.ChangePasswordScreen.route) }
|
||||||
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 分割线
|
SecurityOptionItem(
|
||||||
Box(
|
iconRes = R.mipmap.icons_block,
|
||||||
modifier = Modifier
|
label = stringResource(R.string.blocked_users),
|
||||||
.fillMaxWidth()
|
onClick = {
|
||||||
.height(1.dp)
|
// TODO: 导航到屏蔽用户页面
|
||||||
.background(appColors.divider)
|
}
|
||||||
)
|
)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
SecurityOptionItem(
|
||||||
.fillMaxWidth()
|
iconRes = R.mipmap.icons_remove,
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rider_pro_moment_delete,
|
|
||||||
label = stringResource(R.string.remove_account),
|
label = stringResource(R.string.remove_account),
|
||||||
modifier = Modifier.noRippleClickable {
|
onClick = { navController.navigate(NavigationRoute.RemoveAccountScreen.route) },
|
||||||
navController.navigate(NavigationRoute.RemoveAccountScreen.route)
|
applyColorFilter = false
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(1.dp)
|
|
||||||
.background(appColors.divider)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
@@ -80,6 +81,10 @@ fun MbtiSelectScreen() {
|
|||||||
isSelected = mbti == currentMbti,
|
isSelected = mbti == currentMbti,
|
||||||
onClick = {
|
onClick = {
|
||||||
model.mbti = mbti
|
model.mbti = mbti
|
||||||
|
// 立即保存到本地存储,确保选择后立即生效
|
||||||
|
AppState.UserId?.let { uid ->
|
||||||
|
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
|
||||||
|
}
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,24 +29,55 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
|
||||||
// 星座列表
|
// 星座资源ID列表
|
||||||
val ZODIAC_SIGNS = listOf(
|
val ZODIAC_SIGN_RES_IDS = listOf(
|
||||||
"白羊座", "金牛座", "双子座", "巨蟹座",
|
R.string.zodiac_aries,
|
||||||
"狮子座", "处女座", "天秤座", "天蝎座",
|
R.string.zodiac_taurus,
|
||||||
"射手座", "摩羯座", "水瓶座", "双鱼座"
|
R.string.zodiac_gemini,
|
||||||
|
R.string.zodiac_cancer,
|
||||||
|
R.string.zodiac_leo,
|
||||||
|
R.string.zodiac_virgo,
|
||||||
|
R.string.zodiac_libra,
|
||||||
|
R.string.zodiac_scorpio,
|
||||||
|
R.string.zodiac_sagittarius,
|
||||||
|
R.string.zodiac_capricorn,
|
||||||
|
R.string.zodiac_aquarius,
|
||||||
|
R.string.zodiac_pisces
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据存储的星座字符串(可能是任何语言)找到对应的资源ID
|
||||||
|
* 如果找不到,返回null
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun findZodiacResId(storedZodiac: String?): Int? {
|
||||||
|
if (storedZodiac.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
// 尝试在所有语言的资源中查找匹配
|
||||||
|
ZODIAC_SIGN_RES_IDS.forEachIndexed { index, resId ->
|
||||||
|
val zodiacText = stringResource(resId)
|
||||||
|
if (zodiacText == storedZodiac) {
|
||||||
|
return resId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到精确匹配,尝试通过资源ID索引查找(兼容旧数据)
|
||||||
|
// 这里可以根据需要添加更多兼容逻辑
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ZodiacSelectScreen() {
|
fun ZodiacSelectScreen() {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val model = AccountEditViewModel
|
val model = AccountEditViewModel
|
||||||
val currentZodiac = model.zodiac
|
val currentZodiacResId = findZodiacResId(model.zodiac)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -70,12 +101,20 @@ fun ZodiacSelectScreen() {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
items(ZODIAC_SIGNS) { zodiac ->
|
items(ZODIAC_SIGN_RES_IDS.size) { index ->
|
||||||
|
val zodiacResId = ZODIAC_SIGN_RES_IDS[index]
|
||||||
|
val zodiacText = stringResource(zodiacResId)
|
||||||
ZodiacItem(
|
ZodiacItem(
|
||||||
zodiac = zodiac,
|
zodiac = zodiacText,
|
||||||
isSelected = zodiac == currentZodiac,
|
zodiacResId = zodiacResId,
|
||||||
|
isSelected = zodiacResId == currentZodiacResId,
|
||||||
onClick = {
|
onClick = {
|
||||||
model.zodiac = zodiac
|
// 保存当前语言的星座文本
|
||||||
|
model.zodiac = zodiacText
|
||||||
|
// 立即保存到本地存储,确保选择后立即生效
|
||||||
|
AppState.UserId?.let { uid ->
|
||||||
|
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
|
||||||
|
}
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -88,6 +127,7 @@ fun ZodiacSelectScreen() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ZodiacItem(
|
fun ZodiacItem(
|
||||||
zodiac: String,
|
zodiac: String,
|
||||||
|
zodiacResId: Int,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -4,19 +4,26 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Edit as EditIcon
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -28,10 +35,14 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
@@ -39,28 +50,29 @@ import com.aiosman.ravenow.LocalAppTheme
|
|||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
|
||||||
import com.aiosman.ravenow.ui.composables.form.FormTextInput
|
|
||||||
import com.aiosman.ravenow.ui.composables.debouncedClickable
|
import com.aiosman.ravenow.ui.composables.debouncedClickable
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
|
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.aiosman.ravenow.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
||||||
|
import android.widget.Toast
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,6 +98,10 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
quality = 100
|
quality = 100
|
||||||
) { uri, file ->
|
) { uri, file ->
|
||||||
// 处理选中的图片
|
// 处理选中的图片
|
||||||
|
// 保存到 ViewModel 中,等待保存时一起上传
|
||||||
|
model.bannerImageUrl = uri
|
||||||
|
model.bannerFile = file
|
||||||
|
// 如果提供了回调,也调用它(用于个人主页直接更新)
|
||||||
onUpdateBanner?.invoke(uri, file, context)
|
onUpdateBanner?.invoke(uri, file, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,10 +109,21 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
// 去除换行符,确保昵称不包含换行
|
// 去除换行符,确保昵称不包含换行
|
||||||
val cleanValue = value.replace("\n", "").replace("\r", "")
|
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||||
model.name = cleanValue
|
model.name = cleanValue
|
||||||
|
// 实时验证,但不显示错误(只在保存时显示)
|
||||||
usernameError = when {
|
usernameError = when {
|
||||||
cleanValue.trim().isEmpty() -> "昵称不能为空"
|
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
|
||||||
cleanValue.length < 3 -> "昵称长度不能小于3"
|
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
|
||||||
cleanValue.length > 20 -> "昵称长度不能大于20"
|
cleanValue.length > 20 -> context.getString(R.string.error_nickname_too_long)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateNickname(): String? {
|
||||||
|
val cleanValue = model.name.replace("\n", "").replace("\r", "")
|
||||||
|
return when {
|
||||||
|
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
|
||||||
|
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
|
||||||
|
cleanValue.length > 20 -> context.getString(R.string.error_nickname_too_long)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,8 +134,17 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
// 去除换行符,确保个人简介不包含换行
|
// 去除换行符,确保个人简介不包含换行
|
||||||
val cleanValue = value.replace("\n", "").replace("\r", "")
|
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||||
model.bio = cleanValue
|
model.bio = cleanValue
|
||||||
|
// 实时验证,但不显示错误(只在保存时显示)
|
||||||
bioError = when {
|
bioError = when {
|
||||||
cleanValue.length > 100 -> "个人简介长度不能大于100"
|
cleanValue.length > 100 -> context.getString(R.string.error_bio_too_long)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateBio(): String? {
|
||||||
|
val cleanValue = model.bio.replace("\n", "").replace("\r", "")
|
||||||
|
return when {
|
||||||
|
cleanValue.length > 100 -> context.getString(R.string.error_bio_too_long)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,34 +170,321 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
// 确保显示的是当前登录用户的信息,而不是之前用户的缓存数据
|
// 确保显示的是当前登录用户的信息,而不是之前用户的缓存数据
|
||||||
model.reloadProfile()
|
model.reloadProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置状态栏为透明,根据暗色模式决定图标颜色
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||||
|
}
|
||||||
|
|
||||||
StatusBarMaskLayout(
|
StatusBarMaskLayout(
|
||||||
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp),
|
modifier = Modifier.background(appColors.background),
|
||||||
darkIcons = !AppState.darkMode,
|
darkIcons = !AppState.darkMode, // 根据暗色模式决定图标颜色
|
||||||
maskBoxBackgroundColor = appColors.background
|
maskBoxBackgroundColor = Color.Transparent
|
||||||
) {
|
) {
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(color = appColors.background),
|
.background(appColors.background)
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
//StatusBarSpacer()
|
when {
|
||||||
|
model.isLoading -> {
|
||||||
|
// 加载中状态
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.padding(horizontal = 0.dp, vertical = 16.dp)
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(
|
androidx.compose.material3.CircularProgressIndicator(
|
||||||
title = stringResource(R.string.edit_profile),
|
color = appColors.main
|
||||||
moreIcon = false
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.profile != null -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
// 顶部背景区域(圆角在底部,覆盖状态栏)
|
||||||
Icon(
|
// 优先显示新选择的背景图,如果没有则显示原有的背景图
|
||||||
|
val banner = model.bannerImageUrl?.toString() ?: model.profile?.banner
|
||||||
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.fillMaxWidth()
|
||||||
|
.offset(y = -statusBarPadding)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(402.dp)
|
||||||
|
.height(206.dp)
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.clip(RoundedCornerShape(bottomStart = 32.dp, bottomEnd = 32.dp))
|
||||||
|
) {
|
||||||
|
if (banner != null) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = banner,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "Banner",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Gray.copy(alpha = 0.2f))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更换封面按钮(位于壁纸右下方)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 16.dp, bottom = 20.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color(0x5C7D7C80)) // RGB(125, 120, 128, 0.36)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
Intent(Intent.ACTION_PICK).apply {
|
||||||
|
type = "image/*"
|
||||||
|
pickBannerImageLauncher.launch(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
// 更换封面图标
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.fengm),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.change_cover),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态栏区域(时间、信号、电池)
|
||||||
|
// 这里使用系统状态栏,不单独实现
|
||||||
|
|
||||||
|
// 导航栏区域
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(statusBarPadding + 12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
) {
|
||||||
|
// 返回按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(44.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.White.copy(alpha = 0.3f))
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
.align(Alignment.CenterStart),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color.White)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.edit_profile_info),
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户头像区域(距离顶部-50dp,包含状态栏高度,左右居中)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.offset(y = (-50).dp - statusBarPadding),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val it = model.profile!!
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(96.dp),
|
||||||
|
contentAlignment = Alignment.BottomEnd
|
||||||
|
) {
|
||||||
|
// 头像
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
model.croppedBitmap ?: it.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.border(2.4.dp, appColors.background, CircleShape),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
// 编辑头像按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xFF110C13))
|
||||||
|
.border(2.dp, Color.White, CircleShape)
|
||||||
|
.debouncedClickable(debounceTime = 800L) {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.ImageCrop.route)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.bi),
|
||||||
|
contentDescription = "Edit Avatar",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.offset(y = (-74).dp)//
|
||||||
|
) {
|
||||||
|
// 昵称输入框
|
||||||
|
ProfileInfoCard(
|
||||||
|
label = stringResource(R.string.nickname),
|
||||||
|
value = model.name,
|
||||||
|
placeholder = "Value",
|
||||||
|
onValueChange = { onNicknameChange(it) },
|
||||||
|
isMultiline = false
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 个人简介输入框
|
||||||
|
ProfileInfoCard(
|
||||||
|
label = stringResource(R.string.personal_intro),
|
||||||
|
value = model.bio,
|
||||||
|
placeholder = "Welcome to my fantiac word i will show you something about magic",
|
||||||
|
onValueChange = { onBioChange(it) },
|
||||||
|
isMultiline = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// MBTI 类型和星座
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(appColors.secondaryBackground)
|
||||||
|
) {
|
||||||
|
// MBTI 类型
|
||||||
|
ProfileSelectItem(
|
||||||
|
label = stringResource(R.string.mbti_type),
|
||||||
|
value = model.mbti ?: "ENFP",
|
||||||
|
iconColor = Color(0xFF7C45ED),
|
||||||
|
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
||||||
|
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
||||||
|
onClick = {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.MbtiSelect.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分隔线
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(0.3.dp)
|
||||||
|
.background(appColors.divider)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 星座(使用当前图标)
|
||||||
|
ProfileSelectItem(
|
||||||
|
label = stringResource(R.string.zodiac),
|
||||||
|
value = model.zodiac?.let { storedZodiac ->
|
||||||
|
// 尝试找到对应的资源ID并显示当前语言的文本
|
||||||
|
findZodiacResId(storedZodiac)?.let { resId ->
|
||||||
|
stringResource(resId)
|
||||||
|
} ?: storedZodiac // 如果找不到,显示原始存储的值
|
||||||
|
} ?: stringResource(R.string.zodiac_aries),
|
||||||
|
iconColor = Color(0xFFFFCC00),
|
||||||
|
iconResDark = R.mipmap.frame_4, // 星座暗色模式图标
|
||||||
|
iconResLight = R.mipmap.xingzuo, // 星座亮色模式图标
|
||||||
|
onClick = {
|
||||||
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.ZodiacSelect.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// 保存按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(50.dp)
|
||||||
|
.clip(RoundedCornerShape(1000.dp))
|
||||||
|
.background(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF7C45ED), // RGB(124, 69, 237) - 左侧
|
||||||
|
Color(0xFF7C57EE), // RGB(124, 87, 238) - 中间
|
||||||
|
Color(0xFF7BD8F8) // RGB(123, 216, 248) - 右侧
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.debouncedClickable(
|
.debouncedClickable(
|
||||||
enabled = validate() && !model.isUpdating,
|
enabled = !model.isUpdating,
|
||||||
debounceTime = 1000L
|
debounceTime = 1000L
|
||||||
) {
|
) {
|
||||||
if (validate() && !model.isUpdating) {
|
if (model.isUpdating) return@debouncedClickable
|
||||||
|
|
||||||
|
// 点击保存时重新验证
|
||||||
|
val nicknameErrorMsg = validateNickname()
|
||||||
|
val bioErrorMsg = validateBio()
|
||||||
|
|
||||||
|
// 如果有错误,显示对应的错误提示
|
||||||
|
when {
|
||||||
|
nicknameErrorMsg != null -> {
|
||||||
|
Toast.makeText(context, nicknameErrorMsg, Toast.LENGTH_SHORT).show()
|
||||||
|
return@debouncedClickable
|
||||||
|
}
|
||||||
|
bioErrorMsg != null -> {
|
||||||
|
Toast.makeText(context, bioErrorMsg, Toast.LENGTH_SHORT).show()
|
||||||
|
return@debouncedClickable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通过,执行保存
|
||||||
model.viewModelScope.launch {
|
model.viewModelScope.launch {
|
||||||
model.isUpdating = true
|
model.isUpdating = true
|
||||||
model.updateUserProfile(context)
|
model.updateUserProfile(context)
|
||||||
@@ -172,179 +495,172 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
model.isUpdating = false
|
model.isUpdating = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
imageVector = Icons.Default.Check,
|
contentAlignment = Alignment.Center
|
||||||
contentDescription = "保存",
|
|
||||||
tint = if (validate() && !model.isUpdating) appColors.text else appColors.nonActiveText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 添加横幅图片区域
|
|
||||||
val banner = model.profile?.banner
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(300.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
) {
|
|
||||||
if (banner != null) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = LocalContext.current,
|
|
||||||
imageUrl = banner,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentDescription = "Banner",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.Gray.copy(alpha = 0.1f))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(120.dp)
|
|
||||||
.height(42.dp)
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(end = 12.dp, bottom = 12.dp)
|
|
||||||
.background(
|
|
||||||
color = Color.Black.copy(alpha = 0.4f),
|
|
||||||
shape = RoundedCornerShape(9.dp)
|
|
||||||
)
|
|
||||||
.noRippleClickable {
|
|
||||||
Intent(Intent.ACTION_PICK).apply {
|
|
||||||
type = "image/*"
|
|
||||||
pickBannerImageLauncher.launch(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "change",
|
text = stringResource(R.string.save),
|
||||||
fontSize = 14.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.Normal,
|
||||||
color = Color.White,
|
color = Color.White
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
|
|
||||||
// 显示内容或加载状态
|
|
||||||
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
|
|
||||||
when {
|
|
||||||
model.profile != null -> {
|
|
||||||
Log.d("AccountEditScreen2", "显示用户资料内容")
|
|
||||||
// 有数据时显示内容
|
|
||||||
val it = model.profile!!
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.size(88.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context,
|
|
||||||
model.croppedBitmap ?: it.avatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(88.dp)
|
|
||||||
.clip(
|
|
||||||
RoundedCornerShape(88.dp)
|
|
||||||
),
|
|
||||||
contentDescription = "",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFF7c45ed),
|
|
||||||
Color(0x997c68ef),
|
|
||||||
Color(0xFF7bd8f8)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.debouncedClickable(
|
|
||||||
debounceTime = 800L
|
|
||||||
) {
|
|
||||||
debouncedNavigation {
|
|
||||||
navController.navigate(NavigationRoute.ImageCrop.route)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Add,
|
|
||||||
contentDescription = "Add",
|
|
||||||
tint = Color.White,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
FormTextInput(
|
|
||||||
value = model.name,
|
|
||||||
label = stringResource(R.string.nickname),
|
|
||||||
hint = "Input nickname",
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
error = usernameError
|
|
||||||
) { value ->
|
|
||||||
onNicknameChange(value)
|
|
||||||
}
|
|
||||||
FormTextInput(
|
|
||||||
value = model.bio,
|
|
||||||
label = stringResource(R.string.bio),
|
|
||||||
hint = "Input bio",
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
error = bioError
|
|
||||||
) { value ->
|
|
||||||
onBioChange(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.isLoading -> {
|
|
||||||
Log.d("AccountEditScreen2", "显示加载指示器")
|
|
||||||
// 加载中状态
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
androidx.compose.material3.CircularProgressIndicator(
|
|
||||||
color = appColors.main
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d("AccountEditScreen2", "显示错误信息 - 没有数据且不在加载中")
|
|
||||||
// 没有数据且不在加载中,显示错误信息
|
// 没有数据且不在加载中,显示错误信息
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Text(
|
Text(
|
||||||
text = "加载用户资料失败,请重试",
|
text = stringResource(R.string.error_load_profile_failed),
|
||||||
color = appColors.text
|
color = appColors.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信息输入卡片组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ProfileInfoCard(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
placeholder: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
isMultiline: Boolean = false
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
// 标签
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = appColors.text,
|
||||||
|
modifier = Modifier.width(100.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// 输入框
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = placeholder,
|
||||||
|
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
|
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = appColors.text
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(appColors.text),
|
||||||
|
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
|
||||||
|
singleLine = !isMultiline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择项组件(MBTI、星座)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ProfileSelectItem(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
iconColor: Color,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
iconResDark: Int? = null,
|
||||||
|
iconResLight: Int? = null
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// 自定义图标
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (AppState.darkMode) {
|
||||||
|
// 暗色模式下使用和亮色模式一样的图标
|
||||||
|
iconResLight ?: iconResDark ?: R.mipmap.naoz
|
||||||
|
} else {
|
||||||
|
iconResLight ?: R.mipmap.naoz // 使用传入的亮色模式图标,或默认占位
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = iconColor
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = appColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = appColors.secondaryText
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowForward,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(8.dp),
|
||||||
|
tint = appColors.secondaryText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import androidx.compose.animation.togetherWith
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
|
||||||
@@ -36,6 +37,13 @@ fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 2
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { targetCount ->
|
) { targetCount ->
|
||||||
Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp, color = AppColors.text)
|
Text(
|
||||||
|
text = "$targetCount",
|
||||||
|
modifier = modifier,
|
||||||
|
fontSize = fontSize.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -32,7 +33,7 @@ fun FollowButton(
|
|||||||
.wrapContentWidth()
|
.wrapContentWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(
|
.background(
|
||||||
color = if (isFollowing) AppColors.main else AppColors.nonActive
|
color = if (isFollowing) AppColors.nonActive else AppColors.nonActive
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
@@ -41,11 +42,9 @@ fun FollowButton(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (isFollowing) stringResource(R.string.following_upper) else stringResource(
|
text = if (isFollowing) stringResource(R.string.follow_upper_had) else stringResource(R.string.follow_upper),
|
||||||
R.string.follow_upper
|
|
||||||
),
|
|
||||||
fontSize = fontSize,
|
fontSize = fontSize,
|
||||||
color = if (isFollowing) AppColors.mainText else AppColors.text,
|
color = if (isFollowing) AppColors.text else AppColors.text,
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,7 +426,6 @@ fun MomentOperateBtn(count: String, content: @Composable () -> Unit) {
|
|||||||
fontSize = 14,
|
fontSize = 14,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 7.dp)
|
.padding(start = 7.dp)
|
||||||
.width(24.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ import com.aiosman.ravenow.LocalAppTheme
|
|||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
private val LabelTextColor = Color(red = 60f / 255f, green = 60f / 255f, blue = 67f / 255f, alpha = 0.6f)
|
||||||
|
private val HintTextColor = Color(red = 60f / 255f, green = 60f / 255f, blue = 67f / 255f, alpha = 0.3f)
|
||||||
|
private val PasswordIconColor = Color(red = 17f / 255f, green = 12f / 255f, blue = 19f / 255f)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextInputField(
|
fun TextInputField(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -52,67 +56,94 @@ fun TextInputField(
|
|||||||
label: String? = null,
|
label: String? = null,
|
||||||
hint: String? = null,
|
hint: String? = null,
|
||||||
error: String? = null,
|
error: String? = null,
|
||||||
enabled: Boolean = true
|
enabled: Boolean = true,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
customBackgroundColor: Color? = null,
|
||||||
|
customCornerRadius: Float = 24f
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
var showPassword by remember { mutableStateOf(!password) }
|
var showPassword by remember { mutableStateOf(!password) }
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
val backgroundColor = customBackgroundColor ?: AppColors.inputBackground
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
label?.let {
|
label?.let {
|
||||||
Text(it, color = AppColors.secondaryText)
|
Text(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
text = it,
|
||||||
|
color = LabelTextColor,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(24.dp))
|
.clip(RoundedCornerShape(customCornerRadius.dp))
|
||||||
.background(AppColors.inputBackground)
|
.background(backgroundColor)
|
||||||
.border(
|
.border(
|
||||||
width = 2.dp,
|
width = 2.dp,
|
||||||
color = if (error == null) Color.Transparent else AppColors.error,
|
color = if (error == null) Color.Transparent else AppColors.error,
|
||||||
shape = RoundedCornerShape(24.dp)
|
shape = RoundedCornerShape(customCornerRadius.dp)
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically){
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
){
|
||||||
|
leadingIcon?.let {
|
||||||
|
Box(modifier = Modifier.size(24.dp)) {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxWidth()
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
isFocused = focusState.isFocused
|
isFocused = focusState.isFocused
|
||||||
},
|
},
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W500,
|
fontWeight = FontWeight.W400,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
),
|
),
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = if (password) KeyboardType.Password else KeyboardType.Text
|
keyboardType = if (password) KeyboardType.Password else KeyboardType.Email
|
||||||
),
|
),
|
||||||
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
cursorBrush = SolidColor(AppColors.text),
|
cursorBrush = SolidColor(AppColors.text),
|
||||||
)
|
)
|
||||||
if (password) {
|
if (text.isEmpty() && hint != null) {
|
||||||
Image(
|
Text(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_eye),
|
text = hint,
|
||||||
contentDescription = "Password",
|
color = HintTextColor,
|
||||||
modifier = Modifier
|
fontSize = 16.sp,
|
||||||
.size(18.dp)
|
fontWeight = FontWeight.W400
|
||||||
.noRippleClickable {
|
|
||||||
showPassword = !showPassword
|
|
||||||
},
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (password) {
|
||||||
if (text.isEmpty()) {
|
Image(
|
||||||
hint?.let {
|
painter = painterResource(
|
||||||
Text(it, modifier = Modifier.padding(start = 5.dp), color = AppColors.inputHint, fontWeight = FontWeight.W600)
|
id = if (showPassword) {
|
||||||
|
R.drawable.rider_pro_eye
|
||||||
|
} else {
|
||||||
|
R.mipmap.icon_eyes_closed_light
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentDescription = "Password",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
showPassword = !showPassword
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(PasswordIconColor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -131,17 +133,25 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = "还没有人关注你呢",
|
text = stringResource(R.string.follower_empty_title),
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = "试着发信号出来,某人就会被吸引啦~",
|
text = stringResource(R.string.follower_empty_subtitle),
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
@@ -122,17 +124,25 @@ fun FollowerNoticeScreen() {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
|
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = "还没有人关注你呢",
|
text = stringResource(R.string.follower_empty_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = "试着发信号出来,某人就会被吸引啦~",
|
text = stringResource(R.string.follower_empty_subtitle),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -133,17 +135,25 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = "还没有关注任何灵魂",
|
text = stringResource(R.string.following_empty_title),
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = "探索一下,总有一个你想靠近的光点 ✨",
|
text = stringResource(R.string.following_empty_subtitle),
|
||||||
color = appColors.secondaryText,
|
color = appColors.secondaryText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.ChatState
|
import com.aiosman.ravenow.ChatState
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.data.api.CreatePromptRuleRequestBody
|
import com.aiosman.ravenow.data.api.AgentRule
|
||||||
import com.aiosman.ravenow.data.api.PromptRule
|
import com.aiosman.ravenow.data.api.AgentRuleQuota
|
||||||
import com.aiosman.ravenow.data.api.PromptRuleQuota
|
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
|
||||||
|
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
|
||||||
import com.aiosman.ravenow.data.parseErrorResponse
|
import com.aiosman.ravenow.data.parseErrorResponse
|
||||||
import com.aiosman.ravenow.entity.ChatNotification
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
import com.aiosman.ravenow.entity.GroupInfo
|
import com.aiosman.ravenow.entity.GroupInfo
|
||||||
@@ -33,8 +34,8 @@ class GroupChatInfoViewModel(
|
|||||||
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
||||||
|
|
||||||
// 记忆管理相关状态
|
// 记忆管理相关状态
|
||||||
var memoryQuota by mutableStateOf<PromptRuleQuota?>(null)
|
var memoryQuota by mutableStateOf<AgentRuleQuota?>(null)
|
||||||
var memoryList by mutableStateOf<List<PromptRule>>(emptyList())
|
var memoryList by mutableStateOf<List<AgentRule>>(emptyList())
|
||||||
var isLoadingMemory by mutableStateOf(false)
|
var isLoadingMemory by mutableStateOf(false)
|
||||||
var memoryError by mutableStateOf<String?>(null)
|
var memoryError by mutableStateOf<String?>(null)
|
||||||
var promptOpenId by mutableStateOf<String?>(null)
|
var promptOpenId by mutableStateOf<String?>(null)
|
||||||
@@ -137,12 +138,12 @@ class GroupChatInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建智能体规则(群记忆)
|
// 创建智能体规则(群记忆)
|
||||||
val requestBody = CreatePromptRuleRequestBody(
|
val requestBody = CreateAgentRuleRequestBody(
|
||||||
rule = memoryText,
|
rule = memoryText,
|
||||||
openId = openId
|
openId = openId
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = ApiClient.api.createPromptRule(requestBody)
|
val response = ApiClient.api.createAgentRule(requestBody)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
addMemorySuccess = true
|
addMemorySuccess = true
|
||||||
@@ -184,14 +185,14 @@ class GroupChatInfoViewModel(
|
|||||||
?: throw Exception("无法获取智能体信息")
|
?: throw Exception("无法获取智能体信息")
|
||||||
|
|
||||||
promptOpenId = fetchedOpenId
|
promptOpenId = fetchedOpenId
|
||||||
val quotaResponse = ApiClient.api.getPromptRuleQuota(fetchedOpenId)
|
val quotaResponse = ApiClient.api.getAgentRuleQuota(fetchedOpenId)
|
||||||
if (quotaResponse.isSuccessful) {
|
if (quotaResponse.isSuccessful) {
|
||||||
memoryQuota = quotaResponse.body()?.data
|
memoryQuota = quotaResponse.body()?.data
|
||||||
} else {
|
} else {
|
||||||
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
|
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val quotaResponse = ApiClient.api.getPromptRuleQuota(targetOpenId)
|
val quotaResponse = ApiClient.api.getAgentRuleQuota(targetOpenId)
|
||||||
if (quotaResponse.isSuccessful) {
|
if (quotaResponse.isSuccessful) {
|
||||||
memoryQuota = quotaResponse.body()?.data
|
memoryQuota = quotaResponse.body()?.data
|
||||||
} else {
|
} else {
|
||||||
@@ -226,14 +227,14 @@ class GroupChatInfoViewModel(
|
|||||||
?: throw Exception("无法获取智能体信息")
|
?: throw Exception("无法获取智能体信息")
|
||||||
|
|
||||||
promptOpenId = fetchedOpenId
|
promptOpenId = fetchedOpenId
|
||||||
val listResponse = ApiClient.api.getPromptRuleList(fetchedOpenId, page = page, pageSize = pageSize)
|
val listResponse = ApiClient.api.getAgentRuleList(fetchedOpenId, page = page, pageSize = pageSize)
|
||||||
if (listResponse.isSuccessful) {
|
if (listResponse.isSuccessful) {
|
||||||
memoryList = listResponse.body()?.data?.list ?: emptyList()
|
memoryList = listResponse.body()?.data?.list ?: emptyList()
|
||||||
} else {
|
} else {
|
||||||
throw Exception("获取记忆列表失败: ${listResponse.code()}")
|
throw Exception("获取记忆列表失败: ${listResponse.code()}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val listResponse = ApiClient.api.getPromptRuleList(targetOpenId, page = page, pageSize = pageSize)
|
val listResponse = ApiClient.api.getAgentRuleList(targetOpenId, page = page, pageSize = pageSize)
|
||||||
if (listResponse.isSuccessful) {
|
if (listResponse.isSuccessful) {
|
||||||
memoryList = listResponse.body()?.data?.list ?: emptyList()
|
memoryList = listResponse.body()?.data?.list ?: emptyList()
|
||||||
} else {
|
} else {
|
||||||
@@ -258,7 +259,7 @@ class GroupChatInfoViewModel(
|
|||||||
isLoadingMemory = true
|
isLoadingMemory = true
|
||||||
memoryError = null
|
memoryError = null
|
||||||
|
|
||||||
val response = ApiClient.api.deletePromptRule(ruleId)
|
val response = ApiClient.api.deleteAgentRule(ruleId)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
// 刷新记忆列表和配额
|
// 刷新记忆列表和配额
|
||||||
promptOpenId?.let { openId ->
|
promptOpenId?.let { openId ->
|
||||||
@@ -292,13 +293,13 @@ class GroupChatInfoViewModel(
|
|||||||
val openId = targetOpenId ?: promptOpenId
|
val openId = targetOpenId ?: promptOpenId
|
||||||
?: throw Exception("无法获取智能体ID")
|
?: throw Exception("无法获取智能体ID")
|
||||||
|
|
||||||
val requestBody = com.aiosman.ravenow.data.api.UpdatePromptRuleRequestBody(
|
val requestBody = UpdateAgentRuleRequestBody(
|
||||||
id = ruleId,
|
id = ruleId,
|
||||||
rule = newRuleText,
|
rule = newRuleText,
|
||||||
openId = openId
|
openId = openId
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = ApiClient.api.updatePromptRule(requestBody)
|
val response = ApiClient.api.updateAgentRule(requestBody)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
// 刷新记忆列表和配额
|
// 刷新记忆列表和配额
|
||||||
loadMemoryQuota(openId)
|
loadMemoryQuota(openId)
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ fun GroupMemoryManageContent(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditGroupMemoryDialog(
|
fun EditGroupMemoryDialog(
|
||||||
memory: com.aiosman.ravenow.data.api.PromptRule,
|
memory: com.aiosman.ravenow.data.api.AgentRule,
|
||||||
viewModel: GroupChatInfoViewModel,
|
viewModel: GroupChatInfoViewModel,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onUpdateMemory: (String) -> Unit
|
onUpdateMemory: (String) -> Unit
|
||||||
@@ -403,7 +403,7 @@ fun EditGroupMemoryDialog(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MemoryItem(
|
fun MemoryItem(
|
||||||
memory: com.aiosman.ravenow.data.api.PromptRule,
|
memory: com.aiosman.ravenow.data.api.AgentRule,
|
||||||
isEditing: Boolean = false,
|
isEditing: Boolean = false,
|
||||||
onEdit: () -> Unit = {},
|
onEdit: () -> Unit = {},
|
||||||
onCancel: () -> Unit = {},
|
onCancel: () -> Unit = {},
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
package com.aiosman.ravenow.ui.index
|
package com.aiosman.ravenow.ui.index
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.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.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -11,15 +17,21 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.requiredWidth
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@@ -42,8 +54,10 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
@@ -59,7 +73,11 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.GuestLoginCheckOut
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
@@ -123,152 +141,17 @@ fun IndexScreen() {
|
|||||||
gesturesEnabled = drawerState.isOpen,
|
gesturesEnabled = drawerState.isOpen,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
Column(
|
SideMenuContent(
|
||||||
modifier = Modifier
|
onClose = {
|
||||||
.requiredWidth(250.dp)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.background(
|
|
||||||
AppColors.background
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.height(88.dp))
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rave_now_nav_account,
|
|
||||||
label = stringResource(R.string.account_and_security),
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
drawerState.close()
|
drawerState.close()
|
||||||
navController.navigate(NavigationRoute.AccountSetting.route)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rider_pro_favourited,
|
|
||||||
label = stringResource(R.string.favourites),
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
coroutineScope.launch {
|
|
||||||
drawerState.close()
|
|
||||||
navController.navigate(NavigationRoute.FavouriteList.route)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rave_now_nav_night,
|
|
||||||
label = stringResource(R.string.dark_mode),
|
|
||||||
rightContent = {
|
|
||||||
Switch(
|
|
||||||
checked = AppState.darkMode,
|
|
||||||
onCheckedChange = {
|
|
||||||
AppState.switchTheme()
|
|
||||||
},
|
},
|
||||||
|
navController = navController,
|
||||||
colors = SwitchDefaults.colors(
|
context = context,
|
||||||
checkedThumbColor = Color.White,
|
isDrawerOpen = drawerState.isOpen
|
||||||
checkedTrackColor = AppColors.main,
|
|
||||||
uncheckedThumbColor = Color.White,
|
|
||||||
uncheckedTrackColor = AppColors.main.copy(alpha = 0.5f),
|
|
||||||
uncheckedBorderColor = Color.Transparent
|
|
||||||
),
|
|
||||||
modifier = Modifier.scale(0.8f)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
// divider
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 16.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(1.dp)
|
|
||||||
.background(AppColors.divider)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rave_now_nav_about,
|
|
||||||
label = stringResource(R.string.blocked),
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
coroutineScope.launch {
|
|
||||||
drawerState.close()
|
|
||||||
navController.navigate(NavigationRoute.AboutScreen.route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rave_now_nav_about,
|
|
||||||
label = stringResource(R.string.feedback),
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
coroutineScope.launch {
|
|
||||||
drawerState.close()
|
|
||||||
navController.navigate(NavigationRoute.AboutScreen.route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rave_now_nav_about,
|
|
||||||
label = stringResource(R.string.about_rave_now),
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
coroutineScope.launch {
|
|
||||||
drawerState.close()
|
|
||||||
navController.navigate(NavigationRoute.AboutScreen.route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// divider
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 16.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(1.dp)
|
|
||||||
.background(AppColors.divider)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NavItem(
|
|
||||||
// iconRes = R.drawable.rave_now_nav_switch,
|
|
||||||
// label = "Switch Account"
|
|
||||||
// )
|
|
||||||
// Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
NavItem(
|
|
||||||
iconRes = R.drawable.rave_now_nav_logout,
|
|
||||||
label = stringResource(R.string.logout),
|
|
||||||
modifier = Modifier.noRippleClickable {
|
|
||||||
coroutineScope.launch {
|
|
||||||
drawerState.close()
|
|
||||||
// 只有非游客用户才需要取消注册推送设备
|
|
||||||
if (!AppStore.isGuest) {
|
|
||||||
Messaging.unregisterDevice(context)
|
|
||||||
}
|
|
||||||
AppStore.apply {
|
|
||||||
token = null
|
|
||||||
rememberMe = false
|
|
||||||
isGuest = false // 清除游客状态
|
|
||||||
saveData()
|
|
||||||
}
|
|
||||||
// 删除推送渠道
|
|
||||||
|
|
||||||
navController.navigate(NavigationRoute.Login.route) {
|
|
||||||
popUpTo(NavigationRoute.Login.route) {
|
|
||||||
inclusive = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppState.ReloadAppState(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
@@ -447,7 +330,6 @@ fun IndexScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -624,3 +506,369 @@ fun NavItem(
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SideMenuContent(
|
||||||
|
onClose: () -> Unit,
|
||||||
|
navController: androidx.navigation.NavController,
|
||||||
|
context: android.content.Context,
|
||||||
|
isDrawerOpen: Boolean
|
||||||
|
) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var messageNotificationEnabled by remember { mutableStateOf(true) }
|
||||||
|
var darkModeEnabled by remember { mutableStateOf(AppState.darkMode) }
|
||||||
|
|
||||||
|
// 同步暗色模式状态
|
||||||
|
LaunchedEffect(AppState.darkMode) {
|
||||||
|
darkModeEnabled = AppState.darkMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单背景色 - 根据暗色模式适配
|
||||||
|
val menuBackgroundColor = if (darkModeEnabled) {
|
||||||
|
appColors.secondaryBackground // 暗色模式:深灰色
|
||||||
|
} else {
|
||||||
|
Color(0xFFFAF9FB) // 亮色模式:浅灰色
|
||||||
|
}
|
||||||
|
// 遮罩颜色 黑色透明度0.6
|
||||||
|
val overlayColor = Color.Black.copy(alpha = 0.6f)
|
||||||
|
// 卡片背景色 - 根据暗色模式适配
|
||||||
|
val cardBackgroundColor = if (darkModeEnabled) {
|
||||||
|
appColors.background // 暗色模式:深色背景
|
||||||
|
} else {
|
||||||
|
Color.White // 亮色模式:白色
|
||||||
|
}
|
||||||
|
// 文字颜色 - 根据暗色模式适配
|
||||||
|
val textColor = appColors.text
|
||||||
|
// 图标颜色 - 根据暗色模式适配
|
||||||
|
val iconColor = appColors.text
|
||||||
|
// 跟随系统文字颜色 - 根据暗色模式适配
|
||||||
|
val followSystemTextColor = appColors.secondaryText
|
||||||
|
// 开关开启颜色 #7C45ED
|
||||||
|
val switchActiveColor = Color(0xFF7C45ED)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
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
|
||||||
|
.requiredWidth(302.dp)
|
||||||
|
.requiredHeight(874.dp)
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.background(menuBackgroundColor)
|
||||||
|
) {
|
||||||
|
// 顶部状态栏间距
|
||||||
|
val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
||||||
|
|
||||||
|
// 扫一扫功能入口 - 右边距离右边66pt
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = (-112).dp, y = 88.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// TODO: 实现扫一扫功能
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 扫一扫图标(使用现有图标或占位)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.sao),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(iconColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 绝对定位的"扫一扫"文字:上方71.5dp,右侧66dp
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.scan_qr),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = (-66).dp, y = 91.5.dp)
|
||||||
|
)
|
||||||
|
// QR码图标 - 右边距离右边112dp,上边距离上边68pt
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.qr_code_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = (-26).dp, y = 88.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// TODO: 实现QR码功能
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(iconColor)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 菜单选项卡片组 - 第一组卡片上方距离上方108pt(绝对定位)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.offset(y = 128.dp) // 直接距离顶部128dp(整体下移20dp)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// 第一组卡片:编辑资料、账号安全、收藏
|
||||||
|
MenuCard(
|
||||||
|
backgroundColor = cardBackgroundColor,
|
||||||
|
textColor = textColor,
|
||||||
|
iconColor = iconColor,
|
||||||
|
width = 270.dp,
|
||||||
|
height = 164.dp,
|
||||||
|
items = listOf(
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.icons_edited_data,
|
||||||
|
label = stringResource(R.string.edit_profile_info),
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
onClose()
|
||||||
|
navController.navigate(NavigationRoute.AccountEdit.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.icons_account_and_security,
|
||||||
|
label = stringResource(R.string.account_and_security),
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
onClose()
|
||||||
|
navController.navigate(NavigationRoute.AccountSetting.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.collect,
|
||||||
|
label = stringResource(R.string.favourites),
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
onClose()
|
||||||
|
navController.navigate(NavigationRoute.FavouriteList.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 第二组卡片:暗色模式、消息通知
|
||||||
|
MenuCard(
|
||||||
|
backgroundColor = cardBackgroundColor,
|
||||||
|
textColor = textColor,
|
||||||
|
iconColor = iconColor,
|
||||||
|
width = 270.dp,
|
||||||
|
height = 112.dp, // 根据设计图,第二组卡片高度为112dp
|
||||||
|
items = listOf(
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.icons_dark_mode,
|
||||||
|
label = stringResource(R.string.dark_mode),
|
||||||
|
rightContent = {
|
||||||
|
Switch(
|
||||||
|
checked = darkModeEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
darkModeEnabled = it
|
||||||
|
AppState.darkMode = it
|
||||||
|
AppState.appTheme = if (it) {
|
||||||
|
com.aiosman.ravenow.DarkThemeColors()
|
||||||
|
} else {
|
||||||
|
com.aiosman.ravenow.LightThemeColors()
|
||||||
|
}
|
||||||
|
AppStore.saveDarkMode(it)
|
||||||
|
},
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = Color.White,
|
||||||
|
checkedTrackColor = switchActiveColor,
|
||||||
|
uncheckedThumbColor = Color.White,
|
||||||
|
uncheckedTrackColor = switchActiveColor.copy(alpha = 0.5f),
|
||||||
|
uncheckedBorderColor = Color.Transparent
|
||||||
|
),
|
||||||
|
modifier = Modifier.size(width = 64.dp, height = 28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.icons_bell,
|
||||||
|
label = stringResource(R.string.message_notification),
|
||||||
|
rightContent = {
|
||||||
|
Switch(
|
||||||
|
checked = messageNotificationEnabled,
|
||||||
|
onCheckedChange = { messageNotificationEnabled = it },
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = Color.White,
|
||||||
|
checkedTrackColor = switchActiveColor,
|
||||||
|
uncheckedThumbColor = Color.White,
|
||||||
|
uncheckedTrackColor = switchActiveColor.copy(alpha = 0.5f),
|
||||||
|
uncheckedBorderColor = Color.Transparent
|
||||||
|
),
|
||||||
|
modifier = Modifier.size(width = 64.dp, height = 28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 第三组卡片:关于派派、反馈、退出登录
|
||||||
|
MenuCard(
|
||||||
|
backgroundColor = cardBackgroundColor,
|
||||||
|
textColor = textColor,
|
||||||
|
iconColor = iconColor,
|
||||||
|
width = 270.dp,
|
||||||
|
height = 164.dp,
|
||||||
|
items = listOf(
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.icons_about,
|
||||||
|
label = stringResource(R.string.about_paipai),
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
onClose()
|
||||||
|
navController.navigate(NavigationRoute.AboutScreen.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.feedback_icon,
|
||||||
|
label = stringResource(R.string.feedback),
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
onClose()
|
||||||
|
navController.navigate(NavigationRoute.AboutScreen.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
icon = R.mipmap.log_out_icon,
|
||||||
|
label = stringResource(R.string.logout_confirm),
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
onClose()
|
||||||
|
// 只有非游客用户才需要取消注册推送设备
|
||||||
|
if (!AppStore.isGuest) {
|
||||||
|
Messaging.unregisterDevice(context)
|
||||||
|
}
|
||||||
|
AppStore.apply {
|
||||||
|
token = null
|
||||||
|
rememberMe = false
|
||||||
|
isGuest = false
|
||||||
|
saveData()
|
||||||
|
}
|
||||||
|
navController.navigate(NavigationRoute.Login.route) {
|
||||||
|
popUpTo(NavigationRoute.Login.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppState.ReloadAppState(context)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showRightArrow = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MenuItem(
|
||||||
|
val icon: Int,
|
||||||
|
val label: String,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
val rightContent: @Composable (() -> Unit)? = null,
|
||||||
|
val showRightArrow: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MenuCard(
|
||||||
|
backgroundColor: Color,
|
||||||
|
textColor: Color,
|
||||||
|
iconColor: Color,
|
||||||
|
items: List<MenuItem>,
|
||||||
|
width: androidx.compose.ui.unit.Dp? = null,
|
||||||
|
height: androidx.compose.ui.unit.Dp? = null
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.then(if (width != null) Modifier.requiredWidth(width) else Modifier.fillMaxWidth())
|
||||||
|
.then(if (height != null) Modifier.requiredHeight(height) else Modifier)
|
||||||
|
.background(backgroundColor, RoundedCornerShape(16.dp))
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = if (height != null) Arrangement.SpaceEvenly else Arrangement.spacedBy(8.dp) // 固定高度时均匀分布
|
||||||
|
) {
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.then(if (height != null) Modifier.weight(1f) else Modifier),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
MenuItemRow(item = item, compact = height != null, textColor = textColor, iconColor = iconColor) // 传递颜色参数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MenuItemRow(item: MenuItem, compact: Boolean = false, textColor: Color, iconColor: Color) {
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (item.onClick != null) {
|
||||||
|
Modifier.noRippleClickable { item.onClick?.invoke() }
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(vertical = if (compact) 4.dp else 8.dp), // 紧凑模式下减少垂直padding
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = item.icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(iconColor)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.label,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.rightContent != null) {
|
||||||
|
item.rightContent?.invoke()
|
||||||
|
} else if (item.showRightArrow) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rave_now_nav_right),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(appColors.text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,8 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -981,9 +983,13 @@ fun ChatRoomCard(
|
|||||||
Text(
|
Text(
|
||||||
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
|
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier.alpha(0.6f),
|
modifier = Modifier
|
||||||
|
.alpha(0.6f)
|
||||||
|
.weight(1f),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,22 +234,24 @@ object AgentViewModel: ViewModel() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取完整的语言标记(如 "zh-CN")
|
// 获取完整的语言标记(如 "zh-CN")
|
||||||
val sysLang = com.aiosman.ravenow.utils.Utils.getPreferredLanguageTag()
|
val fullLangTag = com.aiosman.ravenow.utils.Utils.getPreferredLanguageTag()
|
||||||
|
// 转换为后端支持的语言代码(仅支持 zh、cn、ja)
|
||||||
|
val sysLang = convertToSupportedLangCode(fullLangTag)
|
||||||
val response = apiClient.getCategories(
|
val response = apiClient.getCategories(
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 100,
|
pageSize = 100,
|
||||||
isActive = true,
|
isActive = true,
|
||||||
withChildren = false,
|
// withChildren = false,
|
||||||
withParent = false,
|
// withParent = false,
|
||||||
withCount = true,
|
// withCount = true,
|
||||||
hideEmpty = true,
|
// hideEmpty = true,
|
||||||
lang = sysLang
|
lang = sysLang
|
||||||
)
|
)
|
||||||
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 语言标记: $sysLang")
|
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 原始语言标记: $fullLangTag, 转换后: $sysLang")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val categoryList = response.body()?.list ?: emptyList()
|
val categoryList = response.body()?.list ?: emptyList()
|
||||||
println("获取到 ${categoryList.size} 个分类")
|
println("获取到 ${categoryList.size} 个分类")
|
||||||
// 使用当前语言获取翻译后的分类名称
|
// 使用转换后的语言代码获取翻译后的分类名称
|
||||||
categories = categoryList.map { category ->
|
categories = categoryList.map { category ->
|
||||||
CategoryItem.fromCategoryTemplate(category, sysLang)
|
CategoryItem.fromCategoryTemplate(category, sysLang)
|
||||||
}
|
}
|
||||||
@@ -266,6 +268,24 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将完整的语言标记转换为后端支持的语言代码
|
||||||
|
* 后端仅支持: zh, cn, ja
|
||||||
|
*
|
||||||
|
* @param langTag 完整的语言标记,如 "zh-CN", "zh-TW", "ja-JP", "en-US" 等
|
||||||
|
* @return 后端支持的语言代码,默认返回 "zh"
|
||||||
|
*/
|
||||||
|
private fun convertToSupportedLangCode(langTag: String): String {
|
||||||
|
return when {
|
||||||
|
langTag.startsWith("zh", ignoreCase = true) -> "zh"
|
||||||
|
langTag.startsWith("ja", ignoreCase = true) -> "ja"
|
||||||
|
// 如果是中文相关的其他标记,也返回 zh
|
||||||
|
langTag.equals("cn", ignoreCase = true) -> "cn"
|
||||||
|
// 默认返回中文
|
||||||
|
else -> "zh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadAgentsByCategory(categoryId: Int) {
|
fun loadAgentsByCategory(categoryId: Int) {
|
||||||
loadAgentData(categoryId)
|
loadAgentData(categoryId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -108,13 +109,21 @@ fun AgentChatListScreen() {
|
|||||||
text = stringResource(R.string.agent_chat_empty_title),
|
text = stringResource(R.string.agent_chat_empty_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.agent_chat_empty_subtitle),
|
text = stringResource(R.string.agent_chat_empty_subtitle),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -130,13 +139,21 @@ fun AgentChatListScreen() {
|
|||||||
text = stringResource(R.string.friend_chat_no_network_title),
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ReloadButton(
|
ReloadButton(
|
||||||
|
|||||||
@@ -186,13 +186,21 @@ fun AllChatListScreen() {
|
|||||||
text = stringResource(R.string.friend_chat_empty_title),
|
text = stringResource(R.string.friend_chat_empty_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.friend_chat_empty_subtitle),
|
text = stringResource(R.string.friend_chat_empty_subtitle),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.height(39.dp))
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
@@ -207,13 +215,21 @@ fun AllChatListScreen() {
|
|||||||
text = stringResource(R.string.friend_chat_no_network_title),
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ReloadButton(
|
ReloadButton(
|
||||||
|
|||||||
@@ -96,13 +96,21 @@ fun FriendChatListScreen() {
|
|||||||
text = stringResource(R.string.friend_chat_empty_title),
|
text = stringResource(R.string.friend_chat_empty_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.friend_chat_empty_subtitle),
|
text = stringResource(R.string.friend_chat_empty_subtitle),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}else {
|
}else {
|
||||||
Spacer(modifier = Modifier.height(39.dp))
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
@@ -117,13 +125,21 @@ fun FriendChatListScreen() {
|
|||||||
text = stringResource(R.string.friend_chat_no_network_title),
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ReloadButton(
|
ReloadButton(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -88,13 +89,21 @@ fun GroupChatListScreen() {
|
|||||||
text = stringResource(R.string.group_chat_empty),
|
text = stringResource(R.string.group_chat_empty),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.group_chat_empty_join),
|
text = stringResource(R.string.group_chat_empty_join),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}else {
|
}else {
|
||||||
Spacer(modifier = Modifier.height(39.dp))
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
@@ -109,13 +118,21 @@ fun GroupChatListScreen() {
|
|||||||
text = stringResource(R.string.friend_chat_no_network_title),
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ReloadButton(
|
ReloadButton(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.aiosman.ravenow.event.MomentAddEvent
|
|||||||
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentRemoveEvent
|
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||||
|
import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
@@ -122,8 +123,9 @@ open class BaseMomentModel :ViewModel(){
|
|||||||
userService.unFollowUser(moment.authorId.toString())
|
userService.unFollowUser(moment.authorId.toString())
|
||||||
EventBus.getDefault().post(FollowChangeEvent(moment.authorId, false))
|
EventBus.getDefault().post(FollowChangeEvent(moment.authorId, false))
|
||||||
} else {
|
} else {
|
||||||
userService.followUser(moment.authorId.toString())
|
// 调用 FollowerNoticeViewModel.followUser() 实现与 FollowerNotice.kt 相同的效果
|
||||||
EventBus.getDefault().post(FollowChangeEvent(moment.authorId, true))
|
// 该方法内部会调用 userService.followUser() 并发布 FollowChangeEvent 事件
|
||||||
|
FollowerNoticeViewModel.followUser(moment.authorId)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import com.aiosman.ravenow.ui.composables.TabItem
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
import com.aiosman.ravenow.ui.composables.UnderlineTabItem
|
import com.aiosman.ravenow.ui.composables.UnderlineTabItem
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.shorts.ShortVideoScreen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态列表
|
* 动态列表
|
||||||
@@ -71,8 +72,8 @@ fun MomentsList() {
|
|||||||
val navigationBarPaddings =
|
val navigationBarPaddings =
|
||||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
// 现在有6个tab:推荐、短视频、新闻、探索、关注、热门
|
// 根据登录状态设置标签页数量:游客模式5个tab,非游客模式6个tab
|
||||||
val tabCount = 6
|
val tabCount = if (AppStore.isGuest) 5 else 6
|
||||||
var pagerState = rememberPagerState { tabCount }
|
var pagerState = rememberPagerState { tabCount }
|
||||||
var scope = rememberCoroutineScope()
|
var scope = rememberCoroutineScope()
|
||||||
Column(
|
Column(
|
||||||
@@ -173,14 +174,14 @@ fun MomentsList() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 热门标签 (游客模式)
|
// 热门标签 (游客模式) - 在游客模式下,热门标签对应第3页
|
||||||
UnderlineTabItem(
|
UnderlineTabItem(
|
||||||
text = stringResource(R.string.index_hot),
|
text = stringResource(R.string.index_hot),
|
||||||
isSelected = pagerState.currentPage == 4,
|
isSelected = pagerState.currentPage == 3,
|
||||||
onClick = {
|
onClick = {
|
||||||
tabDebouncer {
|
tabDebouncer {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(4)
|
pagerState.animateScrollToPage(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,14 +189,15 @@ fun MomentsList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 新闻标签
|
// 新闻标签 - 在游客模式下对应第4页,非游客模式下对应第5页
|
||||||
|
val newsPageIndex = if (AppStore.isGuest) 4 else 5
|
||||||
UnderlineTabItem(
|
UnderlineTabItem(
|
||||||
text = stringResource(R.string.tab_news),
|
text = stringResource(R.string.tab_news),
|
||||||
isSelected = pagerState.currentPage == 5,
|
isSelected = pagerState.currentPage == newsPageIndex,
|
||||||
onClick = {
|
onClick = {
|
||||||
tabDebouncer {
|
tabDebouncer {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(5)
|
pagerState.animateScrollToPage(newsPageIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,6 +236,7 @@ fun MomentsList() {
|
|||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
// 短视频页面
|
// 短视频页面
|
||||||
|
ShortVideoScreen()
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
// 动态页面 - 暂时显示时间线内容
|
// 动态页面 - 暂时显示时间线内容
|
||||||
@@ -248,17 +251,23 @@ fun MomentsList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
4 -> {
|
4 -> {
|
||||||
// 热门页面 (仅非游客用户)
|
// 热门页面 (仅非游客用户) 或 新闻页面 (游客用户)
|
||||||
|
if (AppStore.isGuest) {
|
||||||
|
NewsScreen()
|
||||||
|
} else {
|
||||||
HotMomentsList()
|
HotMomentsList()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
5 -> {
|
5 -> {
|
||||||
// 新闻页面
|
// 新闻页面 (仅非游客用户)
|
||||||
|
if (!AppStore.isGuest) {
|
||||||
NewsScreen()
|
NewsScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomTabItem(
|
fun CustomTabItem(
|
||||||
text: String,
|
text: String,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.items
|
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
@@ -137,8 +139,18 @@ fun DiscoverView() {
|
|||||||
val debouncer = rememberDebouncer()
|
val debouncer = rememberDebouncer()
|
||||||
|
|
||||||
val textContent = momentItem.momentTextContent
|
val textContent = momentItem.momentTextContent
|
||||||
|
// 对于英文和日文,每行字符数会更少,使用更保守的估算
|
||||||
|
val estimatedCharsPerLine = if (textContent.isNotEmpty()) {
|
||||||
|
// 检测是否包含非中文字符(英文、日文等)
|
||||||
|
val hasNonChinese = textContent.any {
|
||||||
|
val code = it.code
|
||||||
|
!(code >= 0x4E00 && code <= 0x9FFF) // 不在中文字符范围内
|
||||||
|
}
|
||||||
|
if (hasNonChinese) 15 else 20 // 英文/日文每行更少字符
|
||||||
|
} else {
|
||||||
|
20
|
||||||
|
}
|
||||||
val textLines = if (textContent.isNotEmpty()) {
|
val textLines = if (textContent.isNotEmpty()) {
|
||||||
val estimatedCharsPerLine = 20
|
|
||||||
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
|
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
|
||||||
minOf(estimatedLines, 2) // 最多2行
|
minOf(estimatedLines, 2) // 最多2行
|
||||||
} else {
|
} else {
|
||||||
@@ -146,8 +158,8 @@ fun DiscoverView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val baseHeight = 200.dp
|
val baseHeight = 200.dp
|
||||||
val singleLineTextHeight = 20.dp
|
val singleLineTextHeight = 24.dp // 增加高度以适应英文/日文
|
||||||
val doubleLineTextHeight = 40.dp
|
val doubleLineTextHeight = 44.dp // 增加高度以适应英文/日文
|
||||||
val authorInfoHeight = 25.dp
|
val authorInfoHeight = 25.dp
|
||||||
val paddingHeight = 10.dp
|
val paddingHeight = 10.dp
|
||||||
val paddingHeight2 = 3.dp
|
val paddingHeight2 = 3.dp
|
||||||
@@ -160,7 +172,6 @@ fun DiscoverView() {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(totalHeight)
|
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
debouncer {
|
debouncer {
|
||||||
@@ -173,7 +184,9 @@ fun DiscoverView() {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = momentItem.images[0].thumbnail,
|
imageUrl = momentItem.images[0].thumbnail,
|
||||||
@@ -193,9 +206,9 @@ fun DiscoverView() {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(totalHeight - baseHeight)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
|
// 文本内容区域,限制最大高度
|
||||||
if (momentItem.momentTextContent.isNotEmpty()) {
|
if (momentItem.momentTextContent.isNotEmpty()) {
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
text = momentItem.momentTextContent,
|
text = momentItem.momentTextContent,
|
||||||
@@ -203,13 +216,19 @@ fun DiscoverView() {
|
|||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
lineHeight = 16.sp // 设置行高以适应不同语言
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用 Spacer 确保头像昵称栏始终在底部,有足够的空间
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// 头像昵称栏,确保始终完整显示,设置最小高度避免被挤压
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 25.dp) // 最小高度确保完整显示,自适应避免被挤压
|
||||||
.padding(top = 5.dp),
|
.padding(top = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -225,7 +244,9 @@ fun DiscoverView() {
|
|||||||
|
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
text = momentItem.nickname,
|
text = momentItem.nickname,
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier
|
||||||
|
.padding(start = 4.dp)
|
||||||
|
.weight(1f),
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
color = AppColors.text.copy(alpha = 0.6f),
|
color = AppColors.text.copy(alpha = 0.6f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.shorts
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.shorts.ShortViewCompose
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短视频页面
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ShortVideoScreen() {
|
||||||
|
val viewModel = ShortVideoViewModel
|
||||||
|
val allMoments = viewModel.moments
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val momentLoader = viewModel.momentLoader
|
||||||
|
// 记录当前播放的短视频索引,切换 Tab 返回时恢复
|
||||||
|
val currentIndex = rememberSaveable { androidx.compose.runtime.mutableStateOf(0) }
|
||||||
|
|
||||||
|
// 过滤出包含视频的动态
|
||||||
|
val videoMoments = remember(allMoments) {
|
||||||
|
val filtered = allMoments.filter { it.videos != null && it.videos.isNotEmpty() }
|
||||||
|
Log.d("ShortVideoScreen", "过滤视频动态 - 总动态数: ${allMoments.size}, 包含视频的动态数: ${filtered.size}")
|
||||||
|
filtered.forEach { moment ->
|
||||||
|
Log.d("ShortVideoScreen", "视频动态 ID: ${moment.id}, 视频数: ${moment.videos?.size}, 第一个视频URL: ${moment.videos?.firstOrNull()?.url}")
|
||||||
|
}
|
||||||
|
filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载数据
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
Log.d("ShortVideoScreen", "开始加载数据")
|
||||||
|
viewModel.refreshPager()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多数据
|
||||||
|
LaunchedEffect(allMoments.size, videoMoments.size) {
|
||||||
|
Log.d("ShortVideoScreen", "检查是否需要加载更多 - allMoments: ${allMoments.size}, videoMoments: ${videoMoments.size}, hasNext: ${momentLoader.hasNext}, isLoading: ${momentLoader.isLoading}")
|
||||||
|
if (allMoments.isNotEmpty() && videoMoments.size < 10 && momentLoader.hasNext && !momentLoader.isLoading) {
|
||||||
|
// 如果视频数量少于10个,尝试加载更多
|
||||||
|
Log.d("ShortVideoScreen", "开始加载更多数据")
|
||||||
|
viewModel.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
if (momentLoader.isLoading && videoMoments.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = AppColors.main)
|
||||||
|
Text(
|
||||||
|
text = "加载中...",
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 错误状态
|
||||||
|
else if (momentLoader.error != null && videoMoments.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "加载失败: ${momentLoader.error}",
|
||||||
|
color = AppColors.error,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 空状态 - 已加载但无视频
|
||||||
|
else if (!momentLoader.isLoading && videoMoments.isEmpty() && allMoments.isNotEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "暂无短视频\n已加载 ${allMoments.size} 条动态,但都不包含视频",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 初始状态 - 还没有加载过数据
|
||||||
|
else if (!momentLoader.isLoading && videoMoments.isEmpty() && allMoments.isEmpty() && momentLoader.error == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "准备加载...",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 显示视频列表
|
||||||
|
else if (videoMoments.isNotEmpty()) {
|
||||||
|
ShortViewCompose(
|
||||||
|
videoMoments = videoMoments,
|
||||||
|
clickItemPosition = currentIndex.value,
|
||||||
|
onLikeClick = { moment ->
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
if (moment.liked) {
|
||||||
|
viewModel.dislikeMoment(moment.id)
|
||||||
|
} else {
|
||||||
|
viewModel.likeMoment(moment.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCommentClick = { moment ->
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onAddComment(moment.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFavoriteClick = { moment ->
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
if (moment.isFavorite) {
|
||||||
|
viewModel.unfavoriteMoment(moment.id)
|
||||||
|
} else {
|
||||||
|
viewModel.favoriteMoment(moment.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShareClick = { moment ->
|
||||||
|
// TODO: 实现分享功能
|
||||||
|
},
|
||||||
|
onPageChanged = { idx -> currentIndex.value = idx }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.shorts
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
|
object ShortVideoViewModel : BaseMomentModel() {
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
return MomentLoaderExtraArgs(explore = true, videoOnly = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取包含视频的动态列表
|
||||||
|
fun getVideoMoments(): List<com.aiosman.ravenow.entity.MomentEntity> {
|
||||||
|
return moments.filter { it.videos != null && it.videos.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import android.util.Log
|
|||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -86,8 +87,8 @@ import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaff
|
|||||||
import com.aiosman.ravenow.ui.index.IndexViewModel
|
import com.aiosman.ravenow.ui.index.IndexViewModel
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
|
||||||
import com.aiosman.ravenow.ui.post.MenuActionItem
|
import com.aiosman.ravenow.ui.post.MenuActionItem
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction
|
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
|
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
|
||||||
@@ -100,6 +101,13 @@ import kotlinx.coroutines.launch
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -121,7 +129,8 @@ fun ProfileV3(
|
|||||||
postCount: Long? = null, // 新增参数用于传递帖子总数
|
postCount: Long? = null, // 新增参数用于传递帖子总数
|
||||||
) {
|
) {
|
||||||
val model = MyProfileViewModel
|
val model = MyProfileViewModel
|
||||||
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
|
// Tabs: 动态、(可选)智能体、群聊
|
||||||
|
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 2 else 3 })
|
||||||
val enabled by remember { mutableStateOf(true) }
|
val enabled by remember { mutableStateOf(true) }
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
@@ -134,7 +143,8 @@ fun ProfileV3(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val bannerHeight = 400
|
val bannerWidth = 402
|
||||||
|
val bannerHeight = 206
|
||||||
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
||||||
context,
|
context,
|
||||||
scope,
|
scope,
|
||||||
@@ -156,12 +166,13 @@ fun ProfileV3(
|
|||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
val toolbarAlpha by remember {
|
// 计算导航栏背景透明度,根据滚动位置从0到1
|
||||||
|
val toolbarBackgroundAlpha by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (!isSelf) {
|
if (!isSelf) {
|
||||||
1f
|
1f
|
||||||
} else {
|
} else {
|
||||||
val maxScroll = 500f // 最大滚动距离,可调整
|
val maxScroll = 600f // 增加最大滚动距离,让渐变更平缓
|
||||||
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
|
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -308,12 +319,19 @@ fun ProfileV3(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(bannerHeight.dp)
|
.height(bannerHeight.dp)
|
||||||
.background(AppColors.profileBackground)
|
.background(AppColors.profileBackground),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.width(bannerWidth.dp)
|
||||||
.height(bannerHeight.dp - 24.dp)
|
.height(bannerHeight.dp)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
|
bottomStart = 32.dp,
|
||||||
|
bottomEnd = 32.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
.let {
|
.let {
|
||||||
if (isSelf && isMain) {
|
if (isSelf && isMain) {
|
||||||
it.noRippleClickable {
|
it.noRippleClickable {
|
||||||
@@ -326,13 +344,6 @@ fun ProfileV3(
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.shadow(
|
|
||||||
elevation = 6.dp,
|
|
||||||
shape = RoundedCornerShape(
|
|
||||||
bottomStart = 32.dp,
|
|
||||||
bottomEnd = 32.dp
|
|
||||||
),
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
LocalContext.current,
|
LocalContext.current,
|
||||||
@@ -347,6 +358,9 @@ fun ProfileV3(
|
|||||||
Spacer(modifier = Modifier.height(100.dp))
|
Spacer(modifier = Modifier.height(100.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 壁纸下方间距
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -357,31 +371,25 @@ fun ProfileV3(
|
|||||||
profile?.let {
|
profile?.let {
|
||||||
UserItem(
|
UserItem(
|
||||||
accountProfileEntity = it,
|
accountProfileEntity = it,
|
||||||
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size.toLong()
|
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size.toLong(),
|
||||||
|
isSelf = isSelf,
|
||||||
|
onEditClick = {
|
||||||
|
navController.navigate(NavigationRoute.AccountEdit.route)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
// 操作按钮
|
// 操作按钮(仅其他用户显示)
|
||||||
profile?.let {
|
profile?.let {
|
||||||
|
if (!isSelf && it.id != AppState.UserId) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (isSelf) {
|
|
||||||
SelfProfileAction(
|
|
||||||
onEditProfile = {
|
|
||||||
navController.navigate(NavigationRoute.AccountEdit.route)
|
|
||||||
},
|
|
||||||
onPremiumClick = {
|
|
||||||
navController.navigate(NavigationRoute.VipSelPage.route)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (it.id != AppState.UserId) {
|
|
||||||
OtherProfileAction(
|
OtherProfileAction(
|
||||||
it,
|
it,
|
||||||
onFollow = {
|
onFollow = {
|
||||||
@@ -394,7 +402,6 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 用户智能体行
|
// 用户智能体行
|
||||||
if (!isAiAccount) {
|
if (!isAiAccount) {
|
||||||
@@ -445,15 +452,14 @@ fun ProfileV3(
|
|||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
showAgentTab = !isAiAccount
|
showAgentTab = !isAiAccount
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.height(500.dp) // 固定滚动高度
|
modifier = Modifier.height(500.dp) // 固定滚动高度
|
||||||
) { idx ->
|
) { idx ->
|
||||||
when (idx) {
|
when (idx) {
|
||||||
0 ->
|
0 -> GalleryGrid(moments = moments)
|
||||||
GalleryGrid(moments = moments)
|
1 -> {
|
||||||
1 ->
|
if (!isAiAccount) {
|
||||||
UserAgentsList(
|
UserAgentsList(
|
||||||
agents = agents,
|
agents = agents,
|
||||||
onAgentClick = onAgentClick,
|
onAgentClick = onAgentClick,
|
||||||
@@ -475,10 +481,22 @@ fun ProfileV3(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
GroupChatPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
if (!isAiAccount) {
|
||||||
|
GroupChatPlaceholder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部间距,增加滚动距离
|
||||||
|
Spacer(modifier = Modifier.height(100.dp))
|
||||||
|
}
|
||||||
|
|
||||||
// 顶部导航栏
|
// 顶部导航栏
|
||||||
TopNavigationBar(
|
TopNavigationBar(
|
||||||
@@ -486,9 +504,13 @@ fun ProfileV3(
|
|||||||
isSelf = isSelf,
|
isSelf = isSelf,
|
||||||
profile = profile,
|
profile = profile,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
alpha = toolbarAlpha,
|
backgroundAlpha = toolbarBackgroundAlpha,
|
||||||
|
interactionCount = moments.sumOf { it.likeCount }, // 计算总点赞数作为互动数据
|
||||||
onMenuClick = {
|
onMenuClick = {
|
||||||
showOtherUserMenu = true
|
showOtherUserMenu = true
|
||||||
|
},
|
||||||
|
onShareClick = {
|
||||||
|
// TODO: 实现分享功能
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -562,6 +584,11 @@ fun ProfileV3(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GroupChatPlaceholder() {
|
||||||
|
GroupChatEmptyContent()
|
||||||
|
}
|
||||||
|
|
||||||
//顶部导航栏组件
|
//顶部导航栏组件
|
||||||
@Composable
|
@Composable
|
||||||
fun TopNavigationBar(
|
fun TopNavigationBar(
|
||||||
@@ -569,30 +596,182 @@ fun TopNavigationBar(
|
|||||||
isSelf: Boolean,
|
isSelf: Boolean,
|
||||||
profile: AccountProfileEntity?,
|
profile: AccountProfileEntity?,
|
||||||
navController: androidx.navigation.NavController,
|
navController: androidx.navigation.NavController,
|
||||||
alpha: Float,
|
backgroundAlpha: Float,
|
||||||
onMenuClick: () -> Unit = {}
|
interactionCount: Int = 0,
|
||||||
|
onMenuClick: () -> Unit = {},
|
||||||
|
onShareClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
|
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||||
|
|
||||||
|
// 根据背景透明度和暗色模式决定图标颜色
|
||||||
|
// 暗色模式下:图标始终为白色
|
||||||
|
// 亮色模式下:根据背景透明度决定,透明度为1时变黑,否则为白色
|
||||||
|
val iconColor = if (AppState.darkMode) {
|
||||||
|
Color.White // 暗色模式下图标始终为白色
|
||||||
|
} else {
|
||||||
|
if (backgroundAlpha >= 1f) Color.Black else Color.White
|
||||||
|
}
|
||||||
|
val cardBorderColor = if (AppState.darkMode) {
|
||||||
|
Color.White // 暗色模式下边框应为白色
|
||||||
|
} else {
|
||||||
|
if (backgroundAlpha >= 1f) Color.Black else Color.White
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.graphicsLayer { this.alpha = alpha }
|
|
||||||
) {
|
) {
|
||||||
Column(
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
val statusBarHeight = statusBarPadding.calculateTopPadding()
|
||||||
|
val navigationBarHeight = 56.dp // 增加导航栏高度,包括图标和额外空间
|
||||||
|
|
||||||
|
// 导航栏背景层,包括状态栏区域,根据滚动位置逐渐变白
|
||||||
|
val totalHeight = statusBarHeight + navigationBarHeight
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val totalHeightPx = with(density) { totalHeight.toPx() }
|
||||||
|
|
||||||
|
// 根据滚动位置计算基础颜色,从深色平滑过渡到白色,透明度从初始值逐渐减到0
|
||||||
|
val baseColor = remember(backgroundAlpha) {
|
||||||
|
val smoothProgress = backgroundAlpha.coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
if (AppState.darkMode) {
|
||||||
|
// 暗色模式下:从黑色(透明度0)逐渐变为黑色(透明度1)
|
||||||
|
val alpha = smoothProgress.coerceIn(0f, 1f)
|
||||||
|
Color.Black.copy(alpha = alpha)
|
||||||
|
} else {
|
||||||
|
// 亮色模式:初始状态:半透明深色,让白色图标清晰可见
|
||||||
|
val initialDarkAlpha = 0.12f
|
||||||
|
|
||||||
|
// 使用平滑的插值函数,让整个过渡更自然
|
||||||
|
val easedProgress = smoothProgress * smoothProgress * (3f - 2f * smoothProgress) // smoothstep
|
||||||
|
|
||||||
|
// 颜色值:从黑色(0)平滑过渡到白色(1)
|
||||||
|
val colorValue = easedProgress
|
||||||
|
|
||||||
|
// 透明度:从初始值(0.12f)逐渐减少到0
|
||||||
|
// 当smoothProgress从0到1时,alpha从initialDarkAlpha减少到0
|
||||||
|
val alpha = initialDarkAlpha * (1f - easedProgress)
|
||||||
|
|
||||||
|
Color(
|
||||||
|
red = colorValue,
|
||||||
|
green = colorValue,
|
||||||
|
blue = colorValue,
|
||||||
|
alpha = alpha.coerceIn(0f, initialDarkAlpha)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(appColors.profileBackground)
|
.height(totalHeight) // 状态栏高度 + 导航栏高度
|
||||||
) {
|
.align(Alignment.TopCenter)
|
||||||
StatusBarSpacer()
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
baseColor, // 顶部保持基础颜色
|
||||||
|
baseColor, // 中间保持基础颜色
|
||||||
|
baseColor.copy(alpha = baseColor.alpha * 0.5f), // 底部过渡,逐渐变透明
|
||||||
|
Color.Transparent // 最底部完全透明
|
||||||
|
),
|
||||||
|
startY = 0f,
|
||||||
|
endY = totalHeightPx
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 功能按钮区域,图标和文字根据背景透明度改变颜色
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp) // 增加上下内边距
|
||||||
.noRippleClickable {
|
.align(Alignment.TopEnd)
|
||||||
},
|
.padding(top = statusBarHeight), // 从状态栏下方开始
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
// 左侧:互动数据卡片
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.White.copy(alpha = 0.52f),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 0.5.dp,
|
||||||
|
color = cardBorderColor, // 根据背景透明度改变边框颜色
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
// 互动图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.paip_coin_img),
|
||||||
|
contentDescription = "互动",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = numberFormat.format(interactionCount),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W500,
|
||||||
|
color = if (AppState.darkMode) Color.White else Color.Black, // 暗色模式下为白色,亮色模式下为黑色
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// 中间:分享图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.menu_icon),
|
||||||
|
contentDescription = "分享",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onShareClick()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(iconColor) // 根据背景透明度改变颜色
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// 右侧:菜单图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.menu_ico),
|
||||||
|
contentDescription = "菜单",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (isSelf && isMain) {
|
||||||
|
IndexViewModel.openDrawer = true
|
||||||
|
} else {
|
||||||
|
onMenuClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(iconColor) // 根据背景透明度改变颜色
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是主页面,显示返回按钮和用户信息
|
||||||
if (!isMain) {
|
if (!isMain) {
|
||||||
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
// 判断是否有背景图
|
||||||
|
val hasBanner = profile?.banner != null
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.padding(top = statusBarPadding.calculateTopPadding()),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 返回按钮:深色模式下为白色,亮色模式下为黑色
|
||||||
|
val backButtonColor = if (AppState.darkMode) Color.White else Color.Black
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
@@ -601,72 +780,92 @@ fun TopNavigationBar(
|
|||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
.size(24.dp),
|
.size(24.dp),
|
||||||
colorFilter = ColorFilter.tint(appColors.text)
|
colorFilter = ColorFilter.tint(backButtonColor) // 深色模式下为白色,亮色模式下为黑色
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 未设置背景的用户(自己的主页且没有背景图)显示昵称
|
||||||
|
if (isSelf && !hasBanner && profile != null) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
CustomAsyncImage(
|
|
||||||
LocalContext.current,
|
|
||||||
profile?.avatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
contentDescription = "",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = profile?.nickName ?: "",
|
text = profile.nickName ?: "",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.W600,
|
||||||
color = appColors.text
|
color = backButtonColor // 深色模式下为白色,亮色模式下为黑色
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
if (isSelf && isMain) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.padding(16.dp)
|
|
||||||
)
|
|
||||||
} else if (!isSelf) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.noRippleClickable {
|
|
||||||
onMenuClick()
|
|
||||||
}
|
}
|
||||||
.padding(16.dp)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享图标(向上箭头)
|
||||||
|
@Composable
|
||||||
|
fun ShareIcon(
|
||||||
|
color: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Icon(
|
Canvas(modifier = modifier) {
|
||||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
val strokeWidth = 2.dp.toPx()
|
||||||
contentDescription = "菜单",
|
val centerX = size.width / 2
|
||||||
tint = appColors.text,
|
val centerY = size.height / 2
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
|
// 绘制向上的箭头
|
||||||
|
// 底部横线
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(centerX - 9.dp.toPx(), centerY + 6.dp.toPx()),
|
||||||
|
end = Offset(centerX + 9.dp.toPx(), centerY + 6.dp.toPx()),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
// 顶部横线
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(centerX - 5.dp.toPx(), centerY - 6.5.dp.toPx()),
|
||||||
|
end = Offset(centerX + 5.dp.toPx(), centerY - 6.5.dp.toPx()),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
// 中间竖线
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(centerX, centerY - 3.dp.toPx()),
|
||||||
|
end = Offset(centerX, centerY + 6.dp.toPx()),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// 菜单图标(三条横线)
|
||||||
}
|
@Composable
|
||||||
if (isSelf && isMain) {
|
fun MenuIcon(
|
||||||
Box(
|
color: Color,
|
||||||
modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(top = 32.dp, end = 16.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
IndexViewModel.openDrawer = true
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Canvas(modifier = modifier) {
|
||||||
modifier = Modifier.padding(16.dp)
|
val strokeWidth = 2.dp.toPx()
|
||||||
) {
|
val centerX = size.width / 2
|
||||||
Icon(
|
val centerY = size.height / 2
|
||||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
val lineLength = 16.dp.toPx()
|
||||||
contentDescription = "",
|
val spacing = 6.dp.toPx()
|
||||||
tint = appColors.text
|
|
||||||
|
// 绘制三条横线
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(centerX - lineLength / 2, centerY - spacing),
|
||||||
|
end = Offset(centerX + lineLength / 2, centerY - spacing),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(centerX - lineLength / 2, centerY),
|
||||||
|
end = Offset(centerX + lineLength / 2, centerY),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(centerX - lineLength / 2, centerY + spacing),
|
||||||
|
end = Offset(centerX + lineLength / 2, centerY + spacing),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
@@ -199,19 +201,27 @@ fun GalleryGrid(
|
|||||||
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
|
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "你的故事还没开始",
|
text = stringResource(R.string.your_story_not_started),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "发布一条动态,和世界打个招呼吧",
|
text = stringResource(R.string.publish_moment_greeting),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -221,7 +231,7 @@ fun GalleryGrid(
|
|||||||
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
|
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(moments) { idx, moment ->
|
itemsIndexed(moments) { idx, moment ->
|
||||||
if (moment != null) {
|
if (moment != null && moment.images.isNotEmpty()) {
|
||||||
val itemDebouncer = rememberDebouncer()
|
val itemDebouncer = rememberDebouncer()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.graphics.Color
|
||||||
|
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 GroupChatEmptyContent() {
|
||||||
|
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 分段控制器
|
||||||
|
SegmentedControl(
|
||||||
|
selectedIndex = selectedSegment,
|
||||||
|
onSegmentSelected = { selectedSegment = it },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 空状态内容(居中)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// 空状态插图
|
||||||
|
EmptyStateIllustration()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(9.dp))
|
||||||
|
|
||||||
|
// 空状态文本
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.empty_nothing),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SegmentedControl(
|
||||||
|
selectedIndex: Int,
|
||||||
|
onSegmentSelected: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(32.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 全部
|
||||||
|
SegmentButton(
|
||||||
|
text = stringResource(R.string.chat_all),
|
||||||
|
isSelected = selectedIndex == 0,
|
||||||
|
onClick = { onSegmentSelected(0) },
|
||||||
|
width = 54.dp,
|
||||||
|
appColors = AppColors
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// 公开
|
||||||
|
SegmentButton(
|
||||||
|
text = stringResource(R.string.public_label),
|
||||||
|
isSelected = selectedIndex == 1,
|
||||||
|
onClick = { onSegmentSelected(1) },
|
||||||
|
width = 59.dp,
|
||||||
|
appColors = AppColors
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// 私有
|
||||||
|
SegmentButton(
|
||||||
|
text = stringResource(R.string.private_label),
|
||||||
|
isSelected = selectedIndex == 2,
|
||||||
|
onClick = { onSegmentSelected(2) },
|
||||||
|
width = 54.dp,
|
||||||
|
appColors = AppColors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SegmentButton(
|
||||||
|
text: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
width: androidx.compose.ui.unit.Dp,
|
||||||
|
appColors: com.aiosman.ravenow.AppThemeData
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(width)
|
||||||
|
.height(32.dp)
|
||||||
|
.background(
|
||||||
|
color = if (isSelected) {
|
||||||
|
appColors.checkedBackground // 使用选中背景色(暗色模式下是白色,亮色模式下是黑色)
|
||||||
|
} else {
|
||||||
|
Color(0x147C7480) // RGB(124, 116, 128, alpha 0.08)
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(1000.dp)
|
||||||
|
)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = if (isSelected) {
|
||||||
|
appColors.checkedText // 选中时使用选中文本颜色(暗色模式下是黑色,亮色模式下是白色)
|
||||||
|
} else {
|
||||||
|
appColors.text // 未选中时使用文本颜色
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ fun OtherProfileAction(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (profile.isFollowing) "已关注" else stringResource(R.string.follow_upper),
|
text = if (profile.isFollowing) stringResource(R.string.follow_upper_had) else stringResource(R.string.follow_upper),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.W900,
|
fontWeight = FontWeight.W900,
|
||||||
color = if (profile.isFollowing) {
|
color = if (profile.isFollowing) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -60,15 +61,14 @@ fun UserAgentsList(
|
|||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
if (agents.isEmpty()) {
|
||||||
|
// 使用带分段控制器的空状态布局
|
||||||
|
AgentEmptyContentWithSegments()
|
||||||
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
if (agents.isEmpty()) {
|
|
||||||
item {
|
|
||||||
EmptyAgentsView()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items(agents) { agent ->
|
items(agents) { agent ->
|
||||||
UserAgentCard(
|
UserAgentCard(
|
||||||
agent = agent,
|
agent = agent,
|
||||||
@@ -76,7 +76,6 @@ fun UserAgentsList(
|
|||||||
onAvatarClick = onAvatarClick
|
onAvatarClick = onAvatarClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 底部间距
|
// 底部间距
|
||||||
item {
|
item {
|
||||||
@@ -84,6 +83,7 @@ fun UserAgentsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserAgentCard(
|
fun UserAgentCard(
|
||||||
@@ -198,6 +198,188 @@ fun UserAgentCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AgentEmptyContentWithSegments() {
|
||||||
|
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 分段控制器
|
||||||
|
AgentSegmentedControl(
|
||||||
|
selectedIndex = selectedSegment,
|
||||||
|
onSegmentSelected = { selectedSegment = it },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 空状态内容(使用智能体原本的图标和文字)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
if (isNetworkAvailable) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if(AppState.darkMode) R.mipmap.ai_dark
|
||||||
|
else R.mipmap.ai),
|
||||||
|
contentDescription = "暂无Agent",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 181.dp, height = 153.dp)
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 根据是否为深色模式调整间距
|
||||||
|
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.exclusive_ai_waiting),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
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.ai_companion_not_tool),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
} 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 = {
|
||||||
|
MyProfileViewModel.ResetModel()
|
||||||
|
MyProfileViewModel.loadProfile(pullRefresh = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AgentSegmentedControl(
|
||||||
|
selectedIndex: Int,
|
||||||
|
onSegmentSelected: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(32.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 全部
|
||||||
|
AgentSegmentButton(
|
||||||
|
text = stringResource(R.string.chat_all),
|
||||||
|
isSelected = selectedIndex == 0,
|
||||||
|
onClick = { onSegmentSelected(0) },
|
||||||
|
width = 54.dp,
|
||||||
|
appColors = AppColors
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// 公开
|
||||||
|
AgentSegmentButton(
|
||||||
|
text = stringResource(R.string.public_label),
|
||||||
|
isSelected = selectedIndex == 1,
|
||||||
|
onClick = { onSegmentSelected(1) },
|
||||||
|
width = 59.dp,
|
||||||
|
appColors = AppColors
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// 私有
|
||||||
|
AgentSegmentButton(
|
||||||
|
text = stringResource(R.string.private_label),
|
||||||
|
isSelected = selectedIndex == 2,
|
||||||
|
onClick = { onSegmentSelected(2) },
|
||||||
|
width = 54.dp,
|
||||||
|
appColors = AppColors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AgentSegmentButton(
|
||||||
|
text: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
width: androidx.compose.ui.unit.Dp,
|
||||||
|
appColors: com.aiosman.ravenow.AppThemeData
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(width)
|
||||||
|
.height(32.dp)
|
||||||
|
.background(
|
||||||
|
color = if (isSelected) {
|
||||||
|
appColors.checkedBackground // 使用选中背景色(暗色模式下是白色,亮色模式下是黑色)
|
||||||
|
} else {
|
||||||
|
Color(0x147C7480) // RGB(124, 116, 128, alpha 0.08)
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(1000.dp)
|
||||||
|
)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = if (isSelected) {
|
||||||
|
appColors.checkedText // 选中时使用选中文本颜色(暗色模式下是黑色,亮色模式下是白色)
|
||||||
|
} else {
|
||||||
|
appColors.text // 未选中时使用文本颜色
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyAgentsView() {
|
fun EmptyAgentsView() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|||||||
@@ -10,21 +10,14 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -52,7 +45,7 @@ fun UserContentPageIndicator(
|
|||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
// 图片/相册 Tab
|
// 动态 Tab
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -64,15 +57,24 @@ fun UserContentPageIndicator(
|
|||||||
}
|
}
|
||||||
.padding(vertical = 12.dp)
|
.padding(vertical = 12.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Text(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_images),
|
text = stringResource(R.string.index_dynamic),
|
||||||
contentDescription = "Gallery",
|
fontSize = 16.sp,
|
||||||
tint = if (pagerState.currentPage == 0) AppColors.text else AppColors.text.copy(alpha = 0.6f),
|
fontWeight = if (pagerState.currentPage == 0) FontWeight.SemiBold else FontWeight.Medium,
|
||||||
modifier = Modifier.size(24.dp)
|
color = if (pagerState.currentPage == 0) AppColors.text else AppColors.secondaryText
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(if (pagerState.currentPage == 0) 6.dp else 4.dp))
|
||||||
|
if (pagerState.currentPage == 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(20.dp)
|
||||||
|
.height(2.dp)
|
||||||
|
.background(AppColors.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Agent Tab (只在非智能体用户时显示)
|
// 智能体 Tab (只在非智能体用户时显示)
|
||||||
if (showAgentTab) {
|
if (showAgentTab) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -85,40 +87,54 @@ fun UserContentPageIndicator(
|
|||||||
}
|
}
|
||||||
.padding(vertical = 12.dp)
|
.padding(vertical = 12.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Text(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_nav_ai),
|
text = stringResource(R.string.chat_ai),
|
||||||
contentDescription = "Agents",
|
fontSize = 16.sp,
|
||||||
tint = if (pagerState.currentPage == 1) AppColors.text else AppColors.text.copy(alpha = 0.6f),
|
fontWeight = if (pagerState.currentPage == 1) FontWeight.SemiBold else FontWeight.Medium,
|
||||||
modifier = Modifier.size(24.dp)
|
color = if (pagerState.currentPage == 1) AppColors.text else AppColors.secondaryText
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(if (pagerState.currentPage == 1) 6.dp else 4.dp))
|
||||||
|
if (pagerState.currentPage == 1) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(20.dp)
|
||||||
|
.height(2.dp)
|
||||||
|
.background(AppColors.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下划线指示器
|
// 群聊 Tab (只在非智能体用户时显示)
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(2.dp)
|
|
||||||
.background(
|
|
||||||
if (pagerState.currentPage == 0) AppColors.text else Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (showAgentTab) {
|
if (showAgentTab) {
|
||||||
Box(
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.noRippleClickable {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.scrollToPage(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_group),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = if (pagerState.currentPage == 2) FontWeight.SemiBold else FontWeight.Medium,
|
||||||
|
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.secondaryText
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(if (pagerState.currentPage == 2) 6.dp else 4.dp))
|
||||||
|
if (pagerState.currentPage == 2) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(20.dp)
|
||||||
.height(2.dp)
|
.height(2.dp)
|
||||||
.background(
|
.background(AppColors.text)
|
||||||
if (pagerState.currentPage == 1) AppColors.text else Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
@@ -29,23 +42,52 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
|||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserItem(
|
fun UserItem(
|
||||||
accountProfileEntity: AccountProfileEntity,
|
accountProfileEntity: AccountProfileEntity,
|
||||||
postCount: Long = 0
|
postCount: Long = 0,
|
||||||
|
isSelf: Boolean = false,
|
||||||
|
onEditClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val followerDebouncer = rememberDebouncer()
|
val followerDebouncer = rememberDebouncer()
|
||||||
val followingDebouncer = rememberDebouncer()
|
val followingDebouncer = rememberDebouncer()
|
||||||
|
|
||||||
|
// 获取 MBTI 和星座信息
|
||||||
|
val mbti = remember(accountProfileEntity.id) {
|
||||||
|
AppStore.getUserMbti(accountProfileEntity.id)
|
||||||
|
}
|
||||||
|
val zodiac = remember(accountProfileEntity.id) {
|
||||||
|
AppStore.getUserZodiac(accountProfileEntity.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化粉丝数
|
||||||
|
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||||
|
val formattedFollowerCount = remember(accountProfileEntity.followerCount) {
|
||||||
|
if (accountProfileEntity.followerCount >= 10000) {
|
||||||
|
val wan = accountProfileEntity.followerCount / 10000.0
|
||||||
|
if (wan >= 100) {
|
||||||
|
"${wan.toInt()}万"
|
||||||
|
} else {
|
||||||
|
String.format("%.1f万", wan)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
numberFormat.format(accountProfileEntity.followerCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
|
// 顶部:头像和统计数据
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// 头像
|
// 头像
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
@@ -53,39 +95,47 @@ fun UserItem(
|
|||||||
accountProfileEntity.avatar,
|
accountProfileEntity.avatar,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.size(48.dp),
|
.size(96.dp),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(32.dp))
|
|
||||||
//个人统计
|
// 统计数据
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f)
|
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 帖子数
|
// 帖子数
|
||||||
Column(
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(80.dp)
|
||||||
|
.height(40.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.weight(1f)
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = postCount.toString(),
|
text = postCount.toString(),
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 16.sp,
|
fontSize = 15.sp,
|
||||||
color = AppColors.text
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "帖子",
|
text = stringResource(R.string.posts),
|
||||||
color = AppColors.text
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 粉丝数
|
// 粉丝数
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.width(80.dp)
|
||||||
|
.height(40.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
followerDebouncer {
|
followerDebouncer {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -95,26 +145,33 @@ fun UserItem(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = accountProfileEntity.followerCount.toString(),
|
text = formattedFollowerCount,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 16.sp,
|
fontSize = 15.sp,
|
||||||
color = AppColors.text
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "粉丝",
|
text = stringResource(R.string.followers_upper),
|
||||||
color = AppColors.text
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关注数
|
// 关注数
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.width(80.dp)
|
||||||
|
.height(40.dp)
|
||||||
|
.offset(x = 6.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
followingDebouncer {
|
followingDebouncer {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -124,49 +181,161 @@ fun UserItem(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = accountProfileEntity.followingCount.toString(),
|
text = accountProfileEntity.followingCount.toString(),
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 16.sp,
|
fontSize = 15.sp,
|
||||||
color = AppColors.text
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "关注",
|
text = stringResource(R.string.following_upper),
|
||||||
color = AppColors.text
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 中间:昵称、简介、创建者信息
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
// 昵称
|
// 昵称
|
||||||
Text(
|
Text(
|
||||||
text = accountProfileEntity.nickName,
|
text = accountProfileEntity.nickName,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 16.sp,
|
fontSize = 22.sp,
|
||||||
|
letterSpacing = (-0.3).sp,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
// 个人简介
|
// 个人简介
|
||||||
if (accountProfileEntity.bio.isNotEmpty()) {
|
if (accountProfileEntity.bio.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = accountProfileEntity.bio,
|
text = accountProfileEntity.bio,
|
||||||
fontSize = 14.sp,
|
fontSize = 13.sp,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "No bio here.",
|
text = "Welcome to my fantiac word i will show you something about magic",
|
||||||
fontSize = 14.sp,
|
fontSize = 13.sp,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建者信息(如果是 AI 账户,可以显示创建者)
|
||||||
|
// 注意:当前 AccountProfileEntity 没有创建者字段,这里暂时留空
|
||||||
|
// 如果需要显示,需要从其他地方获取创建者信息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 底部:标签按钮
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// MBTI 标签
|
||||||
|
if (!mbti.isNullOrEmpty()) {
|
||||||
|
ProfileTag(
|
||||||
|
text = mbti,
|
||||||
|
backgroundColor = Color(0x33FF8D28), // 255/255, 141/255, 40/255, alpha 0.2
|
||||||
|
textColor = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 星座标签
|
||||||
|
if (!zodiac.isNullOrEmpty()) {
|
||||||
|
ProfileTag(
|
||||||
|
text = zodiac,
|
||||||
|
backgroundColor = Color(0x33FFCC00), // 255/255, 204/255, 0/255, alpha 0.2
|
||||||
|
textColor = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑标签(仅自己可见)
|
||||||
|
if (isSelf) {
|
||||||
|
ProfileTag(
|
||||||
|
text = stringResource(R.string.edit_profile),
|
||||||
|
backgroundColor = Color(0x14947A80), // 124/255, 116/255, 128/255, alpha 0.08
|
||||||
|
textColor = AppColors.text,
|
||||||
|
leadingIcon = {
|
||||||
|
EditIcon(
|
||||||
|
color = AppColors.text,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onEditClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfileTag(
|
||||||
|
text: String,
|
||||||
|
backgroundColor: Color,
|
||||||
|
textColor: Color,
|
||||||
|
leadingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(25.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
|
.then(
|
||||||
|
if (onClick != null) {
|
||||||
|
Modifier.clickable(onClick = onClick)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
leadingIcon?.invoke()
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EditIcon(
|
||||||
|
color: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.bi),
|
||||||
|
contentDescription = stringResource(R.string.edit_profile),
|
||||||
|
modifier = modifier,
|
||||||
|
colorFilter = ColorFilter.tint(color)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -61,27 +62,65 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import androidx.media3.common.util.Util
|
import androidx.media3.common.util.Util
|
||||||
import androidx.media3.datasource.DataSource
|
import androidx.media3.datasource.DataSource
|
||||||
import androidx.media3.datasource.DefaultDataSourceFactory
|
import androidx.media3.datasource.DefaultDataSourceFactory
|
||||||
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShortViewCompose(
|
fun ShortViewCompose(
|
||||||
videoItemsUrl: List<String>,
|
videoItemsUrl: List<String> = emptyList(),
|
||||||
|
videoMoments: List<MomentEntity> = emptyList(),
|
||||||
clickItemPosition: Int = 0,
|
clickItemPosition: Int = 0,
|
||||||
videoHeader: @Composable () -> Unit = {},
|
videoHeader: @Composable () -> Unit = {},
|
||||||
videoBottom: @Composable () -> Unit = {}
|
videoBottom: @Composable ((MomentEntity) -> Unit)? = null,
|
||||||
|
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onShareClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onPageChanged: ((Int) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val pagerState: PagerState = run {
|
// 优先使用 videoMoments,如果没有则使用 videoItemsUrl
|
||||||
remember {
|
val items = if (videoMoments.isNotEmpty()) {
|
||||||
PagerState(clickItemPosition, 0, videoItemsUrl.size - 1)
|
videoMoments.mapNotNull { moment ->
|
||||||
|
// MomentVideoEntity 的 url 已经在 toMomentItem() 中添加了 BASE_SERVER 前缀
|
||||||
|
moment.videos?.firstOrNull()?.url
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
videoItemsUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果视频列表为空,显示空状态
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "暂无视频",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 items 不为空后再创建 PagerState
|
||||||
|
val pagerState: PagerState = remember(items.size) {
|
||||||
|
val maxPage = maxOf(0, items.size - 1)
|
||||||
|
PagerState(
|
||||||
|
currentPage = clickItemPosition.coerceIn(0, maxPage),
|
||||||
|
minPage = 0,
|
||||||
|
maxPage = maxPage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val initialLayout = remember {
|
val initialLayout = remember {
|
||||||
mutableStateOf(true)
|
mutableStateOf(true)
|
||||||
@@ -89,20 +128,39 @@ fun ShortViewCompose(
|
|||||||
val pauseIconVisibleState = remember {
|
val pauseIconVisibleState = remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Pager(
|
Pager(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RectangleShape),
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
orientation = Orientation.Vertical,
|
orientation = Orientation.Vertical,
|
||||||
offscreenLimit = 1
|
offscreenLimit = 1
|
||||||
) {
|
) {
|
||||||
pauseIconVisibleState.value = false
|
pauseIconVisibleState.value = false
|
||||||
|
val currentMoment = if (videoMoments.isNotEmpty() && page < videoMoments.size) {
|
||||||
|
videoMoments[page]
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步页码到外部(用于返回时恢复进度)
|
||||||
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
|
onPageChanged?.invoke(pagerState.currentPage)
|
||||||
|
}
|
||||||
SingleVideoItemContent(
|
SingleVideoItemContent(
|
||||||
videoItemsUrl[page],
|
videoUrl = items[page],
|
||||||
pagerState,
|
moment = currentMoment,
|
||||||
page,
|
pagerState = pagerState,
|
||||||
initialLayout,
|
pager = page,
|
||||||
pauseIconVisibleState,
|
initialLayout = initialLayout,
|
||||||
videoHeader,
|
pauseIconVisibleState = pauseIconVisibleState,
|
||||||
videoBottom
|
VideoHeader = videoHeader,
|
||||||
|
VideoBottom = videoBottom,
|
||||||
|
onLikeClick = onLikeClick,
|
||||||
|
onCommentClick = onCommentClick,
|
||||||
|
onFavoriteClick = onFavoriteClick,
|
||||||
|
onShareClick = onShareClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,18 +174,39 @@ fun ShortViewCompose(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SingleVideoItemContent(
|
private fun SingleVideoItemContent(
|
||||||
videoUrl: String,
|
videoUrl: String,
|
||||||
|
moment: MomentEntity?,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
pager: Int,
|
pager: Int,
|
||||||
initialLayout: MutableState<Boolean>,
|
initialLayout: MutableState<Boolean>,
|
||||||
pauseIconVisibleState: MutableState<Boolean>,
|
pauseIconVisibleState: MutableState<Boolean>,
|
||||||
VideoHeader: @Composable() () -> Unit,
|
VideoHeader: @Composable() () -> Unit = {},
|
||||||
VideoBottom: @Composable() () -> Unit,
|
VideoBottom: @Composable ((MomentEntity) -> Unit)? = null,
|
||||||
|
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onShareClick: ((MomentEntity) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(
|
||||||
VideoPlayer(videoUrl, pagerState, pager, pauseIconVisibleState)
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RectangleShape) // 确保内容不会溢出到box外
|
||||||
|
) {
|
||||||
|
VideoPlayer(
|
||||||
|
videoUrl = videoUrl,
|
||||||
|
moment = moment,
|
||||||
|
pagerState = pagerState,
|
||||||
|
pager = pager,
|
||||||
|
pauseIconVisibleState = pauseIconVisibleState,
|
||||||
|
onLikeClick = onLikeClick,
|
||||||
|
onCommentClick = onCommentClick,
|
||||||
|
onFavoriteClick = onFavoriteClick,
|
||||||
|
onShareClick = onShareClick
|
||||||
|
)
|
||||||
VideoHeader.invoke()
|
VideoHeader.invoke()
|
||||||
|
if (moment != null && VideoBottom != null) {
|
||||||
Box(modifier = Modifier.align(Alignment.BottomStart)) {
|
Box(modifier = Modifier.align(Alignment.BottomStart)) {
|
||||||
VideoBottom.invoke()
|
VideoBottom.invoke(moment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (initialLayout.value) {
|
if (initialLayout.value) {
|
||||||
Box(
|
Box(
|
||||||
@@ -143,9 +222,14 @@ private fun SingleVideoItemContent(
|
|||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayer(
|
fun VideoPlayer(
|
||||||
videoUrl: String,
|
videoUrl: String,
|
||||||
|
moment: MomentEntity?,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
pager: Int,
|
pager: Int,
|
||||||
pauseIconVisibleState: MutableState<Boolean>,
|
pauseIconVisibleState: MutableState<Boolean>,
|
||||||
|
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
||||||
|
onShareClick: ((MomentEntity) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -158,9 +242,20 @@ fun VideoPlayer(
|
|||||||
ExoPlayer.Builder(context)
|
ExoPlayer.Builder(context)
|
||||||
.build()
|
.build()
|
||||||
.apply {
|
.apply {
|
||||||
|
// 创建带有认证头的 HttpDataSource.Factory
|
||||||
|
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
||||||
|
.setUserAgent(Util.getUserAgent(context, context.packageName))
|
||||||
|
.setDefaultRequestProperties(
|
||||||
|
mapOf(
|
||||||
|
"Authorization" to "Bearer ${com.aiosman.ravenow.AppStore.token ?: ""}",
|
||||||
|
"DEVICE-OS" to "Android"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 创建 DataSource.Factory,使用自定义的 HttpDataSource.Factory
|
||||||
val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
|
val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
|
||||||
context,
|
context,
|
||||||
Util.getUserAgent(context, context.packageName)
|
httpDataSourceFactory
|
||||||
)
|
)
|
||||||
val source = ProgressiveMediaSource.Factory(dataSourceFactory)
|
val source = ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||||
.createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl)))
|
.createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl)))
|
||||||
@@ -275,21 +370,54 @@ fun VideoPlayer(
|
|||||||
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
|
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
if (moment != null) {
|
||||||
|
UserAvatar(avatarUrl = moment.avatar)
|
||||||
|
VideoBtn(
|
||||||
|
icon = R.drawable.rider_pro_video_like,
|
||||||
|
text = formatCount(moment.likeCount)
|
||||||
|
) {
|
||||||
|
moment?.let { onLikeClick?.invoke(it) }
|
||||||
|
}
|
||||||
|
VideoBtn(
|
||||||
|
icon = R.drawable.rider_pro_video_comment,
|
||||||
|
text = formatCount(moment.commentCount)
|
||||||
|
) {
|
||||||
|
moment?.let {
|
||||||
|
showCommentModal = true
|
||||||
|
onCommentClick?.invoke(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VideoBtn(
|
||||||
|
icon = R.drawable.rider_pro_video_favor,
|
||||||
|
text = formatCount(moment.favoriteCount)
|
||||||
|
) {
|
||||||
|
moment?.let { onFavoriteClick?.invoke(it) }
|
||||||
|
}
|
||||||
|
VideoBtn(
|
||||||
|
icon = R.drawable.rider_pro_video_share,
|
||||||
|
text = formatCount(moment.shareCount)
|
||||||
|
) {
|
||||||
|
moment?.let { onShareClick?.invoke(it) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
UserAvatar()
|
UserAvatar()
|
||||||
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "975.9k")
|
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "0")
|
||||||
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "1896") {
|
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "0") {
|
||||||
showCommentModal = true
|
showCommentModal = true
|
||||||
}
|
}
|
||||||
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "234")
|
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "0")
|
||||||
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "677k")
|
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "0")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// info
|
// info
|
||||||
|
if (moment != null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.BottomStart
|
contentAlignment = Alignment.BottomStart
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) {
|
Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) {
|
||||||
|
if (moment.location.isNotEmpty() && moment.location != "Worldwide") {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 8.dp)
|
.padding(bottom = 8.dp)
|
||||||
@@ -306,39 +434,43 @@ fun VideoPlayer(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(end = 4.dp),
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
text = "USA",
|
text = moment.location,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "@Kevinlinpr",
|
text = "@${moment.nickname}",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
)
|
)
|
||||||
|
if (moment.momentTextContent.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 4.dp), // 确保Text占用可用宽度
|
.padding(top = 4.dp),
|
||||||
text = "Pedro Acosta to join KTM in 2025 on a multi-year deal! \uD83D\uDFE0",
|
text = moment.momentTextContent,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
overflow = TextOverflow.Ellipsis, // 超出范围时显示省略号
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 2 // 最多显示两行
|
maxLines = 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showCommentModal) {
|
if (showCommentModal && moment != null) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showCommentModal = false },
|
onDismissRequest = { showCommentModal = false },
|
||||||
containerColor = Color.White,
|
containerColor = Color.White,
|
||||||
sheetState = sheetState
|
sheetState = sheetState
|
||||||
) {
|
) {
|
||||||
CommentModalContent() {
|
CommentModalContent(postId = moment.id) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,16 +478,37 @@ fun VideoPlayer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserAvatar() {
|
fun UserAvatar(avatarUrl: String? = null) {
|
||||||
Image(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 16.dp)
|
.padding(bottom = 16.dp)
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
|
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
|
||||||
.clip(
|
.clip(RoundedCornerShape(40.dp))
|
||||||
RoundedCornerShape(40.dp)
|
) {
|
||||||
), painter = painterResource(id = R.drawable.default_avatar), contentDescription = ""
|
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = avatarUrl,
|
||||||
|
contentDescription = "用户头像",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
defaultRes = R.drawable.default_avatar
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.default_avatar),
|
||||||
|
contentDescription = "用户头像"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字显示
|
||||||
|
private fun formatCount(count: Int): String {
|
||||||
|
return when {
|
||||||
|
count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0)
|
||||||
|
count >= 1_000 -> String.format("%.1fK", count / 1_000.0)
|
||||||
|
else -> count.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
package com.aiosman.ravenow.ui.login
|
package com.aiosman.ravenow.ui.login
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -19,9 +23,13 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
@@ -33,25 +41,28 @@ import com.aiosman.ravenow.data.ServiceException
|
|||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.ravenow.data.api.getErrorMessageCode
|
import com.aiosman.ravenow.data.api.getErrorMessageCode
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
|
||||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||||
import com.aiosman.ravenow.ui.composables.CheckboxWithLabel
|
import com.aiosman.ravenow.ui.composables.CheckboxWithLabel
|
||||||
import com.aiosman.ravenow.ui.composables.PolicyCheckbox
|
import com.aiosman.ravenow.ui.composables.PolicyCheckbox
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.ravenow.ui.composables.TextInputField
|
import com.aiosman.ravenow.ui.composables.TextInputField
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.utils.PasswordValidator
|
import com.aiosman.ravenow.utils.PasswordValidator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private val LightGrayBackground = Color(red = 250f / 255f, green = 249f / 255f, blue = 251f / 255f)
|
||||||
|
private val IconGray = Color(red = 151f / 255f, green = 148f / 255f, blue = 153f / 255f)
|
||||||
|
private val PurpleButton = Color(0xFF7C45ED)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmailSignupScreen() {
|
fun EmailSignupScreen() {
|
||||||
var appColor = LocalAppTheme.current
|
val appColor = LocalAppTheme.current
|
||||||
var email by remember { mutableStateOf("") }
|
var email by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var confirmPassword by remember { mutableStateOf("") }
|
var confirmPassword by remember { mutableStateOf("") }
|
||||||
var rememberMe by remember { mutableStateOf(false) }
|
var rememberMe by remember { mutableStateOf(false) }
|
||||||
var acceptTerms by remember { mutableStateOf(false) }
|
var acceptTerms by remember { mutableStateOf(false) }
|
||||||
var acceptPromotions by remember { mutableStateOf(false) }
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -60,14 +71,13 @@ fun EmailSignupScreen() {
|
|||||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
||||||
var termsError by remember { mutableStateOf<Boolean>(false) }
|
var termsError by remember { mutableStateOf<Boolean>(false) }
|
||||||
var promotionsError by remember { mutableStateOf<Boolean>(false) }
|
|
||||||
fun validateForm(): Boolean {
|
fun validateForm(): Boolean {
|
||||||
emailError = when {
|
emailError = when {
|
||||||
// 非空
|
// 非空
|
||||||
email.isEmpty() -> context.getString(R.string.text_error_email_required)
|
email.isEmpty() -> context.getString(R.string.text_error_email_required)
|
||||||
// 邮箱格式
|
// 邮箱格式
|
||||||
!android.util.Patterns.EMAIL_ADDRESS.matcher(email)
|
!android.util.Patterns.EMAIL_ADDRESS.matcher(email)
|
||||||
.matches() -> context.getString(R.string.text_error_email_format)
|
.matches() -> context.getString(R.string.text_error_email_format_1)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -88,22 +98,8 @@ fun EmailSignupScreen() {
|
|||||||
}
|
}
|
||||||
termsError = true
|
termsError = true
|
||||||
return false
|
return false
|
||||||
} else {
|
}
|
||||||
termsError = false
|
termsError = false
|
||||||
}
|
|
||||||
if (!acceptPromotions) {
|
|
||||||
scope.launch(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.error_not_accept_recive_notice),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
promotionsError = true
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
promotionsError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return emailError == null && passwordError == null && confirmPasswordError == null
|
return emailError == null && passwordError == null && confirmPasswordError == null
|
||||||
}
|
}
|
||||||
@@ -158,63 +154,127 @@ fun EmailSignupScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(appColor.background)
|
.background(appColor.background)
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
Box(
|
// 顶部导航栏:返回箭头 + "注册"标题,左对齐
|
||||||
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
|
.padding(top = 15.dp, start = 16.dp, bottom = 15.dp, end = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.sign_up_upper), moreIcon = false)
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(Color.Black)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sign_up_upper),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.padding(32.dp))
|
|
||||||
|
// 输入区域
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 0.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 邮箱输入框
|
||||||
TextInputField(
|
TextInputField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
text = email,
|
text = email,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
email = it
|
email = it
|
||||||
},
|
},
|
||||||
|
label = stringResource(R.string.login_email_label),
|
||||||
hint = stringResource(R.string.text_hint_email),
|
hint = stringResource(R.string.text_hint_email),
|
||||||
error = emailError
|
error = emailError,
|
||||||
|
leadingIcon = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.icon_email_light),
|
||||||
|
contentDescription = "Email",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(IconGray)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.padding(4.dp))
|
},
|
||||||
|
customBackgroundColor = LightGrayBackground,
|
||||||
|
customCornerRadius = 16f
|
||||||
|
)
|
||||||
|
|
||||||
|
// 密码输入框
|
||||||
TextInputField(
|
TextInputField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
text = password,
|
text = password,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
password = it
|
password = it
|
||||||
},
|
},
|
||||||
password = true,
|
password = true,
|
||||||
|
label = stringResource(R.string.text_hint_password).replace("输入", ""),
|
||||||
hint = stringResource(R.string.text_hint_password),
|
hint = stringResource(R.string.text_hint_password),
|
||||||
error = passwordError
|
error = passwordError,
|
||||||
|
leadingIcon = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.icon_lock_light),
|
||||||
|
contentDescription = "Lock",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(IconGray)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.padding(4.dp))
|
},
|
||||||
|
customBackgroundColor = LightGrayBackground,
|
||||||
|
customCornerRadius = 16f
|
||||||
|
)
|
||||||
|
|
||||||
|
// 确认密码输入框
|
||||||
TextInputField(
|
TextInputField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
text = confirmPassword,
|
text = confirmPassword,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
confirmPassword = it
|
confirmPassword = it
|
||||||
},
|
},
|
||||||
password = true,
|
password = true,
|
||||||
hint = stringResource(R.string.text_hint_confirm_password),
|
label = stringResource(R.string.confirm_password_label),
|
||||||
error = confirmPasswordError
|
hint = stringResource(R.string.text_hint_password),
|
||||||
|
error = confirmPasswordError,
|
||||||
|
leadingIcon = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.icon_lock_light),
|
||||||
|
contentDescription = "Lock",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(IconGray)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
},
|
||||||
|
customBackgroundColor = LightGrayBackground,
|
||||||
|
customCornerRadius = 16f
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 功能选项区域
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
) {
|
) {
|
||||||
CheckboxWithLabel(
|
CheckboxWithLabel(
|
||||||
@@ -236,42 +296,31 @@ fun EmailSignupScreen() {
|
|||||||
termsError = false
|
termsError = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
CheckboxWithLabel(
|
|
||||||
checked = acceptPromotions,
|
|
||||||
checkSize = 16,
|
|
||||||
fontSize = 12,
|
|
||||||
label = stringResource(R.string.agree_promotion),
|
|
||||||
error = promotionsError
|
|
||||||
) {
|
|
||||||
acceptPromotions = it
|
|
||||||
// 当用户勾选时,立即清除错误状态
|
|
||||||
if (it) {
|
|
||||||
promotionsError = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(76.dp))
|
||||||
|
|
||||||
|
// 底部注册按钮
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.width(345.dp),
|
text = stringResource(R.string.sign_up_upper),
|
||||||
text = stringResource(R.string.lets_ride_upper),
|
backgroundColor = PurpleButton,
|
||||||
backgroundColor = Color(0xffda3832),
|
color = Color.White,
|
||||||
color = Color.White
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
) {
|
) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
registerUser()
|
registerUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -72,8 +72,17 @@ fun UserAuthScreen() {
|
|||||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||||
var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) }
|
var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) }
|
||||||
fun validateForm(): Boolean {
|
fun validateForm(): Boolean {
|
||||||
emailError =
|
// 如果密码为空,先检查邮箱格式
|
||||||
if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null
|
if (password.isEmpty()) {
|
||||||
|
emailError = when {
|
||||||
|
email.isEmpty() -> context.getString(R.string.text_error_email_required)
|
||||||
|
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() ->
|
||||||
|
context.getString(R.string.text_error_email_format)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emailError = if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null
|
||||||
|
}
|
||||||
|
|
||||||
// 使用通用密码校验器
|
// 使用通用密码校验器
|
||||||
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.heightIn
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -52,6 +54,8 @@ import androidx.compose.ui.draw.rotate
|
|||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
@@ -66,6 +70,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -151,71 +156,45 @@ fun NewPostScreen() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(1.dp)
|
.height(1.dp)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
.background(AppColors.divider)
|
.background(AppColors.divider)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.padding(start = 16.dp)
|
||||||
.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 8.dp),
|
.height(40.dp)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
.widthIn(min = 100.dp, max = 200.dp)
|
||||||
|
.wrapContentWidth()
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF8CDDFF),
|
||||||
|
Color(0xFF9887FF),
|
||||||
|
Color(0xFFFF8D28)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.rider_pro_moment_ai),
|
painter = painterResource(id = R.mipmap.icon_ai),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(16.dp)
|
||||||
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.moment_ai_co),
|
text = stringResource(R.string.moment_ai_co),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 15.sp,
|
fontSize = 13.sp,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 8.dp)
|
.padding(start = 2.dp),
|
||||||
.weight(1f),
|
color = Color.White,
|
||||||
color = AppColors.text,
|
maxLines = 1,
|
||||||
)
|
overflow = TextOverflow.Ellipsis
|
||||||
Switch(
|
|
||||||
checked = isAiEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
isChecked ->
|
|
||||||
isAiEnabled = isChecked
|
|
||||||
if (isChecked) {
|
|
||||||
// 收起键盘
|
|
||||||
keyboardController?.hide()
|
|
||||||
isRequesting = true
|
|
||||||
isRotating = true
|
|
||||||
model.viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
model.agentMoment(model.textContent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}finally {
|
|
||||||
isRequesting = false
|
|
||||||
isRotating = false
|
|
||||||
isAiEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = !isRequesting && model.textContent.isNotEmpty(),
|
|
||||||
colors = SwitchDefaults.colors(
|
|
||||||
checkedThumbColor = Color.White,
|
|
||||||
checkedTrackColor = AppColors.brandColorsColor,
|
|
||||||
uncheckedThumbColor = Color.White,
|
|
||||||
uncheckedTrackColor = AppColors.nonActive,
|
|
||||||
uncheckedBorderColor = Color.White,
|
|
||||||
disabledCheckedTrackColor = AppColors.brandColorsColor.copy(alpha = 0.8f),
|
|
||||||
disabledCheckedThumbColor= Color.White,
|
|
||||||
disabledUncheckedTrackColor = AppColors.nonActive,
|
|
||||||
disabledUncheckedThumbColor= Color.White
|
|
||||||
|
|
||||||
),
|
|
||||||
modifier = Modifier.scale(0.8f)
|
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +331,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
|||||||
modifier = Modifier.align(Alignment.CenterStart),
|
modifier = Modifier.align(Alignment.CenterStart),
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rider_pro_close),
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
@@ -366,9 +345,31 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
|||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
text = stringResource(R.string.publish_dynamic),
|
||||||
|
fontSize = 17.sp,
|
||||||
|
color = AppColors.text,
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.rider_pro_moment_post),
|
painter = painterResource(id = R.mipmap.icon_draft_box_light),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// 添加防抖逻辑
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastSendClickTime > debounceTime) {
|
||||||
|
lastSendClickTime = currentTime
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(20.dp))
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.icon_released_light),
|
||||||
contentDescription = "Send",
|
contentDescription = "Send",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
@@ -391,11 +392,8 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
|||||||
}finally {
|
}finally {
|
||||||
uploading = false
|
uploading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,72 +486,24 @@ fun AddImageGrid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val addImageDebouncer = rememberDebouncer()
|
val addImageDebouncer = rememberDebouncer()
|
||||||
val takePhotoDebouncer = rememberDebouncer()
|
|
||||||
|
|
||||||
val stroke = Stroke(
|
|
||||||
width = 2f,
|
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
|
||||||
)
|
|
||||||
DraggableGrid(
|
|
||||||
items = NewPostViewModel.imageList,
|
|
||||||
onMove = { from, to ->
|
|
||||||
NewPostViewModel.imageList = NewPostViewModel.imageList.toMutableList().apply {
|
|
||||||
add(to, removeAt(from))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lockedIndices = listOf(
|
|
||||||
|
|
||||||
),
|
|
||||||
onDragModeEnd = {},
|
|
||||||
onDragModeStart = {},
|
|
||||||
additionalItems = listOf(
|
|
||||||
|
|
||||||
),
|
|
||||||
getItemId = { it.id }
|
|
||||||
) { item, isDrag ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
LocalContext.current,
|
|
||||||
item.bitmap,
|
|
||||||
contentDescription = "Image",
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
|
||||||
},
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
if (isDrag) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.Black.copy(alpha = 0.4f))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val canAddMoreImages = model.imageList.size < 9
|
val canAddMoreImages = model.imageList.size < 9
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(5),
|
columns = GridCells.Fixed(5),
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(horizontal = 19.dp, vertical = 4.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
// 添加按钮
|
||||||
if (canAddMoreImages) {
|
if (canAddMoreImages) {
|
||||||
item {
|
item {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.clip(RoundedCornerShape(16.dp)) // 设置圆角
|
.clip(RoundedCornerShape(24.dp))
|
||||||
.background(AppColors.basicMain) // 设置背景色
|
.background(Color(0xFFFAF9FB))
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
addImageDebouncer {
|
addImageDebouncer {
|
||||||
if (model.imageList.size < 9) {
|
if (model.imageList.size < 9) {
|
||||||
@@ -572,20 +522,21 @@ fun AddImageGrid() {
|
|||||||
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
||||||
contentDescription = "Add Image",
|
contentDescription = "Add Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(23.3.dp)
|
||||||
.align(Alignment.Center),
|
.align(Alignment.Center),
|
||||||
tint = AppColors.nonActiveText
|
tint = AppColors.nonActiveText
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 相机按钮
|
||||||
item {
|
item {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.clip(RoundedCornerShape(16.dp)) // 设置圆角
|
.clip(RoundedCornerShape(24.dp))
|
||||||
.background(AppColors.basicMain) // 设置背景色
|
.background(Color(0xFFFAF9FB))
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
if (model.imageList.size < 9) {
|
if (model.imageList.size < 9) {
|
||||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
val photoFile = File(context.cacheDir, "photo.jpg")
|
||||||
@@ -605,13 +556,60 @@ fun AddImageGrid() {
|
|||||||
painter = painterResource(id = R.drawable.rider_pro_camera),
|
painter = painterResource(id = R.drawable.rider_pro_camera),
|
||||||
contentDescription = "Take Photo",
|
contentDescription = "Take Photo",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(23.3.dp)
|
||||||
.align(Alignment.Center),
|
.align(Alignment.Center),
|
||||||
tint = AppColors.nonActiveText
|
tint = AppColors.nonActiveText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已添加的图片,显示在相机按钮后面
|
||||||
|
items(model.imageList.size) { index ->
|
||||||
|
val item = model.imageList[index]
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(Color(0xFFFAF9FB))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
item.bitmap,
|
||||||
|
contentDescription = "Image",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
||||||
|
},
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
// // 删除按钮 - 右上角
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .align(Alignment.TopEnd)
|
||||||
|
// .padding(4.dp)
|
||||||
|
// .size(20.dp)
|
||||||
|
// .clip(RoundedCornerShape(10.dp))
|
||||||
|
// .background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
// .noRippleClickable {
|
||||||
|
// model.imageList = model.imageList.toMutableList().apply {
|
||||||
|
// removeAt(index)
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// contentAlignment = Alignment.Center
|
||||||
|
// ) {
|
||||||
|
// Icon(
|
||||||
|
// painter = painterResource(id = R.drawable.rider_pro_close),
|
||||||
|
// contentDescription = "Delete",
|
||||||
|
// modifier = Modifier.size(12.dp),
|
||||||
|
// tint = Color.White
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.aiosman.ravenow.data.UserServiceImpl
|
|||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.entity.MomentEntity
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||||
|
import com.aiosman.ravenow.event.FollowChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
import com.aiosman.ravenow.event.MomentLikeChangeEvent
|
||||||
import com.aiosman.ravenow.event.MomentRemoveEvent
|
import com.aiosman.ravenow.event.MomentRemoveEvent
|
||||||
@@ -166,7 +167,8 @@ class PostViewModel(
|
|||||||
moment?.let {
|
moment?.let {
|
||||||
userService.followUser(it.authorId.toString())
|
userService.followUser(it.authorId.toString())
|
||||||
moment = moment?.copy(followStatus = true)
|
moment = moment?.copy(followStatus = true)
|
||||||
// 更新我的关注页面的关注数
|
// 发送关注事件,通知动态列表更新关注状态
|
||||||
|
EventBus.getDefault().post(FollowChangeEvent(it.authorId, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +176,8 @@ class PostViewModel(
|
|||||||
moment?.let {
|
moment?.let {
|
||||||
userService.unFollowUser(it.authorId.toString())
|
userService.unFollowUser(it.authorId.toString())
|
||||||
moment = moment?.copy(followStatus = false)
|
moment = moment?.copy(followStatus = false)
|
||||||
// 更新我的关注页面的关注数
|
// 发送取消关注事件,通知动态列表更新关注状态
|
||||||
|
EventBus.getDefault().post(FollowChangeEvent(it.authorId, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
app/src/main/res/drawable/icons_circle_camera.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/bi.png
Normal file
|
After Width: | Height: | Size: 331 B |
BIN
app/src/main/res/mipmap-hdpi/collect.png
Normal file
|
After Width: | Height: | Size: 438 B |
BIN
app/src/main/res/mipmap-hdpi/feedback_icon.png
Normal file
|
After Width: | Height: | Size: 434 B |
BIN
app/src/main/res/mipmap-hdpi/fengm.png
Normal file
|
After Width: | Height: | Size: 331 B |
BIN
app/src/main/res/mipmap-hdpi/icons_about.png
Normal file
|
After Width: | Height: | Size: 465 B |
BIN
app/src/main/res/mipmap-hdpi/icons_account_and_security.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
app/src/main/res/mipmap-hdpi/icons_bell.png
Normal file
|
After Width: | Height: | Size: 425 B |
BIN
app/src/main/res/mipmap-hdpi/icons_block.png
Normal file
|
After Width: | Height: | Size: 476 B |
BIN
app/src/main/res/mipmap-hdpi/icons_dark_mode.png
Normal file
|
After Width: | Height: | Size: 381 B |
BIN
app/src/main/res/mipmap-hdpi/icons_edited_data.png
Normal file
|
After Width: | Height: | Size: 428 B |
BIN
app/src/main/res/mipmap-hdpi/icons_padlock.png
Normal file
|
After Width: | Height: | Size: 391 B |
BIN
app/src/main/res/mipmap-hdpi/icons_remove.png
Normal file
|
After Width: | Height: | Size: 396 B |
BIN
app/src/main/res/mipmap-hdpi/l_empty_img.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/log_out_icon.png
Normal file
|
After Width: | Height: | Size: 402 B |
BIN
app/src/main/res/mipmap-hdpi/menu_ico.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
app/src/main/res/mipmap-hdpi/menu_icon.png
Normal file
|
After Width: | Height: | Size: 283 B |
BIN
app/src/main/res/mipmap-hdpi/naoz.png
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
app/src/main/res/mipmap-hdpi/paip_coin_img.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/qr_code_icon.png
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
app/src/main/res/mipmap-hdpi/sao.png
Normal file
|
After Width: | Height: | Size: 367 B |
BIN
app/src/main/res/mipmap-hdpi/xingzuo.png
Normal file
|
After Width: | Height: | Size: 593 B |
BIN
app/src/main/res/mipmap-mdpi/bi.png
Normal file
|
After Width: | Height: | Size: 283 B |
BIN
app/src/main/res/mipmap-mdpi/collect.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
app/src/main/res/mipmap-mdpi/feedback_icon.png
Normal file
|
After Width: | Height: | Size: 326 B |
BIN
app/src/main/res/mipmap-mdpi/fengm.png
Normal file
|
After Width: | Height: | Size: 283 B |
BIN
app/src/main/res/mipmap-mdpi/icon_ai.png
Normal file
|
After Width: | Height: | Size: 300 B |
BIN
app/src/main/res/mipmap-mdpi/icon_draft_box_light.png
Normal file
|
After Width: | Height: | Size: 330 B |
BIN
app/src/main/res/mipmap-mdpi/icon_email_light.png
Normal file
|
After Width: | Height: | Size: 354 B |
BIN
app/src/main/res/mipmap-mdpi/icon_eyes_closed_light.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/mipmap-mdpi/icon_lock_light.png
Normal file
|
After Width: | Height: | Size: 319 B |
BIN
app/src/main/res/mipmap-mdpi/icon_released_light.png
Normal file
|
After Width: | Height: | Size: 308 B |
BIN
app/src/main/res/mipmap-mdpi/icons_about.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
app/src/main/res/mipmap-mdpi/icons_account_and_security.png
Normal file
|
After Width: | Height: | Size: 325 B |
BIN
app/src/main/res/mipmap-mdpi/icons_bell.png
Normal file
|
After Width: | Height: | Size: 328 B |
BIN
app/src/main/res/mipmap-mdpi/icons_block.png
Normal file
|
After Width: | Height: | Size: 404 B |
BIN
app/src/main/res/mipmap-mdpi/icons_dark_mode.png
Normal file
|
After Width: | Height: | Size: 329 B |
BIN
app/src/main/res/mipmap-mdpi/icons_edited_data.png
Normal file
|
After Width: | Height: | Size: 361 B |
BIN
app/src/main/res/mipmap-mdpi/icons_padlock.png
Normal file
|
After Width: | Height: | Size: 323 B |
BIN
app/src/main/res/mipmap-mdpi/icons_remove.png
Normal file
|
After Width: | Height: | Size: 289 B |
BIN
app/src/main/res/mipmap-mdpi/l_empty_img.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-mdpi/log_out_icon.png
Normal file
|
After Width: | Height: | Size: 301 B |
BIN
app/src/main/res/mipmap-mdpi/menu_ico.png
Normal file
|
After Width: | Height: | Size: 143 B |
BIN
app/src/main/res/mipmap-mdpi/menu_icon.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
app/src/main/res/mipmap-mdpi/naoz.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
app/src/main/res/mipmap-mdpi/paip_coin_img.png
Normal file
|
After Width: | Height: | Size: 873 B |
BIN
app/src/main/res/mipmap-mdpi/qr_code_icon.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
app/src/main/res/mipmap-mdpi/sao.png
Normal file
|
After Width: | Height: | Size: 290 B |
BIN
app/src/main/res/mipmap-mdpi/xingzuo.png
Normal file
|
After Width: | Height: | Size: 473 B |
BIN
app/src/main/res/mipmap-xhdpi/bi.png
Normal file
|
After Width: | Height: | Size: 415 B |
BIN
app/src/main/res/mipmap-xhdpi/collect.png
Normal file
|
After Width: | Height: | Size: 548 B |
BIN
app/src/main/res/mipmap-xhdpi/feedback_icon.png
Normal file
|
After Width: | Height: | Size: 516 B |
BIN
app/src/main/res/mipmap-xhdpi/fengm.png
Normal file
|
After Width: | Height: | Size: 415 B |
BIN
app/src/main/res/mipmap-xhdpi/icon_ai.png
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
app/src/main/res/mipmap-xhdpi/icon_draft_box_light.png
Normal file
|
After Width: | Height: | Size: 492 B |
BIN
app/src/main/res/mipmap-xhdpi/icon_email_light.png
Normal file
|
After Width: | Height: | Size: 563 B |
BIN
app/src/main/res/mipmap-xhdpi/icon_eyes_closed_light.png
Normal file
|
After Width: | Height: | Size: 393 B |