From 492c833ffb39ec1b78e64963cf3948fe67c743e0 Mon Sep 17 00:00:00 2001 From: Kevinlinpr Date: Tue, 10 Sep 2024 04:32:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E5=BA=95=E9=83=A8=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E6=A0=8F=E5=9B=BE=E6=A0=87=EF=BC=8C=E5=B0=86=E9=AB=98?= =?UTF-8?q?=E4=BA=AE=E9=A2=9C=E8=89=B2=E6=94=B9=E4=B8=BA=E7=99=BD=E8=89=B2?= =?UTF-8?q?=EF=BC=8C=E5=8E=9F=E8=89=B2=E4=B8=BA=E7=BA=A2=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/aiosman/riderpro/llama.txt | 12920 ---------------- .../com/aiosman/riderpro/ui/index/Index.kt | 2 +- .../riderpro/ui/index/NavigationItem.kt | 20 +- 3 files changed, 11 insertions(+), 12931 deletions(-) delete mode 100644 app/src/main/java/com/aiosman/riderpro/llama.txt diff --git a/app/src/main/java/com/aiosman/riderpro/llama.txt b/app/src/main/java/com/aiosman/riderpro/llama.txt deleted file mode 100644 index 133b7dd..0000000 --- a/app/src/main/java/com/aiosman/riderpro/llama.txt +++ /dev/null @@ -1,12920 +0,0 @@ -### Const.kt ### -package com.aiosman.riderpro - -object ConstVars { - // api 地址 -// const val BASE_SERVER = "http://192.168.31.250:8088" - const val BASE_SERVER = "https://8.137.22.101:8088" -} - -### ImageListScreen.kt ### -import android.content.Context -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -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.rememberAsyncImagePainter -import coil.request.ImageRequest -import coil.ImageLoader -import coil.disk.DiskCache -import coil.memory.MemoryCache - -data class ImageItem(val url: String) - -@Composable -fun ImageListScreen(context: Context, imageList: List) { - val imageLoader = getImageLoader(context) - - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp) - ) { - items(imageList) { item -> - ImageItem(item, imageLoader, context) // 传递 context 参数 - } - } -} - -@Composable -fun ImageItem(item: ImageItem, imageLoader: ImageLoader, context: Context) { // 接收 context 参数 - val painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(context) // 使用 context 参数 - .data(item.url) - .crossfade(true) - .build(), - imageLoader = imageLoader - ) - - Image( - painter = painter, - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .height(200.dp), - contentScale = ContentScale.Crop - ) - Spacer(modifier = Modifier.height(16.dp)) -} - -fun getImageLoader(context: Context): ImageLoader { - return ImageLoader.Builder(context) - .memoryCache { - MemoryCache.Builder(context) - .maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25% - .build() - } - .diskCache { - DiskCache.Builder() - .directory(context.cacheDir.resolve("image_cache")) - .maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2% - .build() - } - .build() -} - -### MainActivity.kt ### -package com.aiosman.riderpro - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemColors -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -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.AccountServiceImpl -import com.aiosman.riderpro.ui.Navigation -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.index.NavigationItem -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.google.android.libraries.places.api.Places -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class MainActivity : ComponentActivity() { - private val scope = CoroutineScope(Dispatchers.Main) - suspend fun getAccount(): Boolean { - val accountService: AccountService = AccountServiceImpl() - try { - val resp = accountService.getMyAccount() - return true - } catch (e: Exception) { - return false - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) - if (!Places.isInitialized()) { - Places.initialize(applicationContext, "AIzaSyDpgLDH1-SECw_pdjJq_msynq1XrxwgKVI") - } - AppStore.init(this) - enableEdgeToEdge() - - scope.launch { - val isAccountValidate = getAccount() - var startDestination = NavigationRoute.Login.route - if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) { - startDestination = NavigationRoute.Index.route - } - setContent { - Navigation(startDestination) - } - } - } -} - -val LocalNavController = compositionLocalOf { - error("NavController not provided") -} - -@OptIn(ExperimentalSharedTransitionApi::class) -val LocalSharedTransitionScope = compositionLocalOf { - error("SharedTransitionScope not provided") -} - -val LocalAnimatedContentScope = compositionLocalOf { - error("AnimatedContentScope not provided") -} - - -// 用于带导航栏的路由的可复用 composable -@Composable -fun ScaffoldWithNavigationBar( - navController: NavHostController, - content: @Composable () -> Unit -) { - val navigationBarHeight = with(LocalDensity.current) { - WindowInsets.navigationBars.getBottom(this).toDp() - } - val item = listOf( - NavigationItem.Home, - NavigationItem.Street, - NavigationItem.Add, - NavigationItem.Message, - NavigationItem.Profile - ) - Scaffold( - modifier = Modifier.statusBarsPadding(), - bottomBar = { - NavigationBar( - modifier = Modifier.height(56.dp + navigationBarHeight), - containerColor = Color.Black - ) { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route - val systemUiController = rememberSystemUiController() - item.forEach { it -> - val isSelected = currentRoute == it.route - val iconTint by animateColorAsState( - targetValue = if (isSelected) Color.Red else Color.White, - animationSpec = tween(durationMillis = 250), label = "" - ) - NavigationBarItem( - selected = currentRoute == it.route, - onClick = { - // Check if the current route is not the same as the tab's route to avoid unnecessary navigation - if (currentRoute != it.route) { - navController.navigate(it.route) { - // Avoid creating a new layer on top of the navigation stack - launchSingleTop = true - // Attempt to pop up to the existing instance of the destination, if present - popUpTo(navController.graph.startDestinationId) { - saveState = true - } - // Restore state when navigating back to the composable - restoreState = true - } - } - // Additional logic for system UI color changes - when (it.route) { - NavigationItem.Add.route -> { - systemUiController.setSystemBarsColor(color = Color.Black) - } - - NavigationItem.Message.route -> { - systemUiController.setSystemBarsColor(color = Color.Black) - } - - else -> { - systemUiController.setSystemBarsColor(color = Color.Transparent) - } - } - }, - colors = NavigationBarItemColors( - selectedTextColor = Color.Red, - selectedIndicatorColor = Color.Black, - unselectedTextColor = Color.Red, - disabledIconColor = Color.Red, - disabledTextColor = Color.Red, - selectedIconColor = iconTint, - unselectedIconColor = iconTint, - ), - icon = { - Icon( - modifier = Modifier.size(24.dp), - imageVector = if (currentRoute == it.route) it.selectedIcon() else it.icon(), - contentDescription = null, - tint = iconTint - ) - } - ) - } - - } - } - ) { innerPadding -> - Box( - modifier = Modifier.padding(innerPadding) - ) { - content() - } - } -} - - - -### llama.txt ### - - -### store.kt ### -package com.aiosman.riderpro - -import android.content.Context -import android.content.SharedPreferences -import com.google.android.gms.auth.api.signin.GoogleSignInOptions - -/** - * 持久化本地数据 - */ -object AppStore { - private const val STORE_VERSION = 1 - private const val PREFS_NAME = "app_prefs_$STORE_VERSION" - var token: String? = null - var rememberMe: Boolean = false - private lateinit var sharedPreferences: SharedPreferences - lateinit var googleSignInOptions: GoogleSignInOptions - fun init(context: Context) { - sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - this.loadData() - - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken("754277015802-uarf8br8k8gkpbj0t9g65bvkvit630q5.apps.googleusercontent.com") // Replace with your server's client ID - .requestEmail() - .build() - googleSignInOptions = gso - } - - suspend fun saveData() { - // shared preferences - sharedPreferences.edit().apply { - putString("token", token) - putBoolean("rememberMe", rememberMe) - }.apply() - } - - fun loadData() { - // shared preferences - token = sharedPreferences.getString("token", null) - rememberMe = sharedPreferences.getBoolean("rememberMe", false) - } -} - -### ui/Navi.kt ### -package com.aiosman.riderpro.ui - -import ChangePasswordScreen -import ImageViewer -import ModificationListScreen -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -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.favourite.FavouriteScreen -import com.aiosman.riderpro.ui.follower.FollowerScreen -import com.aiosman.riderpro.ui.gallery.OfficialGalleryScreen -import com.aiosman.riderpro.ui.gallery.OfficialPhotographerScreen -import com.aiosman.riderpro.ui.gallery.ProfileTimelineScreen -import com.aiosman.riderpro.ui.index.IndexScreen -import com.aiosman.riderpro.ui.like.LikeScreen -import com.aiosman.riderpro.ui.location.LocationDetailScreen -import com.aiosman.riderpro.ui.login.EmailSignupScreen -import com.aiosman.riderpro.ui.login.LoginPage -import com.aiosman.riderpro.ui.login.SignupScreen -import com.aiosman.riderpro.ui.login.UserAuthScreen -import com.aiosman.riderpro.ui.index.tabs.message.NotificationsScreen -import com.aiosman.riderpro.ui.modification.EditModificationScreen -import com.aiosman.riderpro.ui.post.NewPostImageGridScreen -import com.aiosman.riderpro.ui.post.NewPostScreen -import com.aiosman.riderpro.ui.post.PostScreen -import com.aiosman.riderpro.ui.profile.AccountProfile - -sealed class NavigationRoute( - val route: String, -) { - data object Index : NavigationRoute("Index") - data object ProfileTimeline : NavigationRoute("ProfileTimeline") - data object LocationDetail : NavigationRoute("LocationDetail/{x}/{y}") - data object OfficialPhoto : NavigationRoute("OfficialPhoto") - data object OfficialPhotographer : NavigationRoute("OfficialPhotographer") - data object Post : NavigationRoute("Post/{id}") - data object ModificationList : NavigationRoute("ModificationList") - data object MyMessage : NavigationRoute("MyMessage") - data object Comments : NavigationRoute("Comments") - data object Likes : NavigationRoute("Likes") - data object Followers : NavigationRoute("Followers") - data object NewPost : NavigationRoute("NewPost") - data object EditModification : NavigationRoute("EditModification") - data object Login : NavigationRoute("Login") - data object AccountProfile : NavigationRoute("AccountProfile/{id}") - data object SignUp : NavigationRoute("SignUp") - data object UserAuth : NavigationRoute("UserAuth") - data object EmailSignUp : NavigationRoute("EmailSignUp") - data object AccountEdit : NavigationRoute("AccountEditScreen") - data object ImageViewer : NavigationRoute("ImageViewer") - data object ChangePasswordScreen : NavigationRoute("ChangePasswordScreen") - data object FavouritesScreen : NavigationRoute("FavouritesScreen") - data object NewPostImageGrid : NavigationRoute("NewPostImageGrid") -} - - -@Composable -fun NavigationController( - navController: NavHostController, - startDestination: String = NavigationRoute.Login.route -) { - val navigationBarHeight = with(LocalDensity.current) { - WindowInsets.navigationBars.getBottom(this).toDp() - } - - NavHost( - navController = navController, - startDestination = startDestination, - ) { - composable(route = NavigationRoute.Index.route) { - CompositionLocalProvider( - LocalAnimatedContentScope provides this, - ) { - IndexScreen() - } - } - composable(route = NavigationRoute.ProfileTimeline.route) { - ProfileTimelineScreen() - } - composable( - route = NavigationRoute.LocationDetail.route, - arguments = listOf( - navArgument("x") { type = NavType.FloatType }, - navArgument("y") { type = NavType.FloatType } - ) - ) { - Box( - modifier = Modifier.padding(bottom = navigationBarHeight) - ) { - val x = it.arguments?.getFloat("x") ?: 0f - val y = it.arguments?.getFloat("y") ?: 0f - LocationDetailScreen( - x, y - ) - } - } - composable(route = NavigationRoute.OfficialPhoto.route) { - OfficialGalleryScreen() - } - composable(route = NavigationRoute.OfficialPhotographer.route) { - OfficialPhotographerScreen() - } - composable( - route = NavigationRoute.Post.route, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - enterTransition = { - fadeIn(animationSpec = tween(durationMillis = 100)) - }, - exitTransition = { - fadeOut(animationSpec = tween(durationMillis = 100)) - } - ) { backStackEntry -> - CompositionLocalProvider( - LocalAnimatedContentScope provides this, - ) { - val id = backStackEntry.arguments?.getString("id") - PostScreen( - id!! - ) - } - } - composable(route = NavigationRoute.ModificationList.route) { - ModificationListScreen() - } - composable(route = NavigationRoute.MyMessage.route) { - NotificationsScreen() - } - composable(route = NavigationRoute.Comments.route) { - CommentsScreen() - } - composable(route = NavigationRoute.Likes.route) { - LikeScreen() - } - composable(route = NavigationRoute.Followers.route) { - FollowerScreen() - } - composable( - route = NavigationRoute.NewPost.route, - enterTransition = { - - fadeIn(animationSpec = tween(durationMillis = 100)) - }, - exitTransition = { - fadeOut(animationSpec = tween(durationMillis = 100)) - } - ) { - NewPostScreen() - } - composable(route = NavigationRoute.EditModification.route) { - Box( - modifier = Modifier.padding(top = 64.dp) - ) { - EditModificationScreen() - } - } - composable(route = NavigationRoute.Login.route) { - LoginPage() - - } - composable( - route = NavigationRoute.AccountProfile.route, - arguments = listOf(navArgument("id") { type = NavType.StringType }) - ) { - CompositionLocalProvider( - LocalAnimatedContentScope provides this, - ) { - AccountProfile(it.arguments?.getString("id")!!) - } - } - composable(route = NavigationRoute.SignUp.route) { - SignupScreen() - } - composable(route = NavigationRoute.UserAuth.route) { - UserAuthScreen() - } - composable(route = NavigationRoute.EmailSignUp.route) { - EmailSignupScreen() - } - composable(route = NavigationRoute.AccountEdit.route) { - AccountEditScreen() - } - composable(route = NavigationRoute.ImageViewer.route) { - CompositionLocalProvider( - LocalAnimatedContentScope provides this, - ) { - ImageViewer() - } - } - composable(route = NavigationRoute.ChangePasswordScreen.route) { - ChangePasswordScreen() - } - composable(route = NavigationRoute.FavouritesScreen.route) { - FavouriteScreen() - } - composable(route = NavigationRoute.NewPostImageGrid.route) { - NewPostImageGridScreen() - } - } - - -} - -@OptIn(ExperimentalSharedTransitionApi::class) -@Composable -fun Navigation(startDestination: String = NavigationRoute.Login.route) { - val navController = rememberNavController() - SharedTransitionLayout { - CompositionLocalProvider( - LocalNavController provides navController, - LocalSharedTransitionScope provides this@SharedTransitionLayout, - ) { - Box { - NavigationController( - navController = navController, - startDestination = startDestination - ) - } - } - } -} - -### ui/like/LikePage.kt ### -package com.aiosman.riderpro.ui.like - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -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.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.data.AccountLike -import com.aiosman.riderpro.exp.timeAgo -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.comment.NoticeScreenHeader -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import java.util.Date - -@Preview -@Composable -fun LikeScreen() { - val model = LikePageViewModel - val listState = rememberLazyListState() - var dataFlow = model.likeItemsFlow - var likes = dataFlow.collectAsLazyPagingItems() - LaunchedEffect(Unit) { - model.updateNotice() - } - StatusBarMaskLayout( - darkIcons = true, - maskBoxBackgroundColor = Color(0xFFFFFFFF) - ) { - Column( - modifier = Modifier - .weight(1f) - .background(color = Color(0xFFFFFFFF)) - .padding(horizontal = 16.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - ) { - NoticeScreenHeader( - "LIKES", - moreIcon = false - ) - } - -// Spacer(modifier = Modifier.height(28.dp)) - LazyColumn( - modifier = Modifier.weight(1f), - state = listState, - ) { - items(likes.itemCount) { - val likeItem = likes[it] - if (likeItem != null) { - ActionNoticeItem( - avatar = likeItem.user.avatar, - nickName = likeItem.user.nickName, - likeTime = likeItem.likeTime, - thumbnail = likeItem.post.images[0].thumbnail, - action = "like", - userId = likeItem.user.id, - postId = likeItem.post.id - ) - } - } - item { - BottomNavigationPlaceholder() - } - } - } - } -} - - -@Composable -fun ActionNoticeItem( - avatar: String, - nickName: String, - likeTime: Date, - thumbnail: String, - action: String, - userId: Int, - postId: Int -) { - val context = LocalContext.current - val navController = LocalNavController.current - Box( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Top, - ) { - CustomAsyncImage( - context, - imageUrl = avatar, - modifier = Modifier - .size(40.dp) - .noRippleClickable { - navController.navigate( - NavigationRoute.AccountProfile.route.replace( - "{id}", - userId.toString() - ) - ) - }, - contentDescription = action, - ) - Spacer(modifier = Modifier.width(12.dp)) - Column( - modifier = Modifier - .weight(1f) - .noRippleClickable { - navController.navigate( - NavigationRoute.Post.route.replace( - "{id}", - postId.toString() - ) - ) - } - ) { - Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp) - Spacer(modifier = Modifier.height(5.dp)) - Row { - Text(likeTime.timeAgo(), fontSize = 12.sp, color = Color(0x99000000)) - } - } - CustomAsyncImage( - context, - imageUrl = thumbnail, - modifier = Modifier.size(64.dp), - contentDescription = action, - ) - } - } -} - -### ui/like/LikePageViewModel.kt ### -package com.aiosman.riderpro.ui.like - -import android.icu.util.Calendar -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import com.aiosman.riderpro.entity.AccountLikeEntity -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.entity.LikeItemPagingSource -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - - -object LikePageViewModel : ViewModel() { - private val accountService: AccountService = AccountServiceImpl() - private val _likeItemsFlow = MutableStateFlow>(PagingData.empty()) - val likeItemsFlow = _likeItemsFlow.asStateFlow() - - init { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - LikeItemPagingSource( - accountService - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _likeItemsFlow.value = it - } - } - } - - suspend fun updateNotice() { - var now = Calendar.getInstance().time - accountService.updateNotice( - UpdateNoticeRequestBody( - lastLookLikeTime = ApiClient.formatTime(now) - ) - ) - } -} - -### ui/splash/splash.kt ### -package com.aiosman.riderpro.ui.splash - -import android.window.SplashScreen -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.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.R - -@Composable -fun SplashScreen() { - Scaffold { - it - Box( - modifier = Modifier.fillMaxSize() - ) { - // to bottom - Box( - contentAlignment = Alignment.TopCenter, - modifier = Modifier.padding(top = 211.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { - Image( - painter = painterResource(id = R.mipmap.rider_pro_logo), - contentDescription = "Rider Pro", - modifier = Modifier - .width(108.dp) - .height(45.dp) - ) - Spacer(modifier = Modifier.height(32.dp)) - Text( - "Connecting Riders".uppercase(), - fontSize = 28.sp, - fontWeight = FontWeight.Bold - ) - Text("Worldwide".uppercase(), fontSize = 28.sp, fontWeight = FontWeight.Bold) - } - - } - } - - } -} - -### ui/modifiers/ModifierExp.kt ### -package com.aiosman.riderpro.ui.modifiers - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed - -inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed { - this.clickable(indication = null, - interactionSource = remember { MutableInteractionSource() }) { - onClick() - } -} - -### ui/post/NewPost.kt ### -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 -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -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 -import androidx.compose.material3.Text -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.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 -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewModelScope -import coil.compose.AsyncImage -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -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 -import kotlinx.coroutines.launch - - -@Preview -@Composable -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() - } - StatusBarMaskLayout( - darkIcons = true, - modifier = Modifier.fillMaxSize().background(Color.White) - ) { - Column( - modifier = Modifier - .fillMaxSize() - ) { - NewPostTopBar { - model.viewModelScope.launch { - model.createMoment(context = context) - navController.popBackStack() - } - } - 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( - momentEntity = it, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - - AddImageGrid() -// AdditionalPostItem() - } - } -} - -@Composable -fun NewPostTopBar(onSendClick: () -> Unit = {}) { - val navController = LocalNavController.current - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 18.dp, vertical = 10.dp) - ) { - Row( - modifier = Modifier.align(Alignment.CenterStart), - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_close), - contentDescription = "Back", - modifier = Modifier - .size(24.dp) - .noRippleClickable { - navController.popBackStack() - } - ) - Spacer(modifier = Modifier.weight(1f)) - Image( - painter = painterResource(id = R.drawable.rider_pro_send_post), - contentDescription = "Send", - modifier = Modifier - .size(24.dp) - .noRippleClickable { - onSendClick() - } - ) - } - } - -} - -@Composable -fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) { - - Box(modifier = Modifier.fillMaxWidth()) { - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = Modifier - .fillMaxWidth() - .heightIn(200.dp) - .padding(horizontal = 18.dp, vertical = 10.dp) - - ) - if (value.isEmpty()) { - Text( - text = hint, - color = Color.Gray, - modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp) - ) - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun AddImageGrid() { - val navController = LocalNavController.current - val context = LocalContext.current - val model = NewPostViewModel - - val pickImagesLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetMultipleContents() - ) { uris -> - if (uris.isNotEmpty()) { - model.imageUriList += uris.map { it.toString() } - } - } - - val stroke = Stroke( - width = 2f, - pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) - ) - Box( - modifier = Modifier.fillMaxWidth() - ) { - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(18.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - model.imageUriList.forEach { - CustomAsyncImage( - context, - it, - contentDescription = "Image", - modifier = Modifier - .size(110.dp) - - .drawBehind { - drawRoundRect(color = Color(0xFF999999), style = stroke) - }.noRippleClickable { - navController.navigate(NavigationRoute.NewPostImageGrid.route) - }, - contentScale = ContentScale.Crop - ) - } - Box( - modifier = Modifier - .size(110.dp) - - .drawBehind { - drawRoundRect(color = Color(0xFF999999), style = stroke) - } - .noRippleClickable{ - pickImagesLauncher.launch("image/*") - }, - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic), - contentDescription = "Add Image", - modifier = Modifier - .size(48.dp) - .align(Alignment.Center) - ) - } - - } - } - -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AdditionalPostItem() { - val model = NewPostViewModel - val navController = LocalNavController.current - var isShowLocationModal by remember { mutableStateOf(false) } - fun onSelectLocationClick() { - isShowLocationModal = true - } - if (isShowLocationModal) { - ModalBottomSheet( - onDismissRequest = { - isShowLocationModal = false - }, - containerColor = Color.White - - ) { - // Sheet content - SelectLocationModal( - onClose = { - isShowLocationModal = false - } - ) { - isShowLocationModal = false - NewPostViewModel.searchPlaceAddressResult = it - } - } - } - Column( - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 24.dp) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - onSelectLocationClick() - } - ) { - NewPostViewModel.searchPlaceAddressResult?.let { - SelectedLocation(it) { - NewPostViewModel.searchPlaceAddressResult = null - } - } ?: Row( - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_add_location), - contentDescription = "Location", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text("Add Location", color = Color(0xFF333333)) - Spacer(modifier = Modifier.weight(1f)) - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_next), - contentDescription = "Add Location", - modifier = Modifier.size(24.dp) - ) - } - } - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 24.dp) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - navController.navigate("EditModification") - } - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_modification), - contentDescription = "Modification List", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text("Modification List", color = Color(0xFF333333)) - Spacer(modifier = Modifier.weight(1f)) - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_next), - contentDescription = "Modification List", - modifier = Modifier.size(24.dp) - ) - } - - } - } -} - -@Composable -fun SelectedLocation( - searchPlaceAddressResult: SearchPlaceAddressResult, - onRemoveLocation: () -> Unit -) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier - .weight(1f) - .padding(end = 16.dp) - ) { - Text(searchPlaceAddressResult.name, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(4.dp)) - Text(searchPlaceAddressResult.address, color = Color(0xFF9a9a9a)) - } - Image( - painter = painterResource(id = R.drawable.rider_pro_close), - contentDescription = "Next", - modifier = Modifier - .size(24.dp) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - onRemoveLocation() - } - ) - - } - } -} - -### ui/post/NewPostImageGrid.kt ### -package com.aiosman.riderpro.ui.post - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Delete -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.rememberAsyncImagePainter -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.google.accompanist.systemuicontroller.rememberSystemUiController - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun NewPostImageGridScreen() { - val model = NewPostViewModel - val imageList = model.imageUriList - val pagerState = rememberPagerState(pageCount = { imageList.size }) - val systemUiController = rememberSystemUiController() - val paddingValues = WindowInsets.systemBars.asPaddingValues() - val navController = LocalNavController.current - val title = "${pagerState.currentPage + 1}/${imageList.size}" - - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false) - } - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black) - ) { - Column { - Box( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF2e2e2e)) - .padding(horizontal = 16.dp, vertical = 16.dp) - ) { - Column { - Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding())) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "back", - modifier = Modifier - .size(24.dp) - .noRippleClickable { - navController.popBackStack() - }, - tint = Color.White - ) - Text( - title, - color = Color.White, - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - fontSize = 18.sp, - ) - Icon( - Icons.Default.Delete, - contentDescription = "delete", - modifier = Modifier - .size(24.dp) - .noRippleClickable { - model.deleteImage(pagerState.currentPage) - }, - tint = Color.White - ) - } - - } - - } - HorizontalPager( - state = pagerState, - ) { page -> - val imageUrl = imageList[page] - Image( - painter = rememberAsyncImagePainter(model = imageUrl), - contentDescription = "Image $page", - modifier = Modifier.fillMaxSize() - ) - } - } - - - } - - -} - -### ui/post/NewPostViewModel.kt ### -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.setValue -import androidx.lifecycle.ViewModel -import com.aiosman.riderpro.data.MomentService -import com.aiosman.riderpro.entity.MomentServiceImpl -import com.aiosman.riderpro.data.UploadImage -import com.aiosman.riderpro.entity.MomentEntity -import com.aiosman.riderpro.ui.modification.Modification -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream - - -object NewPostViewModel : ViewModel() { - var momentService: MomentService = MomentServiceImpl() - var textContent by mutableStateOf("") - var searchPlaceAddressResult by mutableStateOf(null) - var modificationList by mutableStateOf>(listOf()) - var imageUriList by mutableStateOf(listOf()) - var relPostId by mutableStateOf(null) - var relMoment by mutableStateOf(null) - fun asNewPost() { - textContent = "" - searchPlaceAddressResult = null - modificationList = listOf() - imageUriList = listOf() - relPostId = null - } - - 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 createMoment(context: Context) { - val uploadImageList = emptyList().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 - } - } - fun deleteImage(index: Int) { - imageUriList = imageUriList.toMutableList().apply { - removeAt(index) - } - } -} - -### ui/post/Post.kt ### -package com.aiosman.riderpro.ui.post - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -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.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.map -import com.aiosman.riderpro.LocalAnimatedContentScope -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.LocalSharedTransitionScope -import com.aiosman.riderpro.R -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.entity.CommentEntity -import com.aiosman.riderpro.entity.CommentPagingSource -import com.aiosman.riderpro.data.CommentRemoteDataSource -import com.aiosman.riderpro.data.CommentService -import com.aiosman.riderpro.data.CommentServiceImpl -import com.aiosman.riderpro.data.MomentService -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.entity.MomentServiceImpl -import com.aiosman.riderpro.data.UserServiceImpl -import com.aiosman.riderpro.data.UserService -import com.aiosman.riderpro.exp.formatPostTime -import com.aiosman.riderpro.exp.timeAgo -import com.aiosman.riderpro.entity.MomentEntity -import com.aiosman.riderpro.entity.MomentImageEntity -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.composables.EditCommentBottomModal -import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel -import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -object PostViewModel : ViewModel() { - var service: MomentService = MomentServiceImpl() - var commentService: CommentService = CommentServiceImpl() - var userService: UserService = UserServiceImpl() - private var _commentsFlow = MutableStateFlow>(PagingData.empty()) - val commentsFlow = _commentsFlow.asStateFlow() - var postId: String = "" - - // 预加载的 moment - - var accountProfileEntity by mutableStateOf(null) - var moment by mutableStateOf(null) - var accountService: AccountService = AccountServiceImpl() - - /** - * 预加载,在跳转到 PostScreen 之前设置好内容 - */ - fun preTransit(momentEntity: MomentEntity?) { - this.postId = momentEntity?.id.toString() - this.moment = momentEntity - this._commentsFlow = MutableStateFlow>(PagingData.empty()) - this.accountProfileEntity = null - - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - postId = postId.toInt() - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _commentsFlow.value = it - } - } - } - - suspend fun initData() { - moment = service.getMomentById(postId.toInt()) - moment?.let { - accountProfileEntity = userService.getUserProfile(it.authorId.toString()) - } - } - - suspend fun likeComment(commentId: Int) { - commentService.likeComment(commentId) - val currentPagingData = commentsFlow.value - val updatedPagingData = currentPagingData.map { comment -> - if (comment.id == commentId) { - comment.copy(liked = !comment.liked, likes = comment.likes + 1) - } else { - comment - } - } - _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, likes = comment.likes - 1) - } else { - comment - } - } - _commentsFlow.value = updatedPagingData - } - - suspend fun createComment(content: String) { - commentService.createComment(postId.toInt(), content) - MomentViewModel.updateCommentCount(postId.toInt()) - } - - suspend fun likeMoment() { - moment?.let { - service.likeMoment(it.id) - moment = moment?.copy(likeCount = moment?.likeCount?.plus(1) ?: 0, liked = true) - MomentViewModel.updateLikeCount(it.id) - } - } - - suspend fun dislikeMoment() { - moment?.let { - service.dislikeMoment(it.id) - moment = moment?.copy(likeCount = moment?.likeCount?.minus(1) ?: 0, liked = false) - // update home list - MomentViewModel.updateDislikeMomentById(it.id) - } - } - - suspend fun favoriteMoment() { - moment?.let { - service.favoriteMoment(it.id) - moment = - moment?.copy(favoriteCount = moment?.favoriteCount?.plus(1) ?: 0, isFavorite = true) - } - } - - suspend fun unfavoriteMoment() { - moment?.let { - service.unfavoriteMoment(it.id) - moment = moment?.copy( - favoriteCount = moment?.favoriteCount?.minus(1) ?: 0, - isFavorite = false - ) - } - } - - suspend fun followUser() { - accountProfileEntity?.let { - userService.followUser(it.id.toString()) - accountProfileEntity = accountProfileEntity?.copy(isFollowing = true) - } - } - - suspend fun unfollowUser() { - accountProfileEntity?.let { - userService.unFollowUser(it.id.toString()) - accountProfileEntity = accountProfileEntity?.copy(isFollowing = false) - } - } - - var avatar: String? = null - get() { - accountProfileEntity?.avatar?.let { - return it - } - moment?.avatar?.let { - return it - } - return field - } - var nickname: String? = null - get() { - accountProfileEntity?.nickName?.let { - return it - } - moment?.nickname?.let { - return it - } - return field - } - - -} - -@Composable -fun PostScreen( - id: String, -) { - val viewModel = PostViewModel - val scope = rememberCoroutineScope() - - val commentsPagging = viewModel.commentsFlow.collectAsLazyPagingItems() - var showCollapseContent by remember { mutableStateOf(true) } - val scrollState = rememberLazyListState() - val uiController = rememberSystemUiController() - LaunchedEffect(Unit) { - uiController.setNavigationBarColor(Color.Transparent) - viewModel.initData() - } - StatusBarMaskLayout { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - BottomNavigationBar( - onLikeClick = { - scope.launch { - if (viewModel.moment?.liked == true) { - viewModel.dislikeMoment() - } else { - viewModel.likeMoment() - } - } - }, - onCreateComment = { - scope.launch { - viewModel.createComment(it) - commentsPagging.refresh() - } - }, - onFavoriteClick = { - scope.launch { - if (viewModel.moment?.isFavorite == true) { - viewModel.unfavoriteMoment() - } else { - viewModel.favoriteMoment() - } - } - }, - momentEntity = viewModel.moment - ) - } - ) { - it - Column( - modifier = Modifier - .fillMaxSize() - ) { - Header( - avatar = viewModel.avatar, - nickname = viewModel.nickname, - userId = viewModel.moment?.authorId, - isFollowing = viewModel.accountProfileEntity?.isFollowing ?: false, - onFollowClick = { - scope.launch { - if (viewModel.accountProfileEntity?.isFollowing == true) { - viewModel.unfollowUser() - } else { - viewModel.followUser() - } - } - } - ) - Column(modifier = Modifier.animateContentSize()) { - AnimatedVisibility(visible = showCollapseContent) { - // collapse content - Column( - modifier = Modifier - .fillMaxWidth() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - ) { - PostImageView( - id, - viewModel.moment?.images ?: emptyList() - ) - - } - PostDetails( - id, - viewModel.moment - ) - } - } - } - Box( - modifier = Modifier - .fillMaxWidth() - - ) { - CommentsSection( - lazyPagingItems = commentsPagging, - scrollState, - onLike = { commentEntity: CommentEntity -> - scope.launch { - if (commentEntity.liked) { - viewModel.unlikeComment(commentEntity.id) - } else { - viewModel.likeComment(commentEntity.id) - } - } - }) { - showCollapseContent = it - } - } - } - } - } -} - -@Composable -fun Header( - avatar: String?, - nickname: String?, - userId: Int?, - isFollowing: Boolean, - onFollowClick: () -> Unit -) { - val navController = LocalNavController.current - val context = LocalContext.current - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_back), // Replace with your image resource - contentDescription = "Back", - modifier = Modifier - .noRippleClickable { - navController.popBackStack() - } - .size(32.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(Color.Gray.copy(alpha = 0.1f)) - ) { - CustomAsyncImage( - context, - avatar, - contentDescription = "Profile Picture", - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .noRippleClickable { - userId?.let { - navController.navigate( - NavigationRoute.AccountProfile.route.replace( - "{id}", - userId.toString() - ) - ) - } - }, - contentScale = ContentScale.Crop - ) - } - - Spacer(modifier = Modifier.width(8.dp)) - Text(text = nickname ?: "", fontWeight = FontWeight.Bold) - Box( - modifier = Modifier - .height(20.dp) - .wrapContentWidth() - .padding(start = 6.dp) - .noRippleClickable { - onFollowClick() - }, - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier.height(18.dp), - painter = painterResource(id = R.drawable.follow_bg), - contentDescription = "" - ) - Text( - text = if (isFollowing) "Following" else "Follow", - fontSize = 12.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } - } -} - - -@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class) -@Composable -fun PostImageView( - postId: String, - images: List, -) { - val pagerState = rememberPagerState(pageCount = { images.size }) - val navController = LocalNavController.current - val sharedTransitionScope = LocalSharedTransitionScope.current - val animatedVisibilityScope = LocalAnimatedContentScope.current - val context = LocalContext.current - - Column { - HorizontalPager( - state = pagerState, - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .background(Color.Gray.copy(alpha = 0.1f)), - ) { page -> - val image = images[page] - with(sharedTransitionScope) { - CustomAsyncImage( - context, - image.thumbnail, - contentDescription = "Image", - contentScale = ContentScale.Crop, - modifier = Modifier - .sharedElement( - rememberSharedContentState(key = image), - animatedVisibilityScope = animatedVisibilityScope - ) - .fillMaxSize() - .noRippleClickable { - ImageViewerViewModel.asNew(images, page) - navController.navigate( - NavigationRoute.ImageViewer.route - ) - } - ) - } - } - - // Indicator container - Row( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - images.forEachIndexed { index, _ -> - Box( - modifier = Modifier - .size(8.dp) - .clip(CircleShape) - - .background( - if (pagerState.currentPage == index) Color.Red else Color.Gray.copy( - alpha = 0.5f - ) - ) - .padding(4.dp) - - - ) - Spacer(modifier = Modifier.width(8.dp)) - } - } - } - -} - -@OptIn(ExperimentalSharedTransitionApi::class) -@Composable -fun PostDetails( - postId: String, - momentEntity: MomentEntity? -) { - - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - .wrapContentHeight() - ) { - - Text( - text = momentEntity?.momentTextContent ?: "", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ) - Text(text = "${momentEntity?.time?.formatPostTime()} 发布") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "${momentEntity?.commentCount ?: 0} Comments") - - } -} - -@Composable -fun CommentsSection( - lazyPagingItems: LazyPagingItems, - scrollState: LazyListState = rememberLazyListState(), - onLike: (CommentEntity) -> Unit, - onWillCollapse: (Boolean) -> Unit -) { - LazyColumn( - state = scrollState, modifier = Modifier - .padding(start = 16.dp, end = 16.dp) - .fillMaxHeight() - ) { - items(lazyPagingItems.itemCount) { idx -> - val item = lazyPagingItems[idx] ?: return@items - CommentItem(item, onLike = { - onLike(item) - }) - } - } - - // Detect scroll direction and update showCollapseContent - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(scrollState) { - coroutineScope.launch { - snapshotFlow { scrollState.firstVisibleItemScrollOffset } - .collect { offset -> - onWillCollapse(offset == 0) - } - } - } -} - - -@Composable -fun CommentItem(commentEntity: CommentEntity, onLike: () -> Unit = {}) { - val context = LocalContext.current - Column { - Row(modifier = Modifier.padding(vertical = 8.dp)) { - CustomAsyncImage( - context, - commentEntity.avatar, - contentDescription = "Comment Profile Picture", - modifier = Modifier - .size(40.dp) - .clip(CircleShape), - contentScale = ContentScale.Crop - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { - Text(text = commentEntity.name, fontWeight = FontWeight.Bold) - Text(text = commentEntity.comment) - Text(text = commentEntity.date.timeAgo(), fontSize = 12.sp, color = Color.Gray) - } - Spacer(modifier = Modifier.weight(1f)) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - IconButton(onClick = { - onLike() - }) { - Icon( - Icons.Filled.Favorite, - contentDescription = "Like", - tint = if (commentEntity.liked) Color.Red else Color.Gray - ) - } - Text(text = commentEntity.likes.toString()) - } - } - Spacer(modifier = Modifier.height(8.dp)) - Column( - modifier = Modifier.padding(start = 16.dp) - ) { - commentEntity.replies.forEach { reply -> - CommentItem(reply) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BottomNavigationBar( - onCreateComment: (String) -> Unit = {}, - onLikeClick: () -> Unit = {}, - onFavoriteClick: () -> Unit = {}, - momentEntity: MomentEntity? -) { - var showCommentModal by remember { mutableStateOf(false) } - if (showCommentModal) { - ModalBottomSheet( - onDismissRequest = { showCommentModal = false }, - containerColor = Color.White, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ), - dragHandle = {}, - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - windowInsets = WindowInsets(0) - ) { - EditCommentBottomModal() { - onCreateComment(it) - showCommentModal = false - } - } - } - Column( - modifier = Modifier.background(Color.White) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .background(Color.White) - ) { - // grey round box - Box( - modifier = Modifier - .padding(8.dp) - .clip(RoundedCornerShape(16.dp)) - .background(Color.Gray.copy(alpha = 0.1f)) - .weight(1f) - .height(31.dp) - .padding(8.dp) - .noRippleClickable { - showCommentModal = true - } - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Filled.Edit, contentDescription = "Send") - Spacer(modifier = Modifier.width(8.dp)) - Text(text = "说点什么", fontSize = 12.sp) - } - } - - IconButton( - onClick = { - onLikeClick() - }) { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.rider_pro_like), - contentDescription = "like", - tint = if (momentEntity?.liked == true) Color.Red else Color.Gray - ) - } - Text(text = momentEntity?.likeCount.toString()) - IconButton( - onClick = { - onFavoriteClick() - } - ) { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.rider_pro_favoriate), - contentDescription = "Favourite", - tint = if (momentEntity?.isFavorite == true) Color.Red else Color.Gray - ) - } - Text(text = momentEntity?.favoriteCount.toString()) -// IconButton( -// onClick = { /*TODO*/ }) { -// Icon(Icons.Filled.CheckCircle, contentDescription = "Send") -// } -// Text(text = "2077") - - } - BottomNavigationPlaceholder( - color = Color.White - ) - } - -} - -### ui/post/SelectLocationModal.kt ### -package com.aiosman.riderpro.ui.post - -import android.util.Log -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -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.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -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.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.R -import com.google.android.gms.common.api.ApiException -import com.google.android.libraries.places.api.Places -import com.google.android.libraries.places.api.model.Place -import com.google.android.libraries.places.api.net.PlacesClient -import com.google.android.libraries.places.api.net.SearchByTextRequest - -data class SearchPlaceAddressResult( - val name: String, - val address: String -) - -@Composable -fun SelectLocationModal( - onClose: () -> Unit, - onSelectedLocation: (SearchPlaceAddressResult) -> Unit -) { - val context = LocalContext.current - var queryString by remember { mutableStateOf("") } - var searchPlaceAddressResults by remember { - mutableStateOf>( - emptyList() - ) - } - - fun searchAddrWithGoogleMap(query: String) { - val placesClient: PlacesClient = Places.createClient(context) - val placeFields: List = - listOf(Place.Field.ID, Place.Field.NAME, Place.Field.ADDRESS) - val request = SearchByTextRequest.newInstance(query, placeFields) - placesClient.searchByText(request) - .addOnSuccessListener { response -> - val place = response.places - searchPlaceAddressResults = place.map { - SearchPlaceAddressResult(it.name ?: "", it.address ?: "") - } - - }.addOnFailureListener { exception -> - if (exception is ApiException) { - Log.e("SelectLocationModal", "Place not found: ${exception.statusCode}") - } - } - } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp) - ) { - Text( - "Check In", - fontWeight = FontWeight.Bold, - modifier = Modifier.align(Alignment.Center), - fontSize = 16.sp - ) - Text( - "Cancel", - modifier = Modifier - .align(Alignment.CenterEnd) - .clickable { - onClose() - }, - fontSize = 16.sp - ) - } - LocationSearchTextInput(queryString, onQueryClick = { - searchAddrWithGoogleMap(queryString) - }) { - queryString = it - } - LazyColumn( - modifier = Modifier - .weight(1f) - .padding(top = 28.dp) - ) { - item { - for (searchPlaceAddressResult in searchPlaceAddressResults) { - LocationItem(searchPlaceAddressResult) { - onSelectedLocation(searchPlaceAddressResult) - } - } - } - } - - } -} - -@Composable -fun LocationSearchTextInput( - value: String, - onQueryClick: () -> Unit, - onValueChange: (String) -> Unit -) { - val keyboardController = LocalSoftwareKeyboardController.current - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .background(Color(0xffF5F5F5)) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp), - - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_search_location), - contentDescription = "Search", - modifier = Modifier - .size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - if (value.isEmpty()) { - Text( - "search", - modifier = Modifier.padding(vertical = 16.dp), - color = Color(0xffA0A0A0) - ) - } - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Search - ), - keyboardActions = KeyboardActions( - onSearch = { - onQueryClick() - // hide keyboard - keyboardController?.hide() - - } - ) - - ) - } - - } - -} - -@Composable -fun LocationItem( - searchPlaceAddressResult: SearchPlaceAddressResult, - onLocationItemClick: () -> Unit = {} -) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - .clickable { - onLocationItemClick() - } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier - .weight(1f) - .padding(end = 16.dp) - ) { - Text(searchPlaceAddressResult.name, fontWeight = FontWeight.Bold) - Text(searchPlaceAddressResult.address, color = Color(0xFF9a9a9a)) - } - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_next), - contentDescription = "Next", - modifier = Modifier.size(24.dp) - ) - - } - } -} - -### ui/composables/AnimatedCounter.kt ### -package com.aiosman.riderpro.ui.composables - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.SizeTransform -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith -import androidx.compose.animation.with -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.sp - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 24) { - AnimatedContent( - targetState = count, - transitionSpec = { - // Compare the incoming number with the previous number. - if (targetState > initialState) { - // If the target number is larger, it slides up and fades in - // while the initial (smaller) number slides up and fades out. - (slideInVertically { height -> height } + fadeIn()).togetherWith(slideOutVertically { height -> -height } + fadeOut()) - } else { - // If the target number is smaller, it slides down and fades in - // while the initial number slides down and fades out. - (slideInVertically { height -> -height } + fadeIn()).togetherWith(slideOutVertically { height -> height } + fadeOut()) - }.using( - // Disable clipping since the faded slide-in/out should - // be displayed out of bounds. - SizeTransform(clip = false) - ) - } - ) { targetCount -> - Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp) - } -} - -### ui/composables/AnimatedFavouriteButton.kt ### -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) - ) - } -} - -### ui/composables/AnimatedLikeButton.kt ### -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.clickable -import androidx.compose.foundation.layout.Box -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.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 AnimatedLikeIcon( - modifier: Modifier = Modifier, - liked: Boolean = false, - onClick: (() -> Unit)? = null -) { - val animatableRotation = remember { Animatable(0f) } - val animatedColor by animateColorAsState(targetValue = if (liked) 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_like), - contentDescription = "Like", - modifier = modifier.graphicsLayer { - rotationZ = animatableRotation.value - }, - colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(animatedColor) - ) - } -} - -### ui/composables/BlurHash.kt ### -package com.aiosman.riderpro.ui.composables - -import android.content.Context -import android.graphics.drawable.BitmapDrawable -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.core.graphics.drawable.toDrawable -import coil.ImageLoader -import coil.annotation.ExperimentalCoilApi -import coil.compose.AsyncImage -import coil.request.ImageRequest -import com.aiosman.riderpro.utils.BlurHashDecoder -import com.aiosman.riderpro.utils.Utils.getImageLoader - -const val DEFAULT_HASHED_BITMAP_WIDTH = 4 -const val DEFAULT_HASHED_BITMAP_HEIGHT = 3 - -/** - * This function is used to load an image asynchronously and blur it using BlurHash. - * @param imageUrl The URL of the image to be loaded. - * @param modifier The modifier to be applied to the image. - * @param imageModifier The modifier to be applied to the image. - * @param contentDescription The content description to be applied to the image. - * @param contentScale The content scale to be applied to the image. - * @param isCrossFadeRequired Whether cross-fade is required or not. - * @param onImageLoadSuccess The callback to be called when the image is loaded successfully. - * @param onImageLoadFailure The callback to be called when the image is failed to load. - * @see AsyncImage - */ -@Suppress("LongParameterList") -@ExperimentalCoilApi -@Composable -fun AsyncBlurImage( - imageUrl: String, - blurHash: String, - modifier: Modifier = Modifier, - imageModifier: Modifier? = null, - contentDescription: String? = null, - contentScale: ContentScale = ContentScale.Fit, - isCrossFadeRequired: Boolean = false, - onImageLoadSuccess: () -> Unit = {}, - onImageLoadFailure: () -> Unit = {} -) { - val context = LocalContext.current - val resources = context.resources - val imageLoader = getImageLoader(context) - - val blurBitmap by remember(blurHash) { - mutableStateOf( - BlurHashDecoder.decode( - blurHash = blurHash, - width = DEFAULT_HASHED_BITMAP_WIDTH, - height = DEFAULT_HASHED_BITMAP_HEIGHT - ) - ) - } - - AsyncImage( - modifier = imageModifier ?: modifier, - model = ImageRequest.Builder(context) - .data(imageUrl) - .crossfade(isCrossFadeRequired) - .placeholder( - blurBitmap?.toDrawable(resources) - ) - .fallback(blurBitmap?.toDrawable(resources)) - .build(), - contentDescription = contentDescription, - contentScale = contentScale, - onSuccess = { onImageLoadSuccess() }, - onError = { onImageLoadFailure() }, - imageLoader = imageLoader - ) -} - -### ui/composables/BottomNavigationPlaceholder.kt ### -package com.aiosman.riderpro.ui.composables - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity - -@Composable -fun BottomNavigationPlaceholder( - color: Color? = null -) { - val navigationBarHeight = with(LocalDensity.current) { - WindowInsets.navigationBars.getBottom(this).toDp() - } - Box( - modifier = Modifier.height(navigationBarHeight).fillMaxWidth().background(color ?: Color.Transparent) - ) -} - -### ui/composables/EditCommentBottomModal.kt ### -package com.aiosman.riderpro.ui.composables - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -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.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.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.modifiers.noRippleClickable - -@Composable -fun EditCommentBottomModal(onSend: (String) -> Unit = {}) { - var text by remember { mutableStateOf("") } - var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - val focusRequester = remember { FocusRequester() } - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xfff7f7f7)) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .clip(RoundedCornerShape(20.dp)) - .background(Color(0xffe5e5e5)) - .padding(horizontal = 16.dp, vertical = 12.dp) - ) { - BasicTextField( - value = text, - onValueChange = { - text = it - }, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - textStyle = TextStyle( - color = Color.Black, - fontWeight = FontWeight.Normal - ), - minLines = 5 - ) - } - Spacer(modifier = Modifier.width(16.dp)) - Image( - painter = painterResource(id = R.drawable.rider_pro_send), - contentDescription = "Send", - modifier = Modifier - .size(32.dp) - .noRippleClickable { - onSend(text) - text = "" - }, - ) - } - Spacer(modifier = Modifier.height(navBarHeight)) - } -} - -### ui/composables/Image.kt ### -package com.aiosman.riderpro.ui.composables - -import android.content.Context -import android.graphics.Bitmap -import androidx.annotation.DrawableRes -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.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.core.graphics.drawable.toBitmap -import androidx.core.graphics.drawable.toDrawable -import coil.ImageLoader -import coil.compose.AsyncImage -import coil.request.ImageRequest -import coil.request.SuccessResult -import com.aiosman.riderpro.utils.BlurHashDecoder -import com.aiosman.riderpro.utils.Utils.getImageLoader -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@Composable -fun rememberImageBitmap(imageUrl: String, imageLoader: ImageLoader): Bitmap? { - val context = LocalContext.current - var bitmap by remember { mutableStateOf(null) } - - LaunchedEffect(imageUrl) { - val request = ImageRequest.Builder(context) - .data(imageUrl) - .crossfade(true) - .build() - - val result = withContext(Dispatchers.IO) { - (imageLoader.execute(request) as? SuccessResult)?.drawable?.toBitmap() - } - - bitmap = result - } - - return bitmap -} - -@Composable -fun CustomAsyncImage( - context: Context, - imageUrl: String?, - contentDescription: String?, - modifier: Modifier = Modifier, - blurHash: String? = null, - @DrawableRes - placeholderRes: Int? = null, - contentScale: ContentScale = ContentScale.Crop -) { - val imageLoader = getImageLoader(context) - val blurBitmap = remember(blurHash) { - blurHash?.let { - BlurHashDecoder.decode( - blurHash = it, - width = DEFAULT_HASHED_BITMAP_WIDTH, - height = DEFAULT_HASHED_BITMAP_HEIGHT - ) - } - } - -// var bitmap by remember(imageUrl) { mutableStateOf(null) } -// -// LaunchedEffect(imageUrl) { -// if (bitmap == null) { -// val request = ImageRequest.Builder(context) -// .data(imageUrl) -// .crossfade(3000) -// .build() -// -// val result = withContext(Dispatchers.IO) { -// (imageLoader.execute(request) as? SuccessResult)?.drawable?.toBitmap() -// } -// bitmap = result -// } -// } - - AsyncImage( - model = ImageRequest.Builder(context) - .data(imageUrl) - .crossfade(200) - .apply { - if (placeholderRes != null) { - placeholder(placeholderRes) - return@apply - } - if (blurBitmap != null) { - placeholder(blurBitmap.toDrawable(context.resources)) - } - - } - .build(), - contentDescription = contentDescription, - modifier = modifier, - contentScale = contentScale, - imageLoader = imageLoader - ) -} - -### ui/composables/RelPostCard.kt ### -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.platform.LocalContext -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.aiosman.riderpro.entity.MomentEntity -import com.aiosman.riderpro.ui.index.tabs.moment.MomentTopRowGroup - -@Composable -fun RelPostCard( - momentEntity: MomentEntity, - modifier: Modifier = Modifier, -) { - val image = momentEntity.images.firstOrNull() - val context = LocalContext.current - Column( - modifier = modifier - ) { - MomentTopRowGroup(momentEntity = momentEntity) - Box( - modifier=Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) { - image?.let { - CustomAsyncImage( - context, - image.thumbnail, - contentDescription = null, - modifier = Modifier.size(100.dp), - contentScale = ContentScale.Crop - ) - } - } - } -} - -### ui/composables/StatusBarMask.kt ### -package com.aiosman.riderpro.ui.composables - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.systemBars -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.google.accompanist.systemuicontroller.rememberSystemUiController - -@Composable -fun StatusBarMask(darkIcons: Boolean = true) { - val paddingValues = WindowInsets.systemBars.asPaddingValues() - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = darkIcons) - - } - Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding())) - -} - -@Composable -fun StatusBarMaskLayout( - modifier: Modifier = Modifier, - darkIcons: Boolean = true, - useNavigationBarMask: Boolean = true, - maskBoxBackgroundColor: Color = Color.Transparent, - content: @Composable ColumnScope.() -> Unit -) { - val paddingValues = WindowInsets.systemBars.asPaddingValues() - val systemUiController = rememberSystemUiController() - val navigationBarPaddings = - WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = darkIcons) - } - Column( - modifier = modifier.fillMaxSize() - ) { - Box( - modifier = Modifier - .height(paddingValues.calculateTopPadding()) - .fillMaxWidth() - .background(maskBoxBackgroundColor) - ) { - - } - content() - if (navigationBarPaddings > 24.dp && useNavigationBarMask) { - Box( - modifier = Modifier - .height(navigationBarPaddings).fillMaxWidth().background(Color.White) - ) - } - } -} - -### ui/comment/CommentModal.kt ### -package com.aiosman.riderpro.ui.comment - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars -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.HorizontalDivider -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -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.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -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.entity.CommentEntity -import com.aiosman.riderpro.entity.CommentPagingSource -import com.aiosman.riderpro.data.CommentRemoteDataSource -import com.aiosman.riderpro.data.CommentService -import com.aiosman.riderpro.data.CommentServiceImpl -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch - -/** - * 评论弹窗的 ViewModel - */ -class CommentModalViewModel( - val postId: Int? -) : ViewModel() { - val commentService: CommentService = CommentServiceImpl() - val commentsFlow: Flow> = Pager( - config = PagingConfig(pageSize = 20, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - postId - ) - } - ).flow.cachedIn(viewModelScope) - - /** - * 创建评论 - */ - suspend fun createComment(content: String) { - postId?.let { - commentService.createComment(postId, content) - } - } -} - - -/** - * 评论弹窗 - * @param postId 帖子ID - * @param onCommentAdded 评论添加回调 - * @param onDismiss 关闭回调 - */ -@Composable -fun CommentModalContent( - postId: Int? = null, - onCommentAdded: () -> Unit = {}, - onDismiss: () -> Unit = {} -) { - val model = viewModel( - key = "CommentModalViewModel_$postId", - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return CommentModalViewModel(postId) as T - } - } - ) - var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - - - val scope = rememberCoroutineScope() - val comments = model.commentsFlow.collectAsLazyPagingItems() - val insets = WindowInsets - val imePadding = insets.ime.getBottom(density = LocalDensity.current) - var bottomPadding by remember { mutableStateOf(0.dp) } - LaunchedEffect(imePadding) { - bottomPadding = imePadding.dp - } - DisposableEffect(Unit) { - onDispose { - onDismiss() - } - } - var commentText by remember { mutableStateOf("") } - suspend fun sendComment() { - if (commentText.isNotEmpty()) { - model.createComment(commentText) - } - comments.refresh() - onCommentAdded() - } - Column( - modifier = Modifier - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, bottom = 16.dp, end = 16.dp) - - ) { - Text( - "评论", - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.align(Alignment.Center) - ) - } - - HorizontalDivider( - color = Color(0xFFF7F7F7) - ) - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .weight(1f) - ) { - CommentsSection(lazyPagingItems = comments, onLike = { commentEntity: CommentEntity -> - scope.launch { - if (commentEntity.liked) { - model.commentService.dislikeComment(commentEntity.id) - } else { - model.commentService.likeComment(commentEntity.id) - } - comments.refresh() - } - }) { - } - - } - Column( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xfff7f7f7)) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .height(64.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - verticalAlignment = Alignment.CenterVertically - ) { - // rounded - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .clip(RoundedCornerShape(20.dp)) - .background(Color(0xffe5e5e5)) - .padding(horizontal = 16.dp, vertical = 12.dp) - - ) { - BasicTextField( - value = commentText, - onValueChange = { text -> commentText = text }, - modifier = Modifier - .fillMaxWidth(), - textStyle = TextStyle( - color = Color.Black, - fontWeight = FontWeight.Normal - ) - ) - - } - Spacer(modifier = Modifier.width(16.dp)) - Image( - painter = painterResource(id = R.drawable.rider_pro_send), - contentDescription = "Send", - modifier = Modifier - .size(32.dp) - .noRippleClickable { - scope.launch { - sendComment() - } - } - ) - } - - - } - Spacer(modifier = Modifier.height(navBarHeight)) - - } - - - } - -} - - - - -### ui/comment/CommentsScreen.kt ### -package com.aiosman.riderpro.ui.comment - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder - -@Preview -@Composable -fun CommentsScreen() { - StatusBarMaskLayout( - darkIcons = true, - maskBoxBackgroundColor = Color(0xFFFFFFFF) - ) { - Column( - modifier = Modifier - .weight(1f) - .background(color = Color(0xFFFFFFFF)) - .padding(horizontal = 16.dp) - ) { - NoticeScreenHeader("COMMENTS") - Spacer(modifier = Modifier.height(28.dp)) - LazyColumn( - modifier = Modifier.weight(1f) - ) { - item { - repeat(20) { - CommentsItem() - } - BottomNavigationPlaceholder() - } - } - } - } - -} - -@Composable -fun NoticeScreenHeader( - title:String, - moreIcon: Boolean = true -) { - val nav = LocalNavController.current - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_back), - contentDescription = title, - modifier = Modifier.size(16.dp).clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - nav.popBackStack() - } - ) - Spacer(modifier = Modifier.size(12.dp)) - Text(title, fontWeight = FontWeight.W800, fontSize = 17.sp) - if (moreIcon) { - Spacer(modifier = Modifier.weight(1f)) - Image( - painter = painterResource(id = R.drawable.rider_pro_more_horizon), - contentDescription = "More", - modifier = Modifier.size(24.dp) - ) - } - - - } -} - -@Composable -fun CommentsItem() { - Box( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth() - ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "Avatar", - modifier = Modifier - .size(40.dp) - ) - Spacer(modifier = Modifier.size(12.dp)) - Column( - modifier = Modifier.weight(1f) - ) { - Text("Username", fontWeight = FontWeight.Bold, fontSize = 16.sp) - Spacer(modifier = Modifier.size(4.dp)) - Text("Content", color = Color(0x99000000), fontSize = 12.sp) - Spacer(modifier = Modifier.size(4.dp)) - Text("Date", color = Color(0x99000000), fontSize = 12.sp) - Spacer(modifier = Modifier.height(16.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_like), - contentDescription = "Like", - modifier = Modifier.size(16.dp), - ) - Text( - "270", - color = Color(0x99000000), - fontSize = 12.sp, - modifier = Modifier.padding(start = 4.dp) - ) - Spacer(modifier = Modifier.width(45.dp)) - Image( - painter = painterResource(id = R.drawable.rider_pro_comments), - contentDescription = "Comments", - modifier = Modifier.size(16.dp) - ) - Text( - "270", - color = Color(0x99000000), - fontSize = 12.sp, - modifier = Modifier.padding(start = 4.dp) - ) - } - } - Box { - Image( - painter = painterResource(id = R.drawable.default_moment_img), - contentDescription = "More", - modifier = Modifier.size(64.dp) - ) - } - } - } -} - -### ui/location/LocationDetailScreen.kt ### -package com.aiosman.riderpro.ui.location - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.ScrollableDefaults -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid -import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells -import androidx.compose.foundation.lazy.staggeredgrid.items -import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.LocationOn -import androidx.compose.material3.BottomSheetScaffold -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.Text -import androidx.compose.material3.rememberBottomSheetScaffoldState -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.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R - -data class OfficialGalleryItem( - val id: Int, - val resId: Int, -) - -fun getOfficialGalleryItems(): List { - return listOf( - OfficialGalleryItem(1, R.drawable.default_moment_img), - OfficialGalleryItem(2, R.drawable.default_moment_img), - OfficialGalleryItem(3, R.drawable.default_moment_img), - OfficialGalleryItem(4, R.drawable.default_moment_img), - OfficialGalleryItem(5, R.drawable.default_moment_img), - OfficialGalleryItem(6, R.drawable.default_moment_img), - OfficialGalleryItem(7, R.drawable.default_moment_img), - OfficialGalleryItem(8, R.drawable.default_moment_img), - OfficialGalleryItem(9, R.drawable.default_moment_img), - OfficialGalleryItem(10, R.drawable.default_moment_img), - OfficialGalleryItem(11, R.drawable.default_moment_img), - OfficialGalleryItem(12, R.drawable.default_moment_img), - OfficialGalleryItem(13, R.drawable.default_moment_img), - OfficialGalleryItem(14, R.drawable.default_moment_img), - OfficialGalleryItem(15, R.drawable.default_moment_img), - OfficialGalleryItem(16, R.drawable.default_moment_img), - OfficialGalleryItem(17, R.drawable.default_moment_img), - OfficialGalleryItem(18, R.drawable.default_moment_img), - OfficialGalleryItem(19, R.drawable.default_moment_img), - OfficialGalleryItem(20, R.drawable.default_moment_img), - ) -} - -data class FeedItem( - val id: Int, - val resId: Int, -) - -fun getFeedItems(): List { - val image_pickups = listOf( - R.drawable.default_moment_img, - R.drawable.default_avatar, - R.drawable.rider_pro_moment_demo_1, - R.drawable.rider_pro_moment_demo_2, - R.drawable.rider_pro_moment_demo_3, - ) - return (1..100).map { - FeedItem(it, image_pickups.random()) - } -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) -@Composable -fun LocationDetailScreen(x: Float, y: Float) { - val scope = rememberCoroutineScope() - val scaffoldState = rememberBottomSheetScaffoldState( - SheetState( - skipPartiallyExpanded = false, - density = LocalDensity.current, initialValue = SheetValue.PartiallyExpanded, - skipHiddenState = true - ) - ) - val configuration = LocalConfiguration.current - val officialGalleryItems = getOfficialGalleryItems() - val feedItems = getFeedItems() - val navController = LocalNavController.current - - // 2/3 height of the screen - fun getPeekHeight(): Dp { - val screenHeight = configuration.screenHeightDp - val peekHeight = (screenHeight * 2 / 3).dp - return peekHeight - } - - fun getNoPeekHeight(): Dp { - val screenHeight = configuration.screenHeightDp - val peekHeight = (screenHeight * 1 / 3).dp - return peekHeight - } - val view = LocalView.current - -// LaunchedEffect(key1 = Unit) { -// val locationOnScreen = IntArray(2).apply { -// view.getLocationOnScreen(this) -// } -// val startX = x - locationOnScreen[0] -// val startY = y - locationOnScreen[1] -// val radius = hypot(view.width.toDouble(), view.height.toDouble()).toFloat() -// -// val anim = ViewAnimationUtils.createCircularReveal(view, startX.toInt(), startY.toInt(), 0f, radius).apply { -// duration = 600 -// start() -// } -// -// } - - - - - val staggeredGridState = rememberLazyStaggeredGridState() - val coroutineScope = rememberCoroutineScope() - var showGalleryAndInfo by remember { mutableStateOf(true) } - LaunchedEffect(staggeredGridState) { - snapshotFlow { staggeredGridState.firstVisibleItemIndex } - .collect { index -> - // Assuming index 0 corresponds to the top of the feed - showGalleryAndInfo = index == 0 - } - - } - Box( - modifier = Modifier.fillMaxSize().background(Color.Transparent) - ) { - Image( - painter = painterResource(id = R.drawable.default_moment_img), - contentDescription = "Location Image", - modifier = Modifier - .fillMaxWidth() - .height(getNoPeekHeight() + 100.dp), - contentScale = ContentScale.Crop - ) - val bottomSheetScaffoldState = rememberBottomSheetScaffoldState() - BottomSheetScaffold( - scaffoldState = scaffoldState, - sheetPeekHeight = getPeekHeight(), - sheetShadowElevation = 0.dp, - sheetContainerColor = Color.Transparent, - sheetShape = RoundedCornerShape(16.dp, 16.dp, 0.dp, 0.dp), - sheetDragHandle = null, - sheetContent = { - Column( - Modifier - .fillMaxWidth() - .background(color = Color.White) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(12.dp)) - // 自定义短线 - Box( - modifier = Modifier - .width(32.dp) // 修改宽度 - .height(4.dp) // 修改高度 - .background( - Color(0f, 0f, 0f, 0.4f), - RoundedCornerShape(3.dp) - ) // 修改颜色和圆角 - .padding(top = 12.dp) // 调整位置 - .align(Alignment.CenterHorizontally) - - ) - - } - Spacer(modifier = Modifier.height(16.dp)) - - GalleryAndInfo(showGalleryAndInfo) - // feed - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp) - .animateContentSize() - ) { - AnimatedVisibility(visible = !showGalleryAndInfo) { - Row { - Icon( - Icons.Filled.LocationOn, - contentDescription = "Location", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("在云龟山景区的", fontSize = 16.sp) - } - } - Text( - "车友动态", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f) - ) - } - LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Fixed(2), // Set to 2 columns - modifier = Modifier.fillMaxSize(), - state = staggeredGridState, - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - flingBehavior = ScrollableDefaults.flingBehavior(), - - ) { - items(feedItems) { item -> - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) { - Image( - painter = painterResource(id = item.resId), - contentDescription = "Feed", - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .clickable { - navController.navigate("Post") - }, - contentScale = ContentScale.FillWidth - ) - Spacer(modifier = Modifier.height(8.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - ) { - Box( - modifier = Modifier.fillMaxWidth() - ) { - Text("Some text") - - } - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "Avatar", - modifier = Modifier - .size(18.dp) - .clip(CircleShape), - contentScale = ContentScale.Crop - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - "Username", - color = Color(0xFF666666), - fontSize = 14.sp - ) - Spacer(modifier = Modifier.weight(1f)) - Icon( - Icons.Filled.Favorite, - contentDescription = "Location" - ) - Spacer(modifier = Modifier.width(4.dp)) - Text("100K", fontSize = 14.sp) - } - } - - } - - } - - } - - - } - - }, - ) { - - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun GalleryAndInfo(showGalleryAndInfo: Boolean) { - val navController = LocalNavController.current - Column(modifier = Modifier.animateContentSize()) { - AnimatedVisibility(visible = showGalleryAndInfo) { - Column { - // info panel - Column( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - Text( - "Location Name", - modifier = Modifier.padding(top = 24.dp), - fontSize = 18.sp, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(8.dp)) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - repeat(10) { - Box( - modifier = Modifier - .background(Color(0xFFF5F5F5)) - .padding(horizontal = 7.dp, vertical = 2.dp) - - - ) { - Text("Tag $it", color = Color(0xFFb2b2b2), fontSize = 12.sp) - } - } - } - HorizontalDivider( - modifier = Modifier.padding(top = 16.dp), - color = Color(0xFFF5F5F5) - ) - Row( - modifier = Modifier.padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - "Location name", - fontSize = 14.sp, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(8.dp)) - Text("距离46KM,骑行时间77分钟", fontSize = 12.sp) - } - Spacer(modifier = Modifier.weight(1f)) - Image( - painter = painterResource(id = R.drawable.rider_pro_location_map), - contentDescription = "" - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .height(8.dp) - .background(Color(0xFFF5F5F5)) - ) - // official gallery - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 18.dp) - ) { - Row { - Text("官方摄影师作品", fontSize = 15.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.weight(1f)) - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_next), - contentDescription = "Next", - modifier = Modifier - .size(24.dp) - .clickable { - navController.navigate("OfficialPhoto") - } - ) - } - - Spacer(modifier = Modifier.height(17.dp)) - Row( - modifier = Modifier.height(232.dp) - ) { - Box( - modifier = Modifier.weight(1f) - ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "Avatar", - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(16.dp)), - contentScale = ContentScale.Crop, - ) - } - Spacer(modifier = Modifier.width(16.dp)) - Box( - modifier = Modifier.weight(1f) - ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "Avatar", - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(16.dp)), - contentScale = ContentScale.Crop - ) - } - } - } - Spacer(modifier = Modifier.height(16.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(8.dp) - .background(Color(0xFFF5F5F5)) - ) - } - } - } -} - - - -### ui/gallery/Gallery.kt ### -package com.aiosman.riderpro.ui.gallery - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.R -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@Composable -fun ProfileTimelineScreen() { - val pagerState = rememberPagerState(pageCount = { 2 }) - val scope = rememberCoroutineScope() - val systemUiController = rememberSystemUiController() - fun switchToPage(page: Int) { - scope.launch { - pagerState.animateScrollToPage(page) - } - } - LaunchedEffect(Unit) { - systemUiController.setNavigationBarColor(Color.Transparent) - } - Scaffold( - topBar = { - TopAppBar( - title = { - Text("Gallery") - }, - navigationIcon = { }, - actions = { }) - }, - ) { paddingValues: PaddingValues -> - Box(modifier = Modifier.padding(paddingValues)) { - Column(modifier = Modifier) { - ScrollableTabRow( - edgePadding = 0.dp, - selectedTabIndex = pagerState.currentPage, - modifier = Modifier, - divider = { }, - indicator = { tabPositions -> - Box( - modifier = Modifier - .tabIndicatorOffset(tabPositions[pagerState.currentPage]) - - ) { - Box( - modifier = Modifier - .align(Alignment.Center) - .height(4.dp) - .width(16.dp) - .background(color = Color.Red) - - ) - } - } - ) { - Tab( - text = { Text("Timeline", color = Color.Black) }, - selected = pagerState.currentPage == 0, - onClick = { switchToPage(0) } - - ) - Tab( - text = { Text("Position", color = Color.Black) }, - selected = pagerState.currentPage == 1, - onClick = { switchToPage(1) } - ) - } - HorizontalPager( - state = pagerState, - modifier = Modifier - .weight(1f) - .fillMaxSize() - ) { page -> - when (page) { - 0 -> GalleryTimeline() - 1 -> GalleryPosition() - } - } - } - } - - } -} -@Composable -fun GalleryTimeline() { - val mockList = listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "10") - Box( - modifier = Modifier - .fillMaxSize() - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - ) { - items(mockList) { item -> - TimelineItem() - } - - } - } - -} -@Composable -fun DashedVerticalLine(modifier: Modifier = Modifier) { - BoxWithConstraints(modifier = modifier) { - Canvas(modifier = Modifier.height(maxHeight)) { - val path = Path().apply { - moveTo(size.width / 2, 0f) - lineTo(size.width / 2, size.height) - } - drawPath( - path = path, - color = Color.Gray, - ) - } - } -} -@Composable -fun DashedLine() { - Canvas(modifier = Modifier - .width(1.dp) // 控制线条的宽度 - .fillMaxHeight()) { // 填满父容器的高度 - - val canvasWidth = size.width - val canvasHeight = size.height - - // 创建一个PathEffect来定义如何绘制线段 - val pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) - - drawLine( - color = Color.Gray, // 线条颜色 - start = Offset(x = canvasWidth / 2, y = 0f), // 起始点 - end = Offset(x = canvasWidth / 2, y = canvasHeight), // 终点 - pathEffect = pathEffect // 应用虚线效果 - ) - } -} -@Preview -@Composable -fun TimelineItem() { - val itemsList = listOf("1", "2", "3", "4", "5", "6", "7", "8", "9") - Box( - modifier = Modifier - .padding(16.dp) - .fillMaxSize() - .wrapContentWidth() - - ) { - Row( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - ) { - Column( - modifier = Modifier - .width(64.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("12", fontSize = 22.sp, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) - Text("7月", fontSize = 20.sp,fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) - // add vertical dash line -// Box( -// modifier = Modifier -// .height(120.dp) -// .width(3.dp) -// .background(Color.Gray) -// ) - DashedLine() - } - Column { - Row( - modifier = Modifier - .padding(bottom = 16.dp) - ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "", - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - .clip(CircleShape) // Clip the image to a circle - ) - Text("Onyama Limba") - } - Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - ) { - Column { - repeat(3) { // Create three rows - Row(modifier = Modifier.weight(1f)) { - repeat(3) { // Create three columns in each row - Box( - modifier = Modifier - .weight(1f) - .aspectRatio(1f) // Keep the aspect ratio 1:1 for square shape - .padding(4.dp) - ){ - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Gray) - ) { - Text("1") - } - } - } - } - } - } - } - } - } - } -} - -@Composable -fun GalleryPosition() { - val mockList = listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "10") - Box( - modifier = Modifier - .fillMaxSize() - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - ) { - items(mockList) { item -> - TimelineItem() - } - - } - } -} - -### ui/gallery/OfficialGallery.kt ### -package com.aiosman.riderpro.ui.gallery - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder - -@Preview -@Composable -fun OfficialGalleryScreen() { - StatusBarMaskLayout { - Column( - modifier = Modifier - .fillMaxSize() - .padding(start = 16.dp, end = 16.dp) - ) { - OfficialGalleryPageHeader() - Spacer(modifier = Modifier.height(16.dp)) - ImageGrid() - } - } - -} - -@Composable -fun OfficialGalleryPageHeader() { - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.rider_pro_nav_back), // Replace with your logo resource - contentDescription = "Logo", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = "官方摄影师作品", fontWeight = FontWeight.Bold, fontSize = 16.sp) - } -} - -@Composable -fun CertificationSection() { - Row( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFFFFF3CD), RoundedCornerShape(8.dp)) - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.ic_launcher_foreground), // Replace with your certification icon resource - contentDescription = "Certification Icon", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = "成为认证摄影师", fontWeight = FontWeight.Bold, fontSize = 16.sp) - Spacer(modifier = Modifier.weight(1f)) - Button( - onClick = { /*TODO*/ }, - - ) { - Text(text = "去认证", color = Color.White) - } - } -} - -@Composable -fun ImageGrid() { - val photographers = listOf( - Pair( - "Diego Morata", - R.drawable.rider_pro_moment_demo_1 - ), // Replace with your image resources - Pair("Usha Oliver", R.drawable.rider_pro_moment_demo_2), - Pair("Mohsen Salehi", R.drawable.rider_pro_moment_demo_3), - Pair("Thanawan Chadee", R.drawable.rider_pro_moment_demo_1), - Pair("Photographer 5", R.drawable.rider_pro_moment_demo_2), - Pair("Photographer 6", R.drawable.rider_pro_moment_demo_3), - Pair( - "Diego Morata", - R.drawable.rider_pro_moment_demo_1 - ), // Replace with your image resources - Pair("Usha Oliver", R.drawable.rider_pro_moment_demo_2), - Pair("Mohsen Salehi", R.drawable.rider_pro_moment_demo_3), - Pair("Thanawan Chadee", R.drawable.rider_pro_moment_demo_1), - Pair("Photographer 5", R.drawable.rider_pro_moment_demo_2), - Pair("Photographer 6", R.drawable.rider_pro_moment_demo_3), - Pair( - "Diego Morata", - R.drawable.rider_pro_moment_demo_1 - ), // Replace with your image resources - Pair("Usha Oliver", R.drawable.rider_pro_moment_demo_2), - Pair("Mohsen Salehi", R.drawable.rider_pro_moment_demo_3), - Pair("Thanawan Chadee", R.drawable.rider_pro_moment_demo_1), - Pair("Photographer 5", R.drawable.rider_pro_moment_demo_2), - Pair("Photographer 6", R.drawable.rider_pro_moment_demo_3), - Pair( - "Diego Morata", - R.drawable.rider_pro_moment_demo_1 - ), // Replace with your image resources - Pair("Usha Oliver", R.drawable.rider_pro_moment_demo_2), - Pair("Mohsen Salehi", R.drawable.rider_pro_moment_demo_3), - Pair("Thanawan Chadee", R.drawable.rider_pro_moment_demo_1), - Pair("Photographer 5", R.drawable.rider_pro_moment_demo_2), - Pair("Photographer 6", R.drawable.rider_pro_moment_demo_3) - ) - - - LazyVerticalGrid( - columns = GridCells.Fixed(2), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(photographers.size) { index -> - PhotographerCard(photographers[index].first, photographers[index].second) - } - item{ - BottomNavigationPlaceholder() - } - } -} - -@Composable -fun PhotographerCard(name: String, imageRes: Int) { - val navController = LocalNavController.current - Box( - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(8.dp)) - .background(Color.LightGray) - ) { - Image( - painter = painterResource(id = imageRes), - contentDescription = name, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(270.dp) - ) - Box( - modifier = Modifier - .fillMaxSize() - .background(Color(0x55000000)) - .align(Alignment.BottomStart) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .clickable { - navController.navigate("OfficialPhotographer") - }, - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), // Replace with your profile picture resource - contentDescription = "Profile Picture", - modifier = Modifier - .size(24.dp) - .clip(CircleShape) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = name, color = Color.White) - } - } - - } -} - -### ui/gallery/OfficialPhotographer.kt ### -package com.aiosman.riderpro.ui.gallery - -import android.util.Log -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -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.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder - -data class ArtWork( - val id: Int, - val resId: Int, -) - -fun GenerateMockArtWorks(): List { - val pickupImage = listOf( - R.drawable.default_avatar, - R.drawable.default_moment_img, - R.drawable.rider_pro_moment_demo_1, - R.drawable.rider_pro_moment_demo_2, - R.drawable.rider_pro_moment_demo_3, - ) - return List(30) { - ArtWork( - id = it, - resId = pickupImage.random() - ) - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Preview -@Composable -fun OfficialPhotographerScreen() { - val lazyListState = rememberLazyListState() - var artWorks by remember { mutableStateOf>(emptyList()) } - LaunchedEffect(Unit) { - artWorks = GenerateMockArtWorks() - } - // Observe the scroll state and calculate opacity - val alpha by remember { - derivedStateOf { - // Example calculation: Adjust the range and formula as needed - val alp = minOf(1f, lazyListState.firstVisibleItemScrollOffset / 900f) - Log.d("alpha", "alpha: $alp") - alp - } - } - StatusBarMaskLayout( - maskBoxBackgroundColor = Color.Black, - darkIcons = false - ) { - Column { - Box( - modifier = Modifier - .background(Color.Black) - - - ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState - ) { - item { - Box( - modifier = Modifier - .height(400.dp) - .fillMaxWidth() - ) { - Image( - painter = painterResource(id = R.drawable.default_moment_img), - contentDescription = "Logo", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - // dark alpha overlay - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = alpha)) - ) - - // on bottom of box - Box( - modifier = Modifier - .fillMaxWidth() - .height(120.dp) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Black.copy(alpha = 1f), - Color.Black.copy(alpha = 1f), - Color.Black.copy(alpha = 0f), - ), - startY = Float.POSITIVE_INFINITY, - endY = 0f - ) - ) - .padding(16.dp) - .align(alignment = Alignment.BottomCenter) - - ) { - Column( - modifier = Modifier.fillMaxSize(), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "", - modifier = Modifier - .size(32.dp) - .clip(CircleShape) // Clip the image to a circle - ) - Spacer(modifier = Modifier.width(8.dp)) - // name - Text("Onyama Limba", color = Color.White, fontSize = 14.sp) - Spacer(modifier = Modifier.width(8.dp)) - // round box - Box( - modifier = Modifier - .background(Color.Red, CircleShape) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - // certification - Text("摄影师", color = Color.White, fontSize = 12.sp) - } - Spacer(modifier = Modifier.weight(1f)) - IconButton( - onClick = { /*TODO*/ }, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Filled.Favorite, - contentDescription = null, - tint = Color.White - ) - } - Spacer(modifier = Modifier.width(4.dp)) - Text("123", color = Color.White) - Spacer(modifier = Modifier.width(8.dp)) - IconButton( - onClick = {}, - modifier = Modifier.size(32.dp) - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_eye), - contentDescription = "", - modifier = Modifier - .size(24.dp) - ) - } - Spacer(modifier = Modifier.width(4.dp)) - Text("123", color = Color.White) - - } - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - // description - Text( - "摄影师 Diego Morata 的作品", - color = Color.White, - modifier = Modifier.align(Alignment.Center) - ) - } - } - - // circle avatar - - } - - - } - val screenWidth = LocalConfiguration.current.screenWidthDp.dp - val imageSize = - (screenWidth - (4.dp * 4)) / 3 // Subtracting total padding and divi - val itemWidth = screenWidth / 3 - 4.dp * 2 - FlowRow( - modifier = Modifier.padding(4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - maxItemsInEachRow = 3 - ) { - for (artWork in artWorks) { - Box( - modifier = Modifier - .width(itemWidth) - .aspectRatio(1f) - .background(Color.Gray) - ) { - Image( - painter = painterResource(id = artWork.resId), - contentDescription = "", - contentScale = ContentScale.Crop, - modifier = Modifier - .width(imageSize) - .aspectRatio(1f) - ) - } - } - BottomNavigationPlaceholder() - - } - } - } - Box( - modifier = Modifier - .fillMaxWidth() - .height(64.dp) - .background(Color.Black.copy(alpha = alpha)) - .padding(horizontal = 16.dp) - ) { - Row( - modifier = Modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_back), - colorFilter = ColorFilter.tint(Color.White), - contentDescription = "", - modifier = Modifier - .size(32.dp) - .clip(CircleShape) // Clip the image to a circle - ) - if (alpha == 1f) { - Spacer(modifier = Modifier.width(8.dp)) - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "", - modifier = Modifier - .size(32.dp) - .clip(CircleShape) // Clip the image to a circle - - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Onyama Limba", color = Color.White) - } - } - - } - } - - } - } - -} - -### ui/modification/AddModification.kt ### -package com.aiosman.riderpro.ui.modification - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -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.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.ui.post.NewPostViewModel -import com.aiosman.riderpro.ui.comment.NoticeScreenHeader -import com.aiosman.riderpro.R -import com.aiosman.riderpro.utils.Utils - -@Preview -@Composable -fun EditModificationScreen() { - val model = NewPostViewModel - Column( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFFf8f8f8)) - ) { - Box( - modifier = Modifier.padding(vertical = 16.dp, horizontal = 18.dp) - ) { - NoticeScreenHeader("Modification List") - } - LazyColumn( - modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp) - ) { - items(NewPostViewModel.modificationList) { mod -> - AddModificationItem(mod) { updatedMod -> - NewPostViewModel.modificationList = NewPostViewModel.modificationList.map { existingMod -> - if (existingMod.key == updatedMod.key) updatedMod else existingMod - }.toMutableList() - } - Spacer(modifier = Modifier.height(16.dp)) - } - item { - AddModificationButton { - NewPostViewModel.modificationList += Modification( - key = Utils.generateRandomString(4), - name = "", - price = "0.0" - ) - } - } - } - } -} - -data class Modification( - val key: String, - val name: String, - val price: String -) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddModificationItem(modification: Modification, onUpdate: (Modification) -> Unit) { - var isEditPriceBottomModalVisible by remember { mutableStateOf(false) } - if (isEditPriceBottomModalVisible) { - ModalBottomSheet( - onDismissRequest = { - isEditPriceBottomModalVisible = false - }, - containerColor = Color.White, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ), - dragHandle = {}, - scrimColor = Color.Transparent, - shape = RectangleShape - ) { - EditPriceBottomModal { - isEditPriceBottomModalVisible = false - onUpdate( - modification.copy(price = it) - ) - } - } - } - Column { - Box( - modifier = Modifier - .fillMaxWidth() - .background(Color.White) - .padding(vertical = 13.dp, horizontal = 16.dp), - - - ) { - if (modification.name.isEmpty()) { - Text("Please enter the name", fontSize = 14.sp, color = Color(0xFFd6d6d6)) - } - BasicTextField( - value = modification.name, - onValueChange = { - onUpdate( - modification.copy(name = it) - ) - }, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center) - ) - } - Spacer(modifier = Modifier.height(1.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .background(Color.White) - .padding(top = 13.dp, bottom = 13.dp, start = 16.dp, end = 8.dp) - .clickable { - isEditPriceBottomModalVisible = true - } - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text("Price", fontSize = 16.sp) - Spacer(modifier = Modifier.weight(1f)) - Text(modification.price, fontSize = 16.sp, color = Color(0xffda3832)) - Spacer(modifier = Modifier.width(6.dp)) - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_next), - contentDescription = "Edit", - modifier = Modifier.size(24.dp) - ) - } - } - } -} - -@Composable -fun AddModificationButton(onClick: () -> Unit = {}) { - val stroke = Stroke( - width = 2f, - pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) - ) - Box( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .drawBehind { - drawRoundRect(color = Color(0xFFd6d6d6), style = stroke) - } - .clickable { - onClick() - } - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic), - contentDescription = "Add", - modifier = Modifier - .size(24.dp) - .align(Alignment.Center), - ) - } -} - -@Composable -fun EditPriceBottomModal(onOkClick: (String) -> Unit = {}) { - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - - var text by remember { mutableStateOf("") } - - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - - - // Modal content including BasicTextField - Row( - modifier = Modifier - .fillMaxWidth() - .background(Color.White) - .padding(16.dp) - ) { - Text("Price", fontSize = 16.sp) - Spacer(modifier = Modifier.width(16.dp)) - BasicTextField( - value = text, - onValueChange = { text = it }, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - singleLine = true, - keyboardActions = KeyboardActions( - onDone = { - keyboardController?.hide() - // Logic to close the dialog/modal - onOkClick(text) - } - ), - ) - } -} - -### ui/modification/ModificationList.kt ### -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder -import com.aiosman.riderpro.ui.comment.NoticeScreenHeader -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.google.accompanist.systemuicontroller.rememberSystemUiController - -@Preview -@Composable -fun ModificationListScreen() { - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setNavigationBarColor(Color.Transparent) - } - val modifications = getModifications() - StatusBarMaskLayout { - Column( - modifier = Modifier - .weight(1f) - .padding(horizontal = 16.dp) - .background(Color(0xFFF8F8F8)) - - ) { - NoticeScreenHeader("Modification List") - Spacer(modifier = Modifier.height(8.dp)) - LazyColumn(modifier = Modifier.padding(16.dp)) { - items(modifications.size) { index -> - val modification = modifications[index] - ModificationItem(name = modification.name, price = modification.price) - Spacer(modifier = Modifier.height(8.dp)) - } - item { - BottomNavigationPlaceholder() - } - } - } - } -} - -data class Modification(val name: String, val price: String) - -fun getModifications(): List { - return listOf( - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - Modification("Modification name", "$74.00"), - ) -} - -@Composable -fun ModificationItem(name: String, price: String) { - Card( - shape = RoundedCornerShape(8.dp), - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = Color.White, - ) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = name, fontSize = 16.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = price, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.Red) - } - } -} - - -### ui/follower/FollowerPage.kt ### -package com.aiosman.riderpro.ui.follower - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -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.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.data.AccountFollow -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.comment.NoticeScreenHeader -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import kotlinx.coroutines.launch - -/** - * 关注消息列表 - */ -@Composable -fun FollowerScreen() { - val scope = rememberCoroutineScope() - StatusBarMaskLayout( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - val model = FollowerViewModel - var dataFlow = model.followerItemsFlow - var followers = dataFlow.collectAsLazyPagingItems() - Box( - modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp) - ) { - NoticeScreenHeader("FOLLOWERS") - - } - LaunchedEffect(Unit) { - model.updateNotice() - } - LazyColumn( - modifier = Modifier.weight(1f) - ) { - items(followers.itemCount) { index -> - followers[index]?.let { follower -> - FollowerItem(follower) { - scope.launch { - model.followUser(follower.userId) - } - } - } - } - } - } -} - - -@Composable -fun FollowerItem( - item: AccountFollow, - onFollow: () -> Unit = {} -) { - val context = LocalContext.current - val navController = LocalNavController.current - Box( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - CustomAsyncImage( - context = context, - imageUrl = item.avatar, - contentDescription = item.nickname, - modifier = Modifier - .size(40.dp) - .noRippleClickable { - navController.navigate( - NavigationRoute.AccountProfile.route.replace( - "{id}", - item.userId.toString() - ) - ) - } - ) - Spacer(modifier = Modifier.width(12.dp)) - Column( - modifier = Modifier.weight(1f) - ) { - Text(item.nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp) - } - if (!item.isFollowing) { - Box( - modifier = Modifier.noRippleClickable { - onFollow() - } - ) { - Image( - painter = painterResource(id = R.drawable.follow_bg), - contentDescription = "Follow", - modifier = Modifier - .width(79.dp) - .height(24.dp) - ) - Text( - "FOLLOW", - fontSize = 14.sp, - color = Color(0xFFFFFFFF), - modifier = Modifier.align( - Alignment.Center - ) - ) - } - } - - - } - } -} - -### ui/follower/FollowerViewModel.kt ### -package com.aiosman.riderpro.ui.follower - -import android.icu.util.Calendar -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import com.aiosman.riderpro.data.AccountFollow -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.entity.FollowItemPagingSource -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.UserServiceImpl -import com.aiosman.riderpro.data.UserService -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -/** - * 关注消息列表的 ViewModel - */ -object FollowerViewModel : ViewModel() { - private val accountService: AccountService = AccountServiceImpl() - private val userService: UserService = UserServiceImpl() - private val _followerItemsFlow = - MutableStateFlow>(PagingData.empty()) - val followerItemsFlow = _followerItemsFlow.asStateFlow() - - init { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - FollowItemPagingSource( - accountService - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _followerItemsFlow.value = it - } - } - } - - private fun updateIsFollow(id: Int) { - val currentPagingData = _followerItemsFlow.value - val updatedPagingData = currentPagingData.map { follow -> - if (follow.userId == id) { - follow.copy(isFollowing = true) - } else { - follow - } - } - _followerItemsFlow.value = updatedPagingData - } - suspend fun followUser(userId: Int) { - userService.followUser(userId.toString()) - updateIsFollow(userId) - } - - suspend fun updateNotice() { - var now = Calendar.getInstance().time - accountService.updateNotice( - UpdateNoticeRequestBody( - lastLookFollowTime = ApiClient.formatTime(now) - ) - ) - } -} - -### ui/message/Message.kt ### -package com.aiosman.riderpro.ui.message - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.paging.LoadState -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.ui.index.tabs.moment.MomentListLoading -import com.aiosman.riderpro.R -import com.aiosman.riderpro.model.ChatNotificationData -import com.aiosman.riderpro.model.TestChatBackend - -val chatNotificationData = ChatNotificationData( - R.drawable.default_avatar, - "Tokunaga Yae", - "Memphis", - "3 minutes ago", - 6 -) - -private val ChatData = (0..10).toList().map { chatNotificationData } - -@Composable -fun MessagePage(){ - val myBackend = remember { TestChatBackend(ChatData) } - val pager = remember { - Pager( - PagingConfig( - pageSize = myBackend.DataBatchSize, - enablePlaceholders = true, - maxSize = 200 - ) - ) { - myBackend.getAllData() - } - } - val lazyPagingItems = pager.flow.collectAsLazyPagingItems() - MessageTopSwitchBtnGroup() - MessageBarrierLine() - MessageNotification() - LazyColumn ( - modifier = Modifier.padding(bottom = 20.dp) - ){ - if (lazyPagingItems.loadState.refresh == LoadState.Loading) { - item { - MomentListLoading() - } - } - items(count = lazyPagingItems.itemCount) { index -> - val item = lazyPagingItems[index] - if (item != null) { - ChatItem(item) - } - } - if (lazyPagingItems.loadState.append == LoadState.Loading) { - item { - MomentListLoading() - } - } - } - -} - -@Composable -fun MessageTopSwitchBtnGroup(){ - Column ( - modifier = Modifier - .fillMaxWidth() - .height(113.dp) - ) { - Row(modifier = Modifier.fillMaxSize()){ - val notificationBtnModifier = Modifier - .fillMaxHeight() - .weight(1f) - NotificationBtn(notificationBtnModifier,drawableId = R.drawable.rider_pro_like, - displayText = "LIKE") - NotificationBtn(notificationBtnModifier,drawableId = R.drawable.rider_pro_followers, - displayText = "FOLLOWERS") - NotificationBtn(notificationBtnModifier,drawableId = R.drawable.rider_pro_comments, - displayText = "COMMENTS") - } - } -} - -@Composable -fun MessageBarrierLine(){ - Box(modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .padding(start = 24.dp, end = 24.dp) - .border(width = 1.dp, Color(0f, 0f, 0f, 0.1f)) - ) -} - -@Composable -fun NotificationBtn(modifier: Modifier, drawableId: Int, displayText: String){ - Box(modifier = modifier, - contentAlignment = Alignment.Center){ - Column(modifier = Modifier - .size(width = 79.dp, height = 88.dp), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally){ - Box(modifier = Modifier - .padding(top = 8.dp) - .size(width = 55.dp, height = 55.dp) - .shadow( - spotColor = Color.White, - ambientColor = Color(0f, 0f, 0f, 0.4f), - elevation = 12.dp, - ), - contentAlignment = Alignment.Center, - ){ - Image( - modifier = Modifier - .size(width = 24.dp, height = 24.dp), - painter = painterResource(id = drawableId), - contentDescription = "" - ) - } - Text( - modifier = Modifier.padding(top = 8.dp), - text = displayText, - fontSize = 12.sp, style = TextStyle(fontWeight = FontWeight.Bold) - ) - } - } -} - -@Composable -fun MessageNotification(){ - Row(modifier = Modifier - .fillMaxWidth() - .height(88.dp) - .padding(start = 22.dp, top = 20.dp, bottom = 20.dp, end = 24.dp), - verticalAlignment = Alignment.CenterVertically){ - Box(modifier = Modifier - .size(width = 48.dp, height = 48.dp) - .border(width = 1.dp, Color(0f, 0f, 0f, 0.1f), RoundedCornerShape(2.dp)), - contentAlignment = Alignment.Center){ - Icon( - modifier = Modifier - .size(width = 24.dp, height = 24.dp), - painter = painterResource(R.drawable.rider_pro_notification), - contentDescription = "" - ) - } - Text(text = "NOTIFICATIONS", fontSize = 18.sp, modifier = Modifier.padding(start = 12.dp), style = TextStyle(fontWeight = FontWeight.Bold)) - Box(modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.CenterEnd){ - Box(modifier = Modifier - .height(18.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color.Red), - contentAlignment = Alignment.Center){ - Text(text = "18", fontSize = 10.sp, color = Color.White, modifier = Modifier.padding(start = 6.dp, end = 6.dp, top = 2.dp, bottom = 2.dp)) - } - } - } -} - -@Composable -fun ChatItem(chatNotificationData: ChatNotificationData){ - Row(modifier = Modifier - .fillMaxWidth() - .height(88.dp) - .padding(start = 22.dp, top = 20.dp, bottom = 20.dp, end = 24.dp), - verticalAlignment = Alignment.CenterVertically){ - Image(modifier = Modifier - .size(width = 48.dp, height = 48.dp) - .clip(RoundedCornerShape(2.dp)), - painter = painterResource(chatNotificationData.avatar), - contentDescription = "") - Column ( - modifier = Modifier.fillMaxHeight().padding(start = 12.dp), - verticalArrangement = Arrangement.SpaceAround - ){ - Text(text = chatNotificationData.name, fontSize = 18.sp, style = TextStyle(fontWeight = FontWeight.Bold)) - Text(text = chatNotificationData.message, fontSize = 14.sp, - color = Color(0f, 0f, 0f, 0.6f)) - } - - Box(modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.CenterEnd){ - Column ( - modifier = Modifier.fillMaxHeight(), - verticalArrangement = Arrangement.SpaceAround, - horizontalAlignment = Alignment.End - ){ - Text(text = chatNotificationData.time, fontSize = 12.sp,color = Color(0f, 0f, 0f, 0.4f)) - Box(modifier = Modifier - .height(18.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color.Red), - contentAlignment = Alignment.Center){ - Text(text = chatNotificationData.unread.toString(), fontSize = 10.sp, color = Color.White, modifier = Modifier.padding(start = 6.dp, end = 6.dp, top = 2.dp, bottom = 2.dp)) - } - } - } - } -} - -### ui/search/search.kt ### -package com.aiosman.riderpro.ui.search - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier - -@Composable -fun SearchScreen() { - Scaffold { paddingValues -> - var tabIndex by remember { mutableStateOf(0) } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues) - ) { - TextField( - value = "", - onValueChange = {}, - label = { Text("Search") }, - modifier = Modifier.fillMaxWidth(), - trailingIcon = { - Icon( - imageVector = Icons.Default.Search, - contentDescription = null - ) - } - ) - TabRow(selectedTabIndex = tabIndex) { - Tab(text = { Text("Post") }, - selected = tabIndex == 0, - onClick = { tabIndex = 0 } - ) - Tab(text = { Text("User") }, - selected = tabIndex == 1, - onClick = { tabIndex = 1 } - ) - } - when (tabIndex) { - 0 -> SearchPostResults() - 1 -> SearchUserResults() - } - } - } -} - -@Composable -fun SearchPostResults() { - Text("Post Results") -} -@Composable -fun SearchUserResults(){ - Text("User Results") -} - -### ui/imageviewer/ImageViewerViewModel.kt ### -package com.aiosman.riderpro.ui.imageviewer - -import com.aiosman.riderpro.entity.MomentImageEntity - -object ImageViewerViewModel { - var imageList = mutableListOf() - var initialIndex = 0 - fun asNew(images: List, index: Int = 0) { - imageList.clear() - imageList.addAll(images) - initialIndex = index - } -} - -### ui/imageviewer/imageviewer.kt ### -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import coil.compose.AsyncImage -import com.aiosman.riderpro.LocalAnimatedContentScope -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.LocalSharedTransitionScope -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import net.engawapg.lib.zoomable.rememberZoomState -import net.engawapg.lib.zoomable.zoomable - - -@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class) -@Composable -fun ImageViewer() { - val model = ImageViewerViewModel - val images = model.imageList - val pagerState = rememberPagerState(pageCount = { images.size }, initialPage = model.initialIndex) - val systemUiController = rememberSystemUiController() - val navController = LocalNavController.current - val sharedTransitionScope = LocalSharedTransitionScope.current - val animatedVisibilityScope = LocalAnimatedContentScope.current - val context = LocalContext.current - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Black) - systemUiController.setNavigationBarColor(Color.Black) - } - StatusBarMaskLayout( - modifier = Modifier.background(Color.Black), - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black) - ) { - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxSize() - ) { page -> - val zoomState = rememberZoomState() - with(sharedTransitionScope) { - CustomAsyncImage( - context, - images[page].url, - contentDescription = null, - modifier = Modifier.sharedElement( - rememberSharedContentState(key = images[page]), - animatedVisibilityScope = animatedVisibilityScope - ) - .fillMaxSize() - .zoomable( - zoomState = zoomState, - onTap = { - navController.popBackStack() - } - ), - contentScale = ContentScale.Fit, - ) - } - } - } - - } -} - -### ui/profile/AccountProfile.kt ### -package com.aiosman.riderpro.ui.profile - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -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.unit.dp -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.entity.MomentPagingSource -import com.aiosman.riderpro.entity.MomentRemoteDataSource -import com.aiosman.riderpro.entity.MomentServiceImpl -import com.aiosman.riderpro.data.UserServiceImpl -import com.aiosman.riderpro.data.UserService -import com.aiosman.riderpro.entity.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 -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 userService: UserService = UserServiceImpl() - var userProfile by remember { mutableStateOf(null) } - val momentService = MomentServiceImpl() - var momentsFlow by remember { mutableStateOf>?>(null) } - val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { - userProfile = userService.getUserProfile(id) - momentsFlow = Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - MomentPagingSource( - MomentRemoteDataSource(momentService), - author = id.toInt() - ) - } - ).flow - } - val items = momentsFlow?.collectAsLazyPagingItems() - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setNavigationBarColor( - color = androidx.compose.ui.graphics.Color.Transparent - ) - } - StatusBarMaskLayout( - modifier = Modifier.fillMaxSize(), - useNavigationBarMask = false - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - ) { - item { - CarGroup() - userProfile?.let { - 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() - } - if (items != null) { - items(items.itemCount) { idx -> - val momentItem = items[idx] ?: return@items - MomentPostUnit(momentItem) - } - } - } - } - -} - -### ui/profile/AccountProfileViewModel.kt ### -package com.aiosman.riderpro.ui.profile - -object AccountProfileViewModel { -} - -### ui/theme/Color.kt ### -package com.aiosman.riderpro.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) - -### ui/theme/Theme.kt ### -package com.aiosman.riderpro.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun RiderProTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} - -### ui/theme/Type.kt ### -package com.aiosman.riderpro.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) - -### ui/index/Index.kt ### -package com.aiosman.riderpro.ui.index - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemColors -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -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.platform.LocalDensity -import androidx.compose.ui.unit.dp -import androidx.navigation.navOptions -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.index.tabs.add.AddPage -import com.aiosman.riderpro.ui.index.tabs.message.NotificationsScreen -import com.aiosman.riderpro.ui.index.tabs.moment.MomentsList -import com.aiosman.riderpro.ui.index.tabs.profile.ProfilePage -import com.aiosman.riderpro.ui.index.tabs.search.SearchScreen -import com.aiosman.riderpro.ui.index.tabs.shorts.ShortVideo -import com.aiosman.riderpro.ui.index.tabs.street.StreetPage -import com.aiosman.riderpro.ui.message.MessagePage -import com.aiosman.riderpro.ui.post.NewPostViewModel -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import kotlinx.coroutines.launch - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun IndexScreen() { - val model = IndexViewModel - val navigationBarHeight = with(LocalDensity.current) { - WindowInsets.navigationBars.getBottom(this).toDp() - } - val navController = LocalNavController.current - val item = listOf( - NavigationItem.Home, - NavigationItem.Search, - NavigationItem.Add, - NavigationItem.Notification, - NavigationItem.Profile - ) - val systemUiController = rememberSystemUiController() - val pagerState = rememberPagerState(pageCount = { item.size }) - val coroutineScope = rememberCoroutineScope() - - LaunchedEffect(Unit) { - systemUiController.setNavigationBarColor(Color.Transparent) - } - Scaffold( - bottomBar = { - NavigationBar( - modifier = Modifier.height(56.dp + navigationBarHeight), - containerColor = Color.Black - ) { - item.forEachIndexed { idx, it -> - val isSelected = model.tabIndex == idx - val iconTint by animateColorAsState( - targetValue = if (isSelected) Color.Red else Color.White, - animationSpec = tween(durationMillis = 250), label = "" - ) - NavigationBarItem( - selected = isSelected, - onClick = { - if (it.route === NavigationItem.Add.route) { - NewPostViewModel.asNewPost() - navController.navigate(NavigationRoute.NewPost.route) - return@NavigationBarItem - } - coroutineScope.launch { - pagerState.scrollToPage(idx) - } - model.tabIndex = idx - }, - colors = NavigationBarItemColors( - selectedTextColor = Color.Red, - selectedIndicatorColor = Color.Black, - unselectedTextColor = Color.Red, - disabledIconColor = Color.Red, - disabledTextColor = Color.Red, - selectedIconColor = iconTint, - unselectedIconColor = iconTint, - ), - icon = { - Icon( - modifier = Modifier.size(24.dp), - imageVector = if (isSelected) it.selectedIcon() else it.icon(), - contentDescription = null, - tint = iconTint - ) - } - ) - } - } - } - ) { innerPadding -> - HorizontalPager( - state = pagerState, - modifier = Modifier.padding(innerPadding), - beyondBoundsPageCount = 5, - userScrollEnabled = false - ) { page -> - when (page) { - 0 -> Home() - 1 -> SearchScreen() - 2 -> Add() - 3 -> Notifications() - 4 -> Profile() - } - } - } -} - -@Composable -fun Home() { - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true) - } - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - MomentsList() - } -} - - -@Composable -fun Street() { - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true) - } - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - StreetPage() - } -} - -@Composable -fun Add() { - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Black, darkIcons = false) - } - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.Black), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - AddPage() - } -} - -@Composable -fun Video() { - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Black, darkIcons = false) - } - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - ShortVideo() - } -} - - -@Composable -fun Profile() { - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true) - } - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - ProfilePage() - } -} - -@Composable -fun Notifications() { - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true) - } - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - NotificationsScreen() - } -} - -### ui/index/IndexViewModel.kt ### -package com.aiosman.riderpro.ui.index - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel - -object IndexViewModel:ViewModel() { - var tabIndex by mutableStateOf(0) -} - -### ui/index/NavigationItem.kt ### -package com.aiosman.riderpro.ui.index - -import androidx.compose.material.Icon -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import com.aiosman.riderpro.R - -sealed class NavigationItem( - val route: String, - val icon: @Composable () -> ImageVector, - val selectedIcon: @Composable () -> ImageVector = icon -) { - data object Home : NavigationItem("Home", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_home) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_home_filed) } - ) - - data object Street : NavigationItem("Street", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_location) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_location_filed) } - ) - - data object Add : NavigationItem("Add", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_moment_add) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_moment_add) } - ) - - data object Message : NavigationItem("Message", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_video_outline) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_video) } - ) - - data object Notification : NavigationItem("Notification", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_notification) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_notification) } - ) - - data object Profile : NavigationItem("Profile", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_profile) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_profile_filed) } - ) - - data object Search : NavigationItem("Search", - icon = { Icons.Default.Search }, - selectedIcon = { Icons.Default.Search } - ) -} - -### ui/index/tabs/moment/Moment.kt ### -package com.aiosman.riderpro.ui.index.tabs.moment - -import androidx.annotation.DrawableRes -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Build -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -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.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -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 -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.paging.LoadState -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.LocalAnimatedContentScope -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.LocalSharedTransitionScope -import com.aiosman.riderpro.R -import com.aiosman.riderpro.entity.MomentEntity -import com.aiosman.riderpro.entity.MomentImageEntity -import com.aiosman.riderpro.exp.timeAgo -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.CustomAsyncImage -import com.aiosman.riderpro.ui.composables.RelPostCard -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.aiosman.riderpro.ui.post.NewPostViewModel -import com.aiosman.riderpro.ui.post.PostViewModel -import kotlinx.coroutines.launch - -/** - * 动态列表 - */ -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun MomentsList() { - val model = MomentViewModel - var dataFlow = model.momentsFlow - var moments = dataFlow.collectAsLazyPagingItems() - val scope = rememberCoroutineScope() - var refreshing by remember { mutableStateOf(false) } - val state = rememberPullRefreshState(refreshing, onRefresh = { - model.refreshPager() - }) - LaunchedEffect(moments.loadState) { - if (moments.loadState.refresh is LoadState.Loading) { - refreshing = true - } else { - refreshing = false - } - } - - Box(Modifier.pullRefresh(state)) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items( - moments.itemCount, - key = { idx -> moments[idx]?.id ?: idx } - ) { idx -> - val momentItem = moments[idx] ?: return@items - MomentCard(momentEntity = momentItem, - onAddComment = { - scope.launch { - model.onAddComment(momentItem.id) - } - }, - onLikeClick = { - scope.launch { - if (momentItem.liked) { - model.dislikeMoment(momentItem.id) - } else { - model.likeMoment(momentItem.id) - } - } - }, - onFavoriteClick = { - scope.launch { - if (momentItem.isFavorite) { - model.unfavoriteMoment(momentItem.id) - } else { - model.favoriteMoment(momentItem.id) - } - } - } - ) - } - } - PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) - } -} - -@Composable -fun MomentCard( - momentEntity: MomentEntity, - onLikeClick: () -> Unit = {}, - onFavoriteClick: () -> Unit = {}, - onAddComment: () -> Unit = {}, - hideAction: Boolean = false -) { - val navController = LocalNavController.current - Column( - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) { - MomentTopRowGroup(momentEntity = momentEntity) - } - Column( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { - PostViewModel.preTransit(momentEntity) - navController.navigate("Post/${momentEntity.id}") - } - ) { - MomentContentGroup(momentEntity = momentEntity) - } - val momentOperateBtnBoxModifier = Modifier - .fillMaxHeight() - .weight(1f) -// ModificationListHeader() - if (!hideAction) { - MomentBottomOperateRowGroup( - momentOperateBtnBoxModifier, - momentEntity = momentEntity, - onLikeClick = onLikeClick, - onAddComment = onAddComment, - onShareClick = { - NewPostViewModel.asNewPost() - NewPostViewModel.relPostId = momentEntity.id - navController.navigate(NavigationRoute.NewPost.route) - }, - onFavoriteClick = onFavoriteClick - ) - } - - } -} - -@Composable -fun ModificationListHeader() { - val navController = LocalNavController.current - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFFF8F8F8)) - .padding(4.dp) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - navController.navigate("ModificationList") - } - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .background(Color(0xFFEB4869)) - .padding(8.dp) - ) { - Icon( - Icons.Filled.Build, - contentDescription = "Modification Icon", - tint = Color.White, // Assuming the icon should be white - modifier = Modifier.size(12.dp) - ) - } - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = "Modification List", - color = Color(0xFF333333), - fontSize = 14.sp, - textAlign = TextAlign.Left - ) - } - } - } -} - -@Composable -fun MomentName(name: String) { - Text( - modifier = Modifier, - textAlign = TextAlign.Start, - text = name, - color = Color(0f, 0f, 0f), - fontSize = 16.sp, style = TextStyle(fontWeight = FontWeight.Bold) - ) -} - -@Composable -fun MomentFollowBtn() { - Box( - modifier = Modifier - .size(width = 53.dp, height = 18.dp) - .padding(start = 8.dp), - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier - .fillMaxSize(), - painter = painterResource(id = R.drawable.follow_bg), - contentDescription = "" - ) - Text( - text = "Follow", - color = Color.White, - fontSize = 12.sp - ) - } -} - -@Composable -fun MomentPostLocation(location: String) { - Text( - text = location, - color = Color(0f, 0f, 0f, 0.6f), - fontSize = 12.sp - ) -} - -@Composable -fun MomentPostTime(time: String) { - Text( - modifier = Modifier.padding(start = 8.dp), - text = time, color = Color(0f, 0f, 0f, 0.6f), - fontSize = 12.sp - ) -} - -@Composable -fun MomentTopRowGroup(momentEntity: MomentEntity) { - val navController = LocalNavController.current - val context = LocalContext.current - Row( - modifier = Modifier - ) { - CustomAsyncImage( - context, - momentEntity.avatar, - contentDescription = "", - modifier = Modifier - .size(40.dp) - .noRippleClickable { - navController.navigate( - NavigationRoute.AccountProfile.route.replace( - "{id}", - momentEntity.authorId.toString() - ) - ) - }, - contentScale = ContentScale.Crop - ) - Column( - modifier = Modifier - .defaultMinSize() - .padding(start = 12.dp, end = 12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(22.dp), - verticalAlignment = Alignment.CenterVertically - ) { - MomentName(momentEntity.nickname) -// MomentFollowBtn() - } - Row( - modifier = Modifier - .fillMaxWidth() - .height(21.dp), - verticalAlignment = Alignment.CenterVertically - ) { - MomentPostLocation(momentEntity.location) - MomentPostTime(momentEntity.time.timeAgo()) - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class) -@Composable -fun PostImageView( - postId: String, - images: List, -) { - val pagerState = rememberPagerState(pageCount = { images.size }) - val navController = LocalNavController.current - val sharedTransitionScope = LocalSharedTransitionScope.current - val animatedVisibilityScope = LocalAnimatedContentScope.current - val context = LocalContext.current - - Column( - modifier = Modifier.fillMaxWidth() - ) { - HorizontalPager( - state = pagerState, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f), - ) { page -> - val image = images[page] -// AsyncBlurImage( -// imageUrl = image.url, -// blurHash = image.blurHash ?: "", -// contentDescription = "Image", -// contentScale = ContentScale.Crop, -// modifier = Modifier -// .sharedElement( -// rememberSharedContentState(key = image), -// animatedVisibilityScope = animatedVisibilityScope -// ) -// .fillMaxSize() -// ) - CustomAsyncImage( - context, - image.thumbnail, - contentDescription = "Image", - blurHash = image.blurHash, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() -// .noRippleClickable { -// ImageViewerViewModel.asNew(images, page) -// navController.navigate( -// NavigationRoute.ImageViewer.route -// ) -// } - ) - - } - - // Indicator container - if (images.size > 1) { - Row( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - images.forEachIndexed { index, _ -> - Box( - modifier = Modifier - .size(8.dp) - .clip(CircleShape) - - .background( - if (pagerState.currentPage == index) Color.Red else Color.Gray.copy( - alpha = 0.5f - ) - ) - .padding(4.dp) - - - ) - Spacer(modifier = Modifier.width(8.dp)) - } - } - } - - } - -} - -@Composable -fun MomentContentGroup( - momentEntity: MomentEntity, -) { - if (momentEntity.momentTextContent.isNotEmpty()) { - Text( - text = momentEntity.momentTextContent, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp), - fontSize = 16.sp - ) - } - if (momentEntity.relMoment != null) { - RelPostCard( - momentEntity = momentEntity.relMoment!!, - modifier = Modifier.background(Color(0xFFF8F8F8)) - ) - } else { - Box( - modifier = Modifier.fillMaxWidth() - ) { - PostImageView( - postId = momentEntity.id.toString(), - images = momentEntity.images - ) - } - } - -} - - -@Composable -fun MomentOperateBtn(@DrawableRes icon: Int, count: String) { - Row(verticalAlignment = Alignment.CenterVertically) { - Image( - modifier = Modifier - .size(width = 24.dp, height = 24.dp), - painter = painterResource(id = icon), - contentDescription = "" - ) - Text( - text = count, - modifier = Modifier.padding(start = 7.dp), - fontSize = 12.sp, - ) - } -} - -@Composable -fun MomentOperateBtn(count: String, content: @Composable () -> Unit) { - Row( - modifier = Modifier, - verticalAlignment = Alignment.CenterVertically - ) { - content() - AnimatedCounter( - count = count.toInt(), - fontSize = 14, - modifier = Modifier.padding(start = 7.dp) - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MomentBottomOperateRowGroup( - modifier: Modifier, - onLikeClick: () -> Unit = {}, - onAddComment: () -> Unit = {}, - onFavoriteClick: () -> Unit = {}, - onShareClick: () -> Unit = {}, - momentEntity: MomentEntity -) { - var showCommentModal by remember { mutableStateOf(false) } - if (showCommentModal) { - ModalBottomSheet( - onDismissRequest = { showCommentModal = false }, - containerColor = Color.White, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ), - windowInsets = WindowInsets(0), - dragHandle = { - Box( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .clip(CircleShape) - - ) { - - } - } - - ) { -// systemUiController.setNavigationBarColor(Color(0xfff7f7f7)) - CommentModalContent(postId = momentEntity.id, onCommentAdded = { - showCommentModal = false - onAddComment() - }) { -// systemUiController.setNavigationBarColor(Color.Black) - } - } - } - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - ) { - Box( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - MomentOperateBtn(count = momentEntity.likeCount.toString()) { - AnimatedLikeIcon( - modifier = Modifier.size(24.dp), - liked = momentEntity.liked - ) { - onLikeClick() - } - } - } - Box( - modifier = modifier.clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - showCommentModal = true - }, - contentAlignment = Alignment.Center - ) { - MomentOperateBtn( - icon = R.drawable.rider_pro_moment_comment, - 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 { - onFavoriteClick() - }, - contentAlignment = Alignment.Center - ) { - MomentOperateBtn(count = momentEntity.favoriteCount.toString()) { - AnimatedFavouriteIcon( - modifier = Modifier.size(24.dp), - isFavourite = momentEntity.isFavorite - ) { - onFavoriteClick() - } - } - } - } -} - -@Composable -fun MomentListLoading() { - CircularProgressIndicator( - modifier = - Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.CenterHorizontally), - color = Color.Red - ) -} - -### ui/index/tabs/moment/MomentViewModel.kt ### -package com.aiosman.riderpro.ui.index.tabs.moment - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.entity.MomentPagingSource -import com.aiosman.riderpro.entity.MomentRemoteDataSource -import com.aiosman.riderpro.data.MomentService -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.entity.MomentServiceImpl -import com.aiosman.riderpro.entity.MomentEntity -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - - -object MomentViewModel : ViewModel() { - private val momentService: MomentService = MomentServiceImpl() - private val _momentsFlow = MutableStateFlow>(PagingData.empty()) - val momentsFlow = _momentsFlow.asStateFlow() - val accountService: AccountService = AccountServiceImpl() - init { - viewModelScope.launch { - val profile = accountService.getMyAccountProfile() - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - MomentPagingSource( - MomentRemoteDataSource(momentService), - timelineId = profile.id - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _momentsFlow.value = it - } - } - } - fun refreshPager() { - viewModelScope.launch { - val profile = accountService.getMyAccountProfile() - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - MomentPagingSource( - MomentRemoteDataSource(momentService), - timelineId = profile.id - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _momentsFlow.value = it - } - } - } - - fun updateLikeCount(id: Int) { - val currentPagingData = _momentsFlow.value - val updatedPagingData = currentPagingData.map { momentItem -> - if (momentItem.id == id) { - momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true) - } else { - momentItem - } - } - _momentsFlow.value = updatedPagingData - } - - suspend fun likeMoment(id: Int) { - momentService.likeMoment(id) - updateLikeCount(id) - } - - fun updateCommentCount(id: Int) { - val currentPagingData = _momentsFlow.value - val updatedPagingData = currentPagingData.map { momentItem -> - if (momentItem.id == id) { - momentItem.copy(commentCount = momentItem.commentCount + 1) - } else { - momentItem - } - } - _momentsFlow.value = updatedPagingData - } - suspend fun onAddComment(id: Int) { - val currentPagingData = _momentsFlow.value - updateCommentCount(id) - } - - - fun updateDislikeMomentById(id: Int) { - val currentPagingData = _momentsFlow.value - val updatedPagingData = currentPagingData.map { momentItem -> - if (momentItem.id == id) { - momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false) - } else { - momentItem - } - } - _momentsFlow.value = updatedPagingData - } - - suspend fun dislikeMoment(id: Int) { - momentService.dislikeMoment(id) - updateDislikeMomentById(id) - } - - fun updateFavoriteCount(id: Int) { - val currentPagingData = _momentsFlow.value - val updatedPagingData = currentPagingData.map { momentItem -> - if (momentItem.id == id) { - momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true) - } else { - momentItem - } - } - _momentsFlow.value = updatedPagingData - } - suspend fun favoriteMoment(id: Int) { - momentService.favoriteMoment(id) - updateFavoriteCount(id) - } - fun updateUnfavoriteCount(id: Int) { - val currentPagingData = _momentsFlow.value - val updatedPagingData = currentPagingData.map { momentItem -> - if (momentItem.id == id) { - momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false) - } else { - momentItem - } - } - _momentsFlow.value = updatedPagingData - } - suspend fun unfavoriteMoment(id: Int) { - momentService.unfavoriteMoment(id) - updateUnfavoriteCount(id) - } -} - -### ui/index/tabs/message/MessageList.kt ### -package com.aiosman.riderpro.ui.index.tabs.message - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.entity.CommentEntity -import com.aiosman.riderpro.exp.timeAgo -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.google.accompanist.systemuicontroller.rememberSystemUiController - - -/** - * 消息列表界面 - */ -@Composable -fun NotificationsScreen() { - val model = MessageListViewModel - val navController = LocalNavController.current - val systemUiController = rememberSystemUiController() - var dataFlow = MessageListViewModel.commentItemsFlow - var comments = dataFlow.collectAsLazyPagingItems() - LaunchedEffect(Unit) { - systemUiController.setNavigationBarColor(Color.Transparent) - MessageListViewModel.initData() - } - Column( - modifier = Modifier.fillMaxSize() - - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp) - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_message_title), - contentDescription = "Back", - modifier = Modifier.width(120.dp) - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - NotificationIndicator( - MessageListViewModel.likeNoticeCount, - R.drawable.rider_pro_like, - "LIKE" - ) { - navController.navigate(NavigationRoute.Likes.route) - } - NotificationIndicator( - MessageListViewModel.followNoticeCount, - R.drawable.rider_pro_followers, - "FOLLOWERS" - ) { - navController.navigate(NavigationRoute.Followers.route) - } - NotificationIndicator( - MessageListViewModel.favouriteNoticeCount, - R.drawable.rider_pro_favoriate, - "Favourites" - ) { - navController.navigate(NavigationRoute.FavouritesScreen.route) - } - } - HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp)) - NotificationCounterItem(MessageListViewModel.commentNoticeCount) - LazyColumn( - modifier = Modifier - .weight(1f) - .fillMaxSize() - ) { - items(comments.itemCount) { index -> - comments[index]?.let { comment -> - CommentItem(comment) { - MessageListViewModel.updateReadStatus(comment.id) - navController.navigate( - NavigationRoute.Post.route.replace( - "{id}", - comment.postId.toString() - ) - ) - } - } - } - item { - BottomNavigationPlaceholder() - } - } - } - - -} - -@Composable -fun NotificationIndicator( - notificationCount: Int, - iconRes: Int, - label: String, - onClick: () -> Unit -) { - Box( - modifier = Modifier - ) { - Box( - modifier = Modifier - .padding(16.dp) - .align(Alignment.TopCenter) - .noRippleClickable { - onClick() - } - ) { - if (notificationCount > 0) { - Box( - modifier = Modifier - .background(Color(0xFFE53935), RoundedCornerShape(8.dp)) - .padding(4.dp) - .align(Alignment.TopEnd) - ) { - Text( - text = notificationCount.toString(), - color = Color.White, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.align(Alignment.Center) - ) - } - } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = Modifier.padding(16.dp) - ) { - Image( - painter = painterResource(id = iconRes), - contentDescription = label, - modifier = Modifier.size(24.dp) - ) - } - Box( - modifier = Modifier - ) { - Text(label, modifier = Modifier.align(Alignment.Center)) - } - - } - } - } - -} - -@Composable -fun NotificationCounterItem(count: Int) { - Row( - modifier = Modifier.padding(vertical = 16.dp, horizontal = 32.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_notification), - contentDescription = "", - modifier = Modifier - .size(24.dp) - - ) - - } - Spacer(modifier = Modifier.width(24.dp)) - Text("NOTIFICATIONS", fontSize = 18.sp) - Spacer(modifier = Modifier.weight(1f)) - if (count > 0) { - Box( - modifier = Modifier - .background(Color(0xFFE53935), RoundedCornerShape(16.dp)) - .padding(horizontal = 8.dp, vertical = 2.dp) - ) { - Text( - text = count.toString(), - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(4.dp) - ) - } - } - } -} - - -@Composable -fun CommentItem( - commentItem: CommentEntity, - onPostClick: () -> Unit = {}, -) { - val navController = LocalNavController.current - val context = LocalContext.current - Row( - modifier = Modifier.padding(16.dp) - ) { - Box { - CustomAsyncImage( - context = context, - imageUrl = commentItem.avatar, - contentDescription = commentItem.name, - modifier = Modifier - .size(48.dp) - .clip(RoundedCornerShape(4.dp)) - .noRippleClickable { - navController.navigate( - NavigationRoute.AccountProfile.route.replace( - "{id}", - commentItem.author.toString() - ) - ) - } - ) - } - Row( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp) - .noRippleClickable { - onPostClick() - } - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = commentItem.name, - fontSize = 16.sp, - modifier = Modifier.padding(start = 16.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = commentItem.comment, - fontSize = 14.sp, - modifier = Modifier.padding(start = 16.dp), - maxLines = 1, - color = Color(0x99000000) - ) - } -// Spacer(modifier = Modifier.weight(1f)) - Column( - horizontalAlignment = Alignment.End - ) { - Row { - if (commentItem.unread) { - Box( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(Color(0xFFE53935)) - .padding(4.dp) - ) { - Text( - text = "new", - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.align(Alignment.Center) - ) - } - Spacer(modifier = Modifier.width(8.dp)) - } - Text( - text = commentItem.date.timeAgo(), - fontSize = 14.sp, - color = Color(0x66000000) - ) - } - Spacer(modifier = Modifier.height(4.dp)) - commentItem.post?.let { - CustomAsyncImage( - context = context, - imageUrl = it.images[0].thumbnail, - contentDescription = "Post Image", - modifier = Modifier - .size(32.dp) - .clip(RoundedCornerShape(4.dp)), - ) - } - } - } - - } -} - -### ui/index/tabs/message/MessageListViewModel.kt ### -package com.aiosman.riderpro.ui.index.tabs.message - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import com.aiosman.riderpro.data.AccountNotice -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.entity.CommentEntity -import com.aiosman.riderpro.entity.CommentPagingSource -import com.aiosman.riderpro.data.CommentRemoteDataSource -import com.aiosman.riderpro.data.CommentService -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.CommentServiceImpl -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -object MessageListViewModel : ViewModel() { - val accountService: AccountService = AccountServiceImpl() - var noticeInfo by mutableStateOf(null) - - private val commentService: CommentService = CommentServiceImpl() - private val _commentItemsFlow = MutableStateFlow>(PagingData.empty()) - val commentItemsFlow = _commentItemsFlow.asStateFlow() - - suspend fun initData() { - val info = accountService.getMyNoticeInfo() - noticeInfo = info - } - - val likeNoticeCount - get() = noticeInfo?.likeCount ?: 0 - val followNoticeCount - get() = noticeInfo?.followCount ?: 0 - val favouriteNoticeCount - get() = noticeInfo?.favoriteCount ?: 0 - val commentNoticeCount - get() = noticeInfo?.commentCount ?: 0 - - init { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - selfNotice = true - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _commentItemsFlow.value = it - } - } - } - - private fun updateIsRead(id: Int) { - val currentPagingData = _commentItemsFlow.value - val updatedPagingData = currentPagingData.map { commentEntity -> - if (commentEntity.id == id) { - commentEntity.copy(unread = false) - } else { - commentEntity - } - } - _commentItemsFlow.value = updatedPagingData - } - - fun updateReadStatus(id: Int) { - viewModelScope.launch { - commentService.updateReadStatus(id) - updateIsRead(id) - } - } -} - -### ui/index/tabs/search/SearchScreen.kt ### -package com.aiosman.riderpro.ui.index.tabs.search - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -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.foundation.lazy.LazyColumn -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Icon -import androidx.compose.material.Tab -import androidx.compose.material.TabRow -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.index.tabs.moment.MomentCard -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import kotlinx.coroutines.launch - - -@OptIn(ExperimentalFoundationApi::class) -@Preview -@Composable -fun SearchScreen() { - val model = SearchViewModel - val categories = listOf("Moment", "User") - val coroutineScope = rememberCoroutineScope() - val pagerState = rememberPagerState(pageCount = { categories.size }) - val selectedTabIndex = remember { derivedStateOf { pagerState.currentPage } } - val keyboardController = LocalSoftwareKeyboardController.current - val systemUiController = rememberSystemUiController() - LaunchedEffect(Unit) { - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true) - } - Column( - modifier = Modifier - .background(Color.White) - .fillMaxSize() - - ) { - SearchInput( - modifier = Modifier.fillMaxWidth().padding(top = 16.dp, start = 24.dp, end = 24.dp), - text = model.searchText, - onTextChange = { - model.searchText = it - }, - onSearch = { - model.search() - // hide ime - keyboardController?.hide() // Hide the keyboard - } - ) - if (model.showResult) { - Spacer(modifier = Modifier.padding(8.dp)) - - TabRow( - selectedTabIndex = selectedTabIndex.value, - backgroundColor = Color.White, - ) { - categories.forEachIndexed { index, category -> - Tab( - selected = selectedTabIndex.value == index, - onClick = { - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(category) } - ) - } - } - - SearchPager( - pagerState = pagerState - ) - } - - - } -} - -@Composable -fun SearchInput( - modifier: Modifier = Modifier, - text: String = "", - onTextChange: (String) -> Unit = {}, - onSearch: () -> Unit = {} -) { - Box( - modifier = modifier - .clip(shape = RoundedCornerShape(8.dp)) - - .background(Color(0xFFEEEEEE)) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Default.Search, - contentDescription = null - ) - Box { - if (text.isEmpty()) { - Text( - text = "Search", - modifier = Modifier.padding(start = 8.dp), - color = Color(0xFF9E9E9E), - fontSize = 18.sp - ) - } - BasicTextField( - value = text, - onValueChange = { - onTextChange(it) - }, - modifier = Modifier - .padding(start = 8.dp) - .fillMaxWidth(), - singleLine = true, - textStyle = TextStyle( - fontSize = 18.sp - ), - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Search - ), - keyboardActions = KeyboardActions( - onSearch = { - onSearch() - } - ) - ) - } - } - - - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun SearchPager( - pagerState: PagerState, -) { - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxSize(), - - ) { page -> - when (page) { - 0 -> MomentResultTab() - 1 -> UserResultTab() - } - } -} - -@Composable -fun MomentResultTab() { - val model = SearchViewModel - var dataFlow = model.momentsFlow - var moments = dataFlow.collectAsLazyPagingItems() - Box( - modifier = Modifier.fillMaxSize() - ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items(moments.itemCount) { idx -> - val momentItem = moments[idx] ?: return@items - Spacer(modifier = Modifier.padding(8.dp)) - MomentCard(momentEntity = momentItem, hideAction = true) - } - } - } -} - -@Composable -fun UserResultTab() { - val model = SearchViewModel - val users = model.usersFlow.collectAsLazyPagingItems() - Box( - modifier = Modifier.fillMaxSize() - ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items(users.itemCount) { idx -> - val userItem = users[idx] ?: return@items - UserItem(userItem) - } - } - } -} - -@Composable -fun UserItem(accountProfile: AccountProfileEntity) { - val context = LocalContext.current - val navController = LocalNavController.current - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp).noRippleClickable { - navController.navigate("AccountProfile/${accountProfile.id}") - }, - verticalAlignment = Alignment.CenterVertically - ) { - CustomAsyncImage( - context, - imageUrl = accountProfile.avatar, - modifier = Modifier - .size(64.dp) - .clip(CircleShape), - contentDescription = null - ) - Spacer(modifier = Modifier.padding(16.dp)) - Text(text = accountProfile.nickName, fontSize = 18.sp, fontWeight = FontWeight.Bold) - } -} - -### ui/index/tabs/search/SearchViewModel.kt ### -package com.aiosman.riderpro.ui.index.tabs.search - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import com.aiosman.riderpro.entity.AccountPagingSource -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.entity.MomentPagingSource -import com.aiosman.riderpro.entity.MomentRemoteDataSource -import com.aiosman.riderpro.data.MomentService -import com.aiosman.riderpro.entity.MomentServiceImpl -import com.aiosman.riderpro.data.UserServiceImpl -import com.aiosman.riderpro.entity.MomentEntity -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -object SearchViewModel : ViewModel() { - var searchText by mutableStateOf("") - private val momentService: MomentService = MomentServiceImpl() - private val _momentsFlow = MutableStateFlow>(PagingData.empty()) - val momentsFlow = _momentsFlow.asStateFlow() - - private val userService = UserServiceImpl() - private val _usersFlow = MutableStateFlow>(PagingData.empty()) - val usersFlow = _usersFlow.asStateFlow() - var showResult by mutableStateOf(false) - fun search() { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - MomentPagingSource( - MomentRemoteDataSource(momentService), - contentSearch = searchText - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _momentsFlow.value = it - } - } - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - AccountPagingSource( - userService, - nickname = searchText - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _usersFlow.value = it - } - } - showResult = true - } -} - -### ui/index/tabs/add/AddPage.kt ### -package com.aiosman.riderpro.ui.index.tabs.add - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -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.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.ui.post.NewPostViewModel -import com.aiosman.riderpro.R - -@Composable -fun AddPage(){ - val navController = LocalNavController.current - Column(modifier = Modifier - .fillMaxSize() - .background(Color.Black)) { - AddBtn(icon = R.drawable.rider_pro_icon_rider_share, text = "Rider Share") { - NewPostViewModel.asNewPost() - navController.navigate("NewPost") - } -// AddBtn(icon = R.drawable.rider_pro_location_create, text = "Location Create") - } -} - -@Composable -fun AddBtn(@DrawableRes icon: Int, text: String,onClick: (() -> Unit)? = {}){ - Row (modifier = Modifier - .fillMaxWidth().padding(24.dp).clickable { - onClick?.invoke() - }, - verticalAlignment = Alignment.CenterVertically){ - Image( - modifier = Modifier.size(40.dp), - painter = painterResource(id = icon), contentDescription = null) - Text(modifier = Modifier.padding(start = 24.dp),text = text, color = Color.White,fontSize = 22.sp, style = TextStyle(fontWeight = FontWeight.Bold)) - } -} - -### ui/index/tabs/profile/MyProfileViewModel.kt ### -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.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import com.aiosman.riderpro.AppStore -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.MomentService -import com.aiosman.riderpro.data.UserServiceImpl -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.entity.MomentEntity -import com.aiosman.riderpro.entity.MomentPagingSource -import com.aiosman.riderpro.entity.MomentRemoteDataSource -import com.aiosman.riderpro.entity.MomentServiceImpl -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -object MyProfileViewModel : ViewModel() { - val accountService: AccountService = AccountServiceImpl() - val momentService: MomentService = MomentServiceImpl() - val userService = UserServiceImpl() - var profile by mutableStateOf(null) - private var _momentsFlow = MutableStateFlow>(PagingData.empty()) - var momentsFlow = _momentsFlow.asStateFlow() - fun loadProfile(){ - -// momentsFlow = Pager( -// config = PagingConfig(pageSize = 5, enablePlaceholders = false), -// pagingSourceFactory = { -// MomentPagingSource( -// MomentRemoteDataSource(MomentServiceImpl()), -// author = profile?.id ?: 0, -// -// ) -// } -// ).flow.cachedIn(viewModelScope).collectLatest { -// MomentViewModel._momentsFlow.value = it -// } - viewModelScope.launch { - profile = accountService.getMyAccountProfile() - val profile = accountService.getMyAccountProfile() - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - MomentPagingSource( - MomentRemoteDataSource(momentService), - author = profile.id - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _momentsFlow.value = it - } - } - } - - suspend fun logout() { - accountService.getMyAccountProfile() - AppStore.apply { - token = null - rememberMe = false - saveData() - } - - } - - val followerCount get() = profile?.followerCount ?: 0 - val followingCount get() = profile?.followingCount ?: 0 - val bio get() = profile?.bio ?: "" - val nickName get() = profile?.nickName ?: "" - val avatar get() = profile?.avatar - - -} - -### ui/index/tabs/profile/Profile.kt ### -package com.aiosman.riderpro.ui.index.tabs.profile - -import android.util.Log -import androidx.annotation.DrawableRes -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -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.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -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 -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.navigation.NavController -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.LocalAnimatedContentScope -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.LocalSharedTransitionScope -import com.aiosman.riderpro.R -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.exp.formatPostTime -import com.aiosman.riderpro.entity.MomentEntity -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.index.tabs.moment.MomentCard -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.aiosman.riderpro.ui.post.PostViewModel -import kotlinx.coroutines.launch - - -@Composable -fun ProfilePage() { - val model = MyProfileViewModel - var expanded by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - Log.d("ProfilePage", "loadProfile") - model.loadProfile() - } - val moments = model.momentsFlow.collectAsLazyPagingItems() - val navController: NavController = LocalNavController.current - val scope = rememberCoroutineScope() - - LazyColumn( - modifier = Modifier - .fillMaxSize(), - ) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) { - Box( - modifier = Modifier.align(Alignment.TopEnd) - ) { - Icon( - painter = painterResource(id = R.drawable.rider_pro_more_horizon), - contentDescription = "", - modifier = Modifier.noRippleClickable { - expanded = true - } - ) - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - DropdownMenuItem(onClick = { - scope.launch { - model.logout() - navController.navigate(NavigationRoute.Login.route) { - popUpTo(NavigationRoute.Index.route) { - inclusive = true - } - } - } - }, text = { - Text("Logout") - }) - DropdownMenuItem(onClick = { - scope.launch { - navController.navigate(NavigationRoute.AccountEdit.route) - } - }, text = { - Text("Edit") - }) - DropdownMenuItem(onClick = { - scope.launch { - navController.navigate(NavigationRoute.ChangePasswordScreen.route) - } - }, text = { - Text("Change password") - }) - } - } - - } - CarGroup() - model.profile?.let { - UserInformation(accountProfileEntity = it) - } -// RidingStyle() - } - - items(moments.itemCount) { idx -> - val momentItem = moments[idx] ?: return@items - MomentPostUnit(momentItem) -// MomentCard(momentItem) - } - - - } - - -} - -@Composable -fun CarGroup() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 54.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CarTopInformation() - CarTopPicture() - } -} - -@Composable -fun CarTopInformation() { - Row { - Text( - text = "BMW", - color = Color.Black, - fontSize = 12.sp, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - Text( - modifier = Modifier.padding(start = 4.dp), - text = "/", - color = Color.Gray, - fontSize = 12.sp - ) - Text( - modifier = Modifier.padding(start = 4.dp), - text = "M1000RR", - color = Color.Gray, - fontSize = 12.sp - ) - } -} - -@Composable -fun CarTopPicture() { - Image( - modifier = Modifier - .size(width = 336.dp, height = 224.dp) - .padding(top = 42.dp), - painter = painterResource(id = R.drawable.default_profile_moto), contentDescription = "" - ) -} - -@Composable -fun UserInformation( - isSelf: Boolean = true, - accountProfileEntity: AccountProfileEntity, - onFollowClick: () -> Unit = {} -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, start = 33.dp, end = 33.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row(modifier = Modifier.fillMaxWidth()) { - val userInfoModifier = Modifier.weight(1f) - UserInformationFollowers(userInfoModifier, accountProfileEntity) - UserInformationBasic(userInfoModifier, accountProfileEntity) - UserInformationFollowing(userInfoModifier, accountProfileEntity) - } - UserInformationSlogan() - CommunicationOperatorGroup( - isSelf = isSelf, - isFollowing = accountProfileEntity.isFollowing, - onFollowClick = onFollowClick - ) - } -} - -@Composable -fun UserInformationFollowers(modifier: Modifier, accountProfileEntity: AccountProfileEntity) { - Column(modifier = modifier.padding(top = 31.dp)) { - Text( - modifier = Modifier.padding(bottom = 5.dp), - text = accountProfileEntity.followerCount.toString(), - fontSize = 24.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - Spacer( - modifier = Modifier - .size(width = 88.83.dp, height = 1.dp) - .border(width = 1.dp, color = Color.Gray) - .padding(top = 5.dp, bottom = 5.dp) - ) - Text( - modifier = Modifier.padding(top = 5.dp), - text = "FOLLOWERS", - fontSize = 12.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } -} - -@Composable -fun UserInformationBasic(modifier: Modifier, accountProfileEntity: AccountProfileEntity) { - val context = LocalContext.current - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier.size(width = 112.dp, height = 112.dp), - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier.fillMaxSize(), - painter = painterResource(id = R.drawable.avatar_bold), contentDescription = "" - ) - CustomAsyncImage( - context, - accountProfileEntity.avatar, - modifier = Modifier - .size(width = 88.dp, height = 88.dp) - .clip( - RoundedCornerShape(88.dp) - ), - contentDescription = "", - contentScale = ContentScale.Crop - ) - - } - Text( - modifier = Modifier - .widthIn(max = 220.dp) - .padding(top = 8.dp), - text = accountProfileEntity.nickName, - fontSize = 32.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold), - textAlign = TextAlign.Center - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = accountProfileEntity.country, - fontSize = 12.sp, - color = Color.Gray - ) -// Text( -// modifier = Modifier.padding(top = 4.dp), -// text = "Member since Jun 4.2019", -// fontSize = 12.sp, -// color = Color.Gray -// ) - } -} - -@Composable -fun UserInformationFollowing(modifier: Modifier, accountProfileEntity: AccountProfileEntity) { - Column( - modifier = modifier.padding(top = 6.dp), - horizontalAlignment = Alignment.End - ) { - Text( - modifier = Modifier.padding(bottom = 5.dp), - text = accountProfileEntity.followingCount.toString(), - fontSize = 24.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - Box( - modifier = Modifier - .size(width = 88.83.dp, height = 1.dp) - .border(width = 1.dp, color = Color.Gray) - - ) - Text( - modifier = Modifier.padding(top = 5.dp), - text = "FOLLOWING", - fontSize = 12.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } -} - -@Composable -fun UserInformationSlogan() { - val model = MyProfileViewModel - Text( - modifier = Modifier.padding(top = 23.dp), - text = model.bio, - fontSize = 13.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold) - ) -} - -@Composable -fun CommunicationOperatorGroup( - isSelf: Boolean = true, - isFollowing: Boolean = false, - onFollowClick: () -> Unit -) { - val navController = LocalNavController.current - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), horizontalArrangement = Arrangement.Center - ) { - if (!isSelf) { - Box( - modifier = Modifier - .size(width = 142.dp, height = 40.dp) - .noRippleClickable { - onFollowClick() - }, - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier.fillMaxSize(), - painter = painterResource(id = R.drawable.rider_pro_profile_follow), - contentDescription = "" - ) - Text( - text = if (isFollowing) "FOLLOWING" else "FOLLOW", - fontSize = 16.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } - } - - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun RidingStyle() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, top = 40.dp, end = 24.dp), - horizontalAlignment = Alignment.Start - ) { - Text( - text = "RIDING STYLES", - fontSize = 18.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - Image( - modifier = Modifier - .padding(top = 4.dp) - .height(8.dp), - painter = painterResource(id = R.drawable.rider_pro_profile_line), - contentDescription = "" - ) - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp) - ) { - RidingStyleItem(styleContent = "Cruiser") - RidingStyleItem(styleContent = "Bobber") - RidingStyleItem(styleContent = "Cafe") - RidingStyleItem(styleContent = "Chopper") - RidingStyleItem(styleContent = "Sport") - RidingStyleItem(styleContent = "Vintage") - RidingStyleItem(styleContent = "Trike") - RidingStyleItem(styleContent = "Touring") - } - } -} - -@Composable -fun RidingStyleItem(styleContent: String) { - Box( - modifier = Modifier.padding(bottom = 8.dp, end = 8.dp), - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier.shadow( - ambientColor = Color.Gray, - spotColor = Color(0f, 0f, 0f, 0.2f), - elevation = 20.dp, - ), - painter = painterResource(id = R.drawable.rider_pro_style_wrapper), - contentDescription = "" - ) - Text( - modifier = Modifier.padding(start = 5.dp, end = 5.dp), - text = styleContent, - fontSize = 12.sp, - color = Color.Gray, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } -} - -@Composable -fun UserMoment(scope: LazyListScope) { - - -// LazyColumn( -// modifier = Modifier -// .fillMaxWidth() -// .weight(1f) -// ){ -// -// -// } -} - -@Composable -fun MomentPostUnit(momentEntity: MomentEntity) { - TimeGroup(momentEntity.time.formatPostTime()) - ProfileMomentCard( - momentEntity.momentTextContent, - momentEntity.images[0].thumbnail, - momentEntity.likeCount.toString(), - momentEntity.commentCount.toString(), - momentEntity = momentEntity - ) -} - -@Composable -fun TimeGroup(time: String = "2024.06.08 12:23") { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, top = 40.dp, end = 24.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Image( - modifier = Modifier.padding(end = 12.dp), - painter = painterResource(id = R.drawable.rider_pro_moment_time_flag), - contentDescription = "" - ) - Text( - text = time, - fontSize = 22.sp, - color = Color.Black, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } -} - -@Composable -fun ProfileMomentCard( - content: String, - imageUrl: String, - like: String, - comment: String, - momentEntity: MomentEntity -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 48.dp, top = 18.dp, end = 24.dp) - .border(width = 1.dp, color = Color(0f, 0f, 0f, 0.1f), shape = RoundedCornerShape(6.dp)) - ) { - MomentCardTopContent(content) - MomentCardPicture(imageUrl, momentEntity = momentEntity) - MomentCardOperation(like, comment) - } -} - -@Composable -fun MomentCardTopContent(content: String) { - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.padding(16.dp), - text = content, fontSize = 16.sp, color = Color.Black - ) - } -} - -@Composable -fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) { - val navController = LocalNavController.current - val context = LocalContext.current - CustomAsyncImage( - context, - imageUrl, - modifier = Modifier - .fillMaxSize() - .aspectRatio(1f) - .padding(16.dp) - .noRippleClickable { - PostViewModel.preTransit(momentEntity) - navController.navigate( - NavigationRoute.Post.route.replace( - "{id}", - momentEntity.id.toString() - ) - ) - }, - contentDescription = "", - contentScale = ContentScale.Crop - ) - - -} - -@Composable -fun MomentCardOperation(like: String, comment: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.weight(1f)) - MomentCardOperationItem( - drawable = R.drawable.rider_pro_like, - number = like, - modifier = Modifier.weight(1f) - ) - MomentCardOperationItem( - drawable = R.drawable.rider_pro_moment_comment, - number = comment, - modifier = Modifier.weight(1f) - ) - } -} - -@Composable -fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Image( - modifier = Modifier.padding(start = 16.dp, end = 8.dp), - painter = painterResource(id = drawable), contentDescription = "" - ) - Text(text = number) - } -} - -### ui/index/tabs/shorts/Pager.kt ### -package com.aiosman.riderpro.ui.index.tabs.shorts - -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.layout.ParentDataModifier -import androidx.compose.ui.unit.Density -import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.roundToInt - -class PagerState( - currentPage: Int = 0, - minPage: Int = 0, - maxPage: Int = 0 -) { - private var _minPage by mutableStateOf(minPage) - var minPage: Int - get() = _minPage - set(value) { - _minPage = value.coerceAtMost(_maxPage) - _currentPage = _currentPage.coerceIn(_minPage, _maxPage) - } - - private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy()) - var maxPage: Int - get() = _maxPage - set(value) { - _maxPage = value.coerceAtLeast(_minPage) - _currentPage = _currentPage.coerceIn(_minPage, maxPage) - } - - private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage)) - var currentPage: Int - get() = _currentPage - set(value) { - _currentPage = value.coerceIn(minPage, maxPage) - } - - enum class SelectionState { Selected, Undecided } - - var selectionState by mutableStateOf(SelectionState.Selected) - - suspend inline fun selectPage(block: PagerState.() -> R): R = try { - selectionState = SelectionState.Undecided - block() - } finally { - selectPage() - } - - suspend fun selectPage() { - currentPage -= currentPageOffset.roundToInt() - snapToOffset(0f) - selectionState = SelectionState.Selected - } - - private var _currentPageOffset = Animatable(0f).apply { - updateBounds(-1f, 1f) - } - val currentPageOffset: Float - get() = _currentPageOffset.value - - suspend fun snapToOffset(offset: Float) { - val max = if (currentPage == minPage) 0f else 1f - val min = if (currentPage == maxPage) 0f else -1f - _currentPageOffset.snapTo(offset.coerceIn(min, max)) - } - - suspend fun fling(velocity: Float) { - if (velocity < 0 && currentPage == maxPage) return - if (velocity > 0 && currentPage == minPage) return - - // 根据 fling 的方向滑动到下一页或上一页 - _currentPageOffset.animateTo(velocity) - selectPage() - } - - override fun toString(): String = "PagerState{minPage=$minPage, maxPage=$maxPage, " + - "currentPage=$currentPage, currentPageOffset=$currentPageOffset}" -} - -@Immutable -private data class PageData(val page: Int) : ParentDataModifier { - override fun Density.modifyParentData(parentData: Any?): Any? = this@PageData -} - -private val Measurable.page: Int - get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this") - -@Composable -fun Pager( - modifier: Modifier = Modifier, - state: PagerState, - orientation: Orientation = Orientation.Horizontal, - offscreenLimit: Int = 2, - horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, // 新增水平对齐参数 - verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, // 新增垂直对齐参数 - content: @Composable PagerScope.() -> Unit -) { - var pageSize by remember { mutableStateOf(0) } - val coroutineScope = rememberCoroutineScope() - - Layout( - content = { - // 根据 offscreenLimit 计算页面范围 - val minPage = maxOf(state.currentPage - offscreenLimit, state.minPage) - val maxPage = minOf(state.currentPage + offscreenLimit, state.maxPage) - - for (page in minPage..maxPage) { - val pageData = PageData(page) - val scope = PagerScope(state, page) - key(pageData) { - Column(modifier = pageData) { - scope.content() - } - } - } - }, - modifier = modifier.draggable( - orientation = orientation, - onDragStarted = { - state.selectionState = PagerState.SelectionState.Undecided - }, - onDragStopped = { velocity -> - coroutineScope.launch { - // 根据速度判断是否滑动到下一页 - val threshold = 1000f // 速度阈值,可调整 - if (velocity > threshold) { - state.fling(1f) // 向右滑动 - } else if (velocity < -threshold) { - state.fling(-1f) // 向左滑动 - } else { - state.fling(0f) // 保持当前页 - } - } - }, - state = rememberDraggableState { dy -> - coroutineScope.launch { - with(state) { - val pos = pageSize * currentPageOffset - val max = if (currentPage == minPage) 0 else pageSize - val min = if (currentPage == maxPage) 0 else -pageSize - - // 直接将手指的位移应用到 currentPageOffset - val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat()) - snapToOffset(newPos / pageSize) - } - } - }, - ) - ) { measurables, constraints -> - layout(constraints.maxWidth, constraints.maxHeight) { - val currentPage = state.currentPage - val offset = state.currentPageOffset - val childConstraints = constraints.copy(minWidth = 0, minHeight = 0) - - measurables.forEach { measurable -> - val placeable = measurable.measure(childConstraints) - val page = measurable.page - - // 根据对齐参数计算 x 和 y 位置 - val xPosition = when (horizontalAlignment) { - Alignment.Start -> 0 - Alignment.CenterHorizontally -> (constraints.maxWidth - placeable.width) / 2 - Alignment.End -> constraints.maxWidth - placeable.width - else -> 0 - } - - val yPosition = when (verticalAlignment) { - Alignment.Top -> 0 - Alignment.CenterVertically -> (constraints.maxHeight - placeable.height) / 2 - Alignment.Bottom -> constraints.maxHeight - placeable.height - else -> 0 - } - - if (currentPage == page) { // 只在当前页面设置 pageSize,避免不必要的设置 - pageSize = if (orientation == Orientation.Horizontal) { - placeable.width - } else { - placeable.height - } - } - - val isVisible = abs(page - (currentPage - offset)) <= 1 - - if (isVisible) { - // 修正 x 的计算 - val xOffset = if (orientation == Orientation.Horizontal) { - ((page - currentPage) * pageSize + offset * pageSize).roundToInt() - } else { - 0 - } - - // 使用 placeRelative 进行放置 - placeable.placeRelative( - x = xPosition + xOffset, - y = yPosition + if (orientation == Orientation.Vertical) ((page - (currentPage - offset)) * placeable.height).roundToInt() else 0 - ) - } - } - - } - } -} - -class PagerScope( - private val state: PagerState, - val page: Int -) { - val currentPage: Int - get() = state.currentPage - - val currentPageOffset: Float - get() = state.currentPageOffset - - val selectionState: PagerState.SelectionState - get() = state.selectionState -} - - - - - - - - -### ui/index/tabs/shorts/ShortVideo.kt ### -package com.aiosman.riderpro.ui.index.tabs.shorts - -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import com.aiosman.riderpro.ui.theme.RiderProTheme - -val videoUrls = listOf( - "https://api.rider-pro.com/test/shorts/1.mp4", - "https://api.rider-pro.com/test/shorts/2.mp4", - "https://api.rider-pro.com/test/shorts/3.mp4", - "https://api.rider-pro.com/test/shorts/4.mp4", - "https://api.rider-pro.com/test/shorts/5.webm", - "https://api.rider-pro.com/test/shorts/6.webm", - "https://api.rider-pro.com/test/shorts/7.webm", - "https://api.rider-pro.com/test/shorts/8.webm", - "https://api.rider-pro.com/test/shorts/9.webm", - "https://api.rider-pro.com/test/shorts/10.webm", - "https://api.rider-pro.com/test/shorts/11.webm", - "https://api.rider-pro.com/test/shorts/12.webm", - "https://api.rider-pro.com/test/shorts/13.webm", - "https://api.rider-pro.com/test/shorts/14.webm", - "https://api.rider-pro.com/test/shorts/15.webm", - "https://api.rider-pro.com/test/shorts/16.webm", - "https://api.rider-pro.com/test/shorts/17.webm", - "https://api.rider-pro.com/test/shorts/18.webm", - "https://api.rider-pro.com/test/shorts/19.webm", - "https://api.rider-pro.com/test/shorts/20.webm", - "https://api.rider-pro.com/test/shorts/21.webm", - "https://api.rider-pro.com/test/shorts/22.webm", - "https://api.rider-pro.com/test/shorts/23.webm", - "https://api.rider-pro.com/test/shorts/24.webm", - "https://api.rider-pro.com/test/shorts/25.webm", - "https://api.rider-pro.com/test/shorts/26.webm", - "https://api.rider-pro.com/test/shorts/27.webm", - "https://api.rider-pro.com/test/shorts/28.webm", - "https://api.rider-pro.com/test/shorts/29.webm", - "https://api.rider-pro.com/test/shorts/30.webm", - "https://api.rider-pro.com/test/shorts/31.webm", - "https://api.rider-pro.com/test/shorts/32.webm", - "https://api.rider-pro.com/test/shorts/33.webm", - "https://api.rider-pro.com/test/shorts/34.webm", - "https://api.rider-pro.com/test/shorts/35.webm", - "https://api.rider-pro.com/test/shorts/36.webm", - "https://api.rider-pro.com/test/shorts/37.webm", - "https://api.rider-pro.com/test/shorts/38.webm", - "https://api.rider-pro.com/test/shorts/39.webm", - "https://api.rider-pro.com/test/shorts/40.webm", - "https://api.rider-pro.com/test/shorts/41.webm", - - - - - - -) - -@Composable -fun ShortVideo() { - RiderProTheme { - Surface(color = MaterialTheme.colorScheme.background) { - ShortViewCompose( - videoItemsUrl = videoUrls, - clickItemPosition = 0 - ) - } - } -} - - - -### ui/index/tabs/shorts/ShortViewCompose.kt ### -@file:kotlin.OptIn(ExperimentalMaterial3Api::class) - -package com.aiosman.riderpro.ui.index.tabs.shorts - -import android.net.Uri -import android.view.Gravity -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.annotation.DrawableRes -import androidx.annotation.OptIn -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -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.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -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.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.media3.common.C -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultDataSourceFactory -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.ProgressiveMediaSource -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.PlayerView -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.comment.CommentModalContent -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Composable -fun ShortViewCompose( - videoItemsUrl: List, - clickItemPosition: Int = 0, - videoHeader: @Composable () -> Unit = {}, - videoBottom: @Composable () -> Unit = {} -) { - val pagerState: PagerState = run { - remember { - PagerState(clickItemPosition, 0, videoItemsUrl.size - 1) - } - } - val initialLayout = remember { - mutableStateOf(true) - } - val pauseIconVisibleState = remember { - mutableStateOf(false) - } - Pager( - state = pagerState, - orientation = Orientation.Vertical, - offscreenLimit = 1 - ) { - pauseIconVisibleState.value = false - SingleVideoItemContent( - videoItemsUrl[page], - pagerState, - page, - initialLayout, - pauseIconVisibleState, - videoHeader, - videoBottom - ) - } - - LaunchedEffect(clickItemPosition) { - delay(300) - initialLayout.value = false - } - -} - -@Composable -private fun SingleVideoItemContent( - videoUrl: String, - pagerState: PagerState, - pager: Int, - initialLayout: MutableState, - pauseIconVisibleState: MutableState, - VideoHeader: @Composable() () -> Unit, - VideoBottom: @Composable() () -> Unit, -) { - Box(modifier = Modifier.fillMaxSize()) { - VideoPlayer(videoUrl, pagerState, pager, pauseIconVisibleState) - VideoHeader.invoke() - Box(modifier = Modifier.align(Alignment.BottomStart)) { - VideoBottom.invoke() - } - if (initialLayout.value) { - Box( - modifier = Modifier - .fillMaxSize() - .background(color = Color.Black) - ) - } - } -} - -@OptIn(UnstableApi::class) -@Composable -fun VideoPlayer( - videoUrl: String, - pagerState: PagerState, - pager: Int, - pauseIconVisibleState: MutableState, -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val lifecycleOwner = LocalLifecycleOwner.current - var showCommentModal by remember { mutableStateOf(false) } - var sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) - val exoPlayer = remember { - ExoPlayer.Builder(context) - .build() - .apply { - val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory( - context, - Util.getUserAgent(context, context.packageName) - ) - val source = ProgressiveMediaSource.Factory(dataSourceFactory) - .createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl))) - - this.prepare(source) - } - } - if (pager == pagerState.currentPage) { - exoPlayer.playWhenReady = true - exoPlayer.play() - } else { - exoPlayer.pause() - } - exoPlayer.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT - exoPlayer.repeatMode = Player.REPEAT_MODE_ONE - // player box - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.TopCenter - ) { - var playerView by remember { mutableStateOf(null) } // Store reference to PlayerView - - AndroidView( - factory = { context -> - // 创建一个 FrameLayout 作为容器 - FrameLayout(context).apply { - // 设置背景颜色为黑色,用于显示黑边 - setBackgroundColor(Color.Black.toArgb()) - - // 创建 PlayerView 并添加到 FrameLayout 中 - val view = PlayerView(context).apply { - hideController() - useController = false - player = exoPlayer - resizeMode = - AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT // 或 RESIZE_MODE_ZOOM - layoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.CENTER - ) - } - addView(view) - } - }, - modifier = Modifier.noRippleClickable { - pauseIconVisibleState.value = true - exoPlayer.pause() - scope.launch { - delay(100) - if (exoPlayer.isPlaying) { - exoPlayer.pause() - } else { - pauseIconVisibleState.value = false - exoPlayer.play() - } - } - } - ) - - if (pauseIconVisibleState.value) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = null, - modifier = Modifier - .align(Alignment.Center) - .size(80.dp) - ) - } - - // Release ExoPlayer when the videoUrl changes or the composable leaves composition - DisposableEffect(videoUrl) { - onDispose { - exoPlayer.release() - playerView?.player = null - playerView = null // Release the reference to the PlayerView - } - } - - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_PAUSE -> { - exoPlayer.pause() // 应用进入后台时暂停 - } - - Lifecycle.Event.ON_RESUME -> { - if (pager == pagerState.currentPage) { - exoPlayer.play() // 返回前台且为当前页面时恢复播放 - } - } - - else -> {} - } - } - lifecycleOwner.lifecycle.addObserver(observer) - - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } - - - } - // action buttons - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Column( - modifier = Modifier.padding(bottom = 72.dp, end = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - UserAvatar() - VideoBtn(icon = R.drawable.rider_pro_video_like, text = "975.9k") - VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "1896") { - showCommentModal = true - } - VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "234") - VideoBtn(icon = R.drawable.rider_pro_video_share, text = "677k") - } - } - // info - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomStart - ) { - Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) { - Row( - modifier = Modifier - .padding(bottom = 8.dp) - .background(color = Color.Gray), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - ) { - Image( - modifier = Modifier - .size(20.dp) - .padding(start = 4.dp, end = 6.dp), - painter = painterResource(id = R.drawable.rider_pro_video_location), - contentDescription = "" - ) - Text( - modifier = Modifier.padding(end = 4.dp), - text = "USA", - fontSize = 12.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } - Text( - text = "@Kevinlinpr", - fontSize = 16.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp), // 确保Text占用可用宽度 - text = "Pedro Acosta to join KTM in 2025 on a multi-year deal! \uD83D\uDFE0", - fontSize = 16.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold), - overflow = TextOverflow.Ellipsis, // 超出范围时显示省略号 - maxLines = 2 // 最多显示两行 - ) - } - } - - if (showCommentModal) { - ModalBottomSheet( - onDismissRequest = { showCommentModal = false }, - containerColor = Color.White, - sheetState = sheetState - ) { - CommentModalContent() { - - } - } - } -} - -@Composable -fun UserAvatar() { - Image( - modifier = Modifier - .padding(bottom = 16.dp) - .size(40.dp) - .border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp)) - .clip( - RoundedCornerShape(40.dp) - ), painter = painterResource(id = R.drawable.default_avatar), contentDescription = "" - ) -} - -@Composable -fun VideoBtn(@DrawableRes icon: Int, text: String, onClick: (() -> Unit)? = null) { - Column( - modifier = Modifier - .padding(bottom = 16.dp) - .clickable { - onClick?.invoke() - }, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - modifier = Modifier.size(36.dp), - painter = painterResource(id = icon), - contentDescription = "" - ) - Text( - text = text, - fontSize = 11.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } -} - - - - - - - - - - - -### ui/index/tabs/street/Street.kt ### -package com.aiosman.riderpro.ui.index.tabs.street - -import android.content.pm.PackageManager -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -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.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat -import androidx.navigation.NavOptions -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.test.countries -import com.google.android.gms.location.FusedLocationProviderClient -import com.google.android.gms.location.LocationServices -import com.google.android.gms.maps.model.CameraPosition -import com.google.android.gms.maps.model.LatLng -import com.google.maps.android.compose.GoogleMap -import com.google.maps.android.compose.MapProperties -import com.google.maps.android.compose.MapUiSettings -import com.google.maps.android.compose.MarkerComposable -import com.google.maps.android.compose.MarkerState -import com.google.maps.android.compose.rememberCameraPositionState - -@Composable -fun StreetPage() { - val navController = LocalNavController.current - val context = LocalContext.current - val fusedLocationClient: FusedLocationProviderClient = - LocationServices.getFusedLocationProviderClient(context) - var currentLocation by remember { mutableStateOf(null) } - val navigationBarHeight = with(LocalDensity.current) { - WindowInsets.navigationBars.getBottom(this).toDp() - } - val cameraPositionState = rememberCameraPositionState { - position = CameraPosition.fromLatLngZoom(currentLocation ?: LatLng(0.0, 0.0), 10f) - } - var hasLocationPermission by remember { mutableStateOf(false) } - var searchText by remember { mutableStateOf("") } - val permissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = { isGranted -> - hasLocationPermission = isGranted - if (isGranted) { - fusedLocationClient.lastLocation.addOnSuccessListener { location -> - if (location != null) { - currentLocation = LatLng(location.latitude, location.longitude) - } - } - } - } - ) - - LaunchedEffect(Unit) { - when (PackageManager.PERMISSION_GRANTED) { - ContextCompat.checkSelfPermission( - context, - android.Manifest.permission.ACCESS_FINE_LOCATION - ) -> { - fusedLocationClient.lastLocation.addOnSuccessListener { location -> - if (location != null) { -// currentLocation = LatLng(location.latitude, location.longitude) - } - } - hasLocationPermission = true - } - - else -> { - permissionLauncher.launch(android.Manifest.permission.ACCESS_FINE_LOCATION) - } - } - } - LaunchedEffect(currentLocation) { - - cameraPositionState.position = - CameraPosition.fromLatLngZoom( - currentLocation ?: LatLng( - countries[0].lat, - countries[0].lng - ), 5f - ) - } - - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 56.dp + navigationBarHeight) - ) { - GoogleMap( - modifier = Modifier.fillMaxSize(), - cameraPositionState = cameraPositionState, - properties = MapProperties( - isMyLocationEnabled = hasLocationPermission, - ), - uiSettings = MapUiSettings( - compassEnabled = true, - myLocationButtonEnabled = false, - zoomControlsEnabled = false - ) - ) { - // pins - countries.forEach { position -> - MarkerComposable( - state = MarkerState(position = LatLng(position.lat, position.lng)), - onClick = { - val screenLocation = - cameraPositionState.projection?.toScreenLocation(it.position) - val x = screenLocation?.x ?: 0 - val y = screenLocation?.y ?: 0 - - navController.navigate("LocationDetail/${x}/${y}",NavOptions.Builder() - .setEnterAnim(0) - .setExitAnim(0) - .setPopEnterAnim(0) - .setPopExitAnim(0) - .build()) - true - }, - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_map_mark), - contentDescription = "", - ) - } - } - - - } - Image( - painter = painterResource(id = R.drawable.rider_pro_my_location), - contentDescription = "", - modifier = Modifier - .align(Alignment.BottomStart) - .padding(start = 16.dp, bottom = 16.dp + navigationBarHeight) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - currentLocation?.let { - cameraPositionState.position = - CameraPosition.fromLatLngZoom(it, cameraPositionState.position.zoom) - } - } - ) - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 16.dp, bottom = 16.dp + navigationBarHeight) - ) { - Box( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .background(color = Color(0xffda3832)) - - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic), - contentDescription = "", - modifier = Modifier - .align(Alignment.Center) - .size(36.dp), - colorFilter = ColorFilter.tint(Color.White) - - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.TopCenter) - .padding(top = 64.dp, start = 16.dp, end = 16.dp) - ) { - Box( - modifier = Modifier - .background(Color.White) - .padding(16.dp), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background(Color(0xfff7f7f7)) - .padding(vertical = 8.dp, horizontal = 16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.rider_pro_search_location), - contentDescription = "", - tint = Color(0xffc6c6c6), - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - ) { - if (searchText.isEmpty()) { - Text( - text = "Please enter a search location", - color = Color(0xffc6c6c6), - fontSize = 16.sp, - modifier = Modifier.padding(start = 8.dp) - ) - } - - BasicTextField( - value = searchText, - onValueChange = { - searchText = it - }, - modifier = Modifier - .fillMaxWidth(), - textStyle = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal - ) - ) - } - - } - - } - } - } - - } - - -} - -### ui/favourite/FavouritePageViewModel.kt ### -package com.aiosman.riderpro.ui.favourite - -import android.icu.util.Calendar -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.cachedIn -import com.aiosman.riderpro.entity.AccountFavouriteEntity -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.entity.FavoriteItemPagingSource -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -/** - * 收藏消息列表的 ViewModel - */ -object FavouritePageViewModel : ViewModel() { - private val accountService: AccountService = AccountServiceImpl() - private val _favouriteItemsFlow = - MutableStateFlow>(PagingData.empty()) - val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow() - - init { - viewModelScope.launch { - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - FavoriteItemPagingSource( - accountService - ) - } - ).flow.cachedIn(viewModelScope).collectLatest { - _favouriteItemsFlow.value = it - } - } - } - // 更新收藏消息的查看时间 - suspend fun updateNotice() { - var now = Calendar.getInstance().time - accountService.updateNotice( - UpdateNoticeRequestBody( - lastLookFavouriteTime = ApiClient.formatTime(now) - ) - ) - } -} - -### ui/favourite/FavouriteScreen.kt ### -package com.aiosman.riderpro.ui.favourite - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -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.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.comment.NoticeScreenHeader -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder -import com.aiosman.riderpro.ui.like.ActionNoticeItem -import com.aiosman.riderpro.ui.like.LikePageViewModel - -/** - * 收藏消息界面 - */ -@Composable -fun FavouriteScreen() { - val model = FavouritePageViewModel - val listState = rememberLazyListState() - var dataFlow = model.favouriteItemsFlow - var favourites = dataFlow.collectAsLazyPagingItems() - LaunchedEffect(Unit) { - model.updateNotice() - } - StatusBarMaskLayout( - darkIcons = true, - maskBoxBackgroundColor = Color(0xFFFFFFFF) - ) { - Column( - modifier = Modifier - .weight(1f) - .background(color = Color(0xFFFFFFFF)) - .padding(horizontal = 16.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - ) { - NoticeScreenHeader( - "FAVOURITE", - moreIcon = false - ) - } - LazyColumn( - modifier = Modifier.weight(1f), - state = listState, - ) { - items(favourites.itemCount) { - val favouriteItem = favourites[it] - if (favouriteItem != null) { - ActionNoticeItem( - avatar = favouriteItem.user.avatar, - nickName = favouriteItem.user.nickName, - likeTime = favouriteItem.favoriteTime, - thumbnail = favouriteItem.post.images[0].thumbnail, - action = "favourite", - userId = favouriteItem.user.id, - postId = favouriteItem.post.id, - ) - } - } - item { - BottomNavigationPlaceholder() - } - } - - - } - } - -} - -### ui/account/changepassword.kt ### -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.unit.dp -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.data.AccountServiceImpl -import kotlinx.coroutines.launch - -/** - * 修改密码页面的 ViewModel - */ -class ChangePasswordViewModel { - val accountService: AccountService = AccountServiceImpl() - - /** - * 修改密码 - * @param currentPassword 当前密码 - * @param newPassword 新密码 - */ - suspend fun changePassword(currentPassword: String, newPassword: String) { - accountService.changeAccountPassword(currentPassword, newPassword) - } -} - -/** - * 修改密码页面 - */ -@Composable -fun ChangePasswordScreen() { - val viewModel = remember { ChangePasswordViewModel() } - var currentPassword by remember { mutableStateOf("") } - var newPassword by remember { mutableStateOf("") } - var confirmPassword by remember { mutableStateOf("") } - var errorMessage by remember { mutableStateOf("") } - val scope = rememberCoroutineScope() - val navController = LocalNavController.current - Scaffold { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - verticalArrangement = Arrangement.Center - ) { - TextField( - value = currentPassword, - onValueChange = { currentPassword = it }, - label = { Text("Current Password") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) - TextField( - value = newPassword, - onValueChange = { newPassword = it }, - label = { Text("New Password") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) - TextField( - value = confirmPassword, - onValueChange = { confirmPassword = it }, - label = { Text("Confirm New Password") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) - if (errorMessage.isNotEmpty()) { - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(bottom = 16.dp) - ) - } - Button( - onClick = { - if (newPassword == confirmPassword) { - scope.launch { - viewModel.changePassword(currentPassword, newPassword) - navController.popBackStack() - } - } else { - errorMessage = "Passwords do not match" - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Change Password") - } - } - } - -} - -### ui/account/edit.kt ### -package com.aiosman.riderpro.ui.account - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.util.Log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -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.platform.LocalContext -import androidx.compose.ui.unit.dp -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.data.AccountServiceImpl -import com.aiosman.riderpro.data.UserServiceImpl -import com.aiosman.riderpro.data.UploadImage -import com.aiosman.riderpro.data.UserService -import com.aiosman.riderpro.ui.composables.CustomAsyncImage -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile -import kotlinx.coroutines.launch - -/** - * 编辑用户资料界面 - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AccountEditScreen() { - val accountService: AccountService = AccountServiceImpl() - var name by remember { mutableStateOf("") } - var bio by remember { mutableStateOf("") } - var imageUrl by remember { mutableStateOf(null) } - var profile by remember { - mutableStateOf( - null - ) - } - val scope = rememberCoroutineScope() - val context = LocalContext.current - - /** - * 加载用户资料 - */ - suspend fun reloadProfile() { - accountService.getMyAccountProfile().let { - profile = it - name = it.nickName - bio = it.bio - } - - - } - - fun updateUserProfile() { - scope.launch { - val newAvatar = imageUrl?.let { - val cursor = context.contentResolver.query(it, null, null, null, null) - var newAvatar: UploadImage? = null - cursor?.use { cur -> - if (cur.moveToFirst()) { - val displayName = cur.getString(cur.getColumnIndex("_display_name")) - val extension = displayName.substringAfterLast(".") - Log.d("NewPost", "File name: $displayName, extension: $extension") - // read as file - val file = uriToFile(context, it) - Log.d("NewPost", "File size: ${file.length()}") - newAvatar = UploadImage(file, displayName, it.toString(), extension) - } - } - newAvatar - } - val newName = if (name == profile?.nickName) null else name - - accountService.updateProfile(newAvatar, newName, bio) - reloadProfile() - } - } - - val pickImageLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - val uri = result.data?.data - uri?.let { - imageUrl = it - } - } - } - - 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 - ) { - CustomAsyncImage( - context, - if (imageUrl != null) { - imageUrl.toString() - } else { - 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() - ) - } - } - } - - -} - -### ui/login/emailsignup.kt ### -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 -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.Text -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.AccountServiceImpl -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 = AccountServiceImpl() - 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, - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) { - NoticeScreenHeader("SIGNUP", moreIcon = false) - } - - Spacer(modifier = Modifier.padding(68.dp)) - TextInputField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - text = email, - onValueChange = { - email = it - }, - label = "What's your email?", - hint = "Enter your email" - ) - Spacer(modifier = Modifier.padding(16.dp)) - TextInputField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - text = password, - onValueChange = { - password = it - }, - password = true, - label = "What's your password?", - hint = "Enter your password" - ) - Spacer(modifier = Modifier.padding(16.dp)) - TextInputField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - text = confirmPassword, - onValueChange = { - confirmPassword = it - }, - password = true, - label = "Confirm password", - hint = "Enter your password" - ) - Spacer(modifier = Modifier.height(32.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start, - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = rememberMe, - onCheckedChange = { - rememberMe = it - }, - modifier = Modifier.padding(start = 16.dp), - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text("Remember me", modifier = Modifier.padding(start = 4.dp)) - } - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = acceptTerms, - onCheckedChange = { - acceptTerms = it - }, - modifier = Modifier.padding(start = 16.dp), - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text( - "Yes, I have read and agree to RiderPro’s Terms of Service.", - modifier = Modifier.padding(start = 4.dp), - fontSize = 12.sp - ) - } - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = acceptPromotions, - onCheckedChange = { - acceptPromotions = it - }, - modifier = Modifier.padding(start = 16.dp), - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text( - "Yes, Send me news and promotional content from RiderPro.", - modifier = Modifier.padding(start = 4.dp), - fontSize = 12.sp - ) - } - } - - Spacer(modifier = Modifier.height(64.dp)) - ActionButton( - modifier = Modifier - .width(345.dp) - .height(48.dp), - text = "LET'S RIDE".uppercase(), - backgroundImage = R.mipmap.rider_pro_signup_red_bg - ) { - scope.launch(Dispatchers.IO) { - registerUser() - } - } - } - } -} - -### ui/login/login.kt ### -package com.aiosman.riderpro.ui.login - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.navigation.NavOptions -import androidx.navigation.navOptions -import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.R -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.modifiers.noRippleClickable - -@Composable -fun LoginPage() { - val navController = LocalNavController.current - LaunchedEffect(Unit) { -// navController.navigate(NavigationRoute.Index.route) - } - Scaffold { - it - Box( - modifier = Modifier.fillMaxSize() - ) { - // to bottom - Box( - contentAlignment = Alignment.TopCenter, - modifier = Modifier.padding(top = 211.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { - Image( - painter = painterResource(id = R.mipmap.rider_pro_logo), - contentDescription = "Rider Pro", - modifier = Modifier - .width(108.dp) - .height(45.dp) - ) - Spacer(modifier = Modifier.height(32.dp)) - Text( - "Connecting Riders".uppercase(), - fontSize = 28.sp, - fontWeight = FontWeight.Bold - ) - Text("Worldwide".uppercase(), fontSize = 28.sp, fontWeight = FontWeight.Bold) - } - - } - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 82.dp + 162.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - ActionButton( - modifier = Modifier - .width(162.dp) - .height(48.dp), - text = "Login in".uppercase(), - backgroundImage = R.mipmap.rider_pro_grey_bg_big - ) { - navController.navigate( - NavigationRoute.UserAuth.route, - ) - } - ActionButton( - modifier = Modifier - .width(162.dp) - .height(48.dp), - text = "Sign In".uppercase(), - backgroundImage = R.mipmap.rider_pro_red_bg_big - ){ - navController.navigate( - NavigationRoute.SignUp.route, - ) - } - } - } - } - - } -} - -@Composable -fun ActionButton( - modifier: Modifier, - text: String, - color: Color = Color.White, - @DrawableRes backgroundImage: Int, - leading: @Composable (() -> Unit)? = null, - click: () -> Unit = {} -) { - Box( - modifier = modifier.noRippleClickable { - click() - } - ) { - Image( - painter = painterResource(id = backgroundImage), - contentDescription = "Rider Pro", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.FillBounds - ) - Row( - modifier = Modifier.align(Alignment.Center), - verticalAlignment = Alignment.CenterVertically - ) { - leading?.invoke() - Text(text, fontSize = 16.sp, color = color, fontWeight = FontWeight.W400) - } - - } -} - -### ui/login/signup.kt ### -package com.aiosman.riderpro.ui.login - -import android.content.ContentValues.TAG -import android.util.Log -import android.widget.Toast -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -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.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.credentials.Credential -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -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.AccountServiceImpl -import com.aiosman.riderpro.data.ServiceException -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.aiosman.riderpro.utils.GoogleLogin -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - - -@Composable -fun SignupScreen() { - val navController = LocalNavController.current - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val accountService: AccountService = AccountServiceImpl() - - fun googleLogin() { - coroutineScope.launch { - try { - - GoogleLogin(context) { - coroutineScope.launch { - try { - accountService.regiterUserWithGoogleAccount(it) - } catch (e: Exception) { - Log.e(TAG, "Failed to register with google", e) - return@launch - } - // 获取用户信息 - // 获取 token - val authResp = accountService.loginUserWithGoogle(it) - if (authResp.token != null) { - coroutineScope.launch(Dispatchers.Main) { - Toast.makeText( - context, - "Successfully registered", - Toast.LENGTH_SHORT - ) - .show() - } - } - AppStore.apply { - token = authResp.token - this.rememberMe = true - saveData() - } - // 获取token 信息 - try { - accountService.getMyAccount() - } catch (e: ServiceException) { - coroutineScope.launch(Dispatchers.Main) { - Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT) - .show() - } - } - coroutineScope.launch(Dispatchers.Main) { - navController.navigate(NavigationRoute.Index.route) { - popUpTo(NavigationRoute.Login.route) { inclusive = true } - } - } - } - - } - } catch (e: Exception) { - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - } - - } - } - - - Scaffold( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFFECECEC)) - ) { - it - Box( - modifier = Modifier.fillMaxSize() - ) { - // to bottom - Box( - contentAlignment = Alignment.TopCenter, - modifier = Modifier.padding(top = 211.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { - Image( - painter = painterResource(id = R.mipmap.rider_pro_logo), - contentDescription = "Rider Pro", - modifier = Modifier - .width(108.dp) - .height(45.dp) - ) - Spacer(modifier = Modifier.height(32.dp)) - Text( - "Connecting Riders".uppercase(), - fontSize = 28.sp, - fontWeight = FontWeight.Bold - ) - Text("Worldwide".uppercase(), fontSize = 28.sp, fontWeight = FontWeight.Bold) - } - - } - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 82.dp, start = 24.dp, end = 24.dp) - - ) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - ActionButton( - modifier = Modifier - .width(345.dp) - .height(48.dp), - text = "CONTINUE WITH EMAIL".uppercase(), - backgroundImage = R.mipmap.rider_pro_signup_red_bg, - leading = { - Image( - painter = painterResource(id = R.mipmap.rider_pro_email), - contentDescription = "Email", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - } - ) { - navController.navigate(NavigationRoute.EmailSignUp.route) - } -// Spacer(modifier = Modifier.height(16.dp)) -// ActionButton( -// modifier = Modifier -// .width(345.dp) -// .height(48.dp), -// text = "CONTINUE WITH FACEBOOK".uppercase(), -// backgroundImage = R.mipmap.rider_pro_signup_facebook_bg, -// leading = { -// Image( -// painter = painterResource(id = R.mipmap.rider_pro_signup_facebook), -// contentDescription = "Facebook", -// modifier = Modifier.size(24.dp) -// ) -// Spacer(modifier = Modifier.width(8.dp)) -// } -// ) - Spacer(modifier = Modifier.height(16.dp)) - ActionButton( - modifier = Modifier - .width(345.dp) - .height(48.dp), - color = Color.Black, - leading = { - Image( - painter = painterResource(id = R.mipmap.rider_pro_signup_google), - contentDescription = "Google", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - }, - text = "CONTINUE WITH GOOGLE".uppercase(), - backgroundImage = R.mipmap.rider_pro_signup_white_bg - ) { - googleLogin() - - } -// Spacer(modifier = Modifier.height(16.dp)) -// Box( -// modifier = Modifier -// .width(345.dp) -// .height(48.dp) -// .clip(RoundedCornerShape(8.dp)) -// .background(Color.Black) -// -// ) { -// Text( -// "Sign in with Apple", -// color = Color.White, -// modifier = Modifier.align(Alignment.Center), -// fontSize = 16.sp, -// fontWeight = FontWeight.Bold -// ) -// } - Spacer(modifier = Modifier.height(40.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.noRippleClickable { - navController.popBackStack() - } - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_nav_back), - contentDescription = "Back", - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - "BACK", - color = Color.Black, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - } - } - } - } - - } -} - -### ui/login/userauth.kt ### -package com.aiosman.riderpro.ui.login - -import android.content.ContentValues.TAG -import android.util.Log -import android.widget.Toast -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -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.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.credentials.Credential -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import androidx.credentials.exceptions.GetCredentialProviderConfigurationException -import androidx.credentials.exceptions.NoCredentialException -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.AccountServiceImpl -import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.comment.NoticeScreenHeader -import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout -import com.aiosman.riderpro.ui.modifiers.noRippleClickable -import com.aiosman.riderpro.utils.GoogleLogin -import com.google.android.libraries.identity.googleid.GetGoogleIdOption -import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential -import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun UserAuthScreen() { - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var rememberMe by remember { mutableStateOf(false) } - var accountService: AccountService = AccountServiceImpl() - val scope = rememberCoroutineScope() - val navController = LocalNavController.current - val context = LocalContext.current - val credentialManager = CredentialManager.create(context) - 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 - } - return true - } - - fun onLogin() { - if (!validateForm()) { - return - } - scope.launch { - 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() - } - } - - } - - - fun googleLogin() { - scope.launch { - try { - GoogleLogin(context) { - scope.launch { - try { - val authResp = accountService.loginUserWithGoogle(it) - 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 - e.printStackTrace() - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - } - } - } - } catch (e: Exception) { - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - } - - } - - } - StatusBarMaskLayout { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) { - NoticeScreenHeader("LOGIN", moreIcon = false) - } - - Spacer(modifier = Modifier.padding(68.dp)) - TextInputField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - text = email, - onValueChange = { - email = it - }, - label = "What's your email?", - hint = "Enter your email" - ) - Spacer(modifier = Modifier.padding(16.dp)) - TextInputField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - text = password, - onValueChange = { - password = it - }, - password = true, - label = "What's your password?", - hint = "Enter your password" - ) - Spacer(modifier = Modifier.height(32.dp)) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = rememberMe, - onCheckedChange = { - rememberMe = it - }, - modifier = Modifier.padding(start = 24.dp), - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text("Remember me", modifier = Modifier.padding(start = 4.dp)) - Spacer(modifier = Modifier.weight(1f)) - Text("Forgot password?", modifier = Modifier.padding(end = 24.dp)) - } - Spacer(modifier = Modifier.height(64.dp)) - ActionButton( - modifier = Modifier - .width(345.dp) - .height(48.dp), - text = "LET'S RIDE".uppercase(), - backgroundImage = R.mipmap.rider_pro_signup_red_bg - ) { - onLogin() - } - - Spacer(modifier = Modifier.height(121.dp)) - Text("or login with", color = Color(0xFF999999)) - Spacer(modifier = Modifier.height(16.dp)) - Row { - Box( - modifier = Modifier - .size(96.dp) - .padding(16.dp) - .border(2.dp, Color(0xFFEBEBEB)) - .noRippleClickable { - // login with facebook - googleLogin() - } - ) { - Image( - painter = painterResource(id = R.drawable.rider_pro_google), - contentDescription = "Google", - modifier = Modifier.fillMaxSize() - ) - } - } - } - } - -} - -@Composable -fun TextInputField( - modifier: Modifier = Modifier, - text: String, - onValueChange: (String) -> Unit, - password: Boolean = false, - label: String? = null, - hint: String? = null - -) { - var showPassword by remember { mutableStateOf(!password) } - Column(modifier = modifier) { - label?.let { - Text(it, color = Color(0xFFCCCCCC)) - Spacer(modifier = Modifier.height(16.dp)) - } - Box( - contentAlignment = Alignment.CenterStart - ) { - Row { - BasicTextField( - value = text, - onValueChange = onValueChange, - modifier = Modifier.weight(1f), - textStyle = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.W500 - ), - keyboardOptions = KeyboardOptions( - keyboardType = if (password) KeyboardType.Password else KeyboardType.Text - ), - visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - singleLine = true - ) - if (password) { - Image( - painter = painterResource(id = R.drawable.rider_pro_eye), - contentDescription = "Password", - modifier = Modifier - .size(24.dp) - .noRippleClickable { - showPassword = !showPassword - }, - colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color.Black) - ) - } - } - - if (text.isEmpty()) { - hint?.let { - Text(it, color = Color(0xFFCCCCCC), fontWeight = FontWeight.W600) - } - } - } - Spacer(modifier = Modifier.height(16.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(Color(0xFFF5F5F5)) - ) - } -} - -### test/TestStreetMap.kt ### -package com.aiosman.riderpro.test - -data class StreetPosition( - val name:String, - val lat:Double, - val lng:Double -) -val countries = listOf( - StreetPosition("哈龙湾, 越南",16.5000, 107.1000), - StreetPosition("芽庄, 越南",12.2500, 109.0833), - StreetPosition("岘港, 越南",16.0667, 108.2167), - StreetPosition("美奈, 越南",11.9333, 108.9833), - StreetPosition("富国岛, 越南",10.0000, 104.0000), - StreetPosition("金三角, 泰国, 缅甸, 老挝",20.2500, 99.7500), - StreetPosition("普吉岛, 泰国",7.9444, 98.3000), - StreetPosition("苏梅岛, 泰国",9.5333, 99.9333), - StreetPosition("曼谷, 泰国",13.7500, 100.5000), - StreetPosition("马六甲, 马来西亚",2.2000, 102.2500), - StreetPosition("兰卡威群岛, 马来西亚",6.3000, 99.9000), - StreetPosition("沙巴, 马来西亚",6.0833, 116.0833), - StreetPosition("巴厘岛, 印度尼西亚",8.3333, 115.1000), - StreetPosition("龙目岛, 印度尼西亚",8.3333, 116.4000), - StreetPosition("婆罗洲, 印度尼西亚",3.0000, 114.0000), - StreetPosition("宿务, 菲律宾",10.3167, 123.8833), - StreetPosition("长滩岛, 菲律宾",11.5833, 121.9167), - StreetPosition("保和岛, 菲律宾",10.3000, 123.3333), - StreetPosition("科隆岛, 菲律宾",5.1167, 119.3333) -) - - -### entity/Account.kt ### -package com.aiosman.riderpro.entity - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.aiosman.riderpro.data.AccountFollow -import com.aiosman.riderpro.data.AccountService -import com.aiosman.riderpro.data.Image -import com.aiosman.riderpro.data.api.ApiClient -import java.io.IOException -import java.util.Date - -/** - * 用户点赞 - */ -data class AccountLikeEntity( - // 动态 - val post: NoticePostEntity, - // 点赞用户 - val user: NoticeUserEntity, - // 点赞时间 - val likeTime: Date, -) - -/** - * 用户收藏 - */ -data class AccountFavouriteEntity( - // 动态 - val post: NoticePostEntity, - // 收藏用户 - val user: NoticeUserEntity, - // 收藏时间 - val favoriteTime: Date, -) - -/** - * 用户信息 - */ -data class AccountProfileEntity( - // 用户ID - val id: Int, - // 粉丝数 - val followerCount: Int, - // 关注数 - val followingCount: Int, - // 昵称 - val nickName: String, - // 头像 - val avatar: String, - // 个人简介 - val bio: String, - // 国家 - val country: String, - // 是否关注,针对当前登录用户 - val isFollowing: Boolean -) - -/** - * 消息关联的动态 - */ -data class NoticePostEntity( - // 动态ID - val id: Int, - // 动态内容 - val textContent: String, - // 动态图片 - val images: List, - // 时间 - val time: Date, -) - -/** - * 消息关联的用户 - */ -data class NoticeUserEntity( - // 用户ID - val id: Int, - // 昵称 - val nickName: String, - // 头像 - val avatar: String, -) - -/** - * 用户点赞消息分页数据加载器 - */ -class LikeItemPagingSource( - private val accountService: AccountService, -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - return try { - val currentPage = params.key ?: 1 - val likes = accountService.getMyLikeNotice( - page = currentPage, - pageSize = 20, - ) - - LoadResult.Page( - data = likes.list.map { - it.toAccountLikeEntity() - }, - prevKey = if (currentPage == 1) null else currentPage - 1, - nextKey = if (likes.list.isEmpty()) null else likes.page + 1 - ) - } catch (exception: IOException) { - return LoadResult.Error(exception) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition - } -} - -/** - * 用户收藏消息分页数据加载器 - */ -class FavoriteItemPagingSource( - private val accountService: AccountService, -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - return try { - val currentPage = params.key ?: 1 - val favouriteListContainer = accountService.getMyFavouriteNotice( - page = currentPage, - pageSize = 20, - ) - LoadResult.Page( - data = favouriteListContainer.list.map { - it.toAccountFavouriteEntity() - }, - prevKey = if (currentPage == 1) null else currentPage - 1, - nextKey = if (favouriteListContainer.list.isEmpty()) null else favouriteListContainer.page + 1 - ) - } catch (exception: IOException) { - return LoadResult.Error(exception) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition - } -} - -/** - * 用户关注消息分页数据加载器 - */ -class FollowItemPagingSource( - private val accountService: AccountService, -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - return try { - val currentPage = params.key ?: 1 - val followListContainer = accountService.getMyFollowNotice( - page = currentPage, - pageSize = 20, - ) - - LoadResult.Page( - data = followListContainer.list.map { - it.copy( - avatar = "${ApiClient.BASE_SERVER}${it.avatar}", - ) - }, - prevKey = if (currentPage == 1) null else currentPage - 1, - nextKey = if (followListContainer.list.isEmpty()) null else followListContainer.page + 1 - ) - } catch (exception: IOException) { - return LoadResult.Error(exception) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition - } -} - -### entity/Comment.kt ### -package com.aiosman.riderpro.entity - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.aiosman.riderpro.data.CommentRemoteDataSource -import com.aiosman.riderpro.data.NoticePost -import java.io.IOException -import java.util.Date - -data class CommentEntity( - val id: Int, - val name: String, - val comment: String, - val date: Date, - val likes: Int, - val replies: List, - val postId: Int = 0, - val avatar: String, - val author: Long, - var liked: Boolean, - var unread: Boolean = false, - var post: NoticePost? -) - -class CommentPagingSource( - private val remoteDataSource: CommentRemoteDataSource, - private val postId: Int? = null, - private val postUser: Int? = null, - private val selfNotice: Boolean? = null -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - return try { - val currentPage = params.key ?: 1 - val comments = remoteDataSource.getComments( - pageNumber = currentPage, - postId = postId, - postUser = postUser, - selfNotice = selfNotice - ) - LoadResult.Page( - data = comments.list, - prevKey = if (currentPage == 1) null else currentPage - 1, - nextKey = if (comments.list.isEmpty()) null else comments.page + 1 - ) - } catch (exception: IOException) { - return LoadResult.Error(exception) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition - } - -} - -### entity/Moment.kt ### -package com.aiosman.riderpro.entity - -import androidx.annotation.DrawableRes -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.aiosman.riderpro.data.ListContainer -import com.aiosman.riderpro.data.MomentService -import com.aiosman.riderpro.data.ServiceException -import com.aiosman.riderpro.data.UploadImage -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.entity.MomentEntity -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import java.io.File -import java.io.IOException -import java.util.Date - -/** - * 动态分页加载器 - */ -class MomentPagingSource( - private val remoteDataSource: MomentRemoteDataSource, - private val author: Int? = null, - private val timelineId: Int? = null, - private val contentSearch: String? = null -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - return try { - val currentPage = params.key ?: 1 - val moments = remoteDataSource.getMoments( - pageNumber = currentPage, - author = author, - timelineId = timelineId, - contentSearch = contentSearch - ) - - LoadResult.Page( - data = moments.list, - prevKey = if (currentPage == 1) null else currentPage - 1, - nextKey = if (moments.list.isEmpty()) null else moments.page + 1 - ) - } catch (exception: IOException) { - return LoadResult.Error(exception) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition - } - -} - -class MomentRemoteDataSource( - private val momentService: MomentService, -) { - suspend fun getMoments( - pageNumber: Int, - author: Int?, - timelineId: Int?, - contentSearch: String? - ): ListContainer { - return momentService.getMoments(pageNumber, author, timelineId, contentSearch) - } -} - -class MomentServiceImpl() : MomentService { - val momentBackend = MomentBackend() - - override suspend fun getMoments( - pageNumber: Int, - author: Int?, - timelineId: Int?, - contentSearch: String? - ): ListContainer { - return momentBackend.fetchMomentItems(pageNumber, author, timelineId, contentSearch) - } - - override suspend fun getMomentById(id: Int): MomentEntity { - return momentBackend.getMomentById(id) - } - - - override suspend fun likeMoment(id: Int) { - momentBackend.likeMoment(id) - } - - override suspend fun dislikeMoment(id: Int) { - momentBackend.dislikeMoment(id) - } - - override suspend fun createMoment( - content: String, - authorId: Int, - images: List, - relPostId: Int? - ): MomentEntity { - return momentBackend.createMoment(content, authorId, images, relPostId) - } - - override suspend fun favoriteMoment(id: Int) { - momentBackend.favoriteMoment(id) - } - - override suspend fun unfavoriteMoment(id: Int) { - momentBackend.unfavoriteMoment(id) - } - -} - -class MomentBackend { - val DataBatchSize = 20 - suspend fun fetchMomentItems( - pageNumber: Int, - author: Int? = null, - timelineId: Int?, - contentSearch: String? - ): ListContainer { - val resp = ApiClient.api.getPosts( - pageSize = DataBatchSize, - page = pageNumber, - timelineId = timelineId, - authorId = author, - contentSearch = contentSearch - ) - val body = resp.body() ?: throw ServiceException("Failed to get moments") - return ListContainer( - total = body.total, - page = pageNumber, - pageSize = DataBatchSize, - list = body.list.map { it.toMomentItem() } - ) - } - - 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) { - ApiClient.api.likePost(id) - } - - suspend fun dislikeMoment(id: Int) { - 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, - relPostId: Int? - ): MomentEntity { - val textContent = content.toRequestBody("text/plain".toMediaTypeOrNull()) - val imageList = imageUriList.map { item -> - val file = item.file - createMultipartBody(file, "image") - } - 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) - } - -} - -/** - * 动态图片 - */ -data class MomentImageEntity( - // 图片ID - val id: Long, - // 图片URL - val url: String, - // 缩略图URL - val thumbnail: String, - // 图片BlurHash - val blurHash: String? = null -) - -/** - * 动态 - */ -data class MomentEntity( - // 动态ID - val id: Int, - // 作者头像 - val avatar: String, - // 作者昵称 - val nickname: String, - // 区域 - val location: String, - // 动态时间 - val time: Date, - // 是否关注 - val followStatus: Boolean, - // 动态内容 - val momentTextContent: String, - // 动态图片 - @DrawableRes val momentPicture: Int, - // 点赞数 - val likeCount: Int, - // 评论数 - val commentCount: Int, - // 分享数 - val shareCount: Int, - // 收藏数 - val favoriteCount: Int, - // 动态图片列表 - val images: List = emptyList(), - // 作者ID - val authorId: Int = 0, - // 是否点赞 - var liked: Boolean = false, - // 关联动态ID - var relPostId: Int? = null, - // 关联动态 - var relMoment: MomentEntity? = null, - // 是否收藏 - var isFavorite: Boolean = false -) - -### entity/User.kt ### -package com.aiosman.riderpro.entity - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.aiosman.riderpro.data.UserService -import java.io.IOException - -/** - * 用户信息分页加载器 - */ -class AccountPagingSource( - private val userService: UserService, - private val nickname: String? = null -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - return try { - val currentPage = params.key ?: 1 - val users = userService.getUsers( - page = currentPage, - nickname = nickname - ) - LoadResult.Page( - data = users.list, - prevKey = if (currentPage == 1) null else currentPage - 1, - nextKey = if (users.list.isEmpty()) null else users.page + 1 - ) - } catch (exception: IOException) { - return LoadResult.Error(exception) - } - } - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition - } - -} - -### utils/BlurHashDecoder.kt ### -package com.aiosman.riderpro.utils - -import android.graphics.Bitmap -import android.graphics.Color -import androidx.collection.SparseArrayCompat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlin.math.cos -import kotlin.math.pow -import kotlin.math.withSign - -internal object BlurHashDecoder { - - // cache Math.cos() calculations to improve performance. - // The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps - // the cache is enabled by default, it is recommended to disable it only when just a few images are displayed - private val cacheCosinesX = SparseArrayCompat() - private val cacheCosinesY = SparseArrayCompat() - - /** - * Clear calculations stored in memory cache. - * The cache is not big, but will increase when many image sizes are used, - * if the app needs memory it is recommended to clear it. - */ - private fun clearCache() { - cacheCosinesX.clear() - cacheCosinesY.clear() - } - - /** - * Decode a blur hash into a new bitmap. - * - * @param useCache use in memory cache for the calculated math, reused by images with same size. - * if the cache does not exist yet it will be created and populated with new calculations. - * By default it is true. - */ - @Suppress("ReturnCount") - internal fun decode( - blurHash: String?, - width: Int, - height: Int, - punch: Float = 1f, - useCache: Boolean = true - ): Bitmap? { - if (blurHash == null || blurHash.length < 6) { - return null - } - val numCompEnc = decode83(blurHash, 0, 1) - val numCompX = (numCompEnc % 9) + 1 - val numCompY = (numCompEnc / 9) + 1 - if (blurHash.length != 4 + 2 * numCompX * numCompY) { - return null - } - val maxAcEnc = decode83(blurHash, 1, 2) - val maxAc = (maxAcEnc + 1) / 166f - val colors = Array(numCompX * numCompY) { i -> - if (i == 0) { - val colorEnc = decode83(blurHash, 2, 6) - decodeDc(colorEnc) - } else { - val from = 4 + i * 2 - val colorEnc = decode83(blurHash, from, from + 2) - decodeAc(colorEnc, maxAc * punch) - } - } - return composeBitmap(width, height, numCompX, numCompY, colors, useCache) - } - - private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { - var result = 0 - for (i in from until to) { - val index = charMap[str[i]] ?: -1 - if (index != -1) { - result = result * 83 + index - } - } - return result - } - - private fun decodeDc(colorEnc: Int): FloatArray { - val r = colorEnc shr 16 - val g = (colorEnc shr 8) and 255 - val b = colorEnc and 255 - return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)) - } - - private fun srgbToLinear(colorEnc: Int): Float { - val v = colorEnc / 255f - return if (v <= 0.04045f) { - (v / 12.92f) - } else { - ((v + 0.055f) / 1.055f).pow(2.4f) - } - } - - private fun decodeAc(value: Int, maxAc: Float): FloatArray { - val r = value / (19 * 19) - val g = (value / 19) % 19 - val b = value % 19 - return floatArrayOf( - signedPow2((r - 9) / 9.0f) * maxAc, - signedPow2((g - 9) / 9.0f) * maxAc, - signedPow2((b - 9) / 9.0f) * maxAc - ) - } - - private fun signedPow2(value: Float) = value.pow(2f).withSign(value) - - private fun composeBitmap( - width: Int, - height: Int, - numCompX: Int, - numCompY: Int, - colors: Array, - useCache: Boolean - ): Bitmap { - // use an array for better performance when writing pixel colors - val imageArray = IntArray(width * height) - val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) - val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX) - val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) - val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY) - runBlocking { - CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { - val tasks = ArrayList>() - tasks.add( - async { - for (y in 0 until height) { - for (x in 0 until width) { - var r = 0f - var g = 0f - var b = 0f - for (j in 0 until numCompY) { - for (i in 0 until numCompX) { - val cosX = - cosinesX.getCos(calculateCosX, i, numCompX, x, width) - val cosY = - cosinesY.getCos(calculateCosY, j, numCompY, y, height) - val basis = (cosX * cosY).toFloat() - val color = colors[j * numCompX + i] - r += color[0] * basis - g += color[1] * basis - b += color[2] * basis - } - } - imageArray[x + width * y] = - Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) - } - } - return@async - } - ) - tasks.forEach { it.await() } - }.join() - } - - return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) - } - - private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when { - calculate -> { - DoubleArray(height * numCompY).also { - cacheCosinesY.put(height * numCompY, it) - } - } - - else -> { - cacheCosinesY.get(height * numCompY)!! - } - } - - private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when { - calculate -> { - DoubleArray(width * numCompX).also { - cacheCosinesX.put(width * numCompX, it) - } - } - - else -> cacheCosinesX.get(width * numCompX)!! - } - - private fun DoubleArray.getCos( - calculate: Boolean, - x: Int, - numComp: Int, - y: Int, - size: Int - ): Double { - if (calculate) { - this[x + numComp * y] = cos(Math.PI * y * x / size) - } - return this[x + numComp * y] - } - - private fun linearToSrgb(value: Float): Int { - val v = value.coerceIn(0f, 1f) - return if (v <= 0.0031308f) { - (v * 12.92f * 255f + 0.5f).toInt() - } else { - ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() - } - } - - private val charMap = listOf( - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', - 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', - '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' - ) - .mapIndexed { i, c -> c to i } - .toMap() -} - -### utils/GoogleLogin.kt ### -package com.aiosman.riderpro.utils - -import android.content.Context -import androidx.credentials.Credential -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import com.google.android.libraries.identity.googleid.GetGoogleIdOption -import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential - - -fun handleGoogleSignIn(result: GetCredentialResponse, onLoginWithGoogle: (String) -> Unit) { - val credential: Credential = result.credential - - if (credential is CustomCredential) { - if (GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL.equals(credential.type)) { - val googleIdTokenCredential: GoogleIdTokenCredential = - GoogleIdTokenCredential.createFrom(credential.data) - onLoginWithGoogle(googleIdTokenCredential.idToken) - } - } -} - -suspend fun GoogleLogin(context: Context, onLoginWithGoogle: (String) -> Unit) { - val credentialManager = CredentialManager.create(context) - val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(true) - .setServerClientId("754277015802-pnua6tg8ibnjq69lv8qdcmsdhbe97ag9.apps.googleusercontent.com") - .build() - val request = GetCredentialRequest.Builder().addCredentialOption(googleIdOption) - .build() - - credentialManager.getCredential(context, request).let { - handleGoogleSignIn(it, onLoginWithGoogle) - } -} - -### utils/Utils.kt ### -package com.aiosman.riderpro.utils - -import android.content.Context -import coil.ImageLoader -import coil.disk.DiskCache -import coil.memory.MemoryCache -import coil.request.CachePolicy -import com.aiosman.riderpro.data.api.getUnsafeOkHttpClient -import java.util.Date -import java.util.concurrent.TimeUnit - -object Utils { - fun generateRandomString(length: Int): String { - val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') - return (1..length) - .map { allowedChars.random() } - .joinToString("") - } - - fun getImageLoader(context: Context): ImageLoader { - val okHttpClient = getUnsafeOkHttpClient() - return ImageLoader.Builder(context) - .okHttpClient(okHttpClient) - .memoryCache { - MemoryCache.Builder(context) - .maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25% - .build() - } - .diskCache { - DiskCache.Builder() - .directory(context.cacheDir.resolve("image_cache")) - .maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2% - .build() - } - .build() - } - - fun getTimeAgo(date: Date): String { - val now = Date() - val diffInMillis = now.time - date.time - - val seconds = TimeUnit.MILLISECONDS.toSeconds(diffInMillis) - val minutes = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) - val hours = TimeUnit.MILLISECONDS.toHours(diffInMillis) - val days = TimeUnit.MILLISECONDS.toDays(diffInMillis) - val years = days / 365 - - return when { - seconds < 60 -> "$seconds seconds ago" - minutes < 60 -> "$minutes minutes ago" - hours < 24 -> "$hours hours ago" - days < 365 -> "$days days ago" - else -> "$years years ago" - } - } -} - -### model/ChatNotificationData.kt ### -package com.aiosman.riderpro.model - -import androidx.annotation.DrawableRes - -data class ChatNotificationData( - @DrawableRes val avatar: Int, - val name: String, - val message: String, - val time: String, - val unread: Int -) - - -### model/ChatNotificationUtils.kt ### -package com.aiosman.riderpro.model - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import kotlin.math.ceil -import kotlinx.coroutines.delay - -internal class TestChatBackend( - private val backendDataList: List, - private val loadDelay: Long = 500, -) { - val DataBatchSize = 1 - class DesiredLoadResultPageResponse(val data: List) - /** Returns [DataBatchSize] items for a key */ - fun searchItemsByKey(key: Int): DesiredLoadResultPageResponse { - val maxKey = ceil(backendDataList.size.toFloat() / DataBatchSize).toInt() - if (key >= maxKey) { - return DesiredLoadResultPageResponse(emptyList()) - } - val from = key * DataBatchSize - val to = minOf((key + 1) * DataBatchSize, backendDataList.size) - val currentSublist = backendDataList.subList(from, to) - return DesiredLoadResultPageResponse(currentSublist) - } - fun getAllData() = TestChatPagingSource(this, loadDelay) -} -internal class TestChatPagingSource( - private val backend: TestChatBackend, - private val loadDelay: Long, -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - // Simulate latency - delay(loadDelay) - val pageNumber = params.key ?: 0 - val response = backend.searchItemsByKey(pageNumber) - // Since 0 is the lowest page number, return null to signify no more pages should - // be loaded before it. - val prevKey = if (pageNumber > 0) pageNumber - 1 else null - // This API defines that it's out of data when a page returns empty. When out of - // data, we return `null` to signify no more pages should be loaded - 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? { - return state.anchorPosition?.let { - state.closestPageToPosition(it)?.prevKey?.plus(1) - ?: state.closestPageToPosition(it)?.nextKey?.minus(1) - } - } -} - -### exp/Date.kt ### -package com.aiosman.riderpro.exp - -import android.icu.text.SimpleDateFormat -import android.icu.util.Calendar -import com.aiosman.riderpro.data.api.ApiClient -import java.util.Date -import java.util.Locale - -/** - * 格式化时间为 xx 前 - */ -fun Date.timeAgo(): String { - val now = Date() - val diffInMillis = now.time - this.time - - val seconds = diffInMillis / 1000 - val minutes = seconds / 60 - val hours = minutes / 60 - val days = hours / 24 - val years = days / 365 - - return when { - seconds < 60 -> "$seconds seconds ago" - minutes < 60 -> "$minutes minutes ago" - hours < 24 -> "$hours hours ago" - days < 365 -> "$days days ago" - else -> "$years years ago" - } -} - -/** - * 格式化时间为 xx-xx - */ -fun Date.formatPostTime(): String { - val now = Calendar.getInstance() - val calendar = Calendar.getInstance() - calendar.time = this - val year = calendar.get(Calendar.YEAR) - var nowYear = now.get(Calendar.YEAR) - val dateFormat = if (year == nowYear) { - SimpleDateFormat("MM-dd", Locale.getDefault()) - } else { - SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - } - return dateFormat.format(this) -} - -### exp/StatusBarExp.kt ### -package com.aiosman.riderpro.exp - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.res.Resources -import android.os.Build -import android.util.TypedValue -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import androidx.annotation.ColorInt -//import androidx.appcompat.app.AppCompatActivity - -private const val COLOR_TRANSPARENT = 0 - -@SuppressLint("ObsoleteSdkInt") -@JvmOverloads -fun Activity.immersive(@ColorInt color: Int = COLOR_TRANSPARENT, darkMode: Boolean? = null) { - when { - Build.VERSION.SDK_INT >= 21 -> { - when (color) { - COLOR_TRANSPARENT -> { - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - var systemUiVisibility = window.decorView.systemUiVisibility - systemUiVisibility = systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - systemUiVisibility = systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE - window.decorView.systemUiVisibility = systemUiVisibility - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - window.statusBarColor = color - } - else -> { - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - var systemUiVisibility = window.decorView.systemUiVisibility - systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE - window.decorView.systemUiVisibility = systemUiVisibility - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - window.statusBarColor = color - } - } - } - Build.VERSION.SDK_INT >= 19 -> { - window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - if (color != COLOR_TRANSPARENT) { - setTranslucentView(window.decorView as ViewGroup, color) - } - } - } - if (darkMode != null) { - darkMode(darkMode) - } -} - -@JvmOverloads -fun Activity.darkMode(darkMode: Boolean = true) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - var systemUiVisibility = window.decorView.systemUiVisibility - systemUiVisibility = if (darkMode) { - systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - } else { - systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() - } - window.decorView.systemUiVisibility = systemUiVisibility - } -} - -private fun Context.setTranslucentView(container: ViewGroup, color: Int) { - if (Build.VERSION.SDK_INT >= 19) { - var simulateStatusBar: View? = container.findViewById(android.R.id.custom) - if (simulateStatusBar == null && color != 0) { - simulateStatusBar = View(container.context) - simulateStatusBar.id = android.R.id.custom - val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, statusBarHeight) - container.addView(simulateStatusBar, lp) - } - simulateStatusBar?.setBackgroundColor(color) - } -} - -val Context?.statusBarHeight: Int - get() { - this ?: return 0 - var result = 24 - val resId = resources.getIdentifier("status_bar_height", "dimen", "android") - result = if (resId > 0) { - resources.getDimensionPixelSize(resId) - } else { - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - result.toFloat(), Resources.getSystem().displayMetrics - ).toInt() - } - return result - } - -### data/AccountService.kt ### -package com.aiosman.riderpro.data - -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.data.api.ChangePasswordRequestBody -import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody -import com.aiosman.riderpro.data.api.LoginUserRequestBody -import com.aiosman.riderpro.data.api.RegisterRequestBody -import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody -import com.aiosman.riderpro.entity.AccountFavouriteEntity -import com.aiosman.riderpro.entity.AccountLikeEntity -import com.aiosman.riderpro.entity.AccountProfileEntity -import com.aiosman.riderpro.entity.NoticePostEntity -import com.aiosman.riderpro.entity.NoticeUserEntity -import com.google.gson.annotations.SerializedName -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.asRequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import java.io.File - -/** - * 用户资料 - */ -data class AccountProfile( - // 用户ID - val id: Int, - // 用户名 - val username: String, - // 昵称 - val nickname: String, - // 头像 - val avatar: String, - // 关注数 - val followingCount: Int, - // 粉丝数 - val followerCount: Int, - // 是否关注 - val isFollowing: Boolean -) { - /** - * 转换为Entity - */ - fun toAccountProfileEntity(): AccountProfileEntity { - return AccountProfileEntity( - id = id, - followerCount = followerCount, - followingCount = followingCount, - nickName = nickname, - avatar = "${ApiClient.BASE_SERVER}$avatar", - bio = "", - country = "Worldwide", - isFollowing = isFollowing - ) - } -} - -/** - * 消息关联资料 - */ -data class NoticePost( - // 动态ID - @SerializedName("id") - val id: Int, - // 动态内容 - @SerializedName("textContent") - // 动态图片 - val textContent: String, - // 动态图片 - @SerializedName("images") - val images: List, - // 动态时间 - @SerializedName("time") - val time: String, -) { - /** - * 转换为Entity - */ - fun toNoticePostEntity(): NoticePostEntity { - return NoticePostEntity( - id = id, - textContent = textContent, - images = images.map { - it.copy( - url = "${ApiClient.BASE_SERVER}${it.url}", - thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}", - ) - }, - time = ApiClient.dateFromApiString(time) - ) - } -} - -/** - * 消息关联用户 - */ -data class NoticeUser( - // 用户ID - @SerializedName("id") - val id: Int, - // 昵称 - @SerializedName("nickName") - val nickName: String, - // 头像 - @SerializedName("avatar") - val avatar: String, -) { - /** - * 转换为Entity - */ - fun toNoticeUserEntity(): NoticeUserEntity { - return NoticeUserEntity( - id = id, - nickName = nickName, - avatar = "${ApiClient.BASE_SERVER}$avatar", - ) - } -} - -/** - * 点赞消息通知 - */ -data class AccountLike( - // 是否未读 - @SerializedName("isUnread") - val isUnread: Boolean, - // 动态 - @SerializedName("post") - val post: NoticePost, - // 点赞用户 - @SerializedName("user") - val user: NoticeUser, - // 点赞时间 - @SerializedName("likeTime") - val likeTime: String, -) { - fun toAccountLikeEntity(): AccountLikeEntity { - return AccountLikeEntity( - post = post.toNoticePostEntity(), - user = user.toNoticeUserEntity(), - likeTime = ApiClient.dateFromApiString(likeTime) - ) - } -} - -data class AccountFavourite( - @SerializedName("isUnread") - val isUnread: Boolean, - @SerializedName("post") - val post: NoticePost, - @SerializedName("user") - val user: NoticeUser, - @SerializedName("favoriteTime") - val favouriteTime: String, -) { - fun toAccountFavouriteEntity(): AccountFavouriteEntity { - return AccountFavouriteEntity( - post = post.toNoticePostEntity(), - user = user.toNoticeUserEntity(), - favoriteTime = ApiClient.dateFromApiString(favouriteTime) - ) - } -} - -data class AccountFollow( - @SerializedName("id") - val id: Int, - @SerializedName("username") - val username: String, - @SerializedName("nickname") - val nickname: String, - @SerializedName("avatar") - val avatar: String, - @SerializedName("isUnread") - val isUnread: Boolean, - @SerializedName("userId") - val userId: Int, - @SerializedName("isFollowing") - val isFollowing: Boolean, -) - -//{ -// "likeCount": 0, -// "followCount": 0, -// "favoriteCount": 0 -//} -data class AccountNotice( - @SerializedName("likeCount") - val likeCount: Int, - @SerializedName("followCount") - val followCount: Int, - @SerializedName("favoriteCount") - val favoriteCount: Int, - @SerializedName("commentCount") - val commentCount: Int, -) - - -interface AccountService { - /** - * 获取登录当前用户的资料 - */ - suspend fun getMyAccountProfile(): AccountProfileEntity - - /** - * 获取登录的用户认证信息 - */ - suspend fun getMyAccount(): UserAuth - - /** - * 使用用户名密码登录 - * @param loginName 用户名 - * @param password 密码 - */ - suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth - - /** - * 使用google登录 - * @param googleId googleId - */ - suspend fun loginUserWithGoogle(googleId: String): UserAuth - - /** - * 退出登录 - */ - suspend fun logout() - - /** - * 更新用户资料 - * @param avatar 头像 - * @param nickName 昵称 - * @param bio 简介 - */ - suspend fun updateProfile(avatar: UploadImage?, nickName: String?, bio: String?) - - /** - * 注册用户 - * @param loginName 用户名 - * @param password 密码 - */ - suspend fun registerUserWithPassword(loginName: String, password: String) - - /** - * 使用google账号注册 - * @param idToken googleIdToken - */ - suspend fun regiterUserWithGoogleAccount(idToken: String) - - /** - * 修改密码 - * @param oldPassword 旧密码 - * @param newPassword 新密码 - */ - suspend fun changeAccountPassword(oldPassword: String, newPassword: String) - - /** - * 获取我的点赞通知 - * @param page 页码 - * @param pageSize 每页数量 - */ - suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer - - /** - * 获取我的关注通知 - * @param page 页码 - * @param pageSize 每页数量 - */ - suspend fun getMyFollowNotice(page: Int, pageSize: Int): ListContainer - - /** - * 获取我的收藏通知 - * @param page 页码 - * @param pageSize 每页数量 - */ - suspend fun getMyFavouriteNotice(page: Int, pageSize: Int): ListContainer - - /** - * 获取我的通知信息 - */ - suspend fun getMyNoticeInfo(): AccountNotice - - /** - * 更新通知信息,更新最后一次查看时间 - * @param payload 通知信息 - */ - suspend fun updateNotice(payload: UpdateNoticeRequestBody) -} - -class AccountServiceImpl : AccountService { - 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 getMyAccount(): UserAuth { - 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 { - 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 loginUserWithGoogle(googleId: String): UserAuth { - val resp = ApiClient.api.login(LoginUserRequestBody(googleId = googleId)) - val body = resp.body() ?: throw ServiceException("Failed to login") - return UserAuth(0, body.token) - } - - override suspend fun regiterUserWithGoogleAccount(idToken: String) { - val resp = ApiClient.api.registerWithGoogle(GoogleRegisterRequestBody(idToken)) - if (resp.code() != 200) { - throw ServiceException("Failed to register") - } - } - - override suspend fun logout() { - // do nothing - } - - - fun createMultipartBody(file: File, filename: String, name: String): MultipartBody.Part { - val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) - return MultipartBody.Part.createFormData(name, filename, requestFile) - } - - override suspend fun updateProfile(avatar: UploadImage?, nickName: String?, bio: String?) { - val nicknameField: RequestBody? = nickName?.toRequestBody("text/plain".toMediaTypeOrNull()) - val avatarField: MultipartBody.Part? = avatar?.let { - createMultipartBody(it.file, it.filename, "avatar") - } - ApiClient.api.updateProfile(avatarField, nicknameField) - } - - override suspend fun registerUserWithPassword(loginName: String, password: String) { - ApiClient.api.register(RegisterRequestBody(loginName, password)) - } - - override suspend fun changeAccountPassword(oldPassword: String, newPassword: String) { - ApiClient.api.changePassword(ChangePasswordRequestBody(oldPassword, newPassword)) - } - - override suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer { - val resp = ApiClient.api.getMyLikeNotices(page, pageSize) - val body = resp.body() ?: throw ServiceException("Failed to get account") - return body - } - - override suspend fun getMyFollowNotice(page: Int, pageSize: Int): ListContainer { - val resp = ApiClient.api.getMyFollowNotices(page, pageSize) - val body = resp.body() ?: throw ServiceException("Failed to get account") - return body - } - - override suspend fun getMyFavouriteNotice( - page: Int, - pageSize: Int - ): ListContainer { - val resp = ApiClient.api.getMyFavouriteNotices(page, pageSize) - val body = resp.body() ?: throw ServiceException("Failed to get account") - return body - } - - override suspend fun getMyNoticeInfo(): AccountNotice { - val resp = ApiClient.api.getMyNoticeInfo() - val body = resp.body() ?: throw ServiceException("Failed to get account") - return body.data - } - - override suspend fun updateNotice(payload: UpdateNoticeRequestBody) { - ApiClient.api.updateNoticeInfo(payload) - } - -} - -### data/CommentService.kt ### -package com.aiosman.riderpro.data - -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.data.api.CommentRequestBody -import com.aiosman.riderpro.entity.CommentEntity -import com.google.gson.annotations.SerializedName - -/** - * 评论相关 Service - */ -interface CommentService { - /** - * 获取动态 - * @param pageNumber 页码 - * @param postId 动态ID,过滤条件 - * @param postUser 动态作者ID,获取某个用户所有动态下的评论 - * @param selfNotice 是否是自己的通知 - * @return 评论列表 - */ - suspend fun getComments( - pageNumber: Int, - postId: Int? = null, - postUser: Int? = null, - selfNotice: Boolean? = null - ): ListContainer - - /** - * 创建评论 - * @param postId 动态ID - * @param content 评论内容 - */ - suspend fun createComment(postId: Int, content: String) - - /** - * 点赞评论 - * @param commentId 评论ID - */ - suspend fun likeComment(commentId: Int) - - /** - * 取消点赞评论 - * @param commentId 评论ID - */ - suspend fun dislikeComment(commentId: Int) - - /** - * 更新评论已读状态 - * @param commentId 评论ID - */ - suspend fun updateReadStatus(commentId: Int) -} - -/** - * 评论 - */ -data class Comment( - // 评论ID - @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, - // 动态ID - @SerializedName("postId") - val postId: Int, - // 动态 - @SerializedName("post") - val post: NoticePost?, - // 是否未读 - @SerializedName("isUnread") - val isUnread: Boolean -) { - /** - * 转换为Entity - */ - fun toCommentEntity(): CommentEntity { - return CommentEntity( - id = id, - name = user.nickName, - comment = content, - date = ApiClient.dateFromApiString(createdAt), - likes = likeCount, - replies = emptyList(), - postId = postId, - avatar = "${ApiClient.BASE_SERVER}${user.avatar}", - author = user.id, - liked = isLiked, - unread = isUnread, - post = post?.let { - it.copy( - images = it.images.map { - it.copy( - url = "${ApiClient.BASE_SERVER}${it.url}", - thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}" - ) - } - ) - } - ) - } -} - -class CommentRemoteDataSource( - private val commentService: CommentService, -) { - suspend fun getComments( - pageNumber: Int, - postId: Int?, - postUser: Int?, - selfNotice: Boolean? - ): ListContainer { - return commentService.getComments( - pageNumber, - postId, - postUser = postUser, - selfNotice = selfNotice - ) - } -} - - -class CommentServiceImpl : CommentService { - override suspend fun getComments( - pageNumber: Int, - postId: Int?, - postUser: Int?, - selfNotice: Boolean? - ): ListContainer { - val resp = ApiClient.api.getComments( - pageNumber, - postId, - postUser = postUser, - selfNotice = selfNotice?.let { - if (it) 1 else 0 - } - ) - val body = resp.body() ?: throw ServiceException("Failed to get comments") - return ListContainer( - list = body.list.map { it.toCommentEntity() }, - page = body.page, - total = body.total, - pageSize = body.pageSize - ) - } - - override suspend fun createComment(postId: Int, content: String) { - val resp = ApiClient.api.createComment(postId, CommentRequestBody(content)) - return - } - - override suspend fun likeComment(commentId: Int) { - val resp = ApiClient.api.likeComment(commentId) - return - } - - override suspend fun dislikeComment(commentId: Int) { - val resp = ApiClient.api.dislikeComment(commentId) - return - } - - override suspend fun updateReadStatus(commentId: Int) { - val resp = ApiClient.api.updateReadStatus(commentId) - return - } - -} - -### data/DataContainer.kt ### -package com.aiosman.riderpro.data - -/** - * 通用接口返回数据 - */ -data class DataContainer( - val data: T -) - - -### data/Exception.kt ### -package com.aiosman.riderpro.data - -/** - * 错误返回 - */ -class ServiceException( - override val message: String, - val code: Int = 0, - val data: Any? = null -) : Exception( - message -) - -### data/ListContainer.kt ### -package com.aiosman.riderpro.data - -import com.google.gson.annotations.SerializedName - - -/** - * 通用列表接口返回 - */ -data class ListContainer( - // 总数 - @SerializedName("total") - val total: Int, - // 当前页 - @SerializedName("page") - val page: Int, - // 每页数量 - @SerializedName("pageSize") - val pageSize: Int, - // 列表 - @SerializedName("list") - val list: List -) - -### data/MomentService.kt ### -package com.aiosman.riderpro.data - -import com.aiosman.riderpro.R -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.entity.MomentEntity -import com.aiosman.riderpro.entity.MomentImageEntity -import com.google.gson.annotations.SerializedName -import java.io.File - -data class Moment( - @SerializedName("id") - val id: Long, - @SerializedName("textContent") - val textContent: String, - @SerializedName("images") - val images: List, - @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 = ApiClient.dateFromApiString(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 { - MomentImageEntity( - url = "${ApiClient.BASE_SERVER}${it.url}", - thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}", - id = it.id, - blurHash = it.blurHash - ) - }, - 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, - @SerializedName("blurHash") - val blurHash: 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 { - /** - * 获取动态详情 - * @param id 动态ID - */ - suspend fun getMomentById(id: Int): MomentEntity - - /** - * 点赞动态 - * @param id 动态ID - */ - suspend fun likeMoment(id: Int) - - /** - * 取消点赞动态 - * @param id 动态ID - */ - suspend fun dislikeMoment(id: Int) - - /** - * 获取动态列表 - * @param pageNumber 页码 - * @param author 作者ID,过滤条件 - * @param timelineId 用户时间线ID,指定用户 ID 的时间线 - * @param contentSearch 内容搜索,过滤条件 - * @return 动态列表 - */ - suspend fun getMoments( - pageNumber: Int, - author: Int? = null, - timelineId: Int? = null, - contentSearch: String? = null - ): ListContainer - - /** - * 创建动态 - * @param content 动态内容 - * @param authorId 作者ID - * @param images 图片列表 - * @param relPostId 关联动态ID - */ - suspend fun createMoment( - content: String, - authorId: Int, - images: List, - relPostId: Int? = null - ): MomentEntity - - /** - * 收藏动态 - * @param id 动态ID - */ - suspend fun favoriteMoment(id: Int) - - /** - * 取消收藏动态 - * @param id 动态ID - */ - suspend fun unfavoriteMoment(id: Int) -} - - - - -### data/UserService.kt ### -package com.aiosman.riderpro.data - -import com.aiosman.riderpro.data.api.ApiClient -import com.aiosman.riderpro.entity.AccountProfileEntity - -data class UserAuth( - val id: Int, - val token: String? = null -) - -/** - * 用户相关 Service - */ -interface UserService { - /** - * 获取用户信息 - * @param id 用户ID - * @return 用户信息 - */ - suspend fun getUserProfile(id: String): AccountProfileEntity - - /** - * 关注用户 - * @param id 用户ID - */ - suspend fun followUser(id: String) - - /** - * 取消关注用户 - * @param id 用户ID - */ - suspend fun unFollowUser(id: String) - - /** - * 获取用户列表 - * @param pageSize 分页大小 - * @param page 页码 - * @param nickname 昵称搜索 - * @return 用户列表 - */ - suspend fun getUsers( - pageSize: Int = 20, - page: Int = 1, - nickname: String? = null - ): ListContainer - -} - -class UserServiceImpl : UserService { - 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 - } - - override suspend fun getUsers( - pageSize: Int, - page: Int, - nickname: String? - ): ListContainer { - val resp = ApiClient.api.getUsers(page, pageSize, nickname) - val body = resp.body() ?: throw ServiceException("Failed to get account") - return ListContainer( - list = body.list.map { it.toAccountProfileEntity() }, - page = body.page, - total = body.total, - pageSize = body.pageSize - ) - } -} - -### data/api/ApiClient.kt ### -package com.aiosman.riderpro.data.api - -import android.icu.text.SimpleDateFormat -import android.icu.util.TimeZone -import com.aiosman.riderpro.AppStore -import com.aiosman.riderpro.ConstVars -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Response -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.security.cert.CertificateException -import java.util.Date -import java.util.Locale -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -fun getUnsafeOkHttpClient(): OkHttpClient { - return try { - // Create a trust manager that does not validate certificate chains - val trustAllCerts = arrayOf(object : X509TrustManager { - @Throws(CertificateException::class) - override fun checkClientTrusted( - chain: Array, - authType: String - ) { - } - - @Throws(CertificateException::class) - override fun checkServerTrusted( - chain: Array, - authType: String - ) { - } - - override fun getAcceptedIssuers(): Array = arrayOf() - }) - - // Install the all-trusting trust manager - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, trustAllCerts, java.security.SecureRandom()) - // Create an ssl socket factory with our all-trusting manager - val sslSocketFactory = sslContext.socketFactory - - OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) - .hostnameVerifier { _, _ -> true } - .addInterceptor(AuthInterceptor()) - .build() - } catch (e: Exception) { - throw RuntimeException(e) - } -} - -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 = ConstVars.BASE_SERVER - const val BASE_API_URL = "${BASE_SERVER}/api/v1" - const val RETROFIT_URL = "${BASE_API_URL}/" - const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss" - private val okHttpClient: OkHttpClient by lazy { - getUnsafeOkHttpClient() - } - 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) - } - - fun formatTime(date: Date): String { - val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()) - return dateFormat.format(date) - } - - fun dateFromApiString(apiString: String): Date { - val timeFormat = TIME_FORMAT - val simpleDateFormat = SimpleDateFormat(timeFormat, Locale.getDefault()) - simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") - val date = simpleDateFormat.parse(apiString) - - simpleDateFormat.timeZone = TimeZone.getDefault() - val localDateString = simpleDateFormat.format(date) - return simpleDateFormat.parse(localDateString) - } -} - -### data/api/RiderProAPI.kt ### -package com.aiosman.riderpro.data.api - -import com.aiosman.riderpro.data.AccountFavourite -import com.aiosman.riderpro.data.AccountFollow -import com.aiosman.riderpro.data.AccountLike -import com.aiosman.riderpro.data.AccountNotice -import com.aiosman.riderpro.data.AccountProfile -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.PATCH -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? = null, - @SerializedName("password") - val password: String? = null, - @SerializedName("googleId") - val googleId: String? = null, -) - -data class GoogleRegisterRequestBody( - @SerializedName("idToken") - val idToken: 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 -) - -data class ChangePasswordRequestBody( - @SerializedName("currentPassword") - val oldPassword: String = "", - @SerializedName("newPassword") - val newPassword: String = "" -) - -data class UpdateNoticeRequestBody( - @SerializedName("lastLookLikeTime") - val lastLookLikeTime: String? = null, - @SerializedName("lastLookFollowTime") - val lastLookFollowTime: String? = null, - @SerializedName("lastLookFavoriteTime") - val lastLookFavouriteTime: String? = null -) - -interface RiderProAPI { - @POST("register") - suspend fun register(@Body body: RegisterRequestBody): Response - - @POST("login") - suspend fun login(@Body body: LoginUserRequestBody): Response - - @GET("auth/token") - suspend fun checkToken(): Response - - @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, - @Query("contentSearch") contentSearch: String? = null, - @Query("postUser") postUser: Int? = null, - ): Response> - - @Multipart - @POST("posts") - suspend fun createPost( - @Part image: List, - @Part("textContent") textContent: RequestBody, - ): Response> - - @GET("post/{id}") - suspend fun getPost( - @Path("id") id: Int - ): Response> - - @POST("post/{id}/like") - suspend fun likePost( - @Path("id") id: Int - ): Response - - @POST("post/{id}/dislike") - suspend fun dislikePost( - @Path("id") id: Int - ): Response - - @POST("post/{id}/favorite") - suspend fun favoritePost( - @Path("id") id: Int - ): Response - - @POST("post/{id}/unfavorite") - suspend fun unfavoritePost( - @Path("id") id: Int - ): Response - - @POST("post/{id}/comment") - suspend fun createComment( - @Path("id") id: Int, - @Body body: CommentRequestBody - ): Response - - @POST("comment/{id}/like") - suspend fun likeComment( - @Path("id") id: Int - ): Response - - @POST("comment/{id}/dislike") - suspend fun dislikeComment( - @Path("id") id: Int - ): Response - - @POST("comment/{id}/read") - suspend fun updateReadStatus( - @Path("id") id: Int - ): Response - - - @GET("comments") - suspend fun getComments( - @Query("page") page: Int = 1, - @Query("postId") postId: Int? = null, - @Query("pageSize") pageSize: Int = 20, - @Query("postUser") postUser: Int? = null, - @Query("selfNotice") selfNotice: Int? = 0, - ): Response> - - @GET("account/my") - suspend fun getMyAccount(): Response> - - @Multipart - @PATCH("account/my/profile") - suspend fun updateProfile( - @Part avatar: MultipartBody.Part?, - @Part("nickname") nickname: RequestBody?, - ): Response - - @POST("account/my/password") - suspend fun changePassword( - @Body body: ChangePasswordRequestBody - ): Response - - @GET("account/my/notice/like") - suspend fun getMyLikeNotices( - @Query("page") page: Int = 1, - @Query("pageSize") pageSize: Int = 20, - ): Response> - - @GET("account/my/notice/follow") - suspend fun getMyFollowNotices( - @Query("page") page: Int = 1, - @Query("pageSize") pageSize: Int = 20, - ): Response> - - @GET("account/my/notice/favourite") - suspend fun getMyFavouriteNotices( - @Query("page") page: Int = 1, - @Query("pageSize") pageSize: Int = 20, - ): Response> - - @GET("account/my/notice") - suspend fun getMyNoticeInfo(): Response> - - @POST("account/my/notice") - suspend fun updateNoticeInfo( - @Body body: UpdateNoticeRequestBody - ): Response - - - @GET("profile/{id}") - suspend fun getAccountProfileById( - @Path("id") id: Int - ): Response> - - @POST("user/{id}/follow") - suspend fun followUser( - @Path("id") id: Int - ): Response - - @POST("user/{id}/unfollow") - suspend fun unfollowUser( - @Path("id") id: Int - ): Response - - @GET("users") - suspend fun getUsers( - @Query("page") page: Int = 1, - @Query("pageSize") pageSize: Int = 20, - @Query("nickname") search: String? = null, - ): Response> - - @POST("register/google") - suspend fun registerWithGoogle(@Body body: GoogleRegisterRequestBody): Response - -} - diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/Index.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/Index.kt index 90568ff..b2f0274 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/Index.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/Index.kt @@ -78,7 +78,7 @@ fun IndexScreen() { item.forEachIndexed { idx, it -> val isSelected = model.tabIndex == idx val iconTint by animateColorAsState( - targetValue = if (isSelected) Color.Red else Color.White, + targetValue = if (isSelected) Color.White else Color.White, animationSpec = tween(durationMillis = 250), label = "" ) NavigationBarItem( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/NavigationItem.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/NavigationItem.kt index cb25218..a7163f5 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/NavigationItem.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/NavigationItem.kt @@ -14,8 +14,8 @@ sealed class NavigationItem( val selectedIcon: @Composable () -> ImageVector = icon ) { data object Home : NavigationItem("Home", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_home) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_home_filed) } + icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home) }, + selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home_hl) } ) data object Street : NavigationItem("Street", @@ -24,8 +24,8 @@ sealed class NavigationItem( ) data object Add : NavigationItem("Add", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_moment_add) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_moment_add) } + icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_post_hl) }, + selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_post_hl) } ) data object Message : NavigationItem("Message", @@ -34,17 +34,17 @@ sealed class NavigationItem( ) data object Notification : NavigationItem("Notification", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_notification) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_notification) } + icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification)}, + selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification_hl) } ) data object Profile : NavigationItem("Profile", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_profile) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_profile_filed) } + icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile) }, + selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile_hl) } ) data object Search : NavigationItem("Search", - icon = { Icons.Default.Search }, - selectedIcon = { Icons.Default.Search } + icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search) }, + selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search_hl) } ) } \ No newline at end of file