This commit is contained in:
2024-07-31 14:50:55 +08:00
parent 2c79195f44
commit b17ac76005
63 changed files with 344 additions and 42 deletions

View File

@@ -31,6 +31,8 @@ import androidx.compose.ui.unit.dp
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.TestAccountServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.ui.Navigation
@@ -49,8 +51,8 @@ class MainActivity : ComponentActivity() {
if (!AppStore.rememberMe) {
return
}
val userService: UserService = TestUserServiceImpl()
userService.getMyAccount()
val accountService: AccountService = TestAccountServiceImpl()
accountService.getMyAccount()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -15,14 +15,50 @@ data class AccountProfile(
interface AccountService {
suspend fun getMyAccountProfile(): AccountProfile
suspend fun getAccountProfileById(id: Int): AccountProfile
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)
}
class TestAccountServiceImpl : AccountService {
override suspend fun getMyAccountProfile(): AccountProfile {
return TestDatabase.accountData.first { it.id == 0 }
return TestDatabase.accountData.first { it.id == 1 }
}
override suspend fun getAccountProfileById(id: Int): AccountProfile {
return TestDatabase.accountData.first { it.id == id }
}
override suspend fun getMyAccount(): UserAuth {
return UserAuth(1)
}
override suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth {
return UserAuth(1, "token")
}
override suspend fun logout() {
// do nothing
}
override suspend fun updateAvatar(uri: String) {
TestDatabase.accountData = TestDatabase.accountData.map {
if (it.id == 1) {
it.copy(avatar = uri)
} else {
it
}
}
}
override suspend fun updateProfile(nickName: String, bio: String) {
TestDatabase.accountData = TestDatabase.accountData.map {
if (it.id == 1) {
it.copy(nickName = nickName, bio = bio)
} else {
it
}
}
}
}

View File

@@ -21,7 +21,8 @@ interface MomentService {
suspend fun createMoment(
content: String,
authorId: Int,
imageUriList: List<String>
imageUriList: List<String>,
relPostId: Int? = null
): MomentItem
}
@@ -96,9 +97,10 @@ class TestMomentServiceImpl() : MomentService {
override suspend fun createMoment(
content: String,
authorId: Int,
imageUriList: List<String>
imageUriList: List<String>,
relPostId: Int?
): MomentItem {
return testMomentBackend.createMoment(content, authorId, imageUriList)
return testMomentBackend.createMoment(content, authorId, imageUriList, relPostId)
}
}
@@ -140,6 +142,9 @@ class TestMomentBackend(
if (myLikeIdList.contains(it.id)) {
it.liked = true
}
if (it.relPostId != null) {
it.relMoment = rawList.first { it1 -> it1.id == it.relPostId }
}
}
// delay
@@ -186,7 +191,8 @@ class TestMomentBackend(
suspend fun createMoment(
content: String,
authorId: Int,
imageUriList: List<String>
imageUriList: List<String>,
relPostId: Int?
): MomentItem {
TestDatabase.momentIdCounter += 1
val person = TestDatabase.accountData.first {
@@ -206,7 +212,8 @@ class TestMomentBackend(
shareCount = 0,
favoriteCount = 0,
images = imageUriList,
authorId = person.id
authorId = person.id,
relPostId = relPostId
)
TestDatabase.momentData += newMoment
return newMoment

View File

@@ -9,9 +9,7 @@ data class UserAuth(
interface UserService {
suspend fun getUserProfile(id: String): AccountProfile
suspend fun getMyAccount(): UserAuth
suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth
suspend fun logout()
}
class TestUserServiceImpl : UserService {
@@ -23,17 +21,4 @@ class TestUserServiceImpl : UserService {
}
return AccountProfile(0, 0, 0, "", "", "", "")
}
override suspend fun getMyAccount(): UserAuth {
return UserAuth(1)
}
override suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth {
return UserAuth(1, "token")
}
override suspend fun logout() {
// do nothing
}
}

View File

@@ -19,4 +19,6 @@ data class MomentItem(
val images: List<String> = emptyList(),
val authorId: Int = 0,
var liked: Boolean = false,
var relPostId: Int? = null,
var relMoment: MomentItem? = null
)

View File

@@ -21,6 +21,7 @@ import androidx.navigation.navArgument
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.ui.account.AccountEditScreen
import com.aiosman.riderpro.ui.comment.CommentsScreen
import com.aiosman.riderpro.ui.follower.FollowerScreen
import com.aiosman.riderpro.ui.gallery.OfficialGalleryScreen
@@ -60,11 +61,15 @@ sealed class NavigationRoute(
data object SignUp : NavigationRoute("SignUp")
data object UserAuth : NavigationRoute("UserAuth")
data object EmailSignUp : NavigationRoute("EmailSignUp")
data object AccountEdit : NavigationRoute("AccountEditScreen")
}
@Composable
fun NavigationController(navController: NavHostController,startDestination: String = NavigationRoute.Login.route) {
fun NavigationController(
navController: NavHostController,
startDestination: String = NavigationRoute.Login.route
) {
val navigationBarHeight = with(LocalDensity.current) {
WindowInsets.navigationBars.getBottom(this).toDp()
}
@@ -163,6 +168,9 @@ fun NavigationController(navController: NavHostController,startDestination: Stri
composable(route = NavigationRoute.EmailSignUp.route) {
EmailSignupScreen()
}
composable(route = NavigationRoute.AccountEdit.route) {
AccountEditScreen()
}
}
@@ -178,7 +186,10 @@ fun Navigation(startDestination: String = NavigationRoute.Login.route) {
LocalSharedTransitionScope provides this@SharedTransitionLayout,
) {
Box {
NavigationController(navController = navController,startDestination = startDestination)
NavigationController(
navController = navController,
startDestination = startDestination
)
}
}
}

View File

@@ -0,0 +1,166 @@
package com.aiosman.riderpro.ui.account
import android.app.Activity
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
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
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.AccountService
import com.aiosman.riderpro.data.TestAccountServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountEditScreen() {
val userService: UserService = TestUserServiceImpl()
val accountService: AccountService = TestAccountServiceImpl()
var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") }
var profile by remember {
mutableStateOf<AccountProfile?>(
null
)
}
val scope = rememberCoroutineScope()
suspend fun reloadProfile() {
accountService.getMyAccountProfile().let {
profile = it
name = it.nickName
bio = it.bio
}
}
fun updateUserAvatar(uri: String) {
scope.launch {
accountService.updateAvatar(uri)
reloadProfile()
}
}
fun updateUserProfile() {
scope.launch {
accountService.updateProfile(name, bio)
reloadProfile()
}
}
val pickImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
updateUserAvatar(it.toString())
}
}
}
LaunchedEffect(Unit) {
reloadProfile()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Edit") },
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
updateUserProfile()
}
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Save"
)
}
}
) {
padding ->
profile?.let {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding).padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
it.avatar,
contentDescription = null,
modifier = Modifier
.size(100.dp)
.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickImageLauncher.launch(this)
}
},
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.size(16.dp))
TextField(
value = name,
onValueChange = {
name = it
},
label = {
Text("Name")
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.size(16.dp))
TextField(
value = bio,
onValueChange = {
bio = it
},
label = {
Text("Bio")
},
modifier = Modifier.fillMaxWidth()
)
}
}
}
}

