diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 49afb24..022a71c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { implementation("io.coil-kt:coil:2.7.0") implementation("com.google.android.gms:play-services-auth:21.2.0") implementation("io.github.serpro69:kotlin-faker:2.0.0-rc.5") + implementation("androidx.compose.material:material:1.6.8") } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt index 0de1b98..0876185 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt @@ -2,6 +2,7 @@ package com.aiosman.riderpro.data import androidx.paging.PagingSource import androidx.paging.PagingState +import com.aiosman.riderpro.R import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.test.TestDatabase import java.io.IOException @@ -16,6 +17,12 @@ interface MomentService { author: Int? = null, timelineId: Int? = null ): ListContainer + + suspend fun createMoment( + content: String, + authorId: Int, + imageUriList: List + ): MomentItem } @@ -86,6 +93,14 @@ class TestMomentServiceImpl() : MomentService { testMomentBackend.dislikeMoment(id) } + override suspend fun createMoment( + content: String, + authorId: Int, + imageUriList: List + ): MomentItem { + return testMomentBackend.createMoment(content, authorId, imageUriList) + } + } class TestMomentBackend( @@ -98,6 +113,7 @@ class TestMomentBackend( timelineId: Int? ): ListContainer { var rawList = TestDatabase.momentData + rawList = rawList.sortedBy { it.id }.reversed() if (author != null) { rawList = rawList.filter { it.authorId == author } } @@ -105,7 +121,7 @@ class TestMomentBackend( val followIdList = TestDatabase.followList.filter { it.first == timelineId }.map { it.second } - rawList = rawList.filter { it.authorId in followIdList } + rawList = rawList.filter { it.authorId in followIdList || it.authorId == 1 } } val from = (pageNumber - 1) * DataBatchSize val to = (pageNumber) * DataBatchSize @@ -119,11 +135,13 @@ class TestMomentBackend( } val currentSublist = rawList.subList(from, min(to, rawList.size)) currentSublist.forEach { - val myLikeIdList = TestDatabase.likeMomentList.filter { it.second == 1 }.map { it.first } + val myLikeIdList = + TestDatabase.likeMomentList.filter { it.second == 1 }.map { it.first } if (myLikeIdList.contains(it.id)) { it.liked = true } } + // delay kotlinx.coroutines.delay(loadDelay) return ListContainer( @@ -153,6 +171,7 @@ class TestMomentBackend( TestDatabase.updateMomentById(id, newMoment) TestDatabase.likeMomentList += Pair(id, 1) } + suspend fun dislikeMoment(id: Int) { val oldMoment = TestDatabase.momentData.first { it.id == id @@ -164,4 +183,33 @@ class TestMomentBackend( } } + suspend fun createMoment( + content: String, + authorId: Int, + imageUriList: List + ): MomentItem { + TestDatabase.momentIdCounter += 1 + val person = TestDatabase.accountData.first { + it.id == authorId + } + val newMoment = MomentItem( + id = TestDatabase.momentIdCounter, + avatar = person.avatar, + nickname = person.nickName, + location = person.country, + time = "2023.02.02 11:23", + followStatus = false, + momentTextContent = content, + momentPicture = R.drawable.default_moment_img, + likeCount = 0, + commentCount = 0, + shareCount = 0, + favoriteCount = 0, + images = imageUriList, + authorId = person.id + ) + TestDatabase.momentData += newMoment + return newMoment + } + } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt b/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt index 93d4d90..be8858a 100644 --- a/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt +++ b/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt @@ -14,6 +14,7 @@ object TestDatabase { var accountData = emptyList() var comment = emptyList() var commentIdCounter = 0 + var momentIdCounter = 0 var selfId = 1 var imageList = listOf( "https://img.freepik.com/free-photo/white-billboard-template_23-2147726635.jpg?t=st=1722150015~exp=1722153615~hmac=5540620196d7898215d822be26353c87a63d51bbfb2b814e032626e1948a1583&w=740", @@ -76,6 +77,7 @@ object TestDatabase { } momentData = (0..60).toList().mapIndexed { idx, _ -> + momentIdCounter += 1 val person = accountData.random() // make fake comment val commentCount = faker.random.nextInt(0, 50) @@ -88,7 +90,7 @@ object TestDatabase { date = "2023-02-02 11:23", likes = 0, replies = emptyList(), - postId = idx, + postId = momentIdCounter, avatar = commentPerson.avatar, author = commentPerson.id, id = commentIdCounter, @@ -105,10 +107,10 @@ object TestDatabase { val likeCount = faker.random.nextInt(0, 5) for (i in 0..likeCount) { val likePerson = accountData.random() - likeMomentList += Pair(idx, likePerson.id) + likeMomentList += Pair(momentIdCounter, likePerson.id) } MomentItem( - id = idx, + id = momentIdCounter, avatar = person.avatar, nickname = person.nickName, location = person.country, diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt index 7210494..b3adcec 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/Moment.kt @@ -21,8 +21,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Build +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -30,6 +34,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,6 +50,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import coil.compose.AsyncImage import com.aiosman.riderpro.LocalNavController @@ -56,33 +62,53 @@ import com.aiosman.riderpro.ui.composables.AnimatedCounter import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.google.accompanist.systemuicontroller.rememberSystemUiController +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterialApi::class) @Composable fun MomentsList() { + val model = MomentViewModel var dataFlow = model.momentsFlow var moments = dataFlow.collectAsLazyPagingItems() val scope = rememberCoroutineScope() - LazyColumn { - items(moments.itemCount) { idx -> - val momentItem = moments[idx] ?: return@items - MomentCard(momentItem = momentItem, - onAddComment = { - scope.launch { - model.onAddComment(momentItem.id) - } - }, - onLikeClick = { - scope.launch { - if (momentItem.liked) { - model.dislikeMoment(momentItem.id) - } else { - model.likeMoment(momentItem.id) + var refreshing by remember { mutableStateOf(false) } + moments.loadState + val state = rememberPullRefreshState(refreshing, onRefresh = { + model.refreshPager() + }) + LaunchedEffect(moments.loadState) { + if (moments.loadState.refresh is LoadState.Loading) { + refreshing = true + } else { + refreshing = false + } + } + + Box(Modifier.pullRefresh(state)) { + LazyColumn { + items(moments.itemCount) { idx -> + val momentItem = moments[idx] ?: return@items + MomentCard(momentItem = momentItem, + onAddComment = { + scope.launch { + model.onAddComment(momentItem.id) + } + }, + onLikeClick = { + scope.launch { + if (momentItem.liked) { + model.dislikeMoment(momentItem.id) + } else { + model.likeMoment(momentItem.id) + } } } - }) + ) + } } + PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) } } @@ -366,10 +392,7 @@ fun MomentBottomOperateRowGroup( modifier = Modifier.size(24.dp), liked = momentItem.liked ) { - onLikeClick() - - } } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/MomentViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/MomentViewModel.kt index c924b9f..48457b9 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/MomentViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/moment/MomentViewModel.kt @@ -25,7 +25,6 @@ object MomentViewModel : ViewModel() { private val _momentsFlow = MutableStateFlow>(PagingData.empty()) val momentsFlow = _momentsFlow.asStateFlow() val accountService: AccountService = TestAccountServiceImpl() - init { viewModelScope.launch { val profile = accountService.getMyAccountProfile() @@ -42,6 +41,22 @@ object MomentViewModel : ViewModel() { } } } + fun refreshPager() { + viewModelScope.launch { + val profile = accountService.getMyAccountProfile() + Pager( + config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + MomentPagingSource( + MomentRemoteDataSource(momentService), + timelineId = profile.id + ) + } + ).flow.cachedIn(viewModelScope).collectLatest { + _momentsFlow.value = it + } + } + } fun updateLikeCount(id: Int) { val currentPagingData = _momentsFlow.value diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt index 7362e83..16c07ea 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt @@ -38,15 +38,20 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewModelScope +import coil.compose.AsyncImage import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.R import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout +import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.google.accompanist.systemuicontroller.rememberSystemUiController +import kotlinx.coroutines.launch @Preview @@ -54,6 +59,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController fun NewPostScreen() { val model = NewPostViewModel val systemUiController = rememberSystemUiController() + val navController = LocalNavController.current LaunchedEffect(Unit) { systemUiController.setNavigationBarColor(color = Color.Transparent) } @@ -64,7 +70,12 @@ fun NewPostScreen() { modifier = Modifier .fillMaxSize() ) { - NewPostTopBar() + NewPostTopBar { + model.viewModelScope.launch { + model.createMoment() + navController.popBackStack() + } + } NewPostTextField("Share your adventure…", NewPostViewModel.textContent) { NewPostViewModel.textContent = it } @@ -72,11 +83,10 @@ fun NewPostScreen() { AdditionalPostItem() } } - } @Composable -fun NewPostTopBar() { +fun NewPostTopBar(onSendClick: () -> Unit = {}) { val navController = LocalNavController.current Box( modifier = Modifier @@ -89,18 +99,21 @@ fun NewPostTopBar() { Image( painter = painterResource(id = R.drawable.rider_pro_close), contentDescription = "Back", - modifier = Modifier.size(24.dp).clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - navController.popBackStack() - } + modifier = Modifier + .size(24.dp) + .noRippleClickable { + navController.popBackStack() + } ) Spacer(modifier = Modifier.weight(1f)) Image( painter = painterResource(id = R.drawable.rider_pro_send_post), contentDescription = "Send", - modifier = Modifier.size(24.dp) + modifier = Modifier + .size(24.dp) + .noRippleClickable { + onSendClick() + } ) } } @@ -134,14 +147,15 @@ fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Uni @Composable fun AddImageGrid() { val context = LocalContext.current - var imageUriList by remember { mutableStateOf(listOf()) } + val model = NewPostViewModel + val pickImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == Activity.RESULT_OK) { val uri = result.data?.data if (uri != null) { - imageUriList = imageUriList + uri.toString() + model.imageUriList += uri.toString() } } } @@ -159,16 +173,17 @@ fun AddImageGrid() { verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - imageUriList.forEach { - Image( - painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic), - contentDescription = "Add Image", + model.imageUriList.forEach { + AsyncImage( + it, + contentDescription = "Image", modifier = Modifier .size(110.dp) .background(Color(0xFFFFFFFF)) .drawBehind { drawRoundRect(color = Color(0xFF999999), style = stroke) - } + }, + contentScale = ContentScale.Crop ) } Box( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt index 999f6e7..e87a115 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt @@ -2,19 +2,33 @@ package com.aiosman.riderpro.ui.post import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aiosman.riderpro.data.MomentService +import com.aiosman.riderpro.data.TestMomentServiceImpl import com.aiosman.riderpro.ui.modification.Modification +import kotlinx.coroutines.launch object NewPostViewModel : ViewModel() { + var momentService: MomentService = TestMomentServiceImpl() var textContent by mutableStateOf("") var searchPlaceAddressResult by mutableStateOf(null) var modificationList by mutableStateOf>(listOf()) - + var imageUriList by mutableStateOf(listOf()) fun asNewPost() { textContent = "" searchPlaceAddressResult = null modificationList = listOf() } + + suspend fun createMoment() { + momentService.createMoment( + content = textContent, + authorId = 1, + imageUriList = imageUriList + ) + } } \ No newline at end of file