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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.aiosman.riderpro.data.AccountProfile
import com.aiosman.riderpro.data.AccountProfileEntity
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.TestAccountServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl
@@ -47,7 +47,7 @@ fun AccountEditScreen() {
var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") }
var profile by remember {
mutableStateOf<AccountProfile?>(
mutableStateOf<AccountProfileEntity?>(
null
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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