更新代码

This commit is contained in:
2024-09-05 22:04:41 +08:00
parent e371cd9e47
commit fda6fe4dcf
19 changed files with 800 additions and 179 deletions

View File

@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -17,6 +18,19 @@
tools:targetApi="31">
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBM9xMcybq9IbFSFVneZ4nAqQ0ZmTnHGO4"/>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/googleg_standard_color_18" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
notification message. See README(https://goo.gl/6BKBk7) for more. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/black" />
<!-- [END fcm_default_icon] -->
<!-- [START fcm_default_channel] -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="Default Message" />
<activity
android:name=".MainActivity"
android:exported="true"
@@ -28,6 +42,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,7 @@
package com.aiosman.riderpro
import androidx.compose.ui.graphics.Color
object AppColors {
val mainColor = Color(0xffED1C24)
}

View File

@@ -5,4 +5,7 @@ object ConstVars {
// const val BASE_SERVER = "http://192.168.31.190:8088"
// const val BASE_SERVER = "http://192.168.31.36:8088"
const val BASE_SERVER = "https://8.137.22.101:8088"
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
}

View File

@@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
@@ -28,6 +29,7 @@ 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.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -44,10 +46,40 @@ import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.viewModelScope
import androidx.navigation.findNavController
import com.aiosman.riderpro.ui.post.PostViewModel
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.messaging.FirebaseMessaging
class MainActivity : ComponentActivity() {
// Firebase Analytics
private lateinit var analytics: FirebaseAnalytics
private val scope = CoroutineScope(Dispatchers.Main)
// 请求通知权限
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
if (isGranted) {
// FCM SDK (and your app) can post notifications.
} else {
// TODO: Inform user that that your app will not show notifications.
}
}
/**
* 获取账号信息
*/
suspend fun getAccount(): Boolean {
val accountService: AccountService = AccountServiceImpl()
try {
@@ -60,26 +92,90 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 监听应用生命周期
ProcessLifecycleOwner.get().lifecycle.addObserver(MainActivityLifecycleObserver())
// 创建通知渠道
createNotificationChannel()
// 沉浸式状态栏
WindowCompat.setDecorFitsSystemWindows(window, false)
// 初始化 Places SDK
if (!Places.isInitialized()) {
Places.initialize(applicationContext, "AIzaSyDpgLDH1-SECw_pdjJq_msynq1XrxwgKVI")
}
// 初始化 Firebase Analytics
analytics = Firebase.analytics
// 请求通知权限
askNotificationPermission()
// 加载一些本地化的配置
AppStore.init(this)
enableEdgeToEdge()
scope.launch {
// 检查是否有登录态
val isAccountValidate = getAccount()
var startDestination = NavigationRoute.Login.route
// 如果有登录态,且记住登录状态,且账号有效,则初始化 FCM下一步进入首页
if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) {
Messaging.InitFCM(scope)
startDestination = NavigationRoute.Index.route
}
setContent {
Navigation(startDestination)
Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID")
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
PostViewModel.postId = postId
PostViewModel.viewModelScope.launch {
PostViewModel.initData()
navController.navigate(NavigationRoute.Post.route.replace("{id}", postId))
}
}
}
}
}
}
/**
* 请求通知权限
*/
private fun askNotificationPermission() {
// This is only necessary for API level >= 33 (TIRAMISU)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
) {
// FCM SDK (and your app) can post notifications.
} else if (shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS)) {
} else {
// Directly ask for the permission
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
/**
* 创建通知渠道
*/
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = ConstVars.MOMENT_LIKE_CHANNEL_ID
val channelName = ConstVars.MOMENT_LIKE_CHANNEL_NAME
val channel =
NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
}
}
val LocalNavController = compositionLocalOf<NavHostController> {
@@ -96,97 +192,3 @@ val LocalAnimatedContentScope = compositionLocalOf<AnimatedContentScope> {
}
// 用于带导航栏的路由的可复用 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()
}
}
}

View File

@@ -0,0 +1,18 @@
package com.aiosman.riderpro
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
object MainActivityLifecycle {
var isForeground = false
}
class MainActivityLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
MainActivityLifecycle.isForeground = true
}
override fun onPause(owner: LifecycleOwner) {
MainActivityLifecycle.isForeground = false
}
}

View File

