社团列表

master
pan 3 years ago
parent cf2c48cc9e
commit 4fae920eb1
  1. 8
      app/build.gradle.kts
  2. 156
      app/src/androidTest/java/com/gyf/csams/TestPreview.kt
  3. 25
      app/src/main/java/com/gyf/csams/ui/Base.kt
  4. 313
      app/src/main/java/com/gyf/csams/ui/MainActivity.kt
  5. 72
      app/src/main/java/com/gyf/csams/ui/model/MarqueeViewModel.kt
  6. 146
      app/src/main/java/com/gyf/csams/ui/model/ViewModel.kt
  7. 9
      app/src/main/res/drawable/ic_add_fill.xml
  8. 7
      app/src/test/java/com/gyf/csams/ExampleUnitTest.kt
  9. 2
      build.gradle.kts
  10. 2
      gradle/wrapper/gradle-wrapper.properties

@ -25,7 +25,9 @@ android {
val appName="${rootProject.extra["APP_NAME"]}"
val serverAddress=rootProject.extra["SERVER_ADDRESS"]
debug {
manifestPlaceholders(mapOf("APP_NAME" to appName))
manifestPlaceholders.apply {
this["APP_NAME"] = appName;
}
buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"")
buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"")
}
@ -35,7 +37,9 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
manifestPlaceholders(mapOf("APP_NAME" to appName))
manifestPlaceholders.apply {
this["APP_NAME"] = appName;
}
buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"")
buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"")
}

@ -1,156 +0,0 @@
package com.gyf.csams
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun BoxSetSize(degrees:Float=0F,content: @Composable BoxScope.() -> Unit){
Box(modifier = Modifier
.height(300.dp)
.fillMaxWidth()
.background(Color.Gray)
.rotate(degrees = degrees),contentAlignment = Alignment.Center){
Canvas(modifier = Modifier.size(200.dp)) {
rotate(45f){
drawRect(color = Color.Cyan)
}
}
content()
}
}
@Composable
fun BoxFillSize(degrees:Float=0F, content: @Composable BoxScope.() -> Unit){
Box(modifier = Modifier
.fillMaxSize()
.background(Color.LightGray)
.rotate(degrees = degrees),contentAlignment = Alignment.Center){
Canvas(modifier = Modifier.size(100.dp)) {
rotate(80f){
drawRect(color = Color.Cyan)
}
}
content()
}
}
@Preview
@Composable
fun TestPreview(){
Column(modifier = Modifier.fillMaxSize()){
BoxSetSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("前置子布局固定尺寸")
}
})
}
BoxFillSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("后置子布局使用")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("fillMaxSize修饰符")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("填充父项允许的所有可用空间")
}
})
}
}
}
@Preview
@Composable
fun TestPreview2(){
Column(modifier = Modifier.fillMaxSize()){
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("如果使用示例1方法是")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("无法实现前置子布局填充父项所有可用空间,后置子布局固定尺寸")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("因为前置布局已经填充父项允许的所有可用空间,后置子布局没有剩余空间可用")
}
})
BoxFillSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("前置子布局填充父项允许的所有可用空间")
}
})
}
BoxSetSize {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("后置子布局固定尺寸")
}
})
}
}
}
@Preview
@Composable
fun TestPreview3(){
Column(modifier = Modifier
.fillMaxSize()) {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("折衷方案是通过父项旋转180°实现,子项分别旋转180°复位可")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("实现前置子布局填充父项所有可用空间,后置子布局固定尺寸")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append(",除了旋转,应该有更好的实现方式?")
}
})
Column(modifier = Modifier.rotate(180F)){
BoxSetSize(degrees = 180F) {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("前置子布局固定尺寸,通过旋转和后置子布局对调位置")
}
})
}
BoxFillSize(degrees = 180F) {
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("后置子布局使用")
}
withStyle(style = SpanStyle(color= Color.Red,fontSize = 30.sp)){
append("fillMaxSize修饰符")
}
withStyle(style = SpanStyle(fontSize = 30.sp)){
append("填充父项允许的所有可用空间,通过旋转和前置子布局对调位置")
}
})
}
}
}
}

