This commit is contained in:
2024-08-11 17:15:17 +08:00
parent 2dc0ee3307
commit 19527f17c3
32 changed files with 1082 additions and 417 deletions

View File

@@ -1,8 +1,8 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.android)
id("com.google.gms.google-services")
} }
android { android {
namespace = "com.aiosman.riderpro" namespace = "com.aiosman.riderpro"
compileSdk = 34 compileSdk = 34
@@ -60,7 +60,7 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3.android) implementation(libs.androidx.material3.android)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation (libs.androidx.paging.compose) implementation(libs.androidx.paging.compose)
implementation(libs.androidx.paging.runtime) implementation(libs.androidx.paging.runtime)
implementation(libs.maps.compose) implementation(libs.maps.compose)
implementation(libs.accompanist.systemuicontroller) implementation(libs.accompanist.systemuicontroller)
@@ -76,13 +76,19 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
implementation (libs.places) implementation(libs.places)
implementation(libs.androidx.animation) implementation(libs.androidx.animation)
implementation("io.coil-kt:coil-compose:2.7.0") implementation("io.coil-kt:coil-compose:2.7.0")
implementation("io.coil-kt:coil:2.7.0") implementation("io.coil-kt:coil:2.7.0")
implementation("com.google.android.gms:play-services-auth:21.2.0") implementation("com.google.android.gms:play-services-auth:21.2.0")
implementation("io.github.serpro69:kotlin-faker:2.0.0-rc.5") implementation("io.github.serpro69:kotlin-faker:2.0.0-rc.5")
implementation("androidx.compose.material:material:1.6.8") implementation("androidx.compose.material:material:1.6.8")
implementation("com.facebook.android:facebook-android-sdk:17.0.0")
// platform("com.google.firebase:firebase-bom:33.1.2")
// implementation("com.google.firebase:firebase-analytics")
implementation("net.engawapg.lib:zoomable:1.6.1")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
}
}

View File

@@ -13,6 +13,7 @@
android:roundIcon="@mipmap/rider_pro_log_round" android:roundIcon="@mipmap/rider_pro_log_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.RiderPro" android:theme="@style/Theme.RiderPro"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<meta-data android:name="com.google.android.geo.API_KEY" <meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4"/> android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4"/>

View File

