This commit is contained in:
2024-08-17 17:40:21 +08:00
parent e8140579e0
commit 6137e1c3b5
18 changed files with 494 additions and 63 deletions

View File

@@ -1,10 +1,13 @@
package com.aiosman.riderpro.data package com.aiosman.riderpro.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.AppStore import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.data.api.ChangePasswordRequestBody import com.aiosman.riderpro.data.api.ChangePasswordRequestBody
import com.aiosman.riderpro.data.api.LoginUserRequestBody import com.aiosman.riderpro.data.api.LoginUserRequestBody
import com.aiosman.riderpro.data.api.RegisterRequestBody import com.aiosman.riderpro.data.api.RegisterRequestBody
import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.test.TestDatabase import com.aiosman.riderpro.test.TestDatabase
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
@@ -12,6 +15,7 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
import java.io.IOException
data class AccountProfileEntity( data class AccountProfileEntity(
val id: Int, val id: Int,

View File

@@ -6,6 +6,7 @@ import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.model.MomentEntity import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.model.MomentImageEntity
import com.aiosman.riderpro.test.TestDatabase import com.aiosman.riderpro.test.TestDatabase
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
@@ -57,7 +58,13 @@ data class Moment(
commentCount = commentCount.toInt(), commentCount = commentCount.toInt(),
shareCount = 0, shareCount = 0,
favoriteCount = favoriteCount.toInt(), favoriteCount = favoriteCount.toInt(),
images = images.map { ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}" }, images = images.map {
MomentImageEntity(
url = ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}",
thumbnail = ApiClient.BASE_SERVER + it.thumbnail + "?token=${AppStore.token}",
id = it.id
)
},
authorId = user.id.toInt(), authorId = user.id.toInt(),
liked = isLiked, liked = isLiked,
isFavorite = isFavorite isFavorite = isFavorite
@@ -82,12 +89,14 @@ data class User(
@SerializedName("avatar") @SerializedName("avatar")
val avatar: String val avatar: String
) )
data class UploadImage( data class UploadImage(
val file: File, val file: File,
val filename: String, val filename: String,
val url: String, val url: String,
val ext: String val ext: String
) )
interface MomentService { interface MomentService {
suspend fun getMomentById(id: Int): MomentEntity suspend fun getMomentById(id: Int): MomentEntity
suspend fun likeMoment(id: Int) suspend fun likeMoment(id: Int)
@@ -95,7 +104,8 @@ interface MomentService {
suspend fun getMoments( suspend fun getMoments(
pageNumber: Int, pageNumber: Int,
author: Int? = null, author: Int? = null,
timelineId: Int? = null timelineId: Int? = null,
contentSearch: String? = null
): ListContainer<MomentEntity> ): ListContainer<MomentEntity>
suspend fun createMoment( suspend fun createMoment(
@@ -104,6 +114,7 @@ interface MomentService {
images: List<UploadImage>, images: List<UploadImage>,
relPostId: Int? = null relPostId: Int? = null
): MomentEntity ): MomentEntity
suspend fun favoriteMoment(id: Int) suspend fun favoriteMoment(id: Int)
suspend fun unfavoriteMoment(id: Int) suspend fun unfavoriteMoment(id: Int)
} }
@@ -112,7 +123,8 @@ interface MomentService {
class MomentPagingSource( class MomentPagingSource(
private val remoteDataSource: MomentRemoteDataSource, private val remoteDataSource: MomentRemoteDataSource,
private val author: Int? = null, private val author: Int? = null,
private val timelineId: Int? = null private val timelineId: Int? = null,
private val contentSearch: String? = null
) : PagingSource<Int, MomentEntity>() { ) : PagingSource<Int, MomentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
return try { return try {
@@ -120,7 +132,8 @@ class MomentPagingSource(
val moments = remoteDataSource.getMoments( val moments = remoteDataSource.getMoments(
pageNumber = currentPage, pageNumber = currentPage,
author = author, author = author,
timelineId = timelineId timelineId = timelineId,
contentSearch = contentSearch
) )
LoadResult.Page( LoadResult.Page(
@@ -145,9 +158,10 @@ class MomentRemoteDataSource(
suspend fun getMoments( suspend fun getMoments(
pageNumber: Int, pageNumber: Int,
author: Int?, author: Int?,
timelineId: Int? timelineId: Int?,
contentSearch: String?
): ListContainer<MomentEntity> { ): ListContainer<MomentEntity> {
return momentService.getMoments(pageNumber, author, timelineId) return momentService.getMoments(pageNumber, author, timelineId, contentSearch)
} }
} }
@@ -158,9 +172,10 @@ class TestMomentServiceImpl() : MomentService {
override suspend fun getMoments( override suspend fun getMoments(
pageNumber: Int, pageNumber: Int,
author: Int?, author: Int?,
timelineId: Int? timelineId: Int?,
contentSearch: String?
): ListContainer<MomentEntity> { ): ListContainer<MomentEntity> {
return testMomentBackend.fetchMomentItems(pageNumber, author, timelineId) return testMomentBackend.fetchMomentItems(pageNumber, author, timelineId, contentSearch)
} }
override suspend fun getMomentById(id: Int): MomentEntity { override suspend fun getMomentById(id: Int): MomentEntity {
@@ -202,13 +217,15 @@ class TestMomentBackend(
suspend fun fetchMomentItems( suspend fun fetchMomentItems(
pageNumber: Int, pageNumber: Int,
author: Int? = null, author: Int? = null,
timelineId: Int? timelineId: Int?,
contentSearch: String?
): ListContainer<MomentEntity> { ): ListContainer<MomentEntity> {
val resp = ApiClient.api.getPosts( val resp = ApiClient.api.getPosts(
pageSize = DataBatchSize, pageSize = DataBatchSize,
page = pageNumber, page = pageNumber,
timelineId = timelineId, timelineId = timelineId,
authorId = author authorId = author,
contentSearch = contentSearch
) )
val body = resp.body() ?: throw ServiceException("Failed to get moments") val body = resp.body() ?: throw ServiceException("Failed to get moments")
return ListContainer( return ListContainer(
@@ -258,6 +275,7 @@ class TestMomentBackend(
suspend fun favoriteMoment(id: Int) { suspend fun favoriteMoment(id: Int) {
ApiClient.api.favoritePost(id) ApiClient.api.favoritePost(id)
} }
suspend fun unfavoriteMoment(id: Int) { suspend fun unfavoriteMoment(id: Int) {
ApiClient.api.unfavoritePost(id) ApiClient.api.unfavoritePost(id)
} }

View File

@@ -1,17 +1,53 @@
package com.aiosman.riderpro.data package com.aiosman.riderpro.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.test.TestDatabase import com.aiosman.riderpro.test.TestDatabase
import java.io.IOException
data class UserAuth( data class UserAuth(
val id: Int, val id: Int,
val token: String? = null val token: String? = null
) )
class AccountPagingSource(
private val userService: UserService,
private val nickname: String? = null
) : PagingSource<Int, AccountProfileEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AccountProfileEntity> {
return try {
val currentPage = params.key ?: 1
val users = userService.getUsers(
page = currentPage,
nickname = nickname
)
LoadResult.Page(
data = users.list,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (users.list.isEmpty()) null else users.page + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, AccountProfileEntity>): Int? {
return state.anchorPosition
}
}
interface UserService { interface UserService {
suspend fun getUserProfile(id: String): AccountProfileEntity suspend fun getUserProfile(id: String): AccountProfileEntity
suspend fun followUser(id: String) suspend fun followUser(id: String)
suspend fun unFollowUser(id: String) suspend fun unFollowUser(id: String)
suspend fun getUsers(
pageSize: Int = 20,
page: Int = 1,
nickname: String? = null
): ListContainer<AccountProfileEntity>
} }
@@ -31,4 +67,19 @@ class TestUserServiceImpl : UserService {
val resp = ApiClient.api.unfollowUser(id.toInt()) val resp = ApiClient.api.unfollowUser(id.toInt())
return return
} }
override suspend fun getUsers(
pageSize: Int,
page: Int,
nickname: String?
): ListContainer<AccountProfileEntity> {
val resp = ApiClient.api.getUsers(page, pageSize, nickname)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return ListContainer<AccountProfileEntity>(
list = body.list.map { it.toAccountProfileEntity() },
page = body.page,
total = body.total,
pageSize = body.pageSize
)
}
} }

View File

@@ -75,6 +75,7 @@ interface RiderProAPI {
@Query("pageSize") pageSize: Int = 20, @Query("pageSize") pageSize: Int = 20,
@Query("timelineId") timelineId: Int? = null, @Query("timelineId") timelineId: Int? = null,
@Query("authorId") authorId: Int? = null, @Query("authorId") authorId: Int? = null,
@Query("contentSearch") contentSearch: String? = null,
): Response<ListContainer<Moment>> ): Response<ListContainer<Moment>>
@Multipart @Multipart
@@ -163,4 +164,11 @@ interface RiderProAPI {
@Path("id") id: Int @Path("id") id: Int
): Response<Unit> ): Response<Unit>
@GET("users")
suspend fun getUsers(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("nickname") search: String? = null,
): Response<ListContainer<AccountProfile>>
} }

View File

@@ -1,7 +1,11 @@
package com.aiosman.riderpro.model package com.aiosman.riderpro.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
data class MomentImageEntity(
val id: Long,
val url: String,
val thumbnail: String
)
data class MomentEntity( data class MomentEntity(
val id: Int, val id: Int,
val avatar: String, val avatar: String,
@@ -15,7 +19,7 @@ data class MomentEntity(
val commentCount: Int, val commentCount: Int,
val shareCount: Int, val shareCount: Int,
val favoriteCount: Int, val favoriteCount: Int,
val images: List<String> = emptyList(), val images: List<MomentImageEntity> = emptyList(),
val authorId: Int = 0, val authorId: Int = 0,
var liked: Boolean = false, var liked: Boolean = false,
var relPostId: Int? = null, var relPostId: Int? = null,

View File

@@ -4,6 +4,7 @@ import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountProfileEntity import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.CommentEntity import com.aiosman.riderpro.data.CommentEntity
import com.aiosman.riderpro.model.MomentEntity import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.model.MomentImageEntity
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import io.github.serpro69.kfaker.faker import io.github.serpro69.kfaker.faker
@@ -124,7 +125,13 @@ object TestDatabase {
commentCount = commentCount + 1, commentCount = commentCount + 1,
shareCount = faker.random.nextInt(0, 100), shareCount = faker.random.nextInt(0, 100),
favoriteCount = faker.random.nextInt(0, 100), favoriteCount = faker.random.nextInt(0, 100),
images = imageList.shuffled().take(3), images = imageList.shuffled().take(3).map {
MomentImageEntity(
id = faker.random.nextLong(),
url = it,
thumbnail = it
)
},
authorId = person.id authorId = person.id
) )
} }

View File

@@ -30,7 +30,7 @@ fun RelPostCard(
image?.let { image?.let {
CustomAsyncImage( CustomAsyncImage(
context, context,
image, image.thumbnail,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -1,9 +1,11 @@
package com.aiosman.riderpro.ui.imageviewer package com.aiosman.riderpro.ui.imageviewer
import com.aiosman.riderpro.model.MomentImageEntity
object ImageViewerViewModel { object ImageViewerViewModel {
var imageList = mutableListOf<String>() var imageList = mutableListOf<MomentImageEntity>()
var initialIndex = 0 var initialIndex = 0
fun asNew(images: List<String>, index: Int = 0) { fun asNew(images: List<MomentImageEntity>, index: Int = 0) {
imageList.clear() imageList.clear()
imageList.addAll(images) imageList.addAll(images)
initialIndex = index initialIndex = index

View File

@@ -54,7 +54,7 @@ fun ImageViewer() {
with(sharedTransitionScope) { with(sharedTransitionScope) {
CustomAsyncImage( CustomAsyncImage(
context, context,
images[page], images[page].url,
contentDescription = null, contentDescription = null,
modifier = Modifier.sharedElement( modifier = Modifier.sharedElement(
rememberSharedContentState(key = images[page]), rememberSharedContentState(key = images[page]),

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp
import com.aiosman.riderpro.ui.index.tabs.add.AddPage import com.aiosman.riderpro.ui.index.tabs.add.AddPage
import com.aiosman.riderpro.ui.index.tabs.moment.MomentsList import com.aiosman.riderpro.ui.index.tabs.moment.MomentsList
import com.aiosman.riderpro.ui.index.tabs.profile.ProfilePage import com.aiosman.riderpro.ui.index.tabs.profile.ProfilePage
import com.aiosman.riderpro.ui.index.tabs.search.SearchScreen
import com.aiosman.riderpro.ui.index.tabs.shorts.ShortVideo import com.aiosman.riderpro.ui.index.tabs.shorts.ShortVideo
import com.aiosman.riderpro.ui.index.tabs.street.StreetPage import com.aiosman.riderpro.ui.index.tabs.street.StreetPage
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -40,9 +41,10 @@ fun IndexScreen() {
} }
val item = listOf( val item = listOf(
NavigationItem.Home, NavigationItem.Home,
NavigationItem.Street, NavigationItem.Search,
// NavigationItem.Street,
NavigationItem.Add, NavigationItem.Add,
NavigationItem.Message, // NavigationItem.Message,
NavigationItem.Profile NavigationItem.Profile
) )
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
@@ -99,17 +101,22 @@ fun IndexScreen() {
) { ) {
Home() Home()
} }
1 -> Box(
modifier = Modifier.padding(innerPadding)
) {
SearchScreen()
}
1 -> Street() // 1 -> Street()
2 -> Box( 2 -> Box(
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { Add() } ) { Add() }
3 -> Box( // 3 -> Box(
modifier = Modifier.padding(innerPadding) // modifier = Modifier.padding(innerPadding)
) { Video() } // ) { Video() }
4 -> Box( 3 -> Box(
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { Profile() } ) { Profile() }
} }

View File

@@ -1,5 +1,8 @@
package com.aiosman.riderpro.ui.index package com.aiosman.riderpro.ui.index
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
@@ -34,4 +37,9 @@ sealed class NavigationItem(
icon = { ImageVector.vectorResource(R.drawable.rider_pro_profile) }, icon = { ImageVector.vectorResource(R.drawable.rider_pro_profile) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_profile_filed) } selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_profile_filed) }
) )
data object Search : NavigationItem("Search",
icon = { Icons.Default.Search },
selectedIcon = { Icons.Default.Search }
)
} }

View File

@@ -34,7 +34,7 @@ fun AddPage(){
NewPostViewModel.asNewPost() NewPostViewModel.asNewPost()
navController.navigate("NewPost") navController.navigate("NewPost")
} }
AddBtn(icon = R.drawable.rider_pro_location_create, text = "Location Create") // AddBtn(icon = R.drawable.rider_pro_location_create, text = "Location Create")
} }
} }

View File

@@ -131,9 +131,10 @@ fun MomentsList() {
@Composable @Composable
fun MomentCard( fun MomentCard(
momentEntity: MomentEntity, momentEntity: MomentEntity,
onLikeClick: () -> Unit, onLikeClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {}, onFavoriteClick: () -> Unit = {},
onAddComment: () -> Unit = {} onAddComment: () -> Unit = {},
hideAction: Boolean = false
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
Column( Column(
@@ -153,6 +154,7 @@ fun MomentCard(
.fillMaxHeight() .fillMaxHeight()
.weight(1f) .weight(1f)
// ModificationListHeader() // ModificationListHeader()
if (!hideAction){
MomentBottomOperateRowGroup( MomentBottomOperateRowGroup(
momentOperateBtnBoxModifier, momentOperateBtnBoxModifier,
momentEntity = momentEntity, momentEntity = momentEntity,
@@ -166,6 +168,8 @@ fun MomentCard(
onFavoriteClick = onFavoriteClick onFavoriteClick = onFavoriteClick
) )
} }
}
} }
@Composable @Composable
@@ -342,7 +346,7 @@ fun MomentContentGroup(
with(sharedTransitionScope) { with(sharedTransitionScope) {
CustomAsyncImage( CustomAsyncImage(
context, context,
it, it.thumbnail,
modifier = Modifier modifier = Modifier
.sharedElement( .sharedElement(
rememberSharedContentState(key = it), rememberSharedContentState(key = it),

View File

@@ -396,26 +396,26 @@ fun CommunicationOperatorGroup(
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
) )
} }
Box( // Box(
modifier = Modifier // modifier = Modifier
.size(width = 142.dp, height = 40.dp) // .size(width = 142.dp, height = 40.dp)
.clickable { // .clickable {
navController.navigate("ProfileTimeline") // navController.navigate("ProfileTimeline")
}, // },
contentAlignment = Alignment.Center // contentAlignment = Alignment.Center
) { // ) {
Image( // Image(
modifier = Modifier.fillMaxSize(), // modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.rider_pro_profile_follow), // painter = painterResource(id = R.drawable.rider_pro_profile_follow),
contentDescription = "" // contentDescription = ""
) // )
Text( // Text(
text = "GALLERY", // text = "GALLERY",
fontSize = 16.sp, // fontSize = 16.sp,
color = Color.White, // color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) // style = TextStyle(fontWeight = FontWeight.Bold)
) // )
} // }
} }
} }
@@ -504,7 +504,7 @@ fun MomentPostUnit(momentEntity: MomentEntity) {
TimeGroup(momentEntity.time) TimeGroup(momentEntity.time)
ProfileMomentCard( ProfileMomentCard(
momentEntity.momentTextContent, momentEntity.momentTextContent,
momentEntity.images[0], momentEntity.images[0].thumbnail,
momentEntity.likeCount.toString(), momentEntity.likeCount.toString(),
momentEntity.commentCount.toString(), momentEntity.commentCount.toString(),
momentId = momentEntity.id momentId = momentEntity.id

View File

@@ -0,0 +1,250 @@
package com.aiosman.riderpro.ui.index.tabs.search
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
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.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.index.tabs.moment.MomentCard
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
fun SearchScreen() {
val model = SearchViewModel
val categories = listOf("Moment", "User")
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = { categories.size })
val selectedTabIndex = remember { derivedStateOf { pagerState.currentPage } }
val keyboardController = LocalSoftwareKeyboardController.current
val systemUiController = rememberSystemUiController()
LaunchedEffect(Unit) {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true)
}
Column(
modifier = Modifier
.background(Color.White)
.fillMaxSize()
.padding(start = 16.dp, end = 16.dp, top = 24.dp)
) {
SearchInput(
modifier = Modifier.fillMaxWidth(),
text = model.searchText,
onTextChange = {
model.searchText = it
},
onSearch = {
model.search()
// hide ime
keyboardController?.hide() // Hide the keyboard
}
)
if (model.showResult) {
Spacer(modifier = Modifier.padding(8.dp))
TabRow(
selectedTabIndex = selectedTabIndex.value,
backgroundColor = Color.White,
) {
categories.forEachIndexed { index, category ->
Tab(
selected = selectedTabIndex.value == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(category) }
)
}
}
SearchPager(
pagerState = pagerState
)
}
}
}
@Composable
fun SearchInput(
modifier: Modifier = Modifier,
text: String = "",
onTextChange: (String) -> Unit = {},
onSearch: () -> Unit = {}
) {
Box(
modifier = modifier
.clip(shape = RoundedCornerShape(8.dp))
.background(Color(0xFFEEEEEE))
.padding(horizontal = 16.dp, vertical = 16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Search,
contentDescription = null
)
Box {
if (text.isEmpty()) {
Text(
text = "Search",
modifier = Modifier.padding(start = 8.dp),
color = Color(0xFF9E9E9E),
fontSize = 18.sp
)
}
BasicTextField(
value = text,
onValueChange = {
onTextChange(it)
},
modifier = Modifier
.padding(start = 8.dp)
.fillMaxWidth(),
singleLine = true,
textStyle = TextStyle(
fontSize = 18.sp
),
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
onSearch()
}
)
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchPager(
pagerState: PagerState,
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { page ->
when (page) {
0 -> MomentResultTab()
1 -> UserResultTab()
}
}
}
@Composable
fun MomentResultTab() {
val model = SearchViewModel
var dataFlow = model.momentsFlow
var moments = dataFlow.collectAsLazyPagingItems()
Box(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items
Spacer(modifier = Modifier.padding(8.dp))
MomentCard(momentEntity = momentItem, hideAction = true)
}
}
}
}
@Composable
fun UserResultTab() {
val model = SearchViewModel
val users = model.usersFlow.collectAsLazyPagingItems()
Box(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(users.itemCount) { idx ->
val userItem = users[idx] ?: return@items
UserItem(userItem)
}
}
}
}
@Composable
fun UserItem(accountProfile: AccountProfileEntity) {
val context = LocalContext.current
val navController = LocalNavController.current
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp).noRippleClickable {
navController.navigate("AccountProfile/${accountProfile.id}")
},
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
context,
imageUrl = accountProfile.avatar,
modifier = Modifier
.size(64.dp)
.clip(CircleShape),
contentDescription = null
)
Spacer(modifier = Modifier.padding(16.dp))
Text(text = accountProfile.nickName, fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
}

View File

@@ -0,0 +1,66 @@
package com.aiosman.riderpro.ui.index.tabs.search
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aiosman.riderpro.data.AccountPagingSource
import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.MomentPagingSource
import com.aiosman.riderpro.data.MomentRemoteDataSource
import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.TestMomentServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel
import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel.accountService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
object SearchViewModel : ViewModel() {
var searchText by mutableStateOf("")
private val momentService: MomentService = TestMomentServiceImpl()
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
val momentsFlow = _momentsFlow.asStateFlow()
private val userService = TestUserServiceImpl()
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
val usersFlow = _usersFlow.asStateFlow()
var showResult by mutableStateOf(false)
fun search() {
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
MomentPagingSource(
MomentRemoteDataSource(momentService),
contentSearch = searchText
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_momentsFlow.value = it
}
}
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
AccountPagingSource(
userService,
nickname = searchText
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_usersFlow.value = it
}
}
showResult = true
}
}

View File

@@ -88,6 +88,7 @@ import com.aiosman.riderpro.data.TestMomentServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.data.UserService import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.model.MomentEntity import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.model.MomentImageEntity
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
@@ -426,7 +427,7 @@ fun Header(
@Composable @Composable
fun PostImageView( fun PostImageView(
postId: String, postId: String,
images: List<String>, images: List<MomentImageEntity>,
) { ) {
val pagerState = rememberPagerState(pageCount = { images.size }) val pagerState = rememberPagerState(pageCount = { images.size })
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -445,7 +446,7 @@ fun PostImageView(
with(sharedTransitionScope) { with(sharedTransitionScope) {
CustomAsyncImage( CustomAsyncImage(
context, context,
image, image.thumbnail,
contentDescription = "Image", contentDescription = "Image",
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = Modifier modifier = Modifier

View File

@@ -18,6 +18,7 @@ object Utils {
.okHttpClient(okHttpClient) .okHttpClient(okHttpClient)
.diskCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED)
.components { .components {
} }