This commit is contained in:
2024-08-11 21:17:02 +08:00
parent 19527f17c3
commit 322a4320c7
9 changed files with 160 additions and 31 deletions

View File

@@ -0,0 +1,6 @@
package com.aiosman.riderpro
object ConstVars {
// api 地址
const val BASE_SERVER = "http://192.168.31.57:8088"
}

View File

@@ -1,9 +1,16 @@
package com.aiosman.riderpro.data package com.aiosman.riderpro.data
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.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.test.TestDatabase import com.aiosman.riderpro.test.TestDatabase
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
data class AccountProfileEntity( data class AccountProfileEntity(
val id: Int, val id: Int,
@@ -15,6 +22,7 @@ data class AccountProfileEntity(
val country: String, val country: String,
val isFollowing: Boolean val isFollowing: Boolean
) )
//{ //{
// "id": 1, // "id": 1,
// "username": "root", // "username": "root",
@@ -23,7 +31,7 @@ data class AccountProfileEntity(
// "followingCount": 1, // "followingCount": 1,
// "followerCount": 0 // "followerCount": 0
//} //}
data class AccountProfile ( data class AccountProfile(
val id: Int, val id: Int,
val username: String, val username: String,
val nickname: String, val nickname: String,
@@ -38,13 +46,14 @@ data class AccountProfile (
followerCount = followerCount, followerCount = followerCount,
followingCount = followingCount, followingCount = followingCount,
nickName = nickname, nickName = nickname,
avatar = ApiClient.BASE_SERVER + avatar, avatar = ApiClient.BASE_SERVER + avatar + "?token=${AppStore.token}",
bio = "", bio = "",
country = "Worldwide", country = "Worldwide",
isFollowing = isFollowing isFollowing = isFollowing
) )
} }
} }
interface AccountService { interface AccountService {
suspend fun getMyAccountProfile(): AccountProfileEntity suspend fun getMyAccountProfile(): AccountProfileEntity
suspend fun getAccountProfileById(id: Int): AccountProfileEntity suspend fun getAccountProfileById(id: Int): AccountProfileEntity
@@ -52,7 +61,7 @@ interface AccountService {
suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth
suspend fun logout() suspend fun logout()
suspend fun updateAvatar(uri: String) suspend fun updateAvatar(uri: String)
suspend fun updateProfile(nickName: String, bio: String) suspend fun updateProfile(avatar: UploadImage?, nickName: String?, bio: String?)
suspend fun registerUserWithPassword(loginName: String, password: String) suspend fun registerUserWithPassword(loginName: String, password: String)
} }
@@ -95,14 +104,17 @@ class TestAccountServiceImpl : AccountService {
} }
} }
override suspend fun updateProfile(nickName: String, bio: String) { fun createMultipartBody(file: File, filename:String,name: String): MultipartBody.Part {
TestDatabase.accountData = TestDatabase.accountData.map { val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
if (it.id == 1) { return MultipartBody.Part.createFormData(name, filename, requestFile)
it.copy(nickName = nickName, bio = bio) }
} else {
it override suspend fun updateProfile(avatar: UploadImage?, nickName: String?, bio: String?) {
} val nicknameField: RequestBody? = nickName?.toRequestBody("text/plain".toMediaTypeOrNull())
val avatarField: MultipartBody.Part? = avatar?.let {
createMultipartBody(it.file,it.filename, "avatar")
} }
ApiClient.api.updateProfile(avatarField, nicknameField)
} }
override suspend fun registerUserWithPassword(loginName: String, password: String) { override suspend fun registerUserWithPassword(loginName: String, password: String) {

View File

@@ -2,6 +2,7 @@ package com.aiosman.riderpro.data
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
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.CommentRequestBody import com.aiosman.riderpro.data.api.CommentRequestBody
import com.aiosman.riderpro.test.TestDatabase import com.aiosman.riderpro.test.TestDatabase
@@ -52,7 +53,7 @@ data class Comment(
likes = likeCount, likes = likeCount,
replies = emptyList(), replies = emptyList(),
postId = 0, postId = 0,
avatar = ApiClient.BASE_SERVER + user.avatar, avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}",
author = user.id, author = user.id,
liked = isLiked liked = isLiked
) )