@@ -0,0 +1,30 @@
package com.aiosman.riderpro
import android.util.Log
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
object Messaging {
fun InitFCM(scope: CoroutineScope) {
val accountService: AccountService = AccountServiceImpl()
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w("Pushing", "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
}
// Get new FCM registration token
val token = task.result
// Log and toast
Log.d("Pushing", token)
scope.launch {
accountService.registerMessageChannel(client = "fcm", token)
}
})
}
}

View File

@@ -0,0 +1,95 @@
package com.aiosman.riderpro
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import androidx.compose.material.Icon
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
val MessageTypeLike = "like"
fun showLikeNotification(context: Context, title: String, message: String, postId: Int) {
val channelId = ConstVars.MOMENT_LIKE_CHANNEL_ID
// Create an Intent to open the specific activity
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("POST_ID", postId.toString())
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
// Create a PendingIntent to wrap the Intent
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notificationBuilder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.rider_pro_favoriate)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
// 用户没有授权,不显示通知
return
}
notify(System.currentTimeMillis().toInt(), notificationBuilder.build())
}
}
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
Log.d("Pushing", "Refreshed token: $token")
super.onNewToken(token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
if (remoteMessage.data.containsKey("action")) {
when (remoteMessage.data["action"]) {
MessageTypeLike -> {
val postId = remoteMessage.data["postId"]?.toInt() ?: return
showLikeNotification(
applicationContext,
remoteMessage.data["title"] ?: "FCM Message",
remoteMessage.data["body"] ?: "No message body",
postId
)
}
else -> {
Log.w("Pushing", "Unknown message type: ${remoteMessage.data["messageType"]}")
}
}
}
super.onMessageReceived(remoteMessage)
}
fun prepareIntent(clickAction: String?): Intent {
val intent: Intent
val isAppInBackground: Boolean = !MainActivityLifecycle.isForeground
intent = if (isAppInBackground) {
Intent(this, MainActivity::class.java)
} else {
Intent(clickAction)
}
return intent
}
}

View File

@@ -4,6 +4,7 @@ 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.RegisterMessageChannelRequestBody
import com.aiosman.riderpro.data.api.RegisterRequestBody
import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody
import com.aiosman.riderpro.entity.AccountFavouriteEntity
@@ -300,6 +301,8 @@ interface AccountService {
* @param payload 通知信息
*/
suspend fun updateNotice(payload: UpdateNoticeRequestBody)
suspend fun registerMessageChannel(client:String,identifier:String)
}
class AccountServiceImpl : AccountService {
@@ -400,4 +403,8 @@ class AccountServiceImpl : AccountService {
ApiClient.api.updateNoticeInfo(payload)
}
override suspend fun registerMessageChannel(client: String, identifier: String) {
ApiClient.api.registerMessageChannel(RegisterMessageChannelRequestBody(client,identifier))
}
}

View File

@@ -79,6 +79,12 @@ data class UpdateNoticeRequestBody(
val lastLookFavouriteTime: String? = null
)
data class RegisterMessageChannelRequestBody(
@SerializedName("client")
val client: String,
@SerializedName("identifier")
val identifier: String,
)
interface RiderProAPI {
@POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@@ -211,6 +217,11 @@ interface RiderProAPI {
@Body body: UpdateNoticeRequestBody
): Response<Unit>
@POST("account/my/messaging")
suspend fun registerMessageChannel(
@Body body: RegisterMessageChannelRequestBody
): Response<Unit>
@GET("profile/{id}")
suspend fun getAccountProfileById(

View File

@@ -46,4 +46,18 @@ fun Date.formatPostTime(): String {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
}
return dateFormat.format(this)
}
/**
* YYYY.DD.MM HH:MM
*/
fun Date.formatPostTime2(): String {
val calendar = Calendar.getInstance()
calendar.time = this
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH) + 1
val day = calendar.get(Calendar.DAY_OF_MONTH)
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
return "$year.$month.$day $hour:$minute"
}

View File

