更新
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.aiosman.riderpro"
|
||||
compileSdk = 34
|
||||
@@ -60,7 +60,7 @@ dependencies {
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3.android)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation (libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.paging.runtime)
|
||||
implementation(libs.maps.compose)
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
@@ -76,13 +76,19 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
implementation (libs.places)
|
||||
implementation(libs.places)
|
||||
implementation(libs.androidx.animation)
|
||||
implementation("io.coil-kt:coil-compose:2.7.0")
|
||||
implementation("io.coil-kt:coil:2.7.0")
|
||||
implementation("com.google.android.gms:play-services-auth:21.2.0")
|
||||
implementation("io.github.serpro69:kotlin-faker:2.0.0-rc.5")
|
||||
implementation("androidx.compose.material:material:1.6.8")
|
||||
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")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
android:roundIcon="@mipmap/rider_pro_log_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RiderPro"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<meta-data android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4"/>
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.core.view.WindowCompat
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.aiosman.riderpro.data.AccountService
|
||||
import com.aiosman.riderpro.data.ServiceException
|
||||
import com.aiosman.riderpro.data.TestAccountServiceImpl
|
||||
import com.aiosman.riderpro.data.TestUserServiceImpl
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
suspend fun getAccount() {
|
||||
//TODO apply token to client
|
||||
if (!AppStore.rememberMe) {
|
||||
return
|
||||
}
|
||||
suspend fun getAccount(): Boolean {
|
||||
val accountService: AccountService = TestAccountServiceImpl()
|
||||
accountService.getMyAccount()
|
||||
try {
|
||||
val resp = accountService.getMyAccount()
|
||||
return true
|
||||
} catch (e: ServiceException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
@@ -64,9 +68,9 @@ class MainActivity : ComponentActivity() {
|
||||
enableEdgeToEdge()
|
||||
|
||||
scope.launch {
|
||||
getAccount()
|
||||
val isAccountValidate = getAccount()
|
||||
var startDestination = NavigationRoute.Login.route
|
||||
if (AppStore.token != null && AppStore.rememberMe) {
|
||||
if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) {
|
||||
startDestination = NavigationRoute.Index.route
|
||||
}
|
||||
setContent {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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
|
||||
|
||||
data class AccountProfile(
|
||||
data class AccountProfileEntity(
|
||||
val id: Int,
|
||||
val followerCount: Int,
|
||||
val followingCount: Int,
|
||||
@@ -10,33 +13,72 @@ data class AccountProfile(
|
||||
val avatar: String,
|
||||
val bio: 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 {
|
||||
suspend fun getMyAccountProfile(): AccountProfile
|
||||
suspend fun getAccountProfileById(id: Int): AccountProfile
|
||||
suspend fun getMyAccountProfile(): AccountProfileEntity
|
||||
suspend fun getAccountProfileById(id: Int): AccountProfileEntity
|
||||
suspend fun getMyAccount(): UserAuth
|
||||
suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth
|
||||
suspend fun logout()
|
||||
suspend fun updateAvatar(uri: String)
|
||||
suspend fun updateProfile(nickName: String, bio: String)
|
||||
suspend fun registerUserWithPassword(loginName: String, password: String)
|
||||
}
|
||||
|
||||
class TestAccountServiceImpl : AccountService {
|
||||
override suspend fun getMyAccountProfile(): AccountProfile {
|
||||
return TestDatabase.accountData.first { it.id == 1 }
|
||||
override suspend fun getMyAccountProfile(): AccountProfileEntity {
|
||||
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 {
|
||||
return TestDatabase.accountData.first { it.id == id }
|
||||
override suspend fun getAccountProfileById(id: Int): AccountProfileEntity {
|
||||
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 {
|
||||
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 {
|
||||
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() {
|
||||
@@ -52,6 +94,7 @@ class TestAccountServiceImpl : AccountService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateProfile(nickName: String, bio: String) {
|
||||
TestDatabase.accountData = TestDatabase.accountData.map {
|
||||
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))
|
||||
}
|
||||
}
|
||||
2
app/src/main/java/com/aiosman/riderpro/data/Base.kt
Normal file
2
app/src/main/java/com/aiosman/riderpro/data/Base.kt
Normal file
@@ -0,0 +1,2 @@
|
||||
package com.aiosman.riderpro.data
|
||||
|
||||
@@ -2,38 +2,81 @@ package com.aiosman.riderpro.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.aiosman.riderpro.data.api.ApiClient
|
||||
import com.aiosman.riderpro.data.api.CommentRequestBody
|
||||
import com.aiosman.riderpro.test.TestDatabase
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.io.IOException
|
||||
import java.util.Calendar
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
|
||||
interface CommentService {
|
||||
suspend fun getComments(pageNumber: Int, postId: Int? = null): ListContainer<Comment>
|
||||
suspend fun createComment(postId: Int, content: String, authorId: Int): Comment
|
||||
suspend fun getComments(pageNumber: Int, postId: Int? = null): ListContainer<CommentEntity>
|
||||
suspend fun createComment(postId: Int, content: String)
|
||||
suspend fun likeComment(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(
|
||||
@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 name: String,
|
||||
val comment: String,
|
||||
val date: String,
|
||||
val likes: Int,
|
||||
val replies: List<Comment>,
|
||||
val replies: List<CommentEntity>,
|
||||
val postId: Int = 0,
|
||||
val avatar: String,
|
||||
val author: Int,
|
||||
val author: Long,
|
||||
var liked: Boolean,
|
||||
)
|
||||
|
||||
class CommentPagingSource(
|
||||
private val remoteDataSource: CommentRemoteDataSource,
|
||||
private val postId: Int? = null
|
||||
) : PagingSource<Int, Comment>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Comment> {
|
||||
) : PagingSource<Int, CommentEntity>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CommentEntity> {
|
||||
return try {
|
||||
val currentPage = params.key ?: 1
|
||||
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
|
||||
}
|
||||
|
||||
@@ -59,87 +102,37 @@ class CommentPagingSource(
|
||||
class CommentRemoteDataSource(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestCommentServiceImpl : CommentService {
|
||||
override suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer<Comment> {
|
||||
var rawList = TestDatabase.comment
|
||||
if (postId != null) {
|
||||
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))
|
||||
override suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer<CommentEntity> {
|
||||
val resp = ApiClient.api.getComments(pageNumber, postId)
|
||||
val body = resp.body() ?: throw ServiceException("Failed to get comments")
|
||||
return ListContainer(
|
||||
total = rawList.size,
|
||||
page = pageNumber,
|
||||
pageSize = DataBatchSize,
|
||||
list = currentSublist
|
||||
list = body.list.map { it.toCommentEntity() },
|
||||
page = body.page,
|
||||
total = body.total,
|
||||
pageSize = body.pageSize
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createComment(postId: Int, content: String, authorId: Int): Comment {
|
||||
var author = TestDatabase.accountData.find { it.id == authorId }
|
||||
if (author == null) {
|
||||
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 createComment(postId: Int, content: String) {
|
||||
val resp = ApiClient.api.createComment(postId, CommentRequestBody(content))
|
||||
return
|
||||
}
|
||||
|
||||
override suspend fun likeComment(commentId: Int) {
|
||||
TestDatabase.comment = TestDatabase.comment.map {
|
||||
if (it.id == commentId) {
|
||||
it.copy(likes = it.likes + 1)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
TestDatabase.likeCommentList += Pair(commentId, 1)
|
||||
val resp = ApiClient.api.likeComment(commentId)
|
||||
return
|
||||
}
|
||||
|
||||
override suspend fun dislikeComment(commentId: Int) {
|
||||
TestDatabase.comment = TestDatabase.comment.map {
|
||||
if (it.id == commentId) {
|
||||
it.copy(likes = it.likes - 1)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
TestDatabase.likeCommentList = TestDatabase.likeCommentList.filter { it.first != commentId }
|
||||
val resp = ApiClient.api.dislikeComment(commentId)
|
||||
return
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.aiosman.riderpro.data
|
||||
|
||||
data class DataContainer<T>(
|
||||
val data: T
|
||||
)
|
||||
9
app/src/main/java/com/aiosman/riderpro/data/Exception.kt
Normal file
9
app/src/main/java/com/aiosman/riderpro/data/Exception.kt
Normal 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
|
||||
)
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.aiosman.riderpro.data
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
data class ListContainer<T>(
|
||||
@SerializedName("total")
|
||||
val total: Int,
|
||||
@SerializedName("page")
|
||||
val page: Int,
|
||||
@SerializedName("pageSize")
|
||||
val pageSize: Int,
|
||||
@SerializedName("list")
|
||||
val list: List<T>
|
||||
)
|
||||
@@ -2,28 +2,109 @@ package com.aiosman.riderpro.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.aiosman.riderpro.AppStore
|
||||
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 java.io.IOException
|
||||
import kotlin.math.min
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
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 {
|
||||
suspend fun getMomentById(id: Int): MomentItem
|
||||
suspend fun getMomentById(id: Int): MomentEntity
|
||||
suspend fun likeMoment(id: Int)
|
||||
suspend fun dislikeMoment(id: Int)
|
||||
suspend fun getMoments(
|
||||
pageNumber: Int,
|
||||
author: Int? = null,
|
||||
timelineId: Int? = null
|
||||
): ListContainer<MomentItem>
|
||||
): ListContainer<MomentEntity>
|
||||
|
||||
suspend fun createMoment(
|
||||
content: String,
|
||||
authorId: Int,
|
||||
imageUriList: List<String>,
|
||||
images: List<UploadImage>,
|
||||
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 author: Int? = null,
|
||||
private val timelineId: Int? = null
|
||||
) : PagingSource<Int, MomentItem>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentItem> {
|
||||
) : PagingSource<Int, MomentEntity>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
|
||||
return try {
|
||||
val currentPage = params.key ?: 1
|
||||
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
|
||||
}
|
||||
|
||||
@@ -64,7 +145,7 @@ class MomentRemoteDataSource(
|
||||
pageNumber: Int,
|
||||
author: Int?,
|
||||
timelineId: Int?
|
||||
): ListContainer<MomentItem> {
|
||||
): ListContainer<MomentEntity> {
|
||||
return momentService.getMoments(pageNumber, author, timelineId)
|
||||
}
|
||||
}
|
||||
@@ -77,11 +158,11 @@ class TestMomentServiceImpl() : MomentService {
|
||||
pageNumber: Int,
|
||||
author: Int?,
|
||||
timelineId: Int?
|
||||
): ListContainer<MomentItem> {
|
||||
): ListContainer<MomentEntity> {
|
||||
return testMomentBackend.fetchMomentItems(pageNumber, author, timelineId)
|
||||
}
|
||||
|
||||
override suspend fun getMomentById(id: Int): MomentItem {
|
||||
override suspend fun getMomentById(id: Int): MomentEntity {
|
||||
return testMomentBackend.getMomentById(id)
|
||||
}
|
||||
|
||||
@@ -97,10 +178,18 @@ class TestMomentServiceImpl() : MomentService {
|
||||
override suspend fun createMoment(
|
||||
content: String,
|
||||
authorId: Int,
|
||||
imageUriList: List<String>,
|
||||
images: List<UploadImage>,
|
||||
relPostId: Int?
|
||||
): MomentItem {
|
||||
return testMomentBackend.createMoment(content, authorId, imageUriList, relPostId)
|
||||
): MomentEntity {
|
||||
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,
|
||||
author: Int? = null,
|
||||
timelineId: Int?
|
||||
): ListContainer<MomentItem> {
|
||||
var rawList = TestDatabase.momentData
|
||||
rawList = rawList.sortedBy { it.id }.reversed()
|
||||
if (author != null) {
|
||||
rawList = rawList.filter { it.authorId == author }
|
||||
}
|
||||
if (timelineId != null) {
|
||||
val followIdList = TestDatabase.followList.filter {
|
||||
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)
|
||||
): ListContainer<MomentEntity> {
|
||||
val resp = ApiClient.api.getPosts(
|
||||
pageSize = DataBatchSize,
|
||||
page = pageNumber,
|
||||
timelineId = timelineId,
|
||||
authorId = author
|
||||
)
|
||||
val body = resp.body() ?: throw ServiceException("Failed to get moments")
|
||||
return ListContainer(
|
||||
total = rawList.size,
|
||||
total = body.total,
|
||||
page = pageNumber,
|
||||
pageSize = DataBatchSize,
|
||||
list = currentSublist
|
||||
list = body.list.map { it.toMomentItem() }
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getMomentById(id: Int): MomentItem {
|
||||
var moment = TestDatabase.momentData.first {
|
||||
it.id == id
|
||||
}
|
||||
val isLike = TestDatabase.likeMomentList.any {
|
||||
it.first == id && it.second == 1
|
||||
}
|
||||
moment = moment.copy(liked = isLike)
|
||||
return moment
|
||||
suspend fun getMomentById(id: Int): MomentEntity {
|
||||
var resp = ApiClient.api.getPost(id)
|
||||
var body = resp.body()?.data ?: throw ServiceException("Failed to get moment")
|
||||
return body.toMomentItem()
|
||||
}
|
||||
|
||||
suspend fun likeMoment(id: Int) {
|
||||
val oldMoment = TestDatabase.momentData.first {
|
||||
it.id == id
|
||||
}
|
||||
val newMoment = oldMoment.copy(likeCount = oldMoment.likeCount + 1)
|
||||
TestDatabase.updateMomentById(id, newMoment)
|
||||
TestDatabase.likeMomentList += Pair(id, 1)
|
||||
ApiClient.api.likePost(id)
|
||||
}
|
||||
|
||||
suspend fun dislikeMoment(id: Int) {
|
||||
val oldMoment = TestDatabase.momentData.first {
|
||||
it.id == id
|
||||
}
|
||||
val newMoment = oldMoment.copy(likeCount = oldMoment.likeCount - 1)
|
||||
TestDatabase.updateMomentById(id, newMoment)
|
||||
TestDatabase.likeMomentList = TestDatabase.likeMomentList.filter {
|
||||
it.first != id
|
||||
}
|
||||
ApiClient.api.dislikePost(id)
|
||||
}
|
||||
|
||||
fun createMultipartBody(file: File, name: String): MultipartBody.Part {
|
||||
val requestFile = RequestBody.create("image/*".toMediaTypeOrNull(), file)
|
||||
return MultipartBody.Part.createFormData(name, file.name, requestFile)
|
||||
}
|
||||
|
||||
suspend fun createMoment(
|
||||
content: String,
|
||||
authorId: Int,
|
||||
imageUriList: List<String>,
|
||||
imageUriList: List<UploadImage>,
|
||||
relPostId: Int?
|
||||
): MomentItem {
|
||||
TestDatabase.momentIdCounter += 1
|
||||
val person = TestDatabase.accountData.first {
|
||||
it.id == authorId
|
||||
): MomentEntity {
|
||||
val textContent = content.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val imageList = imageUriList.map { item ->
|
||||
val file = item.file
|
||||
createMultipartBody(file, "image")
|
||||
}
|
||||
val newMoment = MomentItem(
|
||||
id = TestDatabase.momentIdCounter,
|
||||
avatar = person.avatar,
|
||||
nickname = person.nickName,
|
||||
location = person.country,
|
||||
time = "2023.02.02 11:23",
|
||||
followStatus = false,
|
||||
momentTextContent = content,
|
||||
momentPicture = R.drawable.default_moment_img,
|
||||
likeCount = 0,
|
||||
commentCount = 0,
|
||||
shareCount = 0,
|
||||
favoriteCount = 0,
|
||||
images = imageUriList,
|
||||
authorId = person.id,
|
||||
relPostId = relPostId
|
||||
)
|
||||
TestDatabase.momentData += newMoment
|
||||
return newMoment
|
||||
val response = ApiClient.api.createPost(imageList, textContent = textContent)
|
||||
val body = response.body()?.data ?: throw ServiceException("Failed to create moment")
|
||||
return body.toMomentItem()
|
||||
|
||||
}
|
||||
|
||||
suspend fun favoriteMoment(id: Int) {
|
||||
ApiClient.api.favoritePost(id)
|
||||
}
|
||||
suspend fun unfavoriteMoment(id: Int) {
|
||||
ApiClient.api.unfavoritePost(id)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aiosman.riderpro.data
|
||||
|
||||
import com.aiosman.riderpro.data.api.ApiClient
|
||||
import com.aiosman.riderpro.test.TestDatabase
|
||||
|
||||
data class UserAuth(
|
||||
@@ -8,17 +9,26 @@ data class UserAuth(
|
||||
)
|
||||
|
||||
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 {
|
||||
override suspend fun getUserProfile(id: String): AccountProfile {
|
||||
TestDatabase.accountData.forEach {
|
||||
if (it.id == id.toInt()) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return AccountProfile(0, 0, 0, "", "", "", "")
|
||||
override suspend fun getUserProfile(id: String): AccountProfileEntity {
|
||||
val resp = ApiClient.api.getAccountProfileById(id.toInt())
|
||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||
return body.data.toAccountProfileEntity()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
39
app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt
Normal file
39
app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
146
app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt
Normal file
146
app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt
Normal 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>
|
||||
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.aiosman.riderpro.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.aiosman.riderpro.R
|
||||
|
||||
data class MomentItem(
|
||||
data class MomentEntity(
|
||||
val id: Int,
|
||||
val avatar: String,
|
||||
val nickname: String,
|
||||
@@ -20,5 +19,6 @@ data class MomentItem(
|
||||
val authorId: Int = 0,
|
||||
var liked: Boolean = false,
|
||||
var relPostId: Int? = null,
|
||||
var relMoment: MomentItem? = null
|
||||
var relMoment: MomentEntity? = null,
|
||||
var isFavorite: Boolean = false
|
||||
)
|
||||
@@ -2,16 +2,16 @@ package com.aiosman.riderpro.test
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.aiosman.riderpro.model.MomentItem
|
||||
import com.aiosman.riderpro.model.MomentEntity
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.ceil
|
||||
|
||||
class TestBackend(
|
||||
private val backendDataList: List<MomentItem>,
|
||||
private val backendDataList: List<MomentEntity>,
|
||||
private val loadDelay: Long = 500,
|
||||
) {
|
||||
val DataBatchSize = 5
|
||||
class DesiredLoadResultPageResponse(val data: List<MomentItem>)
|
||||
class DesiredLoadResultPageResponse(val data: List<MomentEntity>)
|
||||
/** Returns [DataBatchSize] items for a key */
|
||||
fun searchItemsByKey(key: Int): DesiredLoadResultPageResponse {
|
||||
val maxKey = ceil(backendDataList.size.toFloat() / DataBatchSize).toInt()
|
||||
@@ -28,8 +28,8 @@ class TestBackend(
|
||||
class TestPagingSource(
|
||||
private val backend: TestBackend,
|
||||
private val loadDelay: Long,
|
||||
) : PagingSource<Int, MomentItem>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentItem> {
|
||||
) : PagingSource<Int, MomentEntity>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
|
||||
// Simulate latency
|
||||
delay(loadDelay)
|
||||
val pageNumber = params.key ?: 0
|
||||
@@ -42,7 +42,7 @@ class TestPagingSource(
|
||||
val nextKey = if (response.data.isNotEmpty()) pageNumber + 1 else null
|
||||
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 {
|
||||
state.closestPageToPosition(it)?.prevKey?.plus(1)
|
||||
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.aiosman.riderpro.test
|
||||
|
||||
import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.data.AccountProfile
|
||||
import com.aiosman.riderpro.data.Comment
|
||||
import com.aiosman.riderpro.model.MomentItem
|
||||
import com.aiosman.riderpro.data.AccountProfileEntity
|
||||
import com.aiosman.riderpro.data.CommentEntity
|
||||
import com.aiosman.riderpro.model.MomentEntity
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import io.github.serpro69.kfaker.faker
|
||||
import java.io.File
|
||||
|
||||
object TestDatabase {
|
||||
var momentData = emptyList<MomentItem>()
|
||||
var accountData = emptyList<AccountProfile>()
|
||||
var comment = emptyList<Comment>()
|
||||
var momentData = emptyList<MomentEntity>()
|
||||
var accountData = emptyList<AccountProfileEntity>()
|
||||
var commentEntity = emptyList<CommentEntity>()
|
||||
var commentIdCounter = 0
|
||||
var momentIdCounter = 0
|
||||
var selfId = 1
|
||||
@@ -41,6 +41,7 @@ object TestDatabase {
|
||||
var followList = emptyList<Pair<Int, Int>>()
|
||||
var likeCommentList = emptyList<Pair<Int, Int>>()
|
||||
var likeMomentList = emptyList<Pair<Int, Int>>()
|
||||
|
||||
init {
|
||||
val faker = faker {
|
||||
this.fakerConfig {
|
||||
@@ -48,14 +49,15 @@ object TestDatabase {
|
||||
}
|
||||
}
|
||||
accountData = (0..20).toList().mapIndexed { idx, _ ->
|
||||
AccountProfile(
|
||||
AccountProfileEntity(
|
||||
id = idx,
|
||||
followerCount = 0,
|
||||
followingCount = 0,
|
||||
nickName = faker.name.name(),
|
||||
avatar = imageList.random(),
|
||||
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) {
|
||||
commentIdCounter += 1
|
||||
val commentPerson = accountData.random()
|
||||
var newComment = Comment(
|
||||
var newCommentEntity = CommentEntity(
|
||||
name = commentPerson.nickName,
|
||||
comment = "this is comment ${commentIdCounter}",
|
||||
date = "2023-02-02 11:23",
|
||||
@@ -92,7 +94,7 @@ object TestDatabase {
|
||||
replies = emptyList(),
|
||||
postId = momentIdCounter,
|
||||
avatar = commentPerson.avatar,
|
||||
author = commentPerson.id,
|
||||
author = commentPerson.id.toLong(),
|
||||
id = commentIdCounter,
|
||||
liked = false
|
||||
)
|
||||
@@ -100,16 +102,16 @@ object TestDatabase {
|
||||
for (likeIdx in 0..faker.random.nextInt(0, 5)) {
|
||||
val likePerson = accountData.random()
|
||||
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)
|
||||
for (i in 0..likeCount) {
|
||||
val likePerson = accountData.random()
|
||||
likeMomentList += Pair(momentIdCounter, likePerson.id)
|
||||
}
|
||||
MomentItem(
|
||||
MomentEntity(
|
||||
id = momentIdCounter,
|
||||
avatar = person.avatar,
|
||||
nickname = person.nickName,
|
||||
@@ -129,10 +131,10 @@ object TestDatabase {
|
||||
}
|
||||
|
||||
|
||||
fun updateMomentById(id: Int, momentItem: MomentItem) {
|
||||
fun updateMomentById(id: Int, momentEntity: MomentEntity) {
|
||||
momentData = momentData.map {
|
||||
if (it.id == id) {
|
||||
momentItem
|
||||
momentEntity
|
||||
} else {
|
||||
it
|
||||
}
|
||||
|
||||
@@ -159,7 +159,11 @@ fun NavigationController(
|
||||
route = NavigationRoute.AccountProfile.route,
|
||||
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) {
|
||||
SignupScreen()
|
||||
|
||||
@@ -31,7 +31,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.TestAccountServiceImpl
|
||||
import com.aiosman.riderpro.data.TestUserServiceImpl
|
||||
@@ -47,7 +47,7 @@ fun AccountEditScreen() {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var bio by remember { mutableStateOf("") }
|
||||
var profile by remember {
|
||||
mutableStateOf<AccountProfile?>(
|
||||
mutableStateOf<AccountProfileEntity?>(
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import androidx.paging.cachedIn
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.riderpro.ui.post.CommentsSection
|
||||
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.CommentRemoteDataSource
|
||||
import com.aiosman.riderpro.data.CommentService
|
||||
@@ -57,10 +57,10 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CommentModalViewModel(
|
||||
postId: Int?
|
||||
val postId: Int?
|
||||
) : ViewModel() {
|
||||
val commentService: CommentService = TestCommentServiceImpl()
|
||||
val commentsFlow: Flow<PagingData<Comment>> = Pager(
|
||||
val commentsFlow: Flow<PagingData<CommentEntity>> = Pager(
|
||||
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
CommentPagingSource(
|
||||
@@ -69,6 +69,12 @@ class CommentModalViewModel(
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope)
|
||||
|
||||
suspend fun createComment(content: String) {
|
||||
postId?.let {
|
||||
commentService.createComment(postId, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -104,8 +110,7 @@ fun CommentModalContent(
|
||||
var commentText by remember { mutableStateOf("") }
|
||||
suspend fun sendComment() {
|
||||
if (commentText.isNotEmpty()) {
|
||||
model.commentService.createComment(postId!!, commentText, 1)
|
||||
commentText = ""
|
||||
model.createComment(commentText)
|
||||
}
|
||||
comments.refresh()
|
||||
onCommentAdded()
|
||||
@@ -136,13 +141,16 @@ fun CommentModalContent(
|
||||
.padding(horizontal = 16.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
CommentsSection(lazyPagingItems = comments, onLike = { comment: Comment ->
|
||||
CommentsSection(lazyPagingItems = comments, onLike = { commentEntity: CommentEntity ->
|
||||
scope.launch {
|
||||
model.commentService.likeComment(comment.id)
|
||||
if (commentEntity.liked) {
|
||||
model.commentService.dislikeComment(commentEntity.id)
|
||||
} else {
|
||||
model.commentService.likeComment(commentEntity.id)
|
||||
}
|
||||
comments.refresh()
|
||||
}
|
||||
}) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,19 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun RelPostCard(
|
||||
momentItem: MomentItem,
|
||||
momentEntity: MomentEntity,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val image = momentItem.images.firstOrNull()
|
||||
val image = momentEntity.images.firstOrNull()
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
MomentTopRowGroup(momentItem = momentItem)
|
||||
MomentTopRowGroup(momentEntity = momentEntity)
|
||||
Box(
|
||||
modifier=Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
|
||||
@@ -53,18 +53,20 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import coil.compose.AsyncImage
|
||||
import com.aiosman.riderpro.LocalAnimatedContentScope
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.LocalSharedTransitionScope
|
||||
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.comment.CommentModalContent
|
||||
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.RelPostCard
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@@ -88,10 +90,12 @@ fun MomentsList() {
|
||||
}
|
||||
|
||||
Box(Modifier.pullRefresh(state)) {
|
||||
LazyColumn {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(moments.itemCount) { idx ->
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
MomentCard(momentItem = momentItem,
|
||||
MomentCard(momentEntity = momentItem,
|
||||
onAddComment = {
|
||||
scope.launch {
|
||||
model.onAddComment(momentItem.id)
|
||||
@@ -105,6 +109,15 @@ fun MomentsList() {
|
||||
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
|
||||
fun MomentCard(
|
||||
momentItem: MomentItem,
|
||||
momentEntity: MomentEntity,
|
||||
onLikeClick: () -> Unit,
|
||||
onFavoriteClick: () -> Unit = {},
|
||||
onAddComment: () -> Unit = {}
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
MomentTopRowGroup(momentItem = momentItem)
|
||||
MomentTopRowGroup(momentEntity = momentEntity)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.noRippleClickable {
|
||||
navController.navigate("Post/${momentItem.id}")
|
||||
navController.navigate("Post/${momentEntity.id}")
|
||||
}
|
||||
) {
|
||||
MomentContentGroup(momentItem = momentItem)
|
||||
MomentContentGroup(momentEntity = momentEntity)
|
||||
}
|
||||
val momentOperateBtnBoxModifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(1f)
|
||||
ModificationListHeader()
|
||||
// ModificationListHeader()
|
||||
MomentBottomOperateRowGroup(
|
||||
momentOperateBtnBoxModifier,
|
||||
momentItem = momentItem,
|
||||
momentEntity = momentEntity,
|
||||
onLikeClick = onLikeClick,
|
||||
onAddComment = onAddComment,
|
||||
onShareClick = {
|
||||
NewPostViewModel.asNewPost()
|
||||
NewPostViewModel.relPostId = momentItem.id
|
||||
NewPostViewModel.relPostId = momentEntity.id
|
||||
navController.navigate(NavigationRoute.NewPost.route)
|
||||
}
|
||||
},
|
||||
onFavoriteClick = onFavoriteClick
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -248,7 +263,7 @@ fun MomentPostTime(time: String) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentTopRowGroup(momentItem: MomentItem) {
|
||||
fun MomentTopRowGroup(momentEntity: MomentEntity) {
|
||||
val navController = LocalNavController.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -256,7 +271,7 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
|
||||
.padding(top = 0.dp, bottom = 0.dp, start = 24.dp, end = 24.dp)
|
||||
) {
|
||||
AsyncImage(
|
||||
momentItem.avatar,
|
||||
momentEntity.avatar,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
@@ -264,7 +279,7 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
momentItem.authorId.toString()
|
||||
momentEntity.authorId.toString()
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -281,7 +296,7 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
|
||||
.height(22.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MomentName(momentItem.nickname)
|
||||
MomentName(momentEntity.nickname)
|
||||
MomentFollowBtn()
|
||||
}
|
||||
Row(
|
||||
@@ -290,8 +305,8 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
|
||||
.height(21.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MomentPostLocation(momentItem.location)
|
||||
MomentPostTime(momentItem.time)
|
||||
MomentPostLocation(momentEntity.location)
|
||||
MomentPostTime(momentEntity.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,28 +315,40 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
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 = "${momentItem.id} ${momentItem.momentTextContent}",
|
||||
text = "${momentEntity.momentTextContent}",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 22.dp, bottom = 16.dp, start = 24.dp, end = 24.dp),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (momentItem.relMoment != null) {
|
||||
RelPostCard(momentItem = momentItem.relMoment!!, modifier = Modifier.background(Color(0xFFF8F8F8)))
|
||||
}else{
|
||||
if (momentEntity.relMoment != null) {
|
||||
RelPostCard(
|
||||
momentEntity = momentEntity.relMoment!!,
|
||||
modifier = Modifier.background(Color(0xFFF8F8F8))
|
||||
)
|
||||
} else {
|
||||
displayImageUrl?.let {
|
||||
AsyncImage(
|
||||
it,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = ""
|
||||
)
|
||||
with(sharedTransitionScope) {
|
||||
AsyncImage(
|
||||
it,
|
||||
modifier = Modifier
|
||||
.sharedElement(
|
||||
rememberSharedContentState(key = it),
|
||||
animatedVisibilityScope = animatedVisibilityScope
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,8 +393,9 @@ fun MomentBottomOperateRowGroup(
|
||||
modifier: Modifier,
|
||||
onLikeClick: () -> Unit = {},
|
||||
onAddComment: () -> Unit = {},
|
||||
onFavoriteClick: () -> Unit = {},
|
||||
onShareClick: () -> Unit = {},
|
||||
momentItem: MomentItem
|
||||
momentEntity: MomentEntity
|
||||
) {
|
||||
var systemUiController = rememberSystemUiController()
|
||||
var showCommentModal by remember { mutableStateOf(false) }
|
||||
@@ -380,7 +408,7 @@ fun MomentBottomOperateRowGroup(
|
||||
)
|
||||
) {
|
||||
systemUiController.setNavigationBarColor(Color(0xfff7f7f7))
|
||||
CommentModalContent(postId = momentItem.id, onCommentAdded = {
|
||||
CommentModalContent(postId = momentEntity.id, onCommentAdded = {
|
||||
showCommentModal = false
|
||||
onAddComment()
|
||||
}) {
|
||||
@@ -397,10 +425,10 @@ fun MomentBottomOperateRowGroup(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MomentOperateBtn(count = momentItem.likeCount.toString()) {
|
||||
MomentOperateBtn(count = momentEntity.likeCount.toString()) {
|
||||
AnimatedLikeIcon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
liked = momentItem.liked
|
||||
liked = momentEntity.liked
|
||||
) {
|
||||
onLikeClick()
|
||||
}
|
||||
@@ -417,28 +445,34 @@ fun MomentBottomOperateRowGroup(
|
||||
) {
|
||||
MomentOperateBtn(
|
||||
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(
|
||||
modifier = modifier.noRippleClickable {
|
||||
onShareClick()
|
||||
onFavoriteClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MomentOperateBtn(
|
||||
icon = R.drawable.rider_pro_share,
|
||||
count = momentItem.shareCount.toString()
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MomentOperateBtn(
|
||||
icon = R.drawable.rider_pro_favoriate,
|
||||
count = momentItem.favoriteCount.toString()
|
||||
)
|
||||
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
|
||||
AnimatedFavouriteIcon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
isFavourite = momentEntity.isFavorite
|
||||
) {
|
||||
onFavoriteClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.aiosman.riderpro.data.MomentRemoteDataSource
|
||||
import com.aiosman.riderpro.data.MomentService
|
||||
import com.aiosman.riderpro.data.TestAccountServiceImpl
|
||||
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.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
object MomentViewModel : ViewModel() {
|
||||
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 accountService: AccountService = TestAccountServiceImpl()
|
||||
init {
|
||||
@@ -91,6 +91,7 @@ object MomentViewModel : ViewModel() {
|
||||
updateCommentCount(id)
|
||||
}
|
||||
|
||||
|
||||
fun updateDislikeMomentById(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
@@ -108,4 +109,34 @@ object MomentViewModel : ViewModel() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -3,27 +3,25 @@ package com.aiosman.riderpro.ui.index.tabs.profile
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.navigation.NavController
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.aiosman.riderpro.AppStore
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.data.AccountProfile
|
||||
import com.aiosman.riderpro.data.AccountProfileEntity
|
||||
import com.aiosman.riderpro.data.AccountService
|
||||
import com.aiosman.riderpro.data.TestAccountServiceImpl
|
||||
import com.aiosman.riderpro.data.MomentPagingSource
|
||||
import com.aiosman.riderpro.data.MomentRemoteDataSource
|
||||
import com.aiosman.riderpro.data.TestMomentServiceImpl
|
||||
import com.aiosman.riderpro.data.TestUserServiceImpl
|
||||
import com.aiosman.riderpro.model.MomentItem
|
||||
import com.aiosman.riderpro.model.MomentEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
object MyProfileViewModel {
|
||||
val service: AccountService = TestAccountServiceImpl()
|
||||
val userService = TestUserServiceImpl()
|
||||
var profile by mutableStateOf<AccountProfile?>(null)
|
||||
var momentsFlow by mutableStateOf<Flow<PagingData<MomentItem>>?>(null)
|
||||
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||
var momentsFlow by mutableStateOf<Flow<PagingData<MomentEntity>>?>(null)
|
||||
suspend fun loadProfile() {
|
||||
profile = service.getMyAccountProfile()
|
||||
momentsFlow = Pager(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.aiosman.riderpro.ui.index.tabs.profile
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -46,10 +47,12 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import coil.compose.AsyncImage
|
||||
import com.aiosman.riderpro.LocalAnimatedContentScope
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.LocalSharedTransitionScope
|
||||
import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.data.AccountProfile
|
||||
import com.aiosman.riderpro.model.MomentItem
|
||||
import com.aiosman.riderpro.data.AccountProfileEntity
|
||||
import com.aiosman.riderpro.model.MomentEntity
|
||||
import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -119,9 +122,8 @@ fun ProfilePage() {
|
||||
}
|
||||
CarGroup()
|
||||
model.profile?.let {
|
||||
UserInformation(accountProfile = it)
|
||||
UserInformation(accountProfileEntity = it)
|
||||
}
|
||||
|
||||
RidingStyle()
|
||||
}
|
||||
moments?.let {
|
||||
@@ -184,7 +186,11 @@ fun CarTopPicture() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserInformation(isSelf: Boolean = true, accountProfile: AccountProfile) {
|
||||
fun UserInformation(
|
||||
isSelf: Boolean = true,
|
||||
accountProfileEntity: AccountProfileEntity,
|
||||
onFollowClick: () -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -193,21 +199,25 @@ fun UserInformation(isSelf: Boolean = true, accountProfile: AccountProfile) {
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
val userInfoModifier = Modifier.weight(1f)
|
||||
UserInformationFollowers(userInfoModifier, accountProfile)
|
||||
UserInformationBasic(userInfoModifier, accountProfile)
|
||||
UserInformationFollowing(userInfoModifier, accountProfile)
|
||||
UserInformationFollowers(userInfoModifier, accountProfileEntity)
|
||||
UserInformationBasic(userInfoModifier, accountProfileEntity)
|
||||
UserInformationFollowing(userInfoModifier, accountProfileEntity)
|
||||
}
|
||||
UserInformationSlogan()
|
||||
CommunicationOperatorGroup(isSelf = isSelf)
|
||||
CommunicationOperatorGroup(
|
||||
isSelf = isSelf,
|
||||
isFollowing = accountProfileEntity.isFollowing,
|
||||
onFollowClick = onFollowClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserInformationFollowers(modifier: Modifier, accountProfile: AccountProfile) {
|
||||
fun UserInformationFollowers(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
|
||||
Column(modifier = modifier.padding(top = 31.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 5.dp),
|
||||
text = accountProfile.followerCount.toString(),
|
||||
text = accountProfileEntity.followerCount.toString(),
|
||||
fontSize = 24.sp,
|
||||
color = Color.Black,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||
@@ -229,7 +239,7 @@ fun UserInformationFollowers(modifier: Modifier, accountProfile: AccountProfile)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
|
||||
fun UserInformationBasic(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
@@ -242,7 +252,7 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
|
||||
painter = painterResource(id = R.drawable.avatar_bold), contentDescription = ""
|
||||
)
|
||||
AsyncImage(
|
||||
accountProfile.avatar,
|
||||
accountProfileEntity.avatar,
|
||||
modifier = Modifier
|
||||
.size(width = 88.dp, height = 88.dp)
|
||||
.clip(
|
||||
@@ -257,7 +267,7 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
|
||||
modifier = Modifier
|
||||
.widthIn(max = 220.dp)
|
||||
.padding(top = 8.dp),
|
||||
text = accountProfile.nickName,
|
||||
text = accountProfileEntity.nickName,
|
||||
fontSize = 32.sp,
|
||||
color = Color.Black,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
@@ -265,7 +275,7 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = accountProfile.country,
|
||||
text = accountProfileEntity.country,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
@@ -279,14 +289,14 @@ fun UserInformationBasic(modifier: Modifier, accountProfile: AccountProfile) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserInformationFollowing(modifier: Modifier, accountProfile: AccountProfile) {
|
||||
fun UserInformationFollowing(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
|
||||
Column(
|
||||
modifier = modifier.padding(top = 6.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 5.dp),
|
||||
text = accountProfile.followingCount.toString(),
|
||||
text = accountProfileEntity.followingCount.toString(),
|
||||
fontSize = 24.sp,
|
||||
color = Color.Black,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||
@@ -320,7 +330,11 @@ fun UserInformationSlogan() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CommunicationOperatorGroup(isSelf: Boolean = true) {
|
||||
fun CommunicationOperatorGroup(
|
||||
isSelf: Boolean = true,
|
||||
isFollowing: Boolean = false,
|
||||
onFollowClick: () -> Unit
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -329,7 +343,11 @@ fun CommunicationOperatorGroup(isSelf: Boolean = true) {
|
||||
) {
|
||||
if (!isSelf) {
|
||||
Box(
|
||||
modifier = Modifier.size(width = 142.dp, height = 40.dp),
|
||||
modifier = Modifier
|
||||
.size(width = 142.dp, height = 40.dp)
|
||||
.noRippleClickable {
|
||||
onFollowClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
@@ -338,7 +356,7 @@ fun CommunicationOperatorGroup(isSelf: Boolean = true) {
|
||||
contentDescription = ""
|
||||
)
|
||||
Text(
|
||||
text = "FOLLOW",
|
||||
text = if (isFollowing) "FOLLOWING" else "FOLLOW",
|
||||
fontSize = 16.sp,
|
||||
color = Color.White,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||
@@ -472,13 +490,14 @@ fun UserMoment(scope: LazyListScope) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentPostUnit(momentItem: MomentItem) {
|
||||
TimeGroup(momentItem.time)
|
||||
MomentCard(
|
||||
momentItem.momentTextContent,
|
||||
momentItem.images[0],
|
||||
momentItem.likeCount.toString(),
|
||||
momentItem.commentCount.toString()
|
||||
fun MomentPostUnit(momentEntity: MomentEntity) {
|
||||
TimeGroup(momentEntity.time)
|
||||
ProfileMomentCard(
|
||||
momentEntity.momentTextContent,
|
||||
momentEntity.images[0],
|
||||
momentEntity.likeCount.toString(),
|
||||
momentEntity.commentCount.toString(),
|
||||
momentId = momentEntity.id
|
||||
)
|
||||
}
|
||||
|
||||
@@ -506,7 +525,13 @@ fun TimeGroup(time: String = "2024.06.08 12:23") {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentCard(content: String, imageUrl: String, like: String, comment: String) {
|
||||
fun ProfileMomentCard(
|
||||
content: String,
|
||||
imageUrl: String,
|
||||
like: String,
|
||||
comment: String,
|
||||
momentId: Int
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.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))
|
||||
) {
|
||||
MomentCardTopContent(content)
|
||||
MomentCardPicture(imageUrl)
|
||||
MomentCardPicture(imageUrl, momentId)
|
||||
MomentCardOperation(like, comment)
|
||||
}
|
||||
}
|
||||
@@ -534,16 +559,35 @@ fun MomentCardTopContent(content: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun MomentCardPicture(imageUrl: String) {
|
||||
AsyncImage(
|
||||
imageUrl,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
fun MomentCardPicture(imageUrl: String, momentId: Int) {
|
||||
val navController = LocalNavController.current
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
val animatedVisibilityScope = LocalAnimatedContentScope.current
|
||||
with(sharedTransitionScope) {
|
||||
AsyncImage(
|
||||
imageUrl,
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aiosman.riderpro.ui.login
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -15,23 +16,106 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.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.composables.StatusBarMaskLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun EmailSignupScreen() {
|
||||
var email by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var rememberMe by remember { mutableStateOf(false) }
|
||||
var acceptTerms 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 {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -74,9 +158,9 @@ fun EmailSignupScreen() {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
text = password,
|
||||
text = confirmPassword,
|
||||
onValueChange = {
|
||||
password = it
|
||||
confirmPassword = it
|
||||
},
|
||||
password = true,
|
||||
label = "Confirm password",
|
||||
@@ -149,7 +233,11 @@ fun EmailSignupScreen() {
|
||||
.height(48.dp),
|
||||
text = "LET'S RIDE".uppercase(),
|
||||
backgroundImage = R.mipmap.rider_pro_signup_red_bg
|
||||
)
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
registerUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aiosman.riderpro.ui.login
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
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.R
|
||||
import com.aiosman.riderpro.data.AccountService
|
||||
import com.aiosman.riderpro.data.ServiceException
|
||||
import com.aiosman.riderpro.data.TestAccountServiceImpl
|
||||
import com.aiosman.riderpro.data.TestUserServiceImpl
|
||||
import com.aiosman.riderpro.data.UserService
|
||||
@@ -57,18 +60,24 @@ fun UserAuthScreen() {
|
||||
var accountService: AccountService = TestAccountServiceImpl()
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
fun onLogin() {
|
||||
scope.launch {
|
||||
val authResp = accountService.loginUserWithPassword(email, password)
|
||||
if (authResp.token != null) {
|
||||
AppStore.apply {
|
||||
token = authResp.token
|
||||
this.rememberMe = rememberMe
|
||||
saveData()
|
||||
}
|
||||
navController.navigate(NavigationRoute.Index.route) {
|
||||
popUpTo(NavigationRoute.Login.route) { inclusive = true }
|
||||
try {
|
||||
val authResp = accountService.loginUserWithPassword(email, password)
|
||||
if (authResp.token != null) {
|
||||
AppStore.apply {
|
||||
token = authResp.token
|
||||
this.rememberMe = rememberMe
|
||||
saveData()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.aiosman.riderpro.ui.post
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -63,6 +64,7 @@ fun NewPostScreen() {
|
||||
val model = NewPostViewModel
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setNavigationBarColor(color = Color.Transparent)
|
||||
model.init()
|
||||
@@ -76,7 +78,7 @@ fun NewPostScreen() {
|
||||
) {
|
||||
NewPostTopBar {
|
||||
model.viewModelScope.launch {
|
||||
model.createMoment()
|
||||
model.createMoment(context = context)
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
@@ -93,7 +95,7 @@ fun NewPostScreen() {
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color = Color(0xFFEEEEEE)).padding(24.dp)
|
||||
) {
|
||||
RelPostCard(
|
||||
momentItem = it,
|
||||
momentEntity = it,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -177,7 +179,9 @@ fun AddImageGrid() {
|
||||
val uri = result.data?.data
|
||||
if (uri != null) {
|
||||
model.imageUriList += uri.toString()
|
||||
// get filename and extension
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
val stroke = Stroke(
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.riderpro.data.MomentService
|
||||
import com.aiosman.riderpro.data.TestMomentServiceImpl
|
||||
import com.aiosman.riderpro.model.MomentItem
|
||||
import com.aiosman.riderpro.data.UploadImage
|
||||
import com.aiosman.riderpro.model.MomentEntity
|
||||
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() {
|
||||
@@ -20,7 +26,7 @@ object NewPostViewModel : ViewModel() {
|
||||
var modificationList by mutableStateOf<List<Modification>>(listOf())
|
||||
var imageUriList by mutableStateOf(listOf<String>())
|
||||
var relPostId by mutableStateOf<Int?>(null)
|
||||
var relMoment by mutableStateOf<MomentItem?>(null)
|
||||
var relMoment by mutableStateOf<MomentEntity?>(null)
|
||||
fun asNewPost() {
|
||||
textContent = ""
|
||||
searchPlaceAddressResult = null
|
||||
@@ -29,16 +35,39 @@ object NewPostViewModel : ViewModel() {
|
||||
relPostId = null
|
||||
}
|
||||
|
||||
suspend fun createMoment() {
|
||||
momentService.createMoment(
|
||||
content = textContent,
|
||||
authorId = 1,
|
||||
imageUriList = imageUriList,
|
||||
relPostId = relPostId
|
||||
)
|
||||
suspend fun uriToFile(context: Context, uri: Uri): File {
|
||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||
val tempFile = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("temp", null, context.cacheDir)
|
||||
}
|
||||
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 {
|
||||
val moment = momentService.getMomentById(it)
|
||||
relMoment = moment
|
||||
|
||||
@@ -74,9 +74,9 @@ import com.aiosman.riderpro.LocalAnimatedContentScope
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.LocalSharedTransitionScope
|
||||
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.Comment
|
||||
import com.aiosman.riderpro.data.CommentEntity
|
||||
import com.aiosman.riderpro.data.CommentPagingSource
|
||||
import com.aiosman.riderpro.data.CommentRemoteDataSource
|
||||
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.TestAccountServiceImpl
|
||||
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.composables.StatusBarMaskLayout
|
||||
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
|
||||
@@ -103,7 +103,7 @@ class PostViewModel(
|
||||
) : ViewModel() {
|
||||
var service: MomentService = TestMomentServiceImpl()
|
||||
var commentService: CommentService = TestCommentServiceImpl()
|
||||
private var _commentsFlow = MutableStateFlow<PagingData<Comment>>(PagingData.empty())
|
||||
private var _commentsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
|
||||
val commentsFlow = _commentsFlow.asStateFlow()
|
||||
|
||||
init {
|
||||
@@ -120,17 +120,16 @@ class PostViewModel(
|
||||
_commentsFlow.value = it
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var accountProfile by mutableStateOf<AccountProfile?>(null)
|
||||
var moment by mutableStateOf<MomentItem?>(null)
|
||||
var accountProfileEntity by mutableStateOf<AccountProfileEntity?>(null)
|
||||
var moment by mutableStateOf<MomentEntity?>(null)
|
||||
var accountService: AccountService = TestAccountServiceImpl()
|
||||
|
||||
suspend fun initData() {
|
||||
moment = service.getMomentById(postId.toInt())
|
||||
moment?.let {
|
||||
accountProfile = accountService.getAccountProfileById(it.authorId)
|
||||
accountProfileEntity = accountService.getAccountProfileById(it.authorId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +146,21 @@ class PostViewModel(
|
||||
_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) {
|
||||
commentService.createComment(postId.toInt(), content, 1)
|
||||
commentService.createComment(postId.toInt(), content)
|
||||
MomentViewModel.updateCommentCount(postId.toInt())
|
||||
}
|
||||
|
||||
@@ -212,7 +224,7 @@ fun PostScreen(
|
||||
commentsPagging.refresh()
|
||||
}
|
||||
},
|
||||
momentItem = viewModel.moment
|
||||
momentEntity = viewModel.moment
|
||||
)
|
||||
}
|
||||
) {
|
||||
@@ -221,7 +233,11 @@ fun PostScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Header(viewModel.accountProfile)
|
||||
Header(
|
||||
avatar = viewModel.moment?.avatar,
|
||||
nickname = viewModel.moment?.nickname,
|
||||
userId = viewModel.moment?.authorId
|
||||
)
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
AnimatedVisibility(visible = showCollapseContent) {
|
||||
// collapse content
|
||||
@@ -256,9 +272,13 @@ fun PostScreen(
|
||||
CommentsSection(
|
||||
lazyPagingItems = commentsPagging,
|
||||
scrollState,
|
||||
onLike = { comment: Comment ->
|
||||
onLike = { commentEntity: CommentEntity ->
|
||||
scope.launch {
|
||||
viewModel.likeComment(comment.id)
|
||||
if (commentEntity.liked) {
|
||||
viewModel.unlikeComment(commentEntity.id)
|
||||
} else {
|
||||
viewModel.likeComment(commentEntity.id)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
showCollapseContent = it
|
||||
@@ -270,7 +290,7 @@ fun PostScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Header(accountProfile: AccountProfile?) {
|
||||
fun Header(avatar: String?, nickname: String?, userId: Int?) {
|
||||
val navController = LocalNavController.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -288,30 +308,30 @@ fun Header(accountProfile: AccountProfile?) {
|
||||
.size(32.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
accountProfile?.let {
|
||||
avatar?.let {
|
||||
AsyncImage(
|
||||
accountProfile.avatar,
|
||||
it,
|
||||
contentDescription = "Profile Picture",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
accountProfile.id.toString()
|
||||
userId?.let {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
userId.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
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(
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
@@ -350,7 +370,7 @@ fun PostImageView(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth().background(Color.Black),
|
||||
.fillMaxWidth(),
|
||||
) { page ->
|
||||
val image = images[page]
|
||||
with(sharedTransitionScope) {
|
||||
@@ -407,7 +427,7 @@ fun PostImageView(
|
||||
@Composable
|
||||
fun PostDetails(
|
||||
postId: String,
|
||||
momentItem: MomentItem?
|
||||
momentEntity: MomentEntity?
|
||||
) {
|
||||
|
||||
Column(
|
||||
@@ -418,22 +438,22 @@ fun PostDetails(
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = momentItem?.momentTextContent ?: "",
|
||||
text = momentEntity?.momentTextContent ?: "",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(text = "12-11 发布")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = "${momentItem?.commentCount ?: 0} Comments")
|
||||
Text(text = "${momentEntity?.commentCount ?: 0} Comments")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CommentsSection(
|
||||
lazyPagingItems: LazyPagingItems<Comment>,
|
||||
lazyPagingItems: LazyPagingItems<CommentEntity>,
|
||||
scrollState: LazyListState = rememberLazyListState(),
|
||||
onLike: (Comment) -> Unit,
|
||||
onLike: (CommentEntity) -> Unit,
|
||||
onWillCollapse: (Boolean) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
@@ -463,11 +483,11 @@ fun CommentsSection(
|
||||
|
||||
|
||||
@Composable
|
||||
fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
|
||||
fun CommentItem(commentEntity: CommentEntity, onLike: () -> Unit = {}) {
|
||||
Column {
|
||||
Row(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
AsyncImage(
|
||||
comment.avatar,
|
||||
commentEntity.avatar,
|
||||
contentDescription = "Comment Profile Picture",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
@@ -476,9 +496,9 @@ fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column {
|
||||
Text(text = comment.name, fontWeight = FontWeight.Bold)
|
||||
Text(text = comment.comment)
|
||||
Text(text = comment.date, fontSize = 12.sp, color = Color.Gray)
|
||||
Text(text = commentEntity.name, fontWeight = FontWeight.Bold)
|
||||
Text(text = commentEntity.comment)
|
||||
Text(text = commentEntity.date, fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -488,17 +508,17 @@ fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
|
||||
Icon(
|
||||
Icons.Filled.Favorite,
|
||||
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))
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
) {
|
||||
comment.replies.forEach { reply ->
|
||||
commentEntity.replies.forEach { reply ->
|
||||
CommentItem(reply)
|
||||
}
|
||||
}
|
||||
@@ -510,7 +530,7 @@ fun CommentItem(comment: Comment, onLike: () -> Unit = {}) {
|
||||
fun BottomNavigationBar(
|
||||
onCreateComment: (String) -> Unit = {},
|
||||
onLikeClick: () -> Unit = {},
|
||||
momentItem: MomentItem?
|
||||
momentEntity: MomentEntity?
|
||||
) {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
var showCommentModal by remember { mutableStateOf(false) }
|
||||
@@ -568,10 +588,10 @@ fun BottomNavigationBar(
|
||||
Icon(
|
||||
Icons.Filled.Favorite,
|
||||
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(
|
||||
onClick = { /*TODO*/ }) {
|
||||
Icon(Icons.Filled.Star, contentDescription = "Send")
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -17,13 +18,13 @@ import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
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.MomentRemoteDataSource
|
||||
import com.aiosman.riderpro.data.TestMomentServiceImpl
|
||||
import com.aiosman.riderpro.data.TestUserServiceImpl
|
||||
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.index.tabs.profile.CarGroup
|
||||
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.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AccountProfile(id:String) {
|
||||
// val model = MyProfileViewModel
|
||||
val userService: UserService = TestUserServiceImpl()
|
||||
var userProfile by remember { mutableStateOf<AccountProfile?>(null) }
|
||||
var userProfile by remember { mutableStateOf<AccountProfileEntity?>(null) }
|
||||
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) {
|
||||
userProfile = userService.getUserProfile(id)
|
||||
momentsFlow = Pager(
|
||||
@@ -73,7 +74,21 @@ fun AccountProfile(id:String) {
|
||||
item {
|
||||
CarGroup()
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.jetbrains.kotlin.android) apply false
|
||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user