View File

@@ -0,0 +1,38 @@
package com.aiosman.riderpro.ui.composables
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
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.ui.index.tabs.moment.MomentTopRowGroup
@Composable
fun RelPostCard(
momentItem: MomentItem,
modifier: Modifier = Modifier,
) {
val image = momentItem.images.firstOrNull()
Column(
modifier = modifier
) {
MomentTopRowGroup(momentItem = momentItem)
Box(
modifier=Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
) {
image?.let {
AsyncImage(
image,
contentDescription = null,
modifier = Modifier.size(100.dp),
contentScale = ContentScale.Crop
)
}
}
}
}

View File

@@ -60,7 +60,9 @@ 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.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
@@ -68,7 +70,6 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MomentsList() {
val model = MomentViewModel
var dataFlow = model.momentsFlow
var moments = dataFlow.collectAsLazyPagingItems()
@@ -110,7 +111,6 @@ fun MomentsList() {
}
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
}
@Composable
@@ -141,7 +141,12 @@ fun MomentCard(
momentOperateBtnBoxModifier,
momentItem = momentItem,
onLikeClick = onLikeClick,
onAddComment = onAddComment
onAddComment = onAddComment,
onShareClick = {
NewPostViewModel.asNewPost()
NewPostViewModel.relPostId = momentItem.id
navController.navigate(NavigationRoute.NewPost.route)
}
)
}
}
@@ -305,6 +310,9 @@ fun MomentContentGroup(
.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{
displayImageUrl?.let {
AsyncImage(
it,
@@ -315,6 +323,7 @@ fun MomentContentGroup(
contentDescription = ""
)
}
}
}
@@ -357,6 +366,7 @@ fun MomentBottomOperateRowGroup(
modifier: Modifier,
onLikeClick: () -> Unit = {},
onAddComment: () -> Unit = {},
onShareClick: () -> Unit = {},
momentItem: MomentItem
) {
var systemUiController = rememberSystemUiController()
@@ -411,7 +421,9 @@ fun MomentBottomOperateRowGroup(
)
}
Box(
modifier = modifier,
modifier = modifier.noRippleClickable {
onShareClick()
},
contentAlignment = Alignment.Center
) {
MomentOperateBtn(

View File

@@ -106,6 +106,13 @@ fun ProfilePage() {
}, text = {
Text("Logout")
})
DropdownMenuItem(onClick = {
scope.launch {
navController.navigate(NavigationRoute.AccountEdit.route)
}
}, text = {
Text("Edit")
})
}
}