@@ -14,6 +14,7 @@ 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.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
@@ -27,6 +28,7 @@ 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.account.AccountEditScreen2
import com.aiosman.riderpro.ui.comment.CommentsScreen
import com.aiosman.riderpro.ui.favourite.FavouriteScreen
import com.aiosman.riderpro.ui.follower.FollowerScreen
@@ -201,7 +203,7 @@ fun NavigationController(
EmailSignupScreen()
}
composable(route = NavigationRoute.AccountEdit.route) {
AccountEditScreen()
AccountEditScreen2()
}
composable(route = NavigationRoute.ImageViewer.route) {
CompositionLocalProvider(
@@ -233,8 +235,11 @@ fun NavigationController(
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun Navigation(startDestination: String = NavigationRoute.Login.route) {
fun Navigation(startDestination: String = NavigationRoute.Login.route,onLaunch: (navController: NavHostController) -> Unit) {
val navController = rememberNavController()
LaunchedEffect(Unit) {
onLaunch(navController)
}
SharedTransitionLayout {
CompositionLocalProvider(
LocalNavController provides navController,

View File

@@ -0,0 +1,316 @@
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.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.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
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.IconButton
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.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 com.aiosman.riderpro.AppColors
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.UploadImage
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile
import kotlinx.coroutines.launch
/**
* 编辑用户资料界面
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountEditScreen2() {
val accountService: AccountService = AccountServiceImpl()
var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") }
var imageUrl by remember { mutableStateOf<Uri?>(null) }
var bannerImageUrl by remember { mutableStateOf<Uri?>(null) }
var profile by remember {
mutableStateOf<AccountProfileEntity?>(
null
)
}
val navController = LocalNavController.current
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
}
var newBanner = bannerImageUrl?.let {
val cursor = context.contentResolver.query(it, null, null, null, null)
var newBanner: 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()}")
newBanner = UploadImage(file, displayName, it.toString(), extension)
}
}
newBanner
}
val newName = if (name == profile?.nickName) null else name
accountService.updateProfile(
avatar = newAvatar,
banner = newBanner,
nickName = newName,
bio = bio
)
reloadProfile()
navController.popBackStack()
}
}
val pickImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
imageUrl = it
}
}
}
val pickBannerImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
bannerImageUrl = uri
}
}
}
LaunchedEffect(Unit) {
reloadProfile()
}
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
StatusBarSpacer()
Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) {
NoticeScreenHeader(
title = "编辑资料",
moreIcon = false
) {
Icon(
modifier = Modifier
.size(24.dp)
.noRippleClickable {
updateUserProfile()
},
imageVector = Icons.Default.Check,
contentDescription = "保存"
)
}
}
Spacer(modifier = Modifier.height(32.dp))
profile?.let {
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,
imageUrl?.toString() ?: it.avatar,
modifier = Modifier
.size(width = 88.dp, height = 88.dp)
.clip(
RoundedCornerShape(88.dp)
),
contentDescription = "",
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(AppColors.mainColor)
.align(Alignment.BottomEnd).noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickImageLauncher.launch(this)
}
}
,
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Add,
contentDescription = "Add",
tint = Color.White,
)
}
}
Spacer(modifier = Modifier.height(46.dp))
Column(
modifier = Modifier.padding(horizontal = 24.dp).border(
width = 1.dp,
color = Color(0xFFEBEBEB),
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Name",
modifier = Modifier
.widthIn(100.dp),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF333333)
)
)
BasicTextField(
maxLines = 1,
value = name,
onValueChange = {
name = it
},
textStyle = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal
),
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.padding(horizontal = 16.dp)
.background(Color(0xFFEBEBEB))
)
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Bio",
modifier = Modifier
.widthIn(100.dp),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF333333)
)
)
BasicTextField(
value = bio,
onValueChange = {
bio = it
},
textStyle = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal
),
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
}
}
}
}
}

View File

@@ -63,7 +63,8 @@ fun CommentsScreen() {
@Composable
fun NoticeScreenHeader(
title:String,
moreIcon: Boolean = true
moreIcon: Boolean = true,
rightIcon: @Composable (() -> Unit)? = null
) {
val nav = LocalNavController.current
Row(
@@ -90,8 +91,10 @@ fun NoticeScreenHeader(
modifier = Modifier.size(24.dp)
)
}
if (rightIcon != null) {
Spacer(modifier = Modifier.weight(1f))
rightIcon()
}
}
}

View File

@@ -4,6 +4,7 @@ import android.util.Log
import androidx.annotation.DrawableRes
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
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
@@ -24,6 +26,7 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@@ -32,6 +35,7 @@ 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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -44,13 +48,17 @@ 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.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -64,6 +72,7 @@ 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.exp.formatPostTime2
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
@@ -86,7 +95,7 @@ fun ProfilePage() {
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
LazyColumn(
modifier = Modifier
.fillMaxSize().padding(bottom = with(LocalDensity.current) {
.fillMaxSize().background(Color(0xFFF5F5F5)).padding(bottom = with(LocalDensity.current) {
val da = WindowInsets.navigationBars.getBottom(this).toDp() + 48.dp
da
})
@@ -133,53 +142,71 @@ fun ProfilePage() {
contentDescription = "",
modifier = Modifier.noRippleClickable {
expanded = true
}
},
tint = Color.White
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {
scope.launch {
model.logout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.width(250.dp).background(Color.White)
) {
Box(modifier = Modifier.padding(vertical = 14.dp, horizontal = 24.dp)) {
Row {
Text("Logout", fontWeight = FontWeight.W500)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.mipmap.rider_pro_logout),
contentDescription = "",
modifier = Modifier.size(24.dp).noRippleClickable {
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)
Box(modifier = Modifier.padding(vertical = 14.dp, horizontal = 24.dp)) {
Row {
Text("Change password",fontWeight = FontWeight.W500)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.mipmap.rider_pro_change_password),
contentDescription = "",
modifier = Modifier.size(24.dp).noRippleClickable {
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
}
)
}
}
}, text = {
Text("Edit")
})
DropdownMenuItem(onClick = {
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
}, text = {
Text("Change password")
})
}
}
}
}
// CarGroup()
Spacer(modifier = Modifier.height(32.dp))
model.profile?.let {
UserInformation(accountProfileEntity = it)
UserInformation(
accountProfileEntity = it,
onEditProfileClick = {
navController.navigate(NavigationRoute.AccountEdit.route)
}
)
}
// RidingStyle()
}
items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items
MomentPostUnit(momentItem)
// MomentCard(momentItem)
}
item {
Spacer(modifier = Modifier.height(48.dp))
}
@@ -239,7 +266,8 @@ fun CarTopPicture() {
fun UserInformation(
isSelf: Boolean = true,
accountProfileEntity: AccountProfileEntity,
onFollowClick: () -> Unit = {}
onFollowClick: () -> Unit = {},
onEditProfileClick: () -> Unit = {}
) {
Column(
modifier = Modifier
@@ -257,7 +285,8 @@ fun UserInformation(
CommunicationOperatorGroup(
isSelf = isSelf,
isFollowing = accountProfileEntity.isFollowing,
onFollowClick = onFollowClick
onFollowClick = onFollowClick,
onEditProfileClick = onEditProfileClick
)
}
}
@@ -270,20 +299,26 @@ fun UserInformationFollowers(modifier: Modifier, accountProfileEntity: AccountPr
text = accountProfileEntity.followerCount.toString(),
fontSize = 24.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
style = TextStyle(fontStyle = FontStyle.Italic, fontWeight = FontWeight.Bold)
)
Spacer(
Canvas(
modifier = Modifier
.size(width = 88.83.dp, height = 1.dp)
.border(width = 1.dp, color = Color.Gray)
.padding(top = 5.dp, bottom = 5.dp)
)
.padding(top = 2.dp, bottom = 5.dp)
) {
drawLine(
color = Color(0xFFCCCCCC),
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
strokeWidth = 1f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Text(
modifier = Modifier.padding(top = 5.dp),
text = stringResource(R.string.followers_upper),
fontSize = 12.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
}
@@ -331,12 +366,6 @@ fun UserInformationBasic(modifier: Modifier, accountProfileEntity: AccountProfil
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
// )
}
}
@@ -351,14 +380,21 @@ fun UserInformationFollowing(modifier: Modifier, accountProfileEntity: AccountPr
text = accountProfileEntity.followingCount.toString(),
fontSize = 24.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
style = TextStyle(fontStyle = FontStyle.Italic, fontWeight = FontWeight.Bold)
)
Box(
Canvas(
modifier = Modifier
.size(width = 88.83.dp, height = 1.dp)
.border(width = 1.dp, color = Color.Gray)
)
.padding(top = 2.dp, bottom = 5.dp)
) {
drawLine(
color = Color(0xFFCCCCCC),
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
strokeWidth = 1f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Text(
modifier = Modifier.padding(top = 5.dp),
text = stringResource(R.string.following_upper),
@@ -384,7 +420,8 @@ fun UserInformationSlogan(accountProfileEntity: AccountProfileEntity) {
fun CommunicationOperatorGroup(
isSelf: Boolean = true,
isFollowing: Boolean = false,
onFollowClick: () -> Unit
onFollowClick: () -> Unit = {},
onEditProfileClick: () -> Unit = {}
) {
val navController = LocalNavController.current
Row(
@@ -415,6 +452,30 @@ fun CommunicationOperatorGroup(
}
}
if (isSelf) {
Box(
modifier = Modifier
.size(width = 142.dp, height = 40.dp)
.noRippleClickable {
onEditProfileClick()
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.mipmap.rider_pro_btn_bg_grey),
contentDescription = ""
)
Text(
text = "Edit profile",
fontSize = 14.sp,
color = Color.Black,
fontWeight = FontWeight.Bold,
style = TextStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
)
}
}
}
}
@@ -484,7 +545,7 @@ fun RidingStyleItem(styleContent: String) {
@Composable
fun MomentPostUnit(momentEntity: MomentEntity) {
TimeGroup(momentEntity.time.formatPostTime())
TimeGroup(momentEntity.time.formatPostTime2())
ProfileMomentCard(
momentEntity.momentTextContent,
momentEntity.images[0].thumbnail,
@@ -504,13 +565,14 @@ fun TimeGroup(time: String = "2024.06.08 12:23") {
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(end = 12.dp),
modifier = Modifier.height(16.dp).width(14.dp),
painter = painterResource(id = R.drawable.rider_pro_moment_time_flag),
contentDescription = ""
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = time,
fontSize = 22.sp,
fontSize = 16.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
)
@@ -525,15 +587,46 @@ fun ProfileMomentCard(
comment: String,
momentEntity: MomentEntity
) {
var columnHeight by remember { mutableStateOf(0) }
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))
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
) {
MomentCardTopContent(content)
MomentCardPicture(imageUrl, momentEntity = momentEntity)
MomentCardOperation(like, comment)
Row(
modifier = Modifier
.fillMaxWidth()
) {
Canvas(
modifier = Modifier
.height(with(LocalDensity.current) { columnHeight.toDp() })
.width(14.dp)
) {
drawLine(
color = Color(0xff899DA9),
start = Offset(0f, 0f),
end = Offset(0f, size.height),
strokeWidth = 4f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Spacer(modifier = Modifier.width(10.dp))
Column(
modifier = Modifier
.background(Color.White)
.weight(1f)
.onGloballyPositioned { coordinates ->
columnHeight = coordinates.size.height
}
) {
if (content.isNotEmpty()) {
MomentCardTopContent(content)
}
MomentCardPicture(imageUrl, momentEntity = momentEntity)
MomentCardOperation(like, comment)
}
}
}
}
@@ -561,7 +654,7 @@ fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
imageUrl,
modifier = Modifier
.fillMaxSize()
.aspectRatio(1f)
.aspectRatio(3f/2f)
.padding(16.dp)
.noRippleClickable {
PostViewModel.preTransit(momentEntity)

View File

@@ -26,6 +26,7 @@ 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.Messaging
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.ServiceException
@@ -101,6 +102,7 @@ fun EmailSignupScreen() {
// 获取token 信息
try {
accountService.getMyAccount()
Messaging.InitFCM(scope)
} catch (e: ServiceException) {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT).show()

View File

@@ -36,6 +36,7 @@ import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.Messaging
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
@@ -87,6 +88,7 @@ fun SignupScreen() {
// 获取token 信息
try {
accountService.getMyAccount()
Messaging.InitFCM(coroutineScope)
} catch (e: ServiceException) {
coroutineScope.launch(Dispatchers.Main) {
Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT)

View File

@@ -1,7 +1,5 @@
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
@@ -21,8 +19,6 @@ 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
@@ -42,28 +38,19 @@ 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.Messaging
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.data.ServiceException
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
@@ -103,6 +90,7 @@ fun UserAuthScreen() {
this.rememberMe = rememberMe
saveData()
}
Messaging.InitFCM(scope)
navController.navigate(NavigationRoute.Index.route) {
popUpTo(NavigationRoute.Login.route) { inclusive = true }
}