@ -162,6 +162,28 @@ fun MarqueeText(model: MarqueeViewModel = viewModel(), offset: State<Float>) {
}
/**
* 界面框架
*
* @param background 背景
* @param mainMenu 菜单
* @param nav 导航
* @param body 内容
*/
@Composable
fun MainFrame( background:@Composable ()->Unit,mainMenu: MainMenu,nav: NavController,body:@Composable ColumnScope.()->Unit){
Box(modifier = Modifier.fillMaxSize()) {
background()
Column {
Column(modifier = Modifier.weight(0.9F), content = body)
MainBottomAppBar(
menu = mainMenu,
nav = nav
)
}
}
}
/**
* 图片轮播
*
@ -219,6 +241,9 @@ fun MyBottomAppBarPreview(){
val nav= rememberNavController()
CSAMSTheme {
Surface(color = MaterialTheme.colors.background) {
Column() {
}
MainBottomAppBar(menu = MainMenu.Main, nav = nav)
}
}

@ -6,22 +6,35 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.gyf.csams.R
import com.gyf.csams.ui.model.CarouselViewModel
import com.gyf.csams.ui.model.CommunityDto
import com.gyf.csams.ui.model.ListViewModel
import com.gyf.csams.ui.model.MarqueeViewModel
import com.gyf.csams.ui.theme.CSAMSTheme
@ -51,39 +64,10 @@ fun Body() {
Scaffold(scaffoldState = scaffoldState) {
NavHost(navController, startDestination = MainMenu.Main.name) {
composable(MainMenu.Main.name) {
Box(modifier = Modifier.fillMaxSize()) {
Column {
Notification()
MessageBoard()
Spacer(modifier = Modifier.height(10.dp))
ClubActivitiesTitle()
Spacer(modifier = Modifier.height(10.dp))
Column(Modifier.rotate(180F)) {
MainBottomAppBar(
MainMenu.Main,
navController,
Modifier.rotate(180F)
)
ClubActivitiesImage()
}
}
}
Main(navController = navController)
}
composable(MainMenu.List.name) {
Box(modifier = Modifier.fillMaxSize()) {
Column(Modifier.rotate(180F)) {
MainBottomAppBar(MainMenu.List, navController, Modifier.rotate(180F))
Row(
modifier = Modifier
.fillMaxSize()
.rotate(180F),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
AnimationText(text = "社团列表")
}
}
}
CommunityList(navController = navController)
}
composable(MainMenu.Center.name) {
Box(modifier = Modifier.fillMaxSize()) {
@ -107,6 +91,204 @@ fun Body() {
}
}
/**
* 主界面
*/
@Composable
fun Main(navController: NavController) {
MainFrame(background = { MainBackground() }, mainMenu = MainMenu.Main, nav = navController) {
Column(modifier = Modifier.weight(0.33F)) {
Notification()
MessageBoard()
Spacer(modifier = Modifier.height(10.dp))
ClubActivitiesTitle()
Spacer(modifier = Modifier.height(10.dp))
}
Column(modifier = Modifier.weight(0.66F)) {
ClubActivitiesImage()
}
}
}
/**
* 主界面背景
*
*/
@Composable
fun MainBackground() {
Image(
painter = painterResource(id = R.drawable.mb_bg_fb_08),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxSize()
)
}
/**
* 社团列表
*
* @param navController
*/
@Composable
fun CommunityList(navController: NavController) {
MainFrame(
background = {
CommunityListBackground()
},
mainMenu = MainMenu.List,
nav = navController
) {
RegisterCommunity()
CommunitySearch()
CommunityListBody()
}
}
/**
* 添加社团按钮
*
*/
@Composable
fun RegisterCommunity() {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
Icon(
painter = painterResource(id = R.drawable.ic_add_fill),
contentDescription = null,
modifier = Modifier.size(50.dp)
)
}
}
/**
* 社团列表背景
*
*/
@Composable
fun CommunityListBackground() {
Image(
painter = painterResource(id = R.drawable.mb_bg_fb_07),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxSize()
)
}
/**
* 社团列表
*
*/
@Composable
fun CommunityListBody(model: ListViewModel = viewModel()) {
val communityList: MutableList<CommunityDto>? by model.communityDto.observeAsState()
val listState = rememberLazyListState()
LazyColumn(state = listState) {
communityList?.chunked(2)?.forEach {
item{
Row {
Spacer(modifier = Modifier.weight(0.1F))
Box(modifier = Modifier.weight(0.35F)) {
Community(communityDto = it[0])
}
Spacer(modifier = Modifier.weight(0.05F))
if(it.size==2) {
Box(modifier = Modifier.weight(0.35F)) {
Community(communityDto = it[1])
}
}else{
Box(modifier = Modifier
.weight(0.35F)
.border(width = 1.dp, color = Color.Black))
}
Spacer(modifier = Modifier.weight(0.1F))
}
Spacer(modifier = Modifier
.fillMaxWidth()
.height(10.dp))
}
}
}
if(listState.firstVisibleItemIndex>0){
model.addMore()
}
}
@Composable
fun Community(communityDto: CommunityDto) {
Card {
Image(
painter = painterResource(id = R.drawable.community_list_border),
contentDescription = null
)
Row(modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center) {
Text(text = communityDto.name)
}
}
}
/**
* 社团检索
*
*/
@Composable
fun CommunitySearch(model: ListViewModel = viewModel()) {
val name: String by model.name.observeAsState("")
Card(modifier = Modifier.padding(horizontal = 50.dp, vertical = 10.dp)) {
Column {
Row {
val focusManager = LocalFocusManager.current
Spacer(modifier = Modifier.weight(0.05F))
OutlinedTextField(
modifier = Modifier.weight(0.4F), value = name,
onValueChange = { model.onChangeName(it) },
singleLine = true,
label = { Text(text = model.nameDesc) },
placeholder = { Text(text = model.namePlaceholder) },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done)
)
Spacer(modifier = Modifier.weight(0.1F))
OutlinedTextField(
modifier = Modifier.weight(0.4F), value = name,
onValueChange = { model.onChangeDesc(it) },
singleLine = true,
label = { Text(text = model.descDesc) },
placeholder = { Text(text = model.descPlaceholder) },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done)
)
Spacer(modifier = Modifier.weight(0.05F))
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(10.dp)
)
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(onClick = { model.search() }, modifier = Modifier.width(100.dp)) {
Text(text = "搜索")
}
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(10.dp)
)
}
}
}
/**
* 通知
*
@ -212,64 +394,67 @@ fun ClubActivitiesTitle() {
@Composable
fun ClubActivitiesImage(model: CarouselViewModel = viewModel()) {
Carousel(model = model) {
Column(modifier = Modifier.fillMaxSize()) {
Column {
Card(
modifier = Modifier
.weight(0.4F)
.fillMaxWidth()
.weight(0.6F)
.fillMaxWidth(),
backgroundColor = Color.Transparent
) {
Image(
painter = painterResource(id = R.drawable.hot_activity_desc_background),
painter = painterResource(id = R.drawable.hot_activity_background),
contentDescription = null
)
Box(
modifier = Modifier
.padding(horizontal = 85.dp, vertical = 30.dp)
.rotate(180F)
) {
Text(
text = "文字对任何界面都属于核心内容,而利用 Jetpack Compose 可以更轻松地显示或写入文字。Compose 可以充分利用其构建块的组合,这意味着您无需覆盖各种属性和方法,也无需扩展大型类,即可拥有特定的可组合项设计以及按您期望的方式运行的逻辑。"
.repeat(10), overflow = TextOverflow.Ellipsis
)
}
Image(
painter = painterResource(id = it),
contentDescription = null
)
}
Card(
modifier = Modifier
.weight(0.6F)
.fillMaxWidth()
.rotate(180F)
.weight(0.4F)
.fillMaxWidth(),
backgroundColor = Color.Transparent
) {
Image(
painter = painterResource(id = R.drawable.hot_activity_background),
painter = painterResource(id = R.drawable.hot_activity_desc_background),
contentDescription = null
)
Image(
painter = painterResource(id = it),
contentDescription = null
)
Row(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.weight(0.2F))
Column(
modifier = Modifier
.weight(0.5F)
) {
Spacer(modifier = Modifier.weight(0.1F))
Text(
text = "文字对任何界面都属于核心内容,而利用 Jetpack Compose 可以更轻松地显示或写入文字。Compose 可以充分利用其构建块的组合,这意味着您无需覆盖各种属性和方法,也无需扩展大型类,即可拥有特定的可组合项设计以及按您期望的方式运行的逻辑。"
.repeat(10), overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(0.8F)
)
Spacer(modifier = Modifier.weight(0.1F))
}
Spacer(modifier = Modifier.weight(0.2F))
}
}
}
}
}
//@Preview(showBackground = true)
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
val model = MarqueeViewModel()
CSAMSTheme {
Box(modifier = Modifier.fillMaxSize()) {
Column {
// Marquee(model = model) {
// model, value -> TestA(model = model,
// offset = value)
// }
MessageBoard(model = model)
}
}
// CommunitySearch(model = ListViewModel())
CommunityListBody(model = ListViewModel())
}
}

@ -1,72 +0,0 @@
package com.gyf.csams.ui.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.gyf.csams.R
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* 跑马灯
*
*/
class MarqueeViewModel:ViewModel() {
val marqueeTexts= listOf("床前明月光","疑是地上霜","举头望明月","低头思故乡")
private val _marqueeIndex=MutableLiveData(0)
var marqueeIndex:LiveData<Int> = _marqueeIndex
var marqueeJob:Job? = null
fun addAsync(delayMillis:Int){
if(marqueeJob == null || marqueeJob?.isCompleted==true) {
marqueeJob = viewModelScope.launch {
_marqueeIndex.postValue(
if (_marqueeIndex.value == marqueeTexts.size-1) 0 else _marqueeIndex.value?.plus(
1
)
)
delay(timeMillis = delayMillis.toLong())
}
}
}
}
class CarouselViewModel:ViewModel(){
val imageList= listOf(R.drawable.ic_launcher_foreground,R.drawable.ic_account_fill,R.drawable.ic_all_fill,R.drawable.ic_home_fill)
private val _index=MutableLiveData(0)
val index:LiveData<Int> = _index
var job:Job? = null
init {
start()
}
fun start(){
job = viewModelScope.launch {
do{
_index.postValue(if (_index.value==imageList.size-1) 0 else _index.value?.plus(1))
println("值更新为 :$_index")
delay(5000)
}while (job?.isActive==true)
}
}
fun stop(){
println("停止更新")
job?.cancel()
}
}
class MainViewModel:ViewModel(){
}