View File

@@ -37,6 +37,8 @@ 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.TestAccountServiceImpl
import com.aiosman.riderpro.data.TestUserServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.ui.NavigationRoute
@@ -52,12 +54,12 @@ fun UserAuthScreen() {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) }
var userService: UserService = TestUserServiceImpl()
var accountService: AccountService = TestAccountServiceImpl()
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
fun onLogin() {
scope.launch {
val authResp = userService.loginUserWithPassword(email, password)
val authResp = accountService.loginUserWithPassword(email, password)
if (authResp.token != null) {
AppStore.apply {
token = authResp.token

View File

@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
@@ -34,6 +35,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
@@ -48,6 +50,7 @@ import androidx.lifecycle.viewModelScope
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.composables.RelPostCard
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -62,6 +65,7 @@ fun NewPostScreen() {
val navController = LocalNavController.current
LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(color = Color.Transparent)
model.init()
}
StatusBarMaskLayout(
darkIcons = true,
@@ -79,6 +83,23 @@ fun NewPostScreen() {
NewPostTextField("Share your adventure…", NewPostViewModel.textContent) {
NewPostViewModel.textContent = it
}
Column (
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
) {
model.relMoment?.let {
Text("Share with")
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(color = Color(0xFFEEEEEE)).padding(24.dp)
) {
RelPostCard(
momentItem = it,
modifier = Modifier.fillMaxWidth()
)
}
}
}
AddImageGrid()
AdditionalPostItem()
}

View File

@@ -8,6 +8,7 @@ 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.ui.modification.Modification
import kotlinx.coroutines.launch
@@ -18,17 +19,29 @@ object NewPostViewModel : ViewModel() {
var searchPlaceAddressResult by mutableStateOf<SearchPlaceAddressResult?>(null)
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)
fun asNewPost() {
textContent = ""
searchPlaceAddressResult = null
modificationList = listOf()
imageUriList = listOf()
relPostId = null
}
suspend fun createMoment() {
momentService.createMoment(
content = textContent,
authorId = 1,
imageUriList = imageUriList
imageUriList = imageUriList,
relPostId = relPostId
)
}
suspend fun init(){
relPostId?.let {
val moment = momentService.getMomentById(it)
relMoment = moment
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB