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