diff --git a/app/src/main/java/com/aiosman/riderpro/Const.kt b/app/src/main/java/com/aiosman/riderpro/Const.kt new file mode 100644 index 0000000..7711721 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/Const.kt @@ -0,0 +1,6 @@ +package com.aiosman.riderpro + +object ConstVars { + // api 地址 + const val BASE_SERVER = "http://192.168.31.57:8088" +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt index 255fc6e..1dca9f7 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt @@ -1,9 +1,16 @@ package com.aiosman.riderpro.data +import com.aiosman.riderpro.AppStore import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.LoginUserRequestBody import com.aiosman.riderpro.data.api.RegisterRequestBody 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( val id: Int, @@ -15,6 +22,7 @@ data class AccountProfileEntity( val country: String, val isFollowing: Boolean ) + //{ // "id": 1, // "username": "root", @@ -23,7 +31,7 @@ data class AccountProfileEntity( // "followingCount": 1, // "followerCount": 0 //} -data class AccountProfile ( +data class AccountProfile( val id: Int, val username: String, val nickname: String, @@ -38,13 +46,14 @@ data class AccountProfile ( followerCount = followerCount, followingCount = followingCount, nickName = nickname, - avatar = ApiClient.BASE_SERVER + avatar, + avatar = ApiClient.BASE_SERVER + avatar + "?token=${AppStore.token}", bio = "", country = "Worldwide", isFollowing = isFollowing ) } } + interface AccountService { suspend fun getMyAccountProfile(): AccountProfileEntity suspend fun getAccountProfileById(id: Int): AccountProfileEntity @@ -52,7 +61,7 @@ interface AccountService { suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth suspend fun logout() 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) } @@ -95,14 +104,17 @@ class TestAccountServiceImpl : AccountService { } } - override suspend fun updateProfile(nickName: String, bio: String) { - TestDatabase.accountData = TestDatabase.accountData.map { - if (it.id == 1) { - it.copy(nickName = nickName, bio = bio) - } else { - it - } + fun createMultipartBody(file: File, filename:String,name: String): MultipartBody.Part { + val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) + return MultipartBody.Part.createFormData(name, filename, requestFile) + } + + 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) { diff --git a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt index 71f4177..61e99ab 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt @@ -2,6 +2,7 @@ package com.aiosman.riderpro.data import androidx.paging.PagingSource import androidx.paging.PagingState +import com.aiosman.riderpro.AppStore import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.CommentRequestBody import com.aiosman.riderpro.test.TestDatabase @@ -52,7 +53,7 @@ data class Comment( likes = likeCount, replies = emptyList(), postId = 0, - avatar = ApiClient.BASE_SERVER + user.avatar, + avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}", author = user.id, liked = isLiked ) 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 3c67d23..4a50a55 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt @@ -43,9 +43,10 @@ data class Moment( val time: String ) { fun toMomentItem(): MomentEntity { + val avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}" return MomentEntity( id = id.toInt(), - avatar = ApiClient.BASE_SERVER + user.avatar, + avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}", nickname = user.nickName, location = "Worldwide", time = time, diff --git a/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt b/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt index 45147d0..25fb525 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt @@ -1,6 +1,7 @@ package com.aiosman.riderpro.data.api import com.aiosman.riderpro.AppStore +import com.aiosman.riderpro.ConstVars import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response @@ -16,7 +17,7 @@ class AuthInterceptor() : Interceptor { } 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 RETROFIT_URL = "${BASE_API_URL}/" private val okHttpClient: OkHttpClient by lazy { diff --git a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt index 9fc6373..74ae1ba 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt @@ -13,6 +13,7 @@ import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Multipart +import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Part import retrofit2.http.Path @@ -128,6 +129,13 @@ interface RiderProAPI { @GET("account/my") suspend fun getMyAccount(): Response> + @Multipart + @PATCH("account/my/profile") + suspend fun updateProfile( + @Part avatar: MultipartBody.Part?, + @Part("nickname") nickname: RequestBody?, + ): Response + @GET("profile/{id}") suspend fun getAccountProfileById( @Path("id") id: Int diff --git a/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt b/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt index f1b8767..fdc5311 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/account/edit.kt @@ -2,6 +2,8 @@ package com.aiosman.riderpro.ui.account import android.app.Activity import android.content.Intent +import android.net.Uri +import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column @@ -29,14 +31,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.aiosman.riderpro.data.AccountProfileEntity import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.TestAccountServiceImpl import com.aiosman.riderpro.data.TestUserServiceImpl +import com.aiosman.riderpro.data.UploadImage import com.aiosman.riderpro.data.UserService import com.aiosman.riderpro.ui.modifiers.noRippleClickable +import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -46,12 +51,14 @@ fun AccountEditScreen() { val accountService: AccountService = TestAccountServiceImpl() var name by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") } + var imageUrl by remember { mutableStateOf(null) } var profile by remember { mutableStateOf( null ) } val scope = rememberCoroutineScope() + val context = LocalContext.current suspend fun reloadProfile() { accountService.getMyAccountProfile().let { @@ -72,7 +79,25 @@ fun AccountEditScreen() { } fun updateUserProfile() { 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() } } @@ -83,7 +108,7 @@ fun AccountEditScreen() { if (result.resultCode == Activity.RESULT_OK) { val uri = result.data?.data uri?.let { - updateUserAvatar(it.toString()) + imageUrl = it } } } @@ -120,7 +145,11 @@ fun AccountEditScreen() { horizontalAlignment = Alignment.CenterHorizontally ) { AsyncImage( - it.avatar, + if (imageUrl != null) { + imageUrl.toString() + } else { + it.avatar + }, contentDescription = null, modifier = Modifier .size(100.dp) 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 c2a1820..c19db96 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 @@ -297,7 +297,7 @@ fun MomentTopRowGroup(momentEntity: MomentEntity) { verticalAlignment = Alignment.CenterVertically ) { MomentName(momentEntity.nickname) - MomentFollowBtn() +// MomentFollowBtn() } Row( modifier = Modifier diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt index 08b0dac..f08fe59 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt @@ -84,6 +84,8 @@ import com.aiosman.riderpro.data.TestCommentServiceImpl import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.TestAccountServiceImpl 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.ui.NavigationRoute import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout @@ -103,6 +105,7 @@ class PostViewModel( ) : ViewModel() { var service: MomentService = TestMomentServiceImpl() var commentService: CommentService = TestCommentServiceImpl() + var userService : UserService = TestUserServiceImpl() private var _commentsFlow = MutableStateFlow>(PagingData.empty()) val commentsFlow = _commentsFlow.asStateFlow() @@ -138,7 +141,7 @@ class PostViewModel( val currentPagingData = commentsFlow.value val updatedPagingData = currentPagingData.map { comment -> if (comment.id == commentId) { - comment.copy(liked = !comment.liked) + comment.copy(liked = !comment.liked, likes = comment.likes + 1) } else { comment } @@ -151,7 +154,7 @@ class PostViewModel( val currentPagingData = commentsFlow.value val updatedPagingData = currentPagingData.map { comment -> if (comment.id == commentId) { - comment.copy(liked = !comment.liked) + comment.copy(liked = !comment.liked, likes = comment.likes - 1) } else { comment } @@ -180,6 +183,39 @@ class PostViewModel( 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 @@ -224,6 +260,15 @@ fun PostScreen( commentsPagging.refresh() } }, + onFavoriteClick = { + scope.launch { + if (viewModel.moment?.isFavorite == true) { + viewModel.unfavoriteMoment() + } else { + viewModel.favoriteMoment() + } + } + }, momentEntity = viewModel.moment ) } @@ -236,7 +281,17 @@ fun PostScreen( Header( avatar = viewModel.moment?.avatar, 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()) { AnimatedVisibility(visible = showCollapseContent) { @@ -290,7 +345,13 @@ fun PostScreen( } @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 Row( modifier = Modifier @@ -336,7 +397,9 @@ fun Header(avatar: String?, nickname: String?, userId: Int?) { modifier = Modifier .height(20.dp) .wrapContentWidth() - .padding(start = 6.dp), + .padding(start = 6.dp).noRippleClickable { + onFollowClick() + }, contentAlignment = Alignment.Center ) { Image( @@ -345,7 +408,7 @@ fun Header(avatar: String?, nickname: String?, userId: Int?) { contentDescription = "" ) Text( - text = "FOLLOW", + text = if (isFollowing) "Following" else "Follow", fontSize = 12.sp, color = Color.White, style = TextStyle(fontWeight = FontWeight.Bold) @@ -530,6 +593,7 @@ fun CommentItem(commentEntity: CommentEntity, onLike: () -> Unit = {}) { fun BottomNavigationBar( onCreateComment: (String) -> Unit = {}, onLikeClick: () -> Unit = {}, + onFavoriteClick: () -> Unit = {}, momentEntity: MomentEntity? ) { val systemUiController = rememberSystemUiController() @@ -593,15 +657,22 @@ fun BottomNavigationBar( } Text(text = momentEntity?.likeCount.toString()) IconButton( - onClick = { /*TODO*/ }) { - Icon(Icons.Filled.Star, contentDescription = "Send") + onClick = { + onFavoriteClick() + } + ) { + Icon( + Icons.Filled.Star, + contentDescription = "Favourite", + tint = if (momentEntity?.isFavorite == true) Color.Red else Color.Gray + ) } - Text(text = "2077") - IconButton( - onClick = { /*TODO*/ }) { - Icon(Icons.Filled.CheckCircle, contentDescription = "Send") - } - Text(text = "2077") + Text(text = momentEntity?.favoriteCount.toString()) +// IconButton( +// onClick = { /*TODO*/ }) { +// Icon(Icons.Filled.CheckCircle, contentDescription = "Send") +// } +// Text(text = "2077") } BottomNavigationPlaceholder(