@@ -46,6 +46,7 @@ object AppState {
|
||||
var enableGoogleLogin: Boolean = false
|
||||
var enableChat = false
|
||||
var agentCreatedSuccess by mutableStateOf(false)
|
||||
var chatBackgroundUrl by mutableStateOf<String?>(null)
|
||||
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
||||
// 如果是游客模式,使用简化的初始化流程
|
||||
if (AppStore.isGuest) {
|
||||
|
||||
@@ -30,6 +30,12 @@ object AppStore {
|
||||
AppState.appTheme = DarkThemeColors()
|
||||
}
|
||||
|
||||
// load chat background
|
||||
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
|
||||
if (savedBgUrl != null) {
|
||||
AppState.chatBackgroundUrl = savedBgUrl
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun saveData() {
|
||||
@@ -54,5 +60,15 @@ object AppStore {
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun saveChatBackgroundUrl(url: String?) {
|
||||
sharedPreferences.edit().apply {
|
||||
if (url != null) {
|
||||
putString("chatBackgroundUrl", url)
|
||||
} else {
|
||||
remove("chatBackgroundUrl")
|
||||
}
|
||||
}.apply()
|
||||
AppState.chatBackgroundUrl = url
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
||||
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
|
||||
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
||||
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
||||
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
|
||||
import com.aiosman.ravenow.ui.chat.ChatScreen
|
||||
import com.aiosman.ravenow.ui.chat.GroupChatScreen
|
||||
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
||||
@@ -106,6 +107,7 @@ sealed class NavigationRoute(
|
||||
data object FavouriteList : NavigationRoute("FavouriteList")
|
||||
data object Chat : NavigationRoute("Chat/{id}")
|
||||
data object ChatAi : NavigationRoute("ChatAi/{id}")
|
||||
data object ChatSetting : NavigationRoute("ChatSetting")
|
||||
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
|
||||
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
||||
data object ImageCrop : NavigationRoute("ImageCrop")
|
||||
@@ -491,6 +493,14 @@ fun NavigationController(
|
||||
}
|
||||
}
|
||||
|
||||
composable(route = NavigationRoute.ChatSetting.route) {
|
||||
CompositionLocalProvider(
|
||||
LocalAnimatedContentScope provides this,
|
||||
) {
|
||||
ChatSettingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
composable(
|
||||
route = NavigationRoute.ChatGroup.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.StringType },
|
||||
|
||||
@@ -79,11 +79,12 @@ import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.ChatItem
|
||||
import com.aiosman.ravenow.exp.formatChatTime
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.AppState
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import io.openim.android.sdk.enums.MessageType
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
@@ -91,10 +92,10 @@ import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChatAiScreen(userId: String) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalNavController.current.context
|
||||
val AppColors = LocalAppTheme.current
|
||||
val chatBackgroundUrl = AppState.chatBackgroundUrl
|
||||
var goToNewCount by remember { mutableStateOf(0) }
|
||||
val viewModel = viewModel<ChatAiViewModel>(
|
||||
key = "ChatAiViewModel_$userId",
|
||||
@@ -158,14 +159,25 @@ fun ChatAiScreen(userId: String) {
|
||||
}
|
||||
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (chatBackgroundUrl != null && chatBackgroundUrl.isNotEmpty()) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = chatBackgroundUrl,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentDescription = "chat_background",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
backgroundColor = Color.Transparent,
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.background)
|
||||
.background(Color.Transparent)
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Row(
|
||||
@@ -213,49 +225,24 @@ fun ChatAiScreen(userId: String) {
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.noRippleClickable {
|
||||
isMenuExpanded = true
|
||||
navController.navigate(NavigationRoute.ChatSetting.route)
|
||||
},
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = isMenuExpanded,
|
||||
onDismissRequest = {
|
||||
isMenuExpanded = false
|
||||
},
|
||||
menuItems = listOf(
|
||||
MenuItem(
|
||||
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
|
||||
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
||||
) {
|
||||
|
||||
isMenuExpanded = false
|
||||
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||
viewModel.viewModelScope.launch {
|
||||
if (viewModel.notificationStrategy == "mute") {
|
||||
viewModel.updateNotificationStrategy("active")
|
||||
} else {
|
||||
viewModel.updateNotificationStrategy("mute")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
val hasChatBackground = AppState.chatBackgroundUrl != null && AppState.chatBackgroundUrl!!.isNotEmpty()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.imePadding()
|
||||
) {
|
||||
if (!hasChatBackground) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -263,6 +250,7 @@ fun ChatAiScreen(userId: String) {
|
||||
.background(
|
||||
AppColors.decentBackground)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
ChatAiInput(
|
||||
onSendImage = {
|
||||
@@ -283,7 +271,7 @@ fun ChatAiScreen(userId: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
.background(Color.Transparent)
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
LazyColumn(
|
||||
@@ -346,8 +334,7 @@ fun ChatAiScreen(userId: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,7 +558,8 @@ fun ChatAiInput(
|
||||
}
|
||||
Box( modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
|
||||
.background(Color.Transparent)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -64,6 +64,7 @@ import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -74,10 +75,12 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.ChatItem
|
||||
import com.aiosman.ravenow.exp.formatChatTime
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||
@@ -91,7 +94,6 @@ import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChatScreen(userId: String) {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalNavController.current.context
|
||||
val AppColors = LocalAppTheme.current
|
||||
@@ -158,14 +160,37 @@ fun ChatScreen(userId: String) {
|
||||
}
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
// 背景图层
|
||||
val bgUrl = AppState.chatBackgroundUrl
|
||||
if (bgUrl != null) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = bgUrl,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentDescription = "chat_background",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
// 无背景时使用主题背景色
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
backgroundColor = Color.Transparent,
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.background)
|
||||
.background(Color.Transparent)
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Row(
|
||||
@@ -214,39 +239,12 @@ fun ChatScreen(userId: String) {
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.noRippleClickable {
|
||||
isMenuExpanded = true
|
||||
navController.navigate(NavigationRoute.ChatSetting.route)
|
||||
},
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
AppColors.text)
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = isMenuExpanded,
|
||||
onDismissRequest = {
|
||||
isMenuExpanded = false
|
||||
},
|
||||
menuItems = listOf(
|
||||
MenuItem(
|
||||
title = if (viewModel.notificationStrategy == "mute") "取消静音" else "静音",
|
||||
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
||||
) {
|
||||
|
||||
isMenuExpanded = false
|
||||
if (NetworkUtils.isNetworkAvailable(context)) {
|
||||
viewModel.viewModelScope.launch {
|
||||
if (viewModel.notificationStrategy == "mute") {
|
||||
viewModel.updateNotificationStrategy("active")
|
||||
} else {
|
||||
viewModel.updateNotificationStrategy("mute")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,7 +281,7 @@ fun ChatScreen(userId: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
.background(Color.Transparent)
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
LazyColumn(
|
||||
@@ -347,6 +345,7 @@ fun ChatScreen(userId: String) {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,7 +571,7 @@ fun ChatInput(
|
||||
}
|
||||
Box( modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
package com.aiosman.ravenow.ui.chat
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
|
||||
@Composable
|
||||
fun ChatSettingScreen() {
|
||||
val appColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
var showThemeSheet by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(appColors.secondaryBackground)
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) {
|
||||
NoticeScreenHeader(title = stringResource(R.string.chat_settings), moreIcon = false)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
SettingCard(
|
||||
title = stringResource(R.string.chat_theme_settings),
|
||||
onClick = { showThemeSheet = true }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
SettingCard(
|
||||
title = stringResource(R.string.report),
|
||||
onClick = { /* TODO: 跳转举报 */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showThemeSheet) {
|
||||
ThemePickerSheet(onClose = { showThemeSheet = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingCard(title: String, onClick: () -> Unit) {
|
||||
val appColors = LocalAppTheme.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(appColors.background)
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 12.dp, vertical = 14.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = title,
|
||||
color = appColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rave_now_nav_right),
|
||||
contentDescription = null,
|
||||
tint = appColors.text,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ThemePickerSheet(onClose: () -> Unit) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val sheetHeight = screenHeight * 0.9f
|
||||
var previewUrl by remember { mutableStateOf<String?>(null) }
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onClose,
|
||||
sheetState = sheetState,
|
||||
containerColor = appColors.secondaryBackground,
|
||||
dragHandle = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(sheetHeight),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.custom_background),
|
||||
color = appColors.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
modifier = Modifier.weight(1f).padding(start = 90.dp),
|
||||
)
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_close),
|
||||
contentDescription = "close",
|
||||
tint = appColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 从相册选择
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(appColors.background)
|
||||
.clickable { /* TODO: 打开相册选择 */ }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.select_from_gallery), color = appColors.text, fontSize = 15.sp, modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.group_info_edit),
|
||||
contentDescription = null,
|
||||
tint = appColors.text,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = stringResource(R.string.featured_backgrounds), color = appColors.text, fontSize = 12.sp)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val presets = remember {
|
||||
listOf(
|
||||
"https://picsum.photos/seed/ai1/400/600",
|
||||
"https://picsum.photos/seed/ai2/400/600",
|
||||
"https://picsum.photos/seed/ai3/400/600",
|
||||
"https://picsum.photos/seed/ai4/400/600",
|
||||
"https://picsum.photos/seed/ai5/400/600",
|
||||
"https://picsum.photos/seed/ai6/400/600",
|
||||
"https://picsum.photos/seed/ai7/400/600",
|
||||
"https://picsum.photos/seed/ai8/400/600",
|
||||
"https://picsum.photos/seed/ai9/400/600",
|
||||
"https://picsum.photos/seed/ai10/400/600",
|
||||
"https://picsum.photos/seed/ai11/400/600",
|
||||
"https://picsum.photos/seed/ai12/400/600",
|
||||
"https://picsum.photos/seed/ai13/400/600",
|
||||
"https://picsum.photos/seed/ai14/400/600",
|
||||
"https://picsum.photos/seed/ai15/400/600",
|
||||
)
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
items(presets) { url ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { previewUrl = url }
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(appColors.decentBackground)
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = url,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(3f / 4f),
|
||||
contentDescription = "preset",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = "Heart Drive",
|
||||
color = appColors.text,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览自定义背景弹窗
|
||||
if (previewUrl != null) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { previewUrl = null },
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
containerColor = appColors.secondaryBackground,
|
||||
dragHandle = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(sheetHeight)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = previewUrl!!,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentDescription = "preview_bg",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(horizontal = 12.dp, vertical = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.previewing_custom_background), color = Color.White, fontSize = 15.sp)
|
||||
}
|
||||
|
||||
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Row {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(Color.White)
|
||||
.padding(horizontal = 14.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.each_theme_unique_experience), color = Color.Black, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(Color.White)
|
||||
.padding(horizontal = 14.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.select_apply_to_use_theme), color = Color.Black, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(Color(0xFF7C4DFF))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.tap_cancel_to_preview_other_themes), color = Color.White, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 底部按钮
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(bottom = 60.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White)
|
||||
.clickable { previewUrl = null }
|
||||
.padding(vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = stringResource(R.string.cancel), color = Color.Black, fontSize = 14.sp)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFEE2A33),
|
||||
Color(0xFFD80264),
|
||||
Color(0xFF664C92)
|
||||
)
|
||||
)
|
||||
)
|
||||
.clickable {
|
||||
previewUrl?.let { url ->
|
||||
com.aiosman.ravenow.AppStore.saveChatBackgroundUrl(url)
|
||||
previewUrl = null
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = stringResource(R.string.moment_ai_apply), color = Color.White, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ fun IndexScreen() {
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
modifier = Modifier.height(58.dp + navigationBarHeight),
|
||||
containerColor = AppColors.secondaryBackground
|
||||
containerColor = AppColors.tabUnselectedBackground
|
||||
) {
|
||||
item.forEachIndexed { idx, it ->
|
||||
val isSelected = model.tabIndex == idx
|
||||
|
||||
@@ -891,7 +891,7 @@ fun ChatRoomCard(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(cardSize)
|
||||
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
|
||||
.clickable(enabled = !viewModel.isJoiningRoom) {
|
||||
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||
// 加入群聊房间
|
||||
|
||||
@@ -230,6 +230,7 @@ fun MomentsList() {
|
||||
when (it) {
|
||||
0 -> {
|
||||
// 推荐页面
|
||||
NewsScreen()
|
||||
}
|
||||
1 -> {
|
||||
// 短视频页面
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.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.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.exp.formatPostTime2
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FullArticleModal(
|
||||
moment: MomentEntity,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val sheetHeight = screenHeight * 0.9f // 90% 高度
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(sheetHeight),
|
||||
containerColor = appColors.background,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
// 滚动内容
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// 新闻图片区域 - 固定高度和宽度
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(250.dp)
|
||||
|
||||
.background(color = appColors.secondaryBackground)
|
||||
) {
|
||||
if (moment.images.isNotEmpty()) {
|
||||
val firstImage = moment.images[0]
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = firstImage.url,
|
||||
contentDescription = "新闻图片",
|
||||
contentScale = ContentScale.Fit,
|
||||
blurHash = firstImage.blurHash,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = androidx.compose.ui.res.painterResource(id = R.drawable.default_moment_img),
|
||||
contentDescription = "默认图片",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 新闻标题
|
||||
Text(
|
||||
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = appColors.text,
|
||||
lineHeight = 28.sp,
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 新闻来源和发布时间
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 来源按钮
|
||||
Button(
|
||||
onClick = { },
|
||||
modifier = Modifier.height(28.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF7c68ef)
|
||||
),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (moment.newsSource.isNotEmpty()) moment.newsSource else moment.nickname,
|
||||
fontSize = 12.sp,
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
// 发布时间
|
||||
Text(
|
||||
text = moment.time.formatPostTime2(),
|
||||
fontSize = 12.sp,
|
||||
color = appColors.secondaryText
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 帖子内容
|
||||
NewsContent(
|
||||
content = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
|
||||
images = moment.images,
|
||||
context = context
|
||||
)
|
||||
Spacer(modifier = Modifier.height(200.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsContent(
|
||||
content: String,
|
||||
images: List<com.aiosman.ravenow.entity.MomentImageEntity>,
|
||||
context: android.content.Context
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = content,
|
||||
fontSize = 16.sp,
|
||||
color = appColors.text,
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
// 图片内容
|
||||
if (images.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
images.forEach { image ->
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = image.url,
|
||||
contentDescription = "内容图片",
|
||||
contentScale = ContentScale.Fit,
|
||||
blurHash = image.blurHash,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -72,6 +73,10 @@ fun NewsScreen() {
|
||||
// 评论弹窗状态
|
||||
var showCommentModal by remember { mutableStateOf(false) }
|
||||
var selectedMoment by remember { mutableStateOf<MomentEntity?>(null) }
|
||||
|
||||
// 查看全文弹窗状态
|
||||
var showFullArticleModal by remember { mutableStateOf(false) }
|
||||
var selectedArticleMoment by remember { mutableStateOf<MomentEntity?>(null) }
|
||||
// 垂直翻页状态
|
||||
val pagerState = rememberPagerState(pageCount = { moments.size })
|
||||
|
||||
@@ -121,6 +126,10 @@ fun NewsScreen() {
|
||||
selectedMoment = momentItem
|
||||
showCommentModal = true
|
||||
},
|
||||
onReadFullClick = {
|
||||
selectedArticleMoment = momentItem
|
||||
showFullArticleModal = true
|
||||
},
|
||||
onLikeClick = {
|
||||
likeDebouncer {
|
||||
// 检查游客模式
|
||||
@@ -157,6 +166,16 @@ fun NewsScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查看全文弹窗
|
||||
if (showFullArticleModal && selectedArticleMoment != null) {
|
||||
FullArticleModal(
|
||||
moment = selectedArticleMoment!!,
|
||||
onDismiss = {
|
||||
showFullArticleModal = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 评论弹窗
|
||||
if (showCommentModal && selectedMoment != null) {
|
||||
val configuration = LocalConfiguration.current
|
||||
@@ -201,6 +220,7 @@ fun NewsItem(
|
||||
moment: MomentEntity,
|
||||
modifier: Modifier = Modifier,
|
||||
onCommentClick: () -> Unit = {},
|
||||
onReadFullClick: () -> Unit = {},
|
||||
onLikeClick: () -> Unit = {},
|
||||
onFavoriteClick: () -> Unit = {}
|
||||
) {
|
||||
@@ -303,19 +323,21 @@ fun NewsItem(
|
||||
|
||||
// 查看全文
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.noRippleClickable { onReadFullClick() }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.read_full_article),
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.text
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Color(0xFF7c45ed)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Image(
|
||||
painter = androidx.compose.ui.res.painterResource(id = R.drawable.rider_pro_nav_search),
|
||||
painter = androidx.compose.ui.res.painterResource(id = R.mipmap.arrow),
|
||||
contentDescription = "箭头",
|
||||
modifier = Modifier.size(12.dp),
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
modifier = Modifier.size(18.dp),
|
||||
colorFilter = ColorFilter.tint(Color(0xFF7c45ed))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/mipmap-hdpi/arrow.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 247 B |
BIN
app/src/main/res/mipmap-mdpi/arrow.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 B |
BIN
app/src/main/res/mipmap-xhdpi/arrow.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 259 B |
BIN
app/src/main/res/mipmap-xxhdpi/arrow.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 B |
BIN
app/src/main/res/mipmap-xxxhdpi/arrow.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 368 B |
@@ -236,5 +236,17 @@
|
||||
<string name="block_description_1">相手はあなたにメッセージを送信したり、あなたのプロフィールやコンテンツを見つけることができなくなります</string>
|
||||
<string name="block_description_2">相手はあなたにブロックされた通知を受け取りません</string>
|
||||
<string name="block_description_3">「設定」でいつでもブロックを解除できます</string>
|
||||
|
||||
<!-- Chat Settings -->
|
||||
<string name="chat_settings">チャット設定</string>
|
||||
<string name="chat_theme_settings">チャットテーマを設定</string>
|
||||
<string name="custom_background">カスタム背景</string>
|
||||
<string name="select_from_gallery">ギャラリーから選択</string>
|
||||
<string name="featured_backgrounds">おすすめ背景</string>
|
||||
<string name="previewing_custom_background">カスタム背景をプレビュー中</string>
|
||||
<string name="each_theme_unique_experience">各テーマには独自の体験があります</string>
|
||||
<string name="select_apply_to_use_theme">「適用」を選択してこのテーマを使用</string>
|
||||
<string name="tap_cancel_to_preview_other_themes">「キャンセル」をタップして他のテーマをプレビュー</string>
|
||||
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -240,4 +240,14 @@
|
||||
<string name="block_description_2">对方不会收到自己被你拉黑的通知</string>
|
||||
<string name="block_description_3">你可以随时"设置"中取消拉黑TA</string>
|
||||
|
||||
<!-- Chat Settings -->
|
||||
<string name="chat_settings">聊天设置</string>
|
||||
<string name="chat_theme_settings">设置聊天主题</string>
|
||||
<string name="custom_background">自定义背景</string>
|
||||
<string name="select_from_gallery">从相册选择</string>
|
||||
<string name="featured_backgrounds">精选背景</string>
|
||||
<string name="previewing_custom_background">正在预览自定义背景</string>
|
||||
<string name="each_theme_unique_experience">每个主题都有自己独特的体验</string>
|
||||
<string name="select_apply_to_use_theme">选择"应用"可选中这个主题</string>
|
||||
<string name="tap_cancel_to_preview_other_themes">轻触"取消"可预览其他主题</string>
|
||||
</resources>
|
||||
@@ -234,4 +234,17 @@
|
||||
<string name="block_description_1">They won\'t be able to send you messages or find your profile or content</string>
|
||||
<string name="block_description_2">They won\'t receive a notification that you blocked them</string>
|
||||
<string name="block_description_3">You can unblock them anytime in "Settings"</string>
|
||||
|
||||
<!-- Chat Settings -->
|
||||
<string name="chat_settings">Chat Settings</string>
|
||||
<string name="chat_theme_settings">Set Chat Theme</string>
|
||||
<string name="custom_background">Custom Background</string>
|
||||
<string name="select_from_gallery">Select from Gallery</string>
|
||||
<string name="featured_backgrounds">Featured Backgrounds</string>
|
||||
<string name="previewing_custom_background">Previewing Custom Background</string>
|
||||
<string name="each_theme_unique_experience">Each theme has its own unique experience</string>
|
||||
<string name="select_apply_to_use_theme">Select "Apply" to use this theme</string>
|
||||
<string name="tap_cancel_to_preview_other_themes">Tap "Cancel" to preview other themes</string>
|
||||
|
||||
|
||||
</resources>
|
||||
Reference in New Issue
Block a user