@@ -32,6 +32,7 @@ import androidx.core.view.WindowCompat
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.ServiceException
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.UserService import com.aiosman.riderpro.data.UserService
@@ -43,17 +44,20 @@ import com.google.android.libraries.places.api.Places
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val scope = CoroutineScope(Dispatchers.Main) private val scope = CoroutineScope(Dispatchers.Main)
suspend fun getAccount() { suspend fun getAccount(): Boolean {
//TODO apply token to client
if (!AppStore.rememberMe) {
return
}
val accountService: AccountService = TestAccountServiceImpl() val accountService: AccountService = TestAccountServiceImpl()
accountService.getMyAccount() try {
val resp = accountService.getMyAccount()
return true
} catch (e: ServiceException) {
return false
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -64,9 +68,9 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
scope.launch { scope.launch {
getAccount() val isAccountValidate = getAccount()
var startDestination = NavigationRoute.Login.route var startDestination = NavigationRoute.Login.route
if (AppStore.token != null && AppStore.rememberMe) { if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) {
startDestination = NavigationRoute.Index.route startDestination = NavigationRoute.Index.route
} }
setContent { setContent {

View File

@@ -1,8 +1,11 @@
package com.aiosman.riderpro.data package com.aiosman.riderpro.data
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 com.aiosman.riderpro.test.TestDatabase
data class AccountProfile( data class AccountProfileEntity(
val id: Int, val id: Int,
val followerCount: Int, val followerCount: Int,
val followingCount: Int, val followingCount: Int,
@@ -10,33 +13,72 @@ data class AccountProfile(
val avatar: String, val avatar: String,
val bio: String, val bio: String,
val country: String, val country: String,
val isFollowing: Boolean
) )
//{
// "id": 1,
// "username": "root",
// "nickname": "rider_4351",
// "avatar": "/api/v1/public/default_avatar.jpeg",
// "followingCount": 1,
// "followerCount": 0
//}
data class AccountProfile (
val id: Int,
val username: String,
val nickname: String,
val avatar: String,
val followingCount: Int,
val followerCount: Int,
val isFollowing: Boolean
) {
fun toAccountProfileEntity(): AccountProfileEntity {
return AccountProfileEntity(
id = id,
followerCount = followerCount,
followingCount = followingCount,
nickName = nickname,
avatar = ApiClient.BASE_SERVER + avatar,
bio = "",
country = "Worldwide",
isFollowing = isFollowing
)
}
}
interface AccountService { interface AccountService {
suspend fun getMyAccountProfile(): AccountProfile suspend fun getMyAccountProfile(): AccountProfileEntity
suspend fun getAccountProfileById(id: Int): AccountProfile suspend fun getAccountProfileById(id: Int): AccountProfileEntity
suspend fun getMyAccount(): UserAuth suspend fun getMyAccount(): UserAuth
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(nickName: String, bio: String)
suspend fun registerUserWithPassword(loginName: String, password: String)
} }
class TestAccountServiceImpl : AccountService { class TestAccountServiceImpl : AccountService {
override suspend fun getMyAccountProfile(): AccountProfile { override suspend fun getMyAccountProfile(): AccountProfileEntity {
return TestDatabase.accountData.first { it.id == 1 } val resp = ApiClient.api.getMyAccount()
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
} }
override suspend fun getAccountProfileById(id: Int): AccountProfile { override suspend fun getAccountProfileById(id: Int): AccountProfileEntity {
return TestDatabase.accountData.first { it.id == id } val resp = ApiClient.api.getAccountProfileById(id)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
} }
override suspend fun getMyAccount(): UserAuth { override suspend fun getMyAccount(): UserAuth {
return UserAuth(1) val resp = ApiClient.api.checkToken()
val body = resp.body() ?: throw ServiceException("Failed to get account")
return UserAuth(body.id)
} }
override suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth { override suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth {
return UserAuth(1, "token") val resp = ApiClient.api.login(LoginUserRequestBody(loginName, password))
val body = resp.body() ?: throw ServiceException("Failed to login")
return UserAuth(0, body.token)
} }
override suspend fun logout() { override suspend fun logout() {
@@ -52,6 +94,7 @@ class TestAccountServiceImpl : AccountService {
} }
} }
} }
override suspend fun updateProfile(nickName: String, bio: String) { override suspend fun updateProfile(nickName: String, bio: String) {
TestDatabase.accountData = TestDatabase.accountData.map { TestDatabase.accountData = TestDatabase.accountData.map {
if (it.id == 1) { if (it.id == 1) {
@@ -61,4 +104,8 @@ class TestAccountServiceImpl : AccountService {
} }
} }
} }
override suspend fun registerUserWithPassword(loginName: String, password: String) {
ApiClient.api.register(RegisterRequestBody(loginName, password))
}
} }

View File

@@ -0,0 +1,2 @@
package com.aiosman.riderpro.data

View File

@@ -2,38 +2,81 @@ 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.data.api.ApiClient
import com.aiosman.riderpro.data.api.CommentRequestBody
import com.aiosman.riderpro.test.TestDatabase import com.aiosman.riderpro.test.TestDatabase
import com.google.gson.annotations.SerializedName
import java.io.IOException import java.io.IOException
import java.util.Calendar import java.util.Calendar
import kotlin.math.min import kotlin.math.min
import kotlin.random.Random
interface CommentService { interface CommentService {
suspend fun getComments(pageNumber: Int, postId: Int? = null): ListContainer<Comment> suspend fun getComments(pageNumber: Int, postId: Int? = null): ListContainer<CommentEntity>
suspend fun createComment(postId: Int, content: String, authorId: Int): Comment suspend fun createComment(postId: Int, content: String)
suspend fun likeComment(commentId: Int) suspend fun likeComment(commentId: Int)
suspend fun dislikeComment(commentId: Int) suspend fun dislikeComment(commentId: Int)
} }
//{
// "id": 2,
// "content": "123",
// "User": {
// "id": 1,
// "nickName": "",
// "avatar": "/api/v1/public/default_avatar.jpeg"
//},
// "likeCount": 1,
// "isLiked": true,
// "createdAt": "2024-08-05 02:53:48"
//}
data class Comment( data class Comment(
@SerializedName("id")
val id: Int,
@SerializedName("content")
val content: String,
@SerializedName("user")
val user: User,
@SerializedName("likeCount")
val likeCount: Int,
@SerializedName("isLiked")
val isLiked: Boolean,
@SerializedName("createdAt")
val createdAt: String
) {
fun toCommentEntity(): CommentEntity {
return CommentEntity(
id = id,
name = user.nickName,
comment = content,
date = createdAt,
likes = likeCount,
replies = emptyList(),
postId = 0,
avatar = ApiClient.BASE_SERVER + user.avatar,
author = user.id,
liked = isLiked
)
}
}
data class CommentEntity(
val id: Int, val id: Int,
val name: String, val name: String,
val comment: String, val comment: String,
val date: String, val date: String,
val likes: Int, val likes: Int,
val replies: List<Comment>, val replies: List<CommentEntity>,
val postId: Int = 0, val postId: Int = 0,
val avatar: String, val avatar: String,
val author: Int, val author: Long,
var liked: Boolean, var liked: Boolean,
) )
class CommentPagingSource( class CommentPagingSource(
private val remoteDataSource: CommentRemoteDataSource, private val remoteDataSource: CommentRemoteDataSource,
private val postId: Int? = null private val postId: Int? = null
) : PagingSource<Int, Comment>() { ) : PagingSource<Int, CommentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Comment> { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CommentEntity> {
return try { return try {
val currentPage = params.key ?: 1 val currentPage = params.key ?: 1
val comments = remoteDataSource.getComments( val comments = remoteDataSource.getComments(
@@ -50,7 +93,7 @@ class CommentPagingSource(
} }
} }
override fun getRefreshKey(state: PagingState<Int, Comment>): Int? { override fun getRefreshKey(state: PagingState<Int, CommentEntity>): Int? {
return state.anchorPosition return state.anchorPosition
} }
@@ -59,87 +102,37 @@ class CommentPagingSource(
class CommentRemoteDataSource( class CommentRemoteDataSource(
private val commentService: CommentService, private val commentService: CommentService,
) { ) {
suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer<Comment> { suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer<CommentEntity> {
return commentService.getComments(pageNumber, postId) return commentService.getComments(pageNumber, postId)
} }
} }
class TestCommentServiceImpl : CommentService { class TestCommentServiceImpl : CommentService {
override suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer<Comment> { override suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer<CommentEntity> {
var rawList = TestDatabase.comment val resp = ApiClient.api.getComments(pageNumber, postId)
if (postId != null) { val body = resp.body() ?: throw ServiceException("Failed to get comments")
rawList = rawList.filter { it.postId == postId }
}
val from = (pageNumber - 1) * DataBatchSize
val to = (pageNumber) * DataBatchSize
rawList = rawList.sortedBy { -it.id }
if (from >= rawList.size) {
return ListContainer(
total = rawList.size,
page = pageNumber,
pageSize = DataBatchSize,
list = emptyList()
)
}
rawList = rawList.sortedBy { -it.id }
rawList.forEach {
val myLikeIdList = TestDatabase.likeCommentList.filter { it.second == 1 }.map { it.first }
if (myLikeIdList.contains(it.id)) {
it.liked = true
}
}
val currentSublist = rawList.subList(from, min(to, rawList.size))
return ListContainer( return ListContainer(
total = rawList.size, list = body.list.map { it.toCommentEntity() },
page = pageNumber, page = body.page,
pageSize = DataBatchSize, total = body.total,
list = currentSublist pageSize = body.pageSize
) )
} }
override suspend fun createComment(postId: Int, content: String, authorId: Int): Comment { override suspend fun createComment(postId: Int, content: String) {
var author = TestDatabase.accountData.find { it.id == authorId } val resp = ApiClient.api.createComment(postId, CommentRequestBody(content))
if (author == null) { return
author = TestDatabase.accountData.random()
}
TestDatabase.commentIdCounter += 1
val newComment = Comment(
name = author.nickName,
comment = content,
date = Calendar.getInstance().time.toString(),
likes = 0,
replies = emptyList(),
postId = postId,
avatar = author.avatar,
author = author.id,
id = TestDatabase.commentIdCounter,
liked = false
)
TestDatabase.comment += newComment
return newComment
} }
override suspend fun likeComment(commentId: Int) { override suspend fun likeComment(commentId: Int) {
TestDatabase.comment = TestDatabase.comment.map { val resp = ApiClient.api.likeComment(commentId)
if (it.id == commentId) { return
it.copy(likes = it.likes + 1)
} else {
it
}
}
TestDatabase.likeCommentList += Pair(commentId, 1)
} }
override suspend fun dislikeComment(commentId: Int) { override suspend fun dislikeComment(commentId: Int) {
TestDatabase.comment = TestDatabase.comment.map { val resp = ApiClient.api.dislikeComment(commentId)
if (it.id == commentId) { return
it.copy(likes = it.likes - 1)
} else {
it
}
}
TestDatabase.likeCommentList = TestDatabase.likeCommentList.filter { it.first != commentId }
} }
companion object { companion object {

View File

@@ -0,0 +1,5 @@
package com.aiosman.riderpro.data
data class DataContainer<T>(
val data: T
)

View File

@@ -0,0 +1,9 @@
package com.aiosman.riderpro.data
class ServiceException(
override val message: String,
val code: Int = 0,
val data: Any? = null
) : Exception(
message
)

View File

@@ -1,9 +1,15 @@
package com.aiosman.riderpro.data package com.aiosman.riderpro.data
import com.google.gson.annotations.SerializedName
data class ListContainer<T>( data class ListContainer<T>(
@SerializedName("total")
val total: Int, val total: Int,
@SerializedName("page")
val page: Int, val page: Int,
@SerializedName("pageSize")
val pageSize: Int, val pageSize: Int,
@SerializedName("list")
val list: List<T> val list: List<T>
) )

View File

@@ -2,28 +2,109 @@ 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.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.model.MomentItem 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 import com.google.gson.annotations.SerializedName
import kotlin.math.min
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.URL
data class Moment(
@SerializedName("id")
val id: Long,
@SerializedName("textContent")
val textContent: String,
@SerializedName("images")
val images: List<Image>,
@SerializedName("user")
val user: User,
@SerializedName("likeCount")
val likeCount: Long,
@SerializedName("isLiked")
val isLiked: Boolean,
@SerializedName("favoriteCount")
val favoriteCount: Long,
@SerializedName("isFavorite")
val isFavorite: Boolean,
@SerializedName("shareCount")
val isCommented: Boolean,
@SerializedName("commentCount")
val commentCount: Long,
@SerializedName("time")
val time: String
) {
fun toMomentItem(): MomentEntity {
return MomentEntity(
id = id.toInt(),
avatar = ApiClient.BASE_SERVER + user.avatar,
nickname = user.nickName,
location = "Worldwide",
time = time,
followStatus = false,
momentTextContent = textContent,
momentPicture = R.drawable.default_moment_img,
likeCount = likeCount.toInt(),
commentCount = commentCount.toInt(),
shareCount = 0,
favoriteCount = favoriteCount.toInt(),
images = images.map { ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}" },
authorId = user.id.toInt(),
liked = isLiked,
isFavorite = isFavorite
)
}
}
data class Image(
@SerializedName("id")
val id: Long,
@SerializedName("url")
val url: String,
@SerializedName("thumbnail")
val thumbnail: String
)
data class User(
@SerializedName("id")
val id: Long,
@SerializedName("nickName")
val nickName: String,
@SerializedName("avatar")
val avatar: String
)
data class UploadImage(
val file: File,
val filename: String,
val url: String,
val ext: String
)
interface MomentService { interface MomentService {
suspend fun getMomentById(id: Int): MomentItem suspend fun getMomentById(id: Int): MomentEntity
suspend fun likeMoment(id: Int) suspend fun likeMoment(id: Int)
suspend fun dislikeMoment(id: Int) suspend fun dislikeMoment(id: Int)
suspend fun getMoments( suspend fun getMoments(
pageNumber: Int, pageNumber: Int,
author: Int? = null, author: Int? = null,
timelineId: Int? = null timelineId: Int? = null
): ListContainer<MomentItem> ): ListContainer<MomentEntity>
suspend fun createMoment( suspend fun createMoment(
content: String, content: String,
authorId: Int, authorId: Int,
imageUriList: List<String>, images: List<UploadImage>,
relPostId: Int? = null relPostId: Int? = null
): MomentItem ): MomentEntity
suspend fun favoriteMoment(id: Int)
suspend fun unfavoriteMoment(id: Int)
} }
@@ -31,8 +112,8 @@ 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
) : PagingSource<Int, MomentItem>() { ) : PagingSource<Int, MomentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentItem> { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
return try { return try {
val currentPage = params.key ?: 1 val currentPage = params.key ?: 1
val moments = remoteDataSource.getMoments( val moments = remoteDataSource.getMoments(
@@ -51,7 +132,7 @@ class MomentPagingSource(
} }
} }
override fun getRefreshKey(state: PagingState<Int, MomentItem>): Int? { override fun getRefreshKey(state: PagingState<Int, MomentEntity>): Int? {
return state.anchorPosition return state.anchorPosition
} }
@@ -64,7 +145,7 @@ class MomentRemoteDataSource(
pageNumber: Int, pageNumber: Int,
author: Int?, author: Int?,
timelineId: Int? timelineId: Int?
): ListContainer<MomentItem> { ): ListContainer<MomentEntity> {
return momentService.getMoments(pageNumber, author, timelineId) return momentService.getMoments(pageNumber, author, timelineId)
} }
} }
@@ -77,11 +158,11 @@ class TestMomentServiceImpl() : MomentService {
pageNumber: Int, pageNumber: Int,
author: Int?, author: Int?,
timelineId: Int? timelineId: Int?
): ListContainer<MomentItem> { ): ListContainer<MomentEntity> {
return testMomentBackend.fetchMomentItems(pageNumber, author, timelineId) return testMomentBackend.fetchMomentItems(pageNumber, author, timelineId)
} }
override suspend fun getMomentById(id: Int): MomentItem { override suspend fun getMomentById(id: Int): MomentEntity {
return testMomentBackend.getMomentById(id) return testMomentBackend.getMomentById(id)
} }
@@ -97,10 +178,18 @@ class TestMomentServiceImpl() : MomentService {
override suspend fun createMoment( override suspend fun createMoment(
content: String, content: String,
authorId: Int, authorId: Int,
imageUriList: List<String>, images: List<UploadImage>,
relPostId: Int? relPostId: Int?
): MomentItem { ): MomentEntity {
return testMomentBackend.createMoment(content, authorId, imageUriList, relPostId) return testMomentBackend.createMoment(content, authorId, images, relPostId)
}
override suspend fun favoriteMoment(id: Int) {
testMomentBackend.favoriteMoment(id)
}
override suspend fun unfavoriteMoment(id: Int) {
testMomentBackend.unfavoriteMoment(id)
} }
} }
@@ -113,110 +202,63 @@ class TestMomentBackend(
pageNumber: Int, pageNumber: Int,
author: Int? = null, author: Int? = null,
timelineId: Int? timelineId: Int?
): ListContainer<MomentItem> { ): ListContainer<MomentEntity> {
var rawList = TestDatabase.momentData val resp = ApiClient.api.getPosts(
rawList = rawList.sortedBy { it.id }.reversed() pageSize = DataBatchSize,
if (author != null) { page = pageNumber,
rawList = rawList.filter { it.authorId == author } timelineId = timelineId,
} authorId = author
if (timelineId != null) { )
val followIdList = TestDatabase.followList.filter { val body = resp.body() ?: throw ServiceException("Failed to get moments")
it.first == timelineId
}.map { it.second }
rawList = rawList.filter { it.authorId in followIdList || it.authorId == 1 }
}
val from = (pageNumber - 1) * DataBatchSize
val to = (pageNumber) * DataBatchSize
if (from >= rawList.size) {
return ListContainer(
total = rawList.size,
page = pageNumber,
pageSize = DataBatchSize,
list = emptyList()
)
}
val currentSublist = rawList.subList(from, min(to, rawList.size))
currentSublist.forEach {
val myLikeIdList =
TestDatabase.likeMomentList.filter { it.second == 1 }.map { it.first }
if (myLikeIdList.contains(it.id)) {
it.liked = true
}
if (it.relPostId != null) {
it.relMoment = rawList.first { it1 -> it1.id == it.relPostId }
}
}
// delay
kotlinx.coroutines.delay(loadDelay)
return ListContainer( return ListContainer(
total = rawList.size, total = body.total,
page = pageNumber, page = pageNumber,
pageSize = DataBatchSize, pageSize = DataBatchSize,
list = currentSublist list = body.list.map { it.toMomentItem() }
) )
} }
suspend fun getMomentById(id: Int): MomentItem { suspend fun getMomentById(id: Int): MomentEntity {
var moment = TestDatabase.momentData.first { var resp = ApiClient.api.getPost(id)
it.id == id var body = resp.body()?.data ?: throw ServiceException("Failed to get moment")
} return body.toMomentItem()
val isLike = TestDatabase.likeMomentList.any {
it.first == id && it.second == 1
}
moment = moment.copy(liked = isLike)
return moment
} }
suspend fun likeMoment(id: Int) { suspend fun likeMoment(id: Int) {
val oldMoment = TestDatabase.momentData.first { ApiClient.api.likePost(id)
it.id == id
}
val newMoment = oldMoment.copy(likeCount = oldMoment.likeCount + 1)
TestDatabase.updateMomentById(id, newMoment)
TestDatabase.likeMomentList += Pair(id, 1)
} }
suspend fun dislikeMoment(id: Int) { suspend fun dislikeMoment(id: Int) {
val oldMoment = TestDatabase.momentData.first { ApiClient.api.dislikePost(id)
it.id == id }
}
val newMoment = oldMoment.copy(likeCount = oldMoment.likeCount - 1) fun createMultipartBody(file: File, name: String): MultipartBody.Part {
TestDatabase.updateMomentById(id, newMoment) val requestFile = RequestBody.create("image/*".toMediaTypeOrNull(), file)
TestDatabase.likeMomentList = TestDatabase.likeMomentList.filter { return MultipartBody.Part.createFormData(name, file.name, requestFile)
it.first != id
}
} }
suspend fun createMoment( suspend fun createMoment(
content: String, content: String,
authorId: Int, authorId: Int,
imageUriList: List<String>, imageUriList: List<UploadImage>,
relPostId: Int? relPostId: Int?
): MomentItem { ): MomentEntity {
TestDatabase.momentIdCounter += 1 val textContent = content.toRequestBody("text/plain".toMediaTypeOrNull())
val person = TestDatabase.accountData.first { val imageList = imageUriList.map { item ->
it.id == authorId val file = item.file
createMultipartBody(file, "image")
} }
val newMoment = MomentItem( val response = ApiClient.api.createPost(imageList, textContent = textContent)
id = TestDatabase.momentIdCounter, val body = response.body()?.data ?: throw ServiceException("Failed to create moment")
avatar = person.avatar, return body.toMomentItem()
nickname = person.nickName,
location = person.country, }
time = "2023.02.02 11:23",
followStatus = false, suspend fun favoriteMoment(id: Int) {
momentTextContent = content, ApiClient.api.favoritePost(id)
momentPicture = R.drawable.default_moment_img, }
likeCount = 0, suspend fun unfavoriteMoment(id: Int) {
commentCount = 0, ApiClient.api.unfavoritePost(id)
shareCount = 0,
favoriteCount = 0,
images = imageUriList,
authorId = person.id,
relPostId = relPostId
)
TestDatabase.momentData += newMoment
return newMoment
} }
} }

View File

@@ -1,5 +1,6 @@
package com.aiosman.riderpro.data package com.aiosman.riderpro.data
import com.aiosman.riderpro.data.api.ApiClient
import com.aiosman.riderpro.test.TestDatabase import com.aiosman.riderpro.test.TestDatabase
data class UserAuth( data class UserAuth(
@@ -8,17 +9,26 @@ data class UserAuth(
) )
interface UserService { interface UserService {
suspend fun getUserProfile(id: String): AccountProfile suspend fun getUserProfile(id: String): AccountProfileEntity
suspend fun followUser(id: String)
suspend fun unFollowUser(id: String)
} }
class TestUserServiceImpl : UserService { class TestUserServiceImpl : UserService {
override suspend fun getUserProfile(id: String): AccountProfile { override suspend fun getUserProfile(id: String): AccountProfileEntity {
TestDatabase.accountData.forEach { val resp = ApiClient.api.getAccountProfileById(id.toInt())
if (it.id == id.toInt()) { val body = resp.body() ?: throw ServiceException("Failed to get account")
return it return body.data.toAccountProfileEntity()
} }
}
return AccountProfile(0, 0, 0, "", "", "", "") override suspend fun followUser(id: String) {
val resp = ApiClient.api.followUser(id.toInt())
return
}
override suspend fun unFollowUser(id: String) {
val resp = ApiClient.api.unfollowUser(id.toInt())
return
} }
} }

View File

@@ -0,0 +1,39 @@
package com.aiosman.riderpro.data.api
import com.aiosman.riderpro.AppStore
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class AuthInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
return chain.proceed(requestBuilder.build())
}
}
object ApiClient {
const val BASE_SERVER = "http://192.168.31.57:8088"
const val BASE_API_URL = "${BASE_SERVER}/api/v1"
const val RETROFIT_URL = "${BASE_API_URL}/"
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.build()
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(RETROFIT_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: RiderProAPI by lazy {
retrofit.create(RiderProAPI::class.java)
}
}

View File

@@ -0,0 +1,146 @@
package com.aiosman.riderpro.data.api
import com.aiosman.riderpro.data.AccountProfile
import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.Comment
import com.aiosman.riderpro.data.DataContainer
import com.aiosman.riderpro.data.ListContainer
import com.aiosman.riderpro.data.Moment
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
data class RegisterRequestBody(
@SerializedName("username")
val username: String,
@SerializedName("password")
val password: String
)
data class LoginUserRequestBody(
@SerializedName("username")
val username: String,
@SerializedName("password")
val password: String
)
data class AuthResult(
@SerializedName("code")
val code: Int,
@SerializedName("expire")
val expire: String,
@SerializedName("token")
val token: String
)
data class ValidateTokenResult(
@SerializedName("id")
val id: Int,
)
data class CommentRequestBody(
@SerializedName("content")
val content: String
)
interface RiderProAPI {
@POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@POST("login")
suspend fun login(@Body body: LoginUserRequestBody): Response<AuthResult>
@GET("auth/token")
suspend fun checkToken(): Response<ValidateTokenResult>
@GET("posts")
suspend fun getPosts(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("timelineId") timelineId: Int? = null,
@Query("authorId") authorId: Int? = null,
): Response<ListContainer<Moment>>
@Multipart
@POST("posts")
suspend fun createPost(
@Part image: List<MultipartBody.Part>,
@Part("textContent") textContent: RequestBody,
): Response<DataContainer<Moment>>
@GET("post/{id}")
suspend fun getPost(
@Path("id") id: Int
): Response<DataContainer<Moment>>
@POST("post/{id}/like")
suspend fun likePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/dislike")
suspend fun dislikePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/favorite")
suspend fun favoritePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/unfavorite")
suspend fun unfavoritePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/comment")
suspend fun createComment(
@Path("id") id: Int,
@Body body: CommentRequestBody
): Response<Unit>
@POST("comment/{id}/like")
suspend fun likeComment(
@Path("id") id: Int
): Response<Unit>
@POST("comment/{id}/dislike")
suspend fun dislikeComment(
@Path("id") id: Int
): Response<Unit>
@GET("comments")
suspend fun getComments(
@Query("page") page: Int = 1,
@Query("postId") postId: Int? = null,
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<Comment>>
@GET("account/my")
suspend fun getMyAccount(): Response<DataContainer<AccountProfile>>
@GET("profile/{id}")
suspend fun getAccountProfileById(
@Path("id") id: Int
): Response<DataContainer<AccountProfile>>
@POST("user/{id}/follow")
suspend fun followUser(
@Path("id") id: Int
): Response<Unit>
@POST("user/{id}/unfollow")
suspend fun unfollowUser(
@Path("id") id: Int
): Response<Unit>
}

View File

@@ -1,9 +1,8 @@
package com.aiosman.riderpro.model package com.aiosman.riderpro.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.aiosman.riderpro.R
data class MomentItem( data class MomentEntity(
val id: Int, val id: Int,
val avatar: String, val avatar: String,
val nickname: String, val nickname: String,
@@ -20,5 +19,6 @@ data class MomentItem(
val authorId: Int = 0, val authorId: Int = 0,
var liked: Boolean = false, var liked: Boolean = false,
var relPostId: Int? = null, var relPostId: Int? = null,
var relMoment: MomentItem? = null var relMoment: MomentEntity? = null,
var isFavorite: Boolean = false
) )

View File

@@ -2,16 +2,16 @@ package com.aiosman.riderpro.test
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.model.MomentEntity
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.math.ceil import kotlin.math.ceil
class TestBackend( class TestBackend(
private val backendDataList: List<MomentItem>, private val backendDataList: List<MomentEntity>,
private val loadDelay: Long = 500, private val loadDelay: Long = 500,
) { ) {
val DataBatchSize = 5 val DataBatchSize = 5
class DesiredLoadResultPageResponse(val data: List<MomentItem>) class DesiredLoadResultPageResponse(val data: List<MomentEntity>)
/** Returns [DataBatchSize] items for a key */ /** Returns [DataBatchSize] items for a key */
fun searchItemsByKey(key: Int): DesiredLoadResultPageResponse { fun searchItemsByKey(key: Int): DesiredLoadResultPageResponse {
val maxKey = ceil(backendDataList.size.toFloat() / DataBatchSize).toInt() val maxKey = ceil(backendDataList.size.toFloat() / DataBatchSize).toInt()
@@ -28,8 +28,8 @@ class TestBackend(
class TestPagingSource( class TestPagingSource(
private val backend: TestBackend, private val backend: TestBackend,
private val loadDelay: Long, private val loadDelay: Long,
) : PagingSource<Int, MomentItem>() { ) : PagingSource<Int, MomentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentItem> { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
// Simulate latency // Simulate latency
delay(loadDelay) delay(loadDelay)
val pageNumber = params.key ?: 0 val pageNumber = params.key ?: 0
@@ -42,7 +42,7 @@ class TestPagingSource(
val nextKey = if (response.data.isNotEmpty()) pageNumber + 1 else null val nextKey = if (response.data.isNotEmpty()) pageNumber + 1 else null
return LoadResult.Page(data = response.data, prevKey = prevKey, nextKey = nextKey) return LoadResult.Page(data = response.data, prevKey = prevKey, nextKey = nextKey)
} }
override fun getRefreshKey(state: PagingState<Int, MomentItem>): Int? { override fun getRefreshKey(state: PagingState<Int, MomentEntity>): Int? {
return state.anchorPosition?.let { return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1) state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)

View File

@@ -1,18 +1,18 @@
package com.aiosman.riderpro.test package com.aiosman.riderpro.test
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountProfile import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.Comment import com.aiosman.riderpro.data.CommentEntity
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.model.MomentEntity
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
import java.io.File import java.io.File
object TestDatabase { object TestDatabase {
var momentData = emptyList<MomentItem>() var momentData = emptyList<MomentEntity>()
var accountData = emptyList<AccountProfile>() var accountData = emptyList<AccountProfileEntity>()
var comment = emptyList<Comment>() var commentEntity = emptyList<CommentEntity>()
var commentIdCounter = 0 var commentIdCounter = 0
var momentIdCounter = 0 var momentIdCounter = 0
var selfId = 1 var selfId = 1
@@ -41,6 +41,7 @@ object TestDatabase {
var followList = emptyList<Pair<Int, Int>>() var followList = emptyList<Pair<Int, Int>>()
var likeCommentList = emptyList<Pair<Int, Int>>() var likeCommentList = emptyList<Pair<Int, Int>>()
var likeMomentList = emptyList<Pair<Int, Int>>() var likeMomentList = emptyList<Pair<Int, Int>>()
init { init {
val faker = faker { val faker = faker {
this.fakerConfig { this.fakerConfig {
@@ -48,14 +49,15 @@ object TestDatabase {
} }
} }
accountData = (0..20).toList().mapIndexed { idx, _ -> accountData = (0..20).toList().mapIndexed { idx, _ ->
AccountProfile( AccountProfileEntity(
id = idx, id = idx,
followerCount = 0, followerCount = 0,
followingCount = 0, followingCount = 0,
nickName = faker.name.name(), nickName = faker.name.name(),
avatar = imageList.random(), avatar = imageList.random(),
bio = "I am a software engineer", bio = "I am a software engineer",
country = faker.address.country() country = faker.address.country(),
isFollowing = false
) )
} }
@@ -84,7 +86,7 @@ object TestDatabase {
for (i in 0..commentCount) { for (i in 0..commentCount) {
commentIdCounter += 1 commentIdCounter += 1
val commentPerson = accountData.random() val commentPerson = accountData.random()
var newComment = Comment( var newCommentEntity = CommentEntity(
name = commentPerson.nickName, name = commentPerson.nickName,
comment = "this is comment ${commentIdCounter}", comment = "this is comment ${commentIdCounter}",
date = "2023-02-02 11:23", date = "2023-02-02 11:23",
@@ -92,7 +94,7 @@ object TestDatabase {
replies = emptyList(), replies = emptyList(),
postId = momentIdCounter, postId = momentIdCounter,
avatar = commentPerson.avatar, avatar = commentPerson.avatar,
author = commentPerson.id, author = commentPerson.id.toLong(),
id = commentIdCounter, id = commentIdCounter,
liked = false liked = false
) )
@@ -100,16 +102,16 @@ object TestDatabase {
for (likeIdx in 0..faker.random.nextInt(0, 5)) { for (likeIdx in 0..faker.random.nextInt(0, 5)) {
val likePerson = accountData.random() val likePerson = accountData.random()
likeCommentList += Pair(commentIdCounter, likePerson.id) likeCommentList += Pair(commentIdCounter, likePerson.id)
newComment = newComment.copy(likes = newComment.likes + 1) newCommentEntity = newCommentEntity.copy(likes = newCommentEntity.likes + 1)
} }
comment += newComment commentEntity += newCommentEntity
} }
val likeCount = faker.random.nextInt(0, 5) val likeCount = faker.random.nextInt(0, 5)
for (i in 0..likeCount) { for (i in 0..likeCount) {
val likePerson = accountData.random() val likePerson = accountData.random()
likeMomentList += Pair(momentIdCounter, likePerson.id) likeMomentList += Pair(momentIdCounter, likePerson.id)
} }
MomentItem( MomentEntity(
id = momentIdCounter, id = momentIdCounter,
avatar = person.avatar, avatar = person.avatar,
nickname = person.nickName, nickname = person.nickName,
@@ -129,10 +131,10 @@ object TestDatabase {
} }
fun updateMomentById(id: Int, momentItem: MomentItem) { fun updateMomentById(id: Int, momentEntity: MomentEntity) {
momentData = momentData.map { momentData = momentData.map {
if (it.id == id) { if (it.id == id) {
momentItem momentEntity
} else { } else {
it it
} }

View File

@@ -159,7 +159,11 @@ fun NavigationController(
route = NavigationRoute.AccountProfile.route, route = NavigationRoute.AccountProfile.route,
arguments = listOf(navArgument("id") { type = NavType.StringType }) arguments = listOf(navArgument("id") { type = NavType.StringType })
) { ) {
AccountProfile(it.arguments?.getString("id")!!) CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
AccountProfile(it.arguments?.getString("id")!!)
}
} }
composable(route = NavigationRoute.SignUp.route) { composable(route = NavigationRoute.SignUp.route) {
SignupScreen() SignupScreen()

View File

@@ -31,7 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
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.AccountProfile 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
@@ -47,7 +47,7 @@ fun AccountEditScreen() {
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") }
var profile by remember { var profile by remember {
mutableStateOf<AccountProfile?>( mutableStateOf<AccountProfileEntity?>(
null null
) )
} }

View File

@@ -47,7 +47,7 @@ import androidx.paging.cachedIn
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.ui.post.CommentsSection import com.aiosman.riderpro.ui.post.CommentsSection
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.Comment import com.aiosman.riderpro.data.CommentEntity
import com.aiosman.riderpro.data.CommentPagingSource import com.aiosman.riderpro.data.CommentPagingSource
import com.aiosman.riderpro.data.CommentRemoteDataSource import com.aiosman.riderpro.data.CommentRemoteDataSource
import com.aiosman.riderpro.data.CommentService import com.aiosman.riderpro.data.CommentService
@@ -57,10 +57,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class CommentModalViewModel( class CommentModalViewModel(
postId: Int? val postId: Int?
) : ViewModel() { ) : ViewModel() {
val commentService: CommentService = TestCommentServiceImpl() val commentService: CommentService = TestCommentServiceImpl()
val commentsFlow: Flow<PagingData<Comment>> = Pager( val commentsFlow: Flow<PagingData<CommentEntity>> = Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false), config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { pagingSourceFactory = {
CommentPagingSource( CommentPagingSource(
@@ -69,6 +69,12 @@ class CommentModalViewModel(
) )
} }
).flow.cachedIn(viewModelScope) ).flow.cachedIn(viewModelScope)
suspend fun createComment(content: String) {
postId?.let {
commentService.createComment(postId, content)
}
}
} }
@Preview @Preview
@@ -104,8 +110,7 @@ fun CommentModalContent(
var commentText by remember { mutableStateOf("") } var commentText by remember { mutableStateOf("") }
suspend fun sendComment() { suspend fun sendComment() {
if (commentText.isNotEmpty()) { if (commentText.isNotEmpty()) {
model.commentService.createComment(postId!!, commentText, 1) model.createComment(commentText)
commentText = ""
} }
comments.refresh() comments.refresh()
onCommentAdded() onCommentAdded()
@@ -136,13 +141,16 @@ fun CommentModalContent(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.weight(1f) .weight(1f)
) { ) {
CommentsSection(lazyPagingItems = comments, onLike = { comment: Comment -> CommentsSection(lazyPagingItems = comments, onLike = { commentEntity: CommentEntity ->
scope.launch { scope.launch {
model.commentService.likeComment(comment.id) if (commentEntity.liked) {
model.commentService.dislikeComment(commentEntity.id)
} else {
model.commentService.likeComment(commentEntity.id)
}
comments.refresh() comments.refresh()
} }
}) { }) {
} }
} }

View File

@@ -0,0 +1,68 @@
package com.aiosman.riderpro.ui.composables
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@Composable
fun AnimatedFavouriteIcon(
modifier: Modifier = Modifier,
isFavourite: Boolean = false,
onClick: (() -> Unit)? = null
) {
val animatableRotation = remember { Animatable(0f) }
val animatedColor by animateColorAsState(targetValue = if (isFavourite) Color(0xFFd83737) else Color.Black)
val scope = rememberCoroutineScope()
suspend fun shake() {
repeat(2) {
animatableRotation.animateTo(
targetValue = 10f,
animationSpec = tween(100)
) {
}
animatableRotation.animateTo(
targetValue = -10f,
animationSpec = tween(100)
) {
}
}
animatableRotation.animateTo(
targetValue = 0f,
animationSpec = tween(100)
)
}
Box(contentAlignment = Alignment.Center, modifier = Modifier.noRippleClickable {
onClick?.invoke()
// Trigger shake animation
scope.launch {
shake()
}
}) {
Image(
painter = painterResource(id = R.drawable.rider_pro_favoriate),
contentDescription = "Like",
modifier = modifier.graphicsLayer {
rotationZ = animatableRotation.value
},
colorFilter = ColorFilter.tint(animatedColor)
)
}
}

View File

@@ -9,19 +9,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.ui.index.tabs.moment.MomentTopRowGroup import com.aiosman.riderpro.ui.index.tabs.moment.MomentTopRowGroup
@Composable @Composable
fun RelPostCard( fun RelPostCard(
momentItem: MomentItem, momentEntity: MomentEntity,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val image = momentItem.images.firstOrNull() val image = momentEntity.images.firstOrNull()
Column( Column(
modifier = modifier modifier = modifier
) { ) {
MomentTopRowGroup(momentItem = momentItem) MomentTopRowGroup(momentEntity = momentEntity)
Box( Box(
modifier=Modifier.padding(horizontal = 16.dp, vertical = 8.dp) modifier=Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {

View File

@@ -53,18 +53,20 @@ import androidx.compose.ui.unit.sp
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.comment.CommentModalContent import com.aiosman.riderpro.ui.comment.CommentModalContent
import com.aiosman.riderpro.ui.composables.AnimatedCounter import com.aiosman.riderpro.ui.composables.AnimatedCounter
import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
import com.aiosman.riderpro.ui.composables.RelPostCard import com.aiosman.riderpro.ui.composables.RelPostCard
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel import com.aiosman.riderpro.ui.post.NewPostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@@ -88,10 +90,12 @@ fun MomentsList() {
} }
Box(Modifier.pullRefresh(state)) { Box(Modifier.pullRefresh(state)) {
LazyColumn { LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(moments.itemCount) { idx -> items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items val momentItem = moments[idx] ?: return@items
MomentCard(momentItem = momentItem, MomentCard(momentEntity = momentItem,
onAddComment = { onAddComment = {
scope.launch { scope.launch {
model.onAddComment(momentItem.id) model.onAddComment(momentItem.id)
@@ -105,6 +109,15 @@ fun MomentsList() {
model.likeMoment(momentItem.id) model.likeMoment(momentItem.id)
} }
} }
},
onFavoriteClick = {
scope.launch {
if (momentItem.isFavorite) {
model.unfavoriteMoment(momentItem.id)
} else {
model.favoriteMoment(momentItem.id)
}
}
} }
) )
} }
@@ -115,38 +128,40 @@ fun MomentsList() {
@Composable @Composable
fun MomentCard( fun MomentCard(
momentItem: MomentItem, momentEntity: MomentEntity,
onLikeClick: () -> Unit, onLikeClick: () -> Unit,
onFavoriteClick: () -> Unit = {},
onAddComment: () -> Unit = {} onAddComment: () -> Unit = {}
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
MomentTopRowGroup(momentItem = momentItem) MomentTopRowGroup(momentEntity = momentEntity)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.noRippleClickable { .noRippleClickable {
navController.navigate("Post/${momentItem.id}") navController.navigate("Post/${momentEntity.id}")
} }
) { ) {
MomentContentGroup(momentItem = momentItem) MomentContentGroup(momentEntity = momentEntity)
} }
val momentOperateBtnBoxModifier = Modifier val momentOperateBtnBoxModifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.weight(1f) .weight(1f)
ModificationListHeader() // ModificationListHeader()
MomentBottomOperateRowGroup( MomentBottomOperateRowGroup(
momentOperateBtnBoxModifier, momentOperateBtnBoxModifier,
momentItem = momentItem, momentEntity = momentEntity,
onLikeClick = onLikeClick, onLikeClick = onLikeClick,
onAddComment = onAddComment, onAddComment = onAddComment,
onShareClick = { onShareClick = {
NewPostViewModel.asNewPost() NewPostViewModel.asNewPost()
NewPostViewModel.relPostId = momentItem.id NewPostViewModel.relPostId = momentEntity.id
navController.navigate(NavigationRoute.NewPost.route) navController.navigate(NavigationRoute.NewPost.route)
} },
onFavoriteClick = onFavoriteClick
) )
} }
} }
@@ -248,7 +263,7 @@ fun MomentPostTime(time: String) {
} }
@Composable @Composable
fun MomentTopRowGroup(momentItem: MomentItem) { fun MomentTopRowGroup(momentEntity: MomentEntity) {
val navController = LocalNavController.current val navController = LocalNavController.current
Row( Row(
modifier = Modifier modifier = Modifier
@@ -256,7 +271,7 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
.padding(top = 0.dp, bottom = 0.dp, start = 24.dp, end = 24.dp) .padding(top = 0.dp, bottom = 0.dp, start = 24.dp, end = 24.dp)
) { ) {
AsyncImage( AsyncImage(
momentItem.avatar, momentEntity.avatar,
contentDescription = "", contentDescription = "",
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
@@ -264,7 +279,7 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
navController.navigate( navController.navigate(
NavigationRoute.AccountProfile.route.replace( NavigationRoute.AccountProfile.route.replace(
"{id}", "{id}",
momentItem.authorId.toString() momentEntity.authorId.toString()
) )
) )
}, },
@@ -281,7 +296,7 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
.height(22.dp), .height(22.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MomentName(momentItem.nickname) MomentName(momentEntity.nickname)
MomentFollowBtn() MomentFollowBtn()
} }
Row( Row(
@@ -290,8 +305,8 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
.height(21.dp), .height(21.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MomentPostLocation(momentItem.location) MomentPostLocation(momentEntity.location)
MomentPostTime(momentItem.time) MomentPostTime(momentEntity.time)
} }
} }
} }
@@ -300,28 +315,40 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
@OptIn(ExperimentalSharedTransitionApi::class) @OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun MomentContentGroup( fun MomentContentGroup(
momentItem: MomentItem, momentEntity: MomentEntity,
) { ) {
val displayImageUrl = momentItem.images.firstOrNull() val displayImageUrl = momentEntity.images.firstOrNull()
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedVisibilityScope = LocalAnimatedContentScope.current
Text( Text(
text = "${momentItem.id} ${momentItem.momentTextContent}", text = "${momentEntity.momentTextContent}",
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 22.dp, bottom = 16.dp, start = 24.dp, end = 24.dp), .padding(top = 22.dp, bottom = 16.dp, start = 24.dp, end = 24.dp),
fontSize = 16.sp fontSize = 16.sp
) )
if (momentItem.relMoment != null) { if (momentEntity.relMoment != null) {
RelPostCard(momentItem = momentItem.relMoment!!, modifier = Modifier.background(Color(0xFFF8F8F8))) RelPostCard(
}else{ momentEntity = momentEntity.relMoment!!,
modifier = Modifier.background(Color(0xFFF8F8F8))
)
} else {
displayImageUrl?.let { displayImageUrl?.let {
AsyncImage( with(sharedTransitionScope) {
it, AsyncImage(
modifier = Modifier it,
.fillMaxWidth() modifier = Modifier
.aspectRatio(1f), .sharedElement(
contentScale = ContentScale.Crop, rememberSharedContentState(key = it),
contentDescription = "" animatedVisibilityScope = animatedVisibilityScope
) )
.fillMaxWidth()
.aspectRatio(1f),
contentScale = ContentScale.Crop,
contentDescription = ""
)
}
} }
} }
@@ -366,8 +393,9 @@ fun MomentBottomOperateRowGroup(
modifier: Modifier, modifier: Modifier,
onLikeClick: () -> Unit = {}, onLikeClick: () -> Unit = {},
onAddComment: () -> Unit = {}, onAddComment: () -> Unit = {},
onFavoriteClick: () -> Unit = {},
onShareClick: () -> Unit = {}, onShareClick: () -> Unit = {},
momentItem: MomentItem momentEntity: MomentEntity
) { ) {
var systemUiController = rememberSystemUiController() var systemUiController = rememberSystemUiController()
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
@@ -380,7 +408,7 @@ fun MomentBottomOperateRowGroup(
) )
) { ) {
systemUiController.setNavigationBarColor(Color(0xfff7f7f7)) systemUiController.setNavigationBarColor(Color(0xfff7f7f7))
CommentModalContent(postId = momentItem.id, onCommentAdded = { CommentModalContent(postId = momentEntity.id, onCommentAdded = {
showCommentModal = false showCommentModal = false
onAddComment() onAddComment()
}) { }) {
@@ -397,10 +425,10 @@ fun MomentBottomOperateRowGroup(
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MomentOperateBtn(count = momentItem.likeCount.toString()) { MomentOperateBtn(count = momentEntity.likeCount.toString()) {
AnimatedLikeIcon( AnimatedLikeIcon(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
liked = momentItem.liked liked = momentEntity.liked
) { ) {
onLikeClick() onLikeClick()
} }
@@ -417,28 +445,34 @@ fun MomentBottomOperateRowGroup(
) { ) {
MomentOperateBtn( MomentOperateBtn(
icon = R.drawable.rider_pro_moment_comment, icon = R.drawable.rider_pro_moment_comment,
count = momentItem.commentCount.toString() count = momentEntity.commentCount.toString()
) )
} }
// Box(
// modifier = modifier.noRippleClickable {
// onShareClick()
// },
// contentAlignment = Alignment.Center
// ) {
// MomentOperateBtn(
// icon = R.drawable.rider_pro_share,
// count = momentEntity.shareCount.toString()
// )
// }
Box( Box(
modifier = modifier.noRippleClickable { modifier = modifier.noRippleClickable {
onShareClick() onFavoriteClick()
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MomentOperateBtn( MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
icon = R.drawable.rider_pro_share, AnimatedFavouriteIcon(
count = momentItem.shareCount.toString() modifier = Modifier.size(24.dp),
) isFavourite = momentEntity.isFavorite
} ) {
Box( onFavoriteClick()
modifier = modifier, }
contentAlignment = Alignment.Center }
) {
MomentOperateBtn(
icon = R.drawable.rider_pro_favoriate,
count = momentItem.favoriteCount.toString()
)
} }
} }
} }

View File

@@ -13,7 +13,7 @@ import com.aiosman.riderpro.data.MomentRemoteDataSource
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.model.MomentItem import com.aiosman.riderpro.model.MomentEntity
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
object MomentViewModel : ViewModel() { object MomentViewModel : ViewModel() {
private val momentService: MomentService = TestMomentServiceImpl() private val momentService: MomentService = TestMomentServiceImpl()
private val _momentsFlow = MutableStateFlow<PagingData<MomentItem>>(PagingData.empty()) private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
val momentsFlow = _momentsFlow.asStateFlow() val momentsFlow = _momentsFlow.asStateFlow()
val accountService: AccountService = TestAccountServiceImpl() val accountService: AccountService = TestAccountServiceImpl()
init { init {
@@ -91,6 +91,7 @@ object MomentViewModel : ViewModel() {
updateCommentCount(id) updateCommentCount(id)
} }
fun updateDislikeMomentById(id: Int) { fun updateDislikeMomentById(id: Int) {
val currentPagingData = _momentsFlow.value val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem -> val updatedPagingData = currentPagingData.map { momentItem ->
@@ -108,4 +109,34 @@ object MomentViewModel : ViewModel() {
updateDislikeMomentById(id) updateDislikeMomentById(id)
} }
fun updateFavoriteCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id)
updateFavoriteCount(id)
}
fun updateUnfavoriteCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun unfavoriteMoment(id: Int) {
momentService.unfavoriteMoment(id)
updateUnfavoriteCount(id)
}
} }

View File

@@ -3,27 +3,25 @@ package com.aiosman.riderpro.ui.index.tabs.profile
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.navigation.NavController
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import com.aiosman.riderpro.AppStore import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.AccountProfile
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.MomentPagingSource import com.aiosman.riderpro.data.MomentPagingSource
import com.aiosman.riderpro.data.MomentRemoteDataSource import com.aiosman.riderpro.data.MomentRemoteDataSource
import com.aiosman.riderpro.data.TestMomentServiceImpl import com.aiosman.riderpro.data.TestMomentServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.model.MomentEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
object MyProfileViewModel { object MyProfileViewModel {
val service: AccountService = TestAccountServiceImpl() val service: AccountService = TestAccountServiceImpl()
val userService = TestUserServiceImpl() val userService = TestUserServiceImpl()
var profile by mutableStateOf<AccountProfile?>(null) var profile by mutableStateOf<AccountProfileEntity?>(null)
var momentsFlow by mutableStateOf<Flow<PagingData<MomentItem>>?>(null) var momentsFlow by mutableStateOf<Flow<PagingData<MomentEntity>>?>(null)
suspend fun loadProfile() { suspend fun loadProfile() {
profile = service.getMyAccountProfile() profile = service.getMyAccountProfile()
momentsFlow = Pager( momentsFlow = Pager(

View File

@@ -1,6 +1,7 @@
package com.aiosman.riderpro.ui.index.tabs.profile package com.aiosman.riderpro.ui.index.tabs.profile
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -46,10 +47,12 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountProfile import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -119,9 +122,8 @@ fun ProfilePage() {
} }
CarGroup() CarGroup()
model.profile?.let { model.profile?.let {
UserInformation(accountProfile = it) UserInformation(accountProfileEntity = it)
} }
RidingStyle() RidingStyle()
} }
moments?.let { moments?.let {
@@ -184,7 +186,11 @@ fun CarTopPicture() {
} }
@Composable @Composable
fun UserInformation(isSelf: Boolean = true, accountProfile: AccountProfile) { fun UserInformation(
isSelf: Boolean = true,
accountProfileEntity: AccountProfileEntity,
onFollowClick: () -> Unit = {}
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -193,21 +199,25 @@ fun UserInformation(isSelf: Boolean = true, accountProfile: AccountProfile) {
) { ) {
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
val userInfoModifier = Modifier.weight(1f) val userInfoModifier = Modifier.weight(1f)
UserInformationFollowers(userInfoModifier, accountProfile) UserInformationFollowers(userInfoModifier, accountProfileEntity)
UserInformationBasic(userInfoModifier, accountProfile) UserInformationBasic(userInfoModifier, accountProfileEntity)
UserInformationFollowing(userInfoModifier, accountProfile) UserInformationFollowing(userInfoModifier, accountProfileEntity)
} }
UserInformationSlogan() UserInformationSlogan()
CommunicationOperatorGroup(isSelf = isSelf) CommunicationOperatorGroup(
isSelf = isSelf,
isFollowing = accountProfileEntity.isFollowing,
onFollowClick = onFollowClick
)
} }
} }
@Composable @Composable
fun UserInformationFollowers(modifier: Modifier, accountProfile: AccountProfile) { fun UserInformationFollowers(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
Column(modifier = modifier.padding(top = 31.dp)) { Column(modifier = modifier.padding(top = 31.dp)) {
Text( Text(
modifier = Modifier.padding(bottom = 5.dp), modifier = Modifier.padding(bottom = 5.dp),
text = accountProfile.followerCount.toString(), text = accountProfileEntity.followerCount.toString(),
fontSize = 24.sp, fontSize = 24.sp,
color = Color.Black, color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
@@ -229,7 +239,7 @@ fun UserInformationFollowers(modifier: Modifier, accountProfile: AccountProfile)
} }
@Composable @Composable
fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) { fun UserInformationBasic(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@@ -242,7 +252,7 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
painter = painterResource(id = R.drawable.avatar_bold), contentDescription = "" painter = painterResource(id = R.drawable.avatar_bold), contentDescription = ""
) )
AsyncImage( AsyncImage(
accountProfile.avatar, accountProfileEntity.avatar,
modifier = Modifier modifier = Modifier
.size(width = 88.dp, height = 88.dp) .size(width = 88.dp, height = 88.dp)
.clip( .clip(
@@ -257,7 +267,7 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
modifier = Modifier modifier = Modifier
.widthIn(max = 220.dp) .widthIn(max = 220.dp)
.padding(top = 8.dp), .padding(top = 8.dp),
text = accountProfile.nickName, text = accountProfileEntity.nickName,
fontSize = 32.sp, fontSize = 32.sp,
color = Color.Black, color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold), style = TextStyle(fontWeight = FontWeight.Bold),
@@ -265,7 +275,7 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
) )
Text( Text(
modifier = Modifier.padding(top = 4.dp), modifier = Modifier.padding(top = 4.dp),
text = accountProfile.country, text = accountProfileEntity.country,
fontSize = 12.sp, fontSize = 12.sp,
color = Color.Gray color = Color.Gray
) )
@@ -279,14 +289,14 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
} }
@Composable @Composable
fun UserInformationFollowing(modifier: Modifier, accountProfile: AccountProfile) { fun UserInformationFollowing(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
Column( Column(
modifier = modifier.padding(top = 6.dp), modifier = modifier.padding(top = 6.dp),
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.End
) { ) {
Text( Text(
modifier = Modifier.padding(bottom = 5.dp), modifier = Modifier.padding(bottom = 5.dp),
text = accountProfile.followingCount.toString(), text = accountProfileEntity.followingCount.toString(),
fontSize = 24.sp, fontSize = 24.sp,
color = Color.Black, color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
@@ -320,7 +330,11 @@ fun UserInformationSlogan() {
} }
@Composable @Composable
fun CommunicationOperatorGroup(isSelf: Boolean = true) { fun CommunicationOperatorGroup(
isSelf: Boolean = true,
isFollowing: Boolean = false,
onFollowClick: () -> Unit
) {
val navController = LocalNavController.current val navController = LocalNavController.current
Row( Row(
modifier = Modifier modifier = Modifier
@@ -329,7 +343,11 @@ fun CommunicationOperatorGroup(isSelf: Boolean = true) {
) { ) {
if (!isSelf) { if (!isSelf) {
Box( Box(
modifier = Modifier.size(width = 142.dp, height = 40.dp), modifier = Modifier
.size(width = 142.dp, height = 40.dp)
.noRippleClickable {
onFollowClick()
},
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image( Image(
@@ -338,7 +356,7 @@ fun CommunicationOperatorGroup(isSelf: Boolean = true) {
contentDescription = "" contentDescription = ""
) )
Text( Text(
text = "FOLLOW", text = if (isFollowing) "FOLLOWING" else "FOLLOW",
fontSize = 16.sp, fontSize = 16.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
@@ -472,13 +490,14 @@ fun UserMoment(scope: LazyListScope) {
} }
@Composable @Composable
fun MomentPostUnit(momentItem: MomentItem) { fun MomentPostUnit(momentEntity: MomentEntity) {
TimeGroup(momentItem.time) TimeGroup(momentEntity.time)
MomentCard( ProfileMomentCard(
momentItem.momentTextContent, momentEntity.momentTextContent,
momentItem.images[0], momentEntity.images[0],
momentItem.likeCount.toString(), momentEntity.likeCount.toString(),
momentItem.commentCount.toString() momentEntity.commentCount.toString(),
momentId = momentEntity.id
) )
} }
@@ -506,7 +525,13 @@ fun TimeGroup(time: String = "2024.06.08 12:23") {
} }
@Composable @Composable
fun MomentCard(content: String, imageUrl: String, like: String, comment: String) { fun ProfileMomentCard(
content: String,
imageUrl: String,
like: String,
comment: String,
momentId: Int
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -514,7 +539,7 @@ fun MomentCard(content: String, imageUrl: String, like: String, comment: String)
.border(width = 1.dp, color = Color(0f, 0f, 0f, 0.1f), shape = RoundedCornerShape(6.dp)) .border(width = 1.dp, color = Color(0f, 0f, 0f, 0.1f), shape = RoundedCornerShape(6.dp))
) { ) {
MomentCardTopContent(content) MomentCardTopContent(content)
MomentCardPicture(imageUrl) MomentCardPicture(imageUrl, momentId)
MomentCardOperation(like, comment) MomentCardOperation(like, comment)
} }
} }
@@ -534,16 +559,35 @@ fun MomentCardTopContent(content: String) {
} }
} }
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun MomentCardPicture(imageUrl: String) { fun MomentCardPicture(imageUrl: String, momentId: Int) {
AsyncImage( val navController = LocalNavController.current
imageUrl, val sharedTransitionScope = LocalSharedTransitionScope.current
modifier = Modifier val animatedVisibilityScope = LocalAnimatedContentScope.current
.fillMaxSize() with(sharedTransitionScope) {
.padding(16.dp), AsyncImage(
contentDescription = "", imageUrl,
contentScale = ContentScale.FillWidth modifier = Modifier
) .sharedElement(
rememberSharedContentState(key = imageUrl),
animatedVisibilityScope = animatedVisibilityScope
)
.fillMaxSize()
.padding(16.dp)
.noRippleClickable {
navController.navigate(
NavigationRoute.Post.route.replace(
"{id}",
momentId.toString()
)
)
},
contentDescription = "",
contentScale = ContentScale.FillWidth
)
}
} }
@Composable @Composable

View File

@@ -1,5 +1,6 @@
package com.aiosman.riderpro.ui.login package com.aiosman.riderpro.ui.login
import android.widget.Toast
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -15,23 +16,106 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.ServiceException
import com.aiosman.riderpro.data.TestAccountServiceImpl
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun EmailSignupScreen() { fun EmailSignupScreen() {
var email by remember { mutableStateOf("") } var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) } var rememberMe by remember { mutableStateOf(false) }
var acceptTerms by remember { mutableStateOf(false) } var acceptTerms by remember { mutableStateOf(false) }
var acceptPromotions by remember { mutableStateOf(false) } var acceptPromotions by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
val context = LocalContext.current
val accountService: AccountService = TestAccountServiceImpl()
fun validateForm(): Boolean {
if (email.isEmpty()) {
Toast.makeText(context, "Email is required", Toast.LENGTH_SHORT).show()
return false
}
if (password.isEmpty()) {
Toast.makeText(context, "Password is required", Toast.LENGTH_SHORT).show()
return false
}
if (confirmPassword.isEmpty()) {
Toast.makeText(context, "Confirm password is required", Toast.LENGTH_SHORT).show()
return false
}
if (password != confirmPassword) {
Toast.makeText(context, "Password does not match", Toast.LENGTH_SHORT).show()
return false
}
if (!acceptTerms) {
Toast.makeText(context, "You must accept terms", Toast.LENGTH_SHORT).show()
return false
}
if (!acceptPromotions) {
Toast.makeText(context, "You must accept promotions", Toast.LENGTH_SHORT).show()
return false
}
return true
}
suspend fun registerUser() {
if (!validateForm()) return
// 注册
try {
accountService.registerUserWithPassword(email, password)
} catch (e: ServiceException) {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, "Failed to register", Toast.LENGTH_SHORT).show()
}
}
// 获取 token
val authResp = accountService.loginUserWithPassword(email, password)
if (authResp.token != null) {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, "Successfully registered", Toast.LENGTH_SHORT).show()
}
}
AppStore.apply {
token = authResp.token
this.rememberMe = rememberMe
saveData()
}
// 获取token 信息
try {
accountService.getMyAccount()
} catch (e: ServiceException) {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT).show()
}
}
scope.launch(Dispatchers.Main) {
navController.navigate(NavigationRoute.Index.route) {
popUpTo(NavigationRoute.Login.route) { inclusive = true }
}
}
}
StatusBarMaskLayout { StatusBarMaskLayout {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -74,9 +158,9 @@ fun EmailSignupScreen() {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp), .padding(horizontal = 24.dp),
text = password, text = confirmPassword,
onValueChange = { onValueChange = {
password = it confirmPassword = it
}, },
password = true, password = true,
label = "Confirm password", label = "Confirm password",
@@ -149,7 +233,11 @@ fun EmailSignupScreen() {
.height(48.dp), .height(48.dp),
text = "LET'S RIDE".uppercase(), text = "LET'S RIDE".uppercase(),
backgroundImage = R.mipmap.rider_pro_signup_red_bg backgroundImage = R.mipmap.rider_pro_signup_red_bg
) ) {
scope.launch(Dispatchers.IO) {
registerUser()
}
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package com.aiosman.riderpro.ui.login package com.aiosman.riderpro.ui.login
import android.widget.Toast
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -26,6 +27,7 @@ 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.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -38,6 +40,7 @@ import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.ServiceException
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.UserService import com.aiosman.riderpro.data.UserService
@@ -57,18 +60,24 @@ fun UserAuthScreen() {
var accountService: AccountService = TestAccountServiceImpl() var accountService: AccountService = TestAccountServiceImpl()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current
fun onLogin() { fun onLogin() {
scope.launch { scope.launch {
val authResp = accountService.loginUserWithPassword(email, password) try {
if (authResp.token != null) { val authResp = accountService.loginUserWithPassword(email, password)
AppStore.apply { if (authResp.token != null) {
token = authResp.token AppStore.apply {
this.rememberMe = rememberMe token = authResp.token
saveData() this.rememberMe = rememberMe
} saveData()
navController.navigate(NavigationRoute.Index.route) { }
popUpTo(NavigationRoute.Login.route) { inclusive = true } navController.navigate(NavigationRoute.Index.route) {
popUpTo(NavigationRoute.Login.route) { inclusive = true }
}
} }
} catch (e: ServiceException) {
// handle error
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -2,6 +2,7 @@ package com.aiosman.riderpro.ui.post
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
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.Image import androidx.compose.foundation.Image
@@ -63,6 +64,7 @@ fun NewPostScreen() {
val model = NewPostViewModel val model = NewPostViewModel
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(color = Color.Transparent) systemUiController.setNavigationBarColor(color = Color.Transparent)
model.init() model.init()
@@ -76,7 +78,7 @@ fun NewPostScreen() {
) { ) {
NewPostTopBar { NewPostTopBar {
model.viewModelScope.launch { model.viewModelScope.launch {
model.createMoment() model.createMoment(context = context)
navController.popBackStack() navController.popBackStack()
} }
} }
@@ -93,7 +95,7 @@ fun NewPostScreen() {
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color = Color(0xFFEEEEEE)).padding(24.dp) modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color = Color(0xFFEEEEEE)).padding(24.dp)
) { ) {
RelPostCard( RelPostCard(
momentItem = it, momentEntity = it,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
@@ -177,7 +179,9 @@ fun AddImageGrid() {
val uri = result.data?.data val uri = result.data?.data
if (uri != null) { if (uri != null) {
model.imageUriList += uri.toString() model.imageUriList += uri.toString()
// get filename and extension
} }
} }
} }
val stroke = Stroke( val stroke = Stroke(

View File

@@ -1,16 +1,22 @@
package com.aiosman.riderpro.ui.post package com.aiosman.riderpro.ui.post
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.TestMomentServiceImpl import com.aiosman.riderpro.data.TestMomentServiceImpl
import com.aiosman.riderpro.model.MomentItem import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.ui.modification.Modification import com.aiosman.riderpro.ui.modification.Modification
import kotlinx.coroutines.launch import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
object NewPostViewModel : ViewModel() { object NewPostViewModel : ViewModel() {
@@ -20,7 +26,7 @@ object NewPostViewModel : ViewModel() {
var modificationList by mutableStateOf<List<Modification>>(listOf()) var modificationList by mutableStateOf<List<Modification>>(listOf())
var imageUriList by mutableStateOf(listOf<String>()) var imageUriList by mutableStateOf(listOf<String>())
var relPostId by mutableStateOf<Int?>(null) var relPostId by mutableStateOf<Int?>(null)
var relMoment by mutableStateOf<MomentItem?>(null) var relMoment by mutableStateOf<MomentEntity?>(null)
fun asNewPost() { fun asNewPost() {
textContent = "" textContent = ""
searchPlaceAddressResult = null searchPlaceAddressResult = null
@@ -29,16 +35,39 @@ object NewPostViewModel : ViewModel() {
relPostId = null relPostId = null
} }
suspend fun createMoment() { suspend fun uriToFile(context: Context, uri: Uri): File {
momentService.createMoment( val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
content = textContent, val tempFile = withContext(Dispatchers.IO) {
authorId = 1, File.createTempFile("temp", null, context.cacheDir)
imageUriList = imageUriList, }
relPostId = relPostId inputStream?.use { input ->
) FileOutputStream(tempFile).use { output ->
input.copyTo(output)
}
}
return tempFile
} }
suspend fun init(){ suspend fun createMoment(context: Context) {
val uploadImageList = emptyList<UploadImage>().toMutableList()
for (uri in imageUriList) {
val cursor = context.contentResolver.query(Uri.parse(uri), null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val displayName = it.getString(it.getColumnIndex("_display_name"))
val extension = displayName.substringAfterLast(".")
Log.d("NewPost", "File name: $displayName, extension: $extension")
// read as file
val file = uriToFile(context, Uri.parse(uri))
Log.d("NewPost", "File size: ${file.length()}")
uploadImageList += UploadImage(file, displayName, uri, extension)
}
}
}
momentService.createMoment(textContent, 1, uploadImageList, relPostId)
}
suspend fun init() {
relPostId?.let { relPostId?.let {
val moment = momentService.getMomentById(it) val moment = momentService.getMomentById(it)
relMoment = moment relMoment = moment

View File

@@ -74,9 +74,9 @@ import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountProfile import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.Comment import com.aiosman.riderpro.data.CommentEntity
import com.aiosman.riderpro.data.CommentPagingSource import com.aiosman.riderpro.data.CommentPagingSource
import com.aiosman.riderpro.data.CommentRemoteDataSource import com.aiosman.riderpro.data.CommentRemoteDataSource
import com.aiosman.riderpro.data.CommentService import com.aiosman.riderpro.data.CommentService
@@ -84,7 +84,7 @@ 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.model.MomentItem 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
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
@@ -103,7 +103,7 @@ class PostViewModel(
) : ViewModel() { ) : ViewModel() {
var service: MomentService = TestMomentServiceImpl() var service: MomentService = TestMomentServiceImpl()
var commentService: CommentService = TestCommentServiceImpl() var commentService: CommentService = TestCommentServiceImpl()
private var _commentsFlow = MutableStateFlow<PagingData<Comment>>(PagingData.empty()) private var _commentsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
val commentsFlow = _commentsFlow.asStateFlow() val commentsFlow = _commentsFlow.asStateFlow()
init { init {
@@ -120,17 +120,16 @@ class PostViewModel(
_commentsFlow.value = it _commentsFlow.value = it
} }
} }
} }
var accountProfile by mutableStateOf<AccountProfile?>(null) var accountProfileEntity by mutableStateOf<AccountProfileEntity?>(null)
var moment by mutableStateOf<MomentItem?>(null) var moment by mutableStateOf<MomentEntity?>(null)
var accountService: AccountService = TestAccountServiceImpl() var accountService: AccountService = TestAccountServiceImpl()
suspend fun initData() { suspend fun initData() {
moment = service.getMomentById(postId.toInt()) moment = service.getMomentById(postId.toInt())
moment?.let { moment?.let {
accountProfile = accountService.getAccountProfileById(it.authorId) accountProfileEntity = accountService.getAccountProfileById(it.authorId)
} }
} }
@@ -147,8 +146,21 @@ class PostViewModel(
_commentsFlow.value = updatedPagingData _commentsFlow.value = updatedPagingData
} }
suspend fun unlikeComment(commentId: Int) {
commentService.dislikeComment(commentId)
val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) {
comment.copy(liked = !comment.liked)
} else {
comment
}
}
_commentsFlow.value = updatedPagingData
}
suspend fun createComment(content: String) { suspend fun createComment(content: String) {
commentService.createComment(postId.toInt(), content, 1) commentService.createComment(postId.toInt(), content)
MomentViewModel.updateCommentCount(postId.toInt()) MomentViewModel.updateCommentCount(postId.toInt())
} }
@@ -212,7 +224,7 @@ fun PostScreen(
commentsPagging.refresh() commentsPagging.refresh()
} }
}, },
momentItem = viewModel.moment momentEntity = viewModel.moment
) )
} }
) { ) {
@@ -221,7 +233,11 @@ fun PostScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
Header(viewModel.accountProfile) Header(
avatar = viewModel.moment?.avatar,
nickname = viewModel.moment?.nickname,
userId = viewModel.moment?.authorId
)
Column(modifier = Modifier.animateContentSize()) { Column(modifier = Modifier.animateContentSize()) {
AnimatedVisibility(visible = showCollapseContent) { AnimatedVisibility(visible = showCollapseContent) {
// collapse content // collapse content
@@ -256,9 +272,13 @@ fun PostScreen(
CommentsSection( CommentsSection(
lazyPagingItems = commentsPagging, lazyPagingItems = commentsPagging,
scrollState, scrollState,
onLike = { comment: Comment -> onLike = { commentEntity: CommentEntity ->
scope.launch { scope.launch {
viewModel.likeComment(comment.id) if (commentEntity.liked) {
viewModel.unlikeComment(commentEntity.id)
} else {
viewModel.likeComment(commentEntity.id)
}
} }
}) { }) {
showCollapseContent = it showCollapseContent = it
@@ -270,7 +290,7 @@ fun PostScreen(
} }
@Composable @Composable
fun Header(accountProfile: AccountProfile?) { fun Header(avatar: String?, nickname: String?, userId: Int?) {
val navController = LocalNavController.current val navController = LocalNavController.current
Row( Row(
modifier = Modifier modifier = Modifier
@@ -288,30 +308,30 @@ fun Header(accountProfile: AccountProfile?) {
.size(32.dp) .size(32.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
accountProfile?.let { avatar?.let {
AsyncImage( AsyncImage(
accountProfile.avatar, it,
contentDescription = "Profile Picture", contentDescription = "Profile Picture",
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.noRippleClickable { .noRippleClickable {
navController.navigate( userId?.let {
NavigationRoute.AccountProfile.route.replace( navController.navigate(
"{id}", NavigationRoute.AccountProfile.route.replace(
accountProfile.id.toString() "{id}",
userId.toString()
)
) )
) }
}, },
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }
Spacer(modifier = Modifier.width(8.dp))
accountProfile?.let {
Text(text = accountProfile.nickName, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
Text(text = nickname ?: "", fontWeight = FontWeight.Bold)
Box( Box(
modifier = Modifier modifier = Modifier
.height(20.dp) .height(20.dp)
@@ -350,7 +370,7 @@ fun PostImageView(
state = pagerState, state = pagerState,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth().background(Color.Black), .fillMaxWidth(),
) { page -> ) { page ->
val image = images[page] val image = images[page]
with(sharedTransitionScope) { with(sharedTransitionScope) {
@@ -407,7 +427,7 @@ fun PostImageView(
@Composable @Composable
fun PostDetails( fun PostDetails(
postId: String, postId: String,
momentItem: MomentItem? momentEntity: MomentEntity?
) { ) {
Column( Column(
@@ -418,22 +438,22 @@ fun PostDetails(
) { ) {
Text( Text(
text = momentItem?.momentTextContent ?: "", text = momentEntity?.momentTextContent ?: "",
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
Text(text = "12-11 发布") Text(text = "12-11 发布")
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = "${momentItem?.commentCount ?: 0} Comments") Text(text = "${momentEntity?.commentCount ?: 0} Comments")
} }
} }
@Composable @Composable
fun CommentsSection( fun CommentsSection(
lazyPagingItems: LazyPagingItems<Comment>, lazyPagingItems: LazyPagingItems<CommentEntity>,
scrollState: LazyListState = rememberLazyListState(), scrollState: LazyListState = rememberLazyListState(),
onLike: (Comment) -> Unit, onLike: (CommentEntity) -> Unit,
onWillCollapse: (Boolean) -> Unit onWillCollapse: (Boolean) -> Unit
) { ) {
LazyColumn( LazyColumn(
@@ -463,11 +483,11 @@ fun CommentsSection(
@Composable @Composable
fun CommentItem(comment: Comment, onLike: () -> Unit = {}) { fun CommentItem(commentEntity: CommentEntity, onLike: () -> Unit = {}) {
Column { Column {
Row(modifier = Modifier.padding(vertical = 8.dp)) { Row(modifier = Modifier.padding(vertical = 8.dp)) {
AsyncImage( AsyncImage(
comment.avatar, commentEntity.avatar,
contentDescription = "Comment Profile Picture", contentDescription = "Comment Profile Picture",
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
@@ -476,9 +496,9 @@ fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column { Column {
Text(text = comment.name, fontWeight = FontWeight.Bold) Text(text = commentEntity.name, fontWeight = FontWeight.Bold)
Text(text = comment.comment) Text(text = commentEntity.comment)
Text(text = comment.date, fontSize = 12.sp, color = Color.Gray) Text(text = commentEntity.date, fontSize = 12.sp, color = Color.Gray)
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -488,17 +508,17 @@ fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
Icon( Icon(
Icons.Filled.Favorite, Icons.Filled.Favorite,
contentDescription = "Like", contentDescription = "Like",
tint = if (comment.liked) Color.Red else Color.Gray tint = if (commentEntity.liked) Color.Red else Color.Gray
) )
} }
Text(text = comment.likes.toString()) Text(text = commentEntity.likes.toString())
} }
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Column( Column(
modifier = Modifier.padding(start = 16.dp) modifier = Modifier.padding(start = 16.dp)
) { ) {
comment.replies.forEach { reply -> commentEntity.replies.forEach { reply ->
CommentItem(reply) CommentItem(reply)
} }
} }
@@ -510,7 +530,7 @@ fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
fun BottomNavigationBar( fun BottomNavigationBar(
onCreateComment: (String) -> Unit = {}, onCreateComment: (String) -> Unit = {},
onLikeClick: () -> Unit = {}, onLikeClick: () -> Unit = {},
momentItem: MomentItem? momentEntity: MomentEntity?
) { ) {
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
@@ -568,10 +588,10 @@ fun BottomNavigationBar(
Icon( Icon(
Icons.Filled.Favorite, Icons.Filled.Favorite,
contentDescription = "like", contentDescription = "like",
tint = if (momentItem?.liked == true) Color.Red else Color.Gray tint = if (momentEntity?.liked == true) Color.Red else Color.Gray
) )
} }
Text(text = momentItem?.likeCount.toString()) Text(text = momentEntity?.likeCount.toString())
IconButton( IconButton(
onClick = { /*TODO*/ }) { onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Star, contentDescription = "Send") Icon(Icons.Filled.Star, contentDescription = "Send")

View File

@@ -9,6 +9,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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
@@ -17,13 +18,13 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.data.AccountProfile import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.MomentPagingSource import com.aiosman.riderpro.data.MomentPagingSource
import com.aiosman.riderpro.data.MomentRemoteDataSource import com.aiosman.riderpro.data.MomentRemoteDataSource
import com.aiosman.riderpro.data.TestMomentServiceImpl 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.MomentItem import com.aiosman.riderpro.model.MomentEntity
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.index.tabs.profile.CarGroup import com.aiosman.riderpro.ui.index.tabs.profile.CarGroup
import com.aiosman.riderpro.ui.index.tabs.profile.MomentPostUnit import com.aiosman.riderpro.ui.index.tabs.profile.MomentPostUnit
@@ -31,15 +32,15 @@ import com.aiosman.riderpro.ui.index.tabs.profile.RidingStyle
import com.aiosman.riderpro.ui.index.tabs.profile.UserInformation import com.aiosman.riderpro.ui.index.tabs.profile.UserInformation
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
@Composable @Composable
fun AccountProfile(id:String) { fun AccountProfile(id:String) {
// val model = MyProfileViewModel
val userService: UserService = TestUserServiceImpl() val userService: UserService = TestUserServiceImpl()
var userProfile by remember { mutableStateOf<AccountProfile?>(null) } var userProfile by remember { mutableStateOf<AccountProfileEntity?>(null) }
val momentService = TestMomentServiceImpl() val momentService = TestMomentServiceImpl()
var momentsFlow by remember { mutableStateOf<Flow<PagingData<MomentItem>>?>(null) } var momentsFlow by remember { mutableStateOf<Flow<PagingData<MomentEntity>>?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
userProfile = userService.getUserProfile(id) userProfile = userService.getUserProfile(id)
momentsFlow = Pager( momentsFlow = Pager(
@@ -73,7 +74,21 @@ fun AccountProfile(id:String) {
item { item {
CarGroup() CarGroup()
userProfile?.let { userProfile?.let {
UserInformation(isSelf = false, accountProfile = it) UserInformation(
isSelf = false,
accountProfileEntity = it,
onFollowClick = {
scope.launch {
if (it.isFollowing) {
userService.unFollowUser(id)
userProfile = userProfile?.copy(isFollowing = false)
} else {
userService.followUser(id)
userProfile = userProfile?.copy(isFollowing = true)
}
}
},
)
} }
RidingStyle() RidingStyle()
} }

View File

@@ -2,4 +2,5 @@
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false
id("com.google.gms.google-services") version "4.4.2" apply false
} }