View File

@@ -43,9 +43,10 @@ data class Moment(
val time: String val time: String
) { ) {
fun toMomentItem(): MomentEntity { fun toMomentItem(): MomentEntity {
val avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}"
return MomentEntity( return MomentEntity(
id = id.toInt(), id = id.toInt(),
avatar = ApiClient.BASE_SERVER + user.avatar, avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}",
nickname = user.nickName, nickname = user.nickName,
location = "Worldwide", location = "Worldwide",
time = time, time = time,

View File

@@ -1,6 +1,7 @@
package com.aiosman.riderpro.data.api package com.aiosman.riderpro.data.api
import com.aiosman.riderpro.AppStore import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.ConstVars
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
@@ -16,7 +17,7 @@ class AuthInterceptor() : Interceptor {
} }
object ApiClient { object ApiClient {
const val BASE_SERVER = "http://192.168.31.57:8088" const val BASE_SERVER = ConstVars.BASE_SERVER
const val BASE_API_URL = "${BASE_SERVER}/api/v1" const val BASE_API_URL = "${BASE_SERVER}/api/v1"
const val RETROFIT_URL = "${BASE_API_URL}/" const val RETROFIT_URL = "${BASE_API_URL}/"
private val okHttpClient: OkHttpClient by lazy { private val okHttpClient: OkHttpClient by lazy {

View File

@@ -13,6 +13,7 @@ import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Multipart import retrofit2.http.Multipart
import retrofit2.http.PATCH
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.Path import retrofit2.http.Path
@@ -128,6 +129,13 @@ interface RiderProAPI {
@GET("account/my") @GET("account/my")
suspend fun getMyAccount(): Response<DataContainer<AccountProfile>> suspend fun getMyAccount(): Response<DataContainer<AccountProfile>>
@Multipart
@PATCH("account/my/profile")
suspend fun updateProfile(
@Part avatar: MultipartBody.Part?,
@Part("nickname") nickname: RequestBody?,
): Response<Unit>
@GET("profile/{id}") @GET("profile/{id}")
suspend fun getAccountProfileById( suspend fun getAccountProfileById(
@Path("id") id: Int @Path("id") id: Int

View File

@@ -2,6 +2,8 @@ package com.aiosman.riderpro.ui.account
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -29,14 +31,17 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.aiosman.riderpro.data.AccountProfileEntity import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.TestAccountServiceImpl import com.aiosman.riderpro.data.TestAccountServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.data.UserService import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -46,12 +51,14 @@ fun AccountEditScreen() {
val accountService: AccountService = TestAccountServiceImpl() val accountService: AccountService = TestAccountServiceImpl()
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") }
var imageUrl by remember { mutableStateOf<Uri?>(null) }
var profile by remember { var profile by remember {
mutableStateOf<AccountProfileEntity?>( mutableStateOf<AccountProfileEntity?>(
null null
) )
} }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current
suspend fun reloadProfile() { suspend fun reloadProfile() {
accountService.getMyAccountProfile().let { accountService.getMyAccountProfile().let {
@@ -72,7 +79,25 @@ fun AccountEditScreen() {
} }
fun updateUserProfile() { fun updateUserProfile() {
scope.launch { scope.launch {
accountService.updateProfile(name, bio) val newAvatar = imageUrl?.let {
val cursor = context.contentResolver.query(it, null, null, null, null)
var newAvatar: UploadImage? = null
cursor?.use {cur ->
if (cur.moveToFirst()) {
val displayName = cur.getString(cur.getColumnIndex("_display_name"))
val extension = displayName.substringAfterLast(".")
Log.d("NewPost", "File name: $displayName, extension: $extension")
// read as file
val file = uriToFile(context, it)
Log.d("NewPost", "File size: ${file.length()}")
newAvatar = UploadImage(file, displayName, it.toString(), extension)
}
}
newAvatar
}
val newName = if (name == profile?.nickName) null else name
accountService.updateProfile(newAvatar, newName, bio)
reloadProfile() reloadProfile()
} }
} }
@@ -83,7 +108,7 @@ fun AccountEditScreen() {
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data val uri = result.data?.data
uri?.let { uri?.let {
updateUserAvatar(it.toString()) imageUrl = it
} }
} }
} }
@@ -120,7 +145,11 @@ fun AccountEditScreen() {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
AsyncImage( AsyncImage(
it.avatar, if (imageUrl != null) {
imageUrl.toString()
} else {
it.avatar
},
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(100.dp) .size(100.dp)

View File

@@ -297,7 +297,7 @@ fun MomentTopRowGroup(momentEntity: MomentEntity) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MomentName(momentEntity.nickname) MomentName(momentEntity.nickname)
MomentFollowBtn() // MomentFollowBtn()
} }
Row( Row(
modifier = Modifier modifier = Modifier

View File

@@ -84,6 +84,8 @@ import com.aiosman.riderpro.data.TestCommentServiceImpl
import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.TestAccountServiceImpl import com.aiosman.riderpro.data.TestAccountServiceImpl
import com.aiosman.riderpro.data.TestMomentServiceImpl import com.aiosman.riderpro.data.TestMomentServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.model.MomentEntity import com.aiosman.riderpro.model.MomentEntity
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
@@ -103,6 +105,7 @@ class PostViewModel(
) : ViewModel() { ) : ViewModel() {
var service: MomentService = TestMomentServiceImpl() var service: MomentService = TestMomentServiceImpl()
var commentService: CommentService = TestCommentServiceImpl() var commentService: CommentService = TestCommentServiceImpl()
var userService : UserService = TestUserServiceImpl()
private var _commentsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty()) private var _commentsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
val commentsFlow = _commentsFlow.asStateFlow() val commentsFlow = _commentsFlow.asStateFlow()
@@ -138,7 +141,7 @@ class PostViewModel(
val currentPagingData = commentsFlow.value val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment -> val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) { if (comment.id == commentId) {
comment.copy(liked = !comment.liked) comment.copy(liked = !comment.liked, likes = comment.likes + 1)
} else { } else {
comment comment
} }
@@ -151,7 +154,7 @@ class PostViewModel(
val currentPagingData = commentsFlow.value val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment -> val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) { if (comment.id == commentId) {
comment.copy(liked = !comment.liked) comment.copy(liked = !comment.liked, likes = comment.likes - 1)
} else { } else {
comment comment
} }
@@ -180,6 +183,39 @@ class PostViewModel(
MomentViewModel.updateDislikeMomentById(it.id) MomentViewModel.updateDislikeMomentById(it.id)
} }
} }
suspend fun favoriteMoment() {
moment?.let {
service.favoriteMoment(it.id)
moment =
moment?.copy(favoriteCount = moment?.favoriteCount?.plus(1) ?: 0, isFavorite = true)
}
}
suspend fun unfavoriteMoment() {
moment?.let {
service.unfavoriteMoment(it.id)
moment = moment?.copy(
favoriteCount = moment?.favoriteCount?.minus(1) ?: 0,
isFavorite = false
)
}
}
suspend fun followUser() {
accountProfileEntity?.let {
userService.followUser(it.id.toString())
accountProfileEntity = accountProfileEntity?.copy(isFollowing = true)
}
}
suspend fun unfollowUser() {
accountProfileEntity?.let {
userService.unFollowUser(it.id.toString())
accountProfileEntity = accountProfileEntity?.copy(isFollowing = false)
}
}
} }
@Composable @Composable
@@ -224,6 +260,15 @@ fun PostScreen(
commentsPagging.refresh() commentsPagging.refresh()
} }
}, },
onFavoriteClick = {
scope.launch {
if (viewModel.moment?.isFavorite == true) {
viewModel.unfavoriteMoment()
} else {
viewModel.favoriteMoment()
}
}
},
momentEntity = viewModel.moment momentEntity = viewModel.moment
) )
} }
@@ -236,7 +281,17 @@ fun PostScreen(
Header( Header(
avatar = viewModel.moment?.avatar, avatar = viewModel.moment?.avatar,
nickname = viewModel.moment?.nickname, nickname = viewModel.moment?.nickname,
userId = viewModel.moment?.authorId userId = viewModel.moment?.authorId,
isFollowing = viewModel.accountProfileEntity?.isFollowing ?: false,
onFollowClick = {
scope.launch {
if (viewModel.accountProfileEntity?.isFollowing == true) {
viewModel.unfollowUser()
} else {
viewModel.followUser()
}
}
}
) )
Column(modifier = Modifier.animateContentSize()) { Column(modifier = Modifier.animateContentSize()) {
AnimatedVisibility(visible = showCollapseContent) { AnimatedVisibility(visible = showCollapseContent) {
@@ -290,7 +345,13 @@ fun PostScreen(
} }
@Composable @Composable
fun Header(avatar: String?, nickname: String?, userId: Int?) { fun Header(
avatar: String?,
nickname: String?,
userId: Int?,
isFollowing: Boolean,
onFollowClick: () -> Unit
) {
val navController = LocalNavController.current val navController = LocalNavController.current
Row( Row(
modifier = Modifier modifier = Modifier
@@ -336,7 +397,9 @@ fun Header(avatar: String?, nickname: String?, userId: Int?) {
modifier = Modifier modifier = Modifier
.height(20.dp) .height(20.dp)
.wrapContentWidth() .wrapContentWidth()
.padding(start = 6.dp), .padding(start = 6.dp).noRippleClickable {
onFollowClick()
},
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image( Image(
@@ -345,7 +408,7 @@ fun Header(avatar: String?, nickname: String?, userId: Int?) {
contentDescription = "" contentDescription = ""
) )
Text( Text(
text = "FOLLOW", text = if (isFollowing) "Following" else "Follow",
fontSize = 12.sp, fontSize = 12.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
@@ -530,6 +593,7 @@ fun CommentItem(commentEntity: CommentEntity, onLike: () -> Unit = {}) {
fun BottomNavigationBar( fun BottomNavigationBar(
onCreateComment: (String) -> Unit = {}, onCreateComment: (String) -> Unit = {},
onLikeClick: () -> Unit = {}, onLikeClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {},
momentEntity: MomentEntity? momentEntity: MomentEntity?
) { ) {
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
@@ -593,15 +657,22 @@ fun BottomNavigationBar(
} }
Text(text = momentEntity?.likeCount.toString()) Text(text = momentEntity?.likeCount.toString())
IconButton( IconButton(
onClick = { /*TODO*/ }) { onClick = {
Icon(Icons.Filled.Star, contentDescription = "Send") onFavoriteClick()
}
) {
Icon(
Icons.Filled.Star,
contentDescription = "Favourite",
tint = if (momentEntity?.isFavorite == true) Color.Red else Color.Gray
)
} }
Text(text = "2077") Text(text = momentEntity?.favoriteCount.toString())
IconButton( // IconButton(
onClick = { /*TODO*/ }) { // onClick = { /*TODO*/ }) {
Icon(Icons.Filled.CheckCircle, contentDescription = "Send") // Icon(Icons.Filled.CheckCircle, contentDescription = "Send")
} // }
Text(text = "2077") // Text(text = "2077")
} }
BottomNavigationPlaceholder( BottomNavigationPlaceholder(