@ -0,0 +1,146 @@
package com.gyf.csams.ui.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.gyf.csams.R
import com.orhanobut.logger.Logger
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* 跑马灯
*
*/
class MarqueeViewModel:ViewModel() {
val marqueeTexts= listOf("床前明月光","疑是地上霜","举头望明月","低头思故乡")
private val _marqueeIndex=MutableLiveData(0)
var marqueeIndex:LiveData<Int> = _marqueeIndex
var marqueeJob:Job? = null
fun addAsync(delayMillis:Int){
if(marqueeJob == null || marqueeJob?.isCompleted==true) {
marqueeJob = viewModelScope.launch {
_marqueeIndex.postValue(
if (_marqueeIndex.value == marqueeTexts.size-1) 0 else _marqueeIndex.value?.plus(
1
)
)
delay(timeMillis = delayMillis.toLong())
}
}
}
}
class CarouselViewModel:ViewModel(){
val imageList= listOf(R.drawable.ic_launcher_foreground,R.drawable.ic_account_fill,R.drawable.ic_all_fill,R.drawable.ic_home_fill)
private val _index=MutableLiveData(0)
val index:LiveData<Int> = _index
var job:Job? = null
init {
start()
}
fun start(){
job = viewModelScope.launch {
do{
_index.postValue(if (_index.value==imageList.size-1) 0 else _index.value?.plus(1))
delay(5000)
}while (job?.isActive==true)
}
}
fun stop(){
println("停止更新")
job?.cancel()
}
}
data class CommunityDto(val name:String)
class ListViewModel:ViewModel(){
private val _name=MutableLiveData<String>()
val name:LiveData<String> = _name
val nameDesc="社团名称"
val namePlaceholder="请输入$nameDesc"
private val _desc=MutableLiveData<String>()
val desc:LiveData<String> = _desc
val descDesc="社团简介"
val descPlaceholder="请输入$descDesc"
//社团列表
private val _communityList=MutableLiveData<MutableList<CommunityDto>>(mutableListOf())
val communityDto:LiveData<MutableList<CommunityDto>> = _communityList
init {
loadCommunity()
}
fun onChangeName(name:String){
_name.value=name
}
fun onChangeDesc(desc:String){
_desc.value=desc
}
fun search(){
Logger.i("使用社团名称:${_name.value}和社团简介${_desc.value}搜索社团")
}
/**
* 加载社团列表
*
*/
private fun loadCommunity(){
viewModelScope.launch {
_communityList.value?.apply {
repeat(2
) {
add(CommunityDto(name = "羽毛球社"))
add(CommunityDto(name = "篮球社"))
add(CommunityDto(name = "足球社"))
add(CommunityDto(name = "桌游社"))
add(CommunityDto(name = "乒乓球社"))
}
}
Logger.i("初始化社团size=${_communityList.value?.size}")
}
}
/**
* 加载更多社团列表
*
*/
fun addMore(){
viewModelScope.launch {
Logger.i("加载更多")
_communityList.value?.apply {
add(CommunityDto(name = "羽毛球社"))
add(CommunityDto(name = "篮球社"))
add(CommunityDto(name = "足球社"))
add(CommunityDto(name = "桌游社"))
add(CommunityDto(name = "乒乓球社"))
}
}
}
}

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M512,149.33c200.3,0 362.67,162.37 362.67,362.67s-162.37,362.67 -362.67,362.67S149.33,712.3 149.33,512 311.7,149.33 512,149.33zM544,320h-64v159.98L320,480v64l160,-0.02L480,704h64v-160L704,544v-64h-160L544,320z"/>
</vector>

@ -39,5 +39,12 @@ class ExampleUnitTest {
println( LocalDateTime.now().get(ChronoField.YEAR))
}
@Test
fun testChunked(){
IntRange(0,13).chunked(3){
println(it)
}
}
}

@ -13,7 +13,7 @@ buildscript {
maven("https://maven.aliyun.com/repository/public")
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.0-alpha14")
classpath("com.android.tools.build:gradle:7.0.0-alpha15")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32")
// NOTE: Do not place your application dependencies here; they belong

@ -1,6 +1,6 @@
#Sat Apr 17 15:25:36 CST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-rc-1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

Loading…
Cancel
Save