feat: 新增头像上传、更新架构

1.新增头像上传功能
2.使用Retrofit代替接口访问
This commit is contained in:
糕小菜 2024-11-15 15:07:51 +08:00
parent 279fc96d52
commit 92e622792a
42 changed files with 922 additions and 528 deletions

View File

@ -75,6 +75,11 @@ dependencies {
implementation(libs.pinyin4j) implementation(libs.pinyin4j)
implementation(libs.retrofit)
implementation(libs.retrofit2.converter.gson)
implementation(libs.okhttp3.logging.interceptor)
// implementation(libs.therouter) // implementation(libs.therouter)
// ksp(libs.therouter.ksp) // ksp(libs.therouter.ksp)

View File

@ -21,3 +21,52 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep class com.hjq.shape.** {*;} -keep class com.hjq.shape.** {*;}
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod
# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
-keepattributes AnnotationDefault
# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit
# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
# Keep inherited services.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface * extends <1>
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
# R8 full mode strips generic signatures from return types if not kept.
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
# With R8 full mode generic signatures are stripped for classes that are not kept.
-keep,allowobfuscation,allowshrinking class retrofit2.Response

View File

@ -6,6 +6,15 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<application <application
android:name=".KchatApplication" android:name=".KchatApplication"
android:allowBackup="true" android:allowBackup="true"

View File

@ -9,5 +9,5 @@ data class RegisterRequest(
val telephone: String, val telephone: String,
val password: String, val password: String,
val signature: String, val signature: String,
val username: String val username: String? = null
) )

View File

@ -8,9 +8,9 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
data class UserRequest( data class UserRequest(
val username: String, var username: String,
val password: String? = null, var password: String? = null,
val telephone: String? = null, var telephone: String? = null,
val nickname: String? = null, var nickname: String? = null,
val signature: String? = null, var signature: String? = null,
) )

View File

@ -0,0 +1,14 @@
package com.kaixed.kchat.model.response.user
import kotlinx.serialization.Serializable
/**
* @Author: kaixed
* @Date: 2024/11/14 21:38
*/
@Serializable
data class UploadAvatar(
var code: String,
var msg: String,
var data: String?
)

View File

@ -27,4 +27,5 @@ object NetworkInterface {
const val ACCEPT_CONTACT_REQUEST = "/friend/accept" const val ACCEPT_CONTACT_REQUEST = "/friend/accept"
const val UPDATE_USER_INFO = "/users/info" const val UPDATE_USER_INFO = "/users/info"
const val UPLOAD_AVATAR = "/users/avatar"
} }

View File

@ -1,8 +1,10 @@
package com.kaixed.kchat.network package com.kaixed.kchat.network
import android.util.Log
import com.kaixed.kchat.network.OkhttpHelper.getInstance import com.kaixed.kchat.network.OkhttpHelper.getInstance
import okhttp3.Callback import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
@ -44,6 +46,23 @@ class NetworkRequest {
call.enqueue(callback) call.enqueue(callback)
} }
fun postAsync(url: String, multipartBody: MultipartBody, callback: Callback) {
val contentType = "multipart/form-data; boundary=" + multipartBody.boundary
Log.d("haha", contentType)
val request = Request.Builder()
.url(url)
.post(multipartBody)
.addHeader("Connection", "keep-alive")
.addHeader("Content-Type", contentType)
.build()
val call = client.newCall(request)
call.enqueue(callback)
}
fun getAsync(url: String, callback: Callback) { fun getAsync(url: String, callback: Callback) {
val request = Request.Builder() val request = Request.Builder()

View File

@ -0,0 +1,42 @@
package com.kaixed.kchat.network
import com.kaixed.kchat.network.service.ContactService
import com.kaixed.kchat.network.service.UserApiService
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
/**
* @Author: kaixed
* @Date: 2024/11/15 11:05
*/
object RetrofitClient {
private const val BASE_URL = "https://app.kaixed.com/kchat/"
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
}
val userApiService: UserApiService by lazy {
retrofit.create(UserApiService::class.java)
}
val contactApiService: ContactService by lazy {
retrofit.create(ContactService::class.java)
}
}

View File

@ -0,0 +1,53 @@
package com.kaixed.kchat.network.service
import com.kaixed.kchat.model.friend.AcceptContactRequest
import com.kaixed.kchat.model.friend.ContactRequestResponse
import com.kaixed.kchat.model.response.ApplyFriend
import com.kaixed.kchat.model.response.friend.FriendList
import com.kaixed.kchat.model.response.friend.SearchFriends
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface ContactService {
// 获取联系人请求列表
@FormUrlEncoded
@POST("friend/request/list")
suspend fun getContactRequestList(
@Field("userId") username: String
): Response<ContactRequestResponse>
// 接受联系人请求
@FormUrlEncoded
@POST("friend/accept")
suspend fun acceptContactRequest(
@Field("requestId") contactId: String,
@Field("receiverId") username: String
): Response<AcceptContactRequest>
// 添加联系人
@FormUrlEncoded
@POST("friend/request")
suspend fun addContact(
@Field("senderId") senderId: String,
@Field("receiverId") receiverId: String,
@Field("message") message: String
): Response<ApplyFriend>
// 搜索联系人
@GET("users/{username}")
suspend fun searchContact(
@Path("username") username: String
): Response<SearchFriends>
// 获取联系人列表
@FormUrlEncoded
@POST("friend/list")
suspend fun getContactList(
@Field("userId") username: String
): Response<FriendList>
}

View File

@ -0,0 +1,68 @@
package com.kaixed.kchat.network.service
import com.kaixed.kchat.model.request.RegisterRequest
import com.kaixed.kchat.model.request.UserRequest
import com.kaixed.kchat.model.response.login.Login
import com.kaixed.kchat.model.response.register.Register
import com.kaixed.kchat.model.response.search.UserList
import com.kaixed.kchat.model.response.user.ChangeNickname
import com.kaixed.kchat.model.response.user.UploadAvatar
import okhttp3.MultipartBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path
/**
* @Author: kaixed
* @Date: 2024/11/15 11:00
*/
interface UserApiService {
// 注册接口
@POST("users/register")
suspend fun register(
@Body registerRequest: RegisterRequest
): Response<Register>
// 登录接口(根据用户名登录)
@FormUrlEncoded
@POST("users/login/username")
suspend fun loginByUsername(
@Field("username") username: String,
@Field("password") password: String
): Response<Login>
// 登录接口(根据电话登录)
@FormUrlEncoded
@POST("users/login/telephone")
suspend fun loginByTelephone(
@Field("telephone") telephone: String,
@Field("password") password: String
): Response<Login>
// 获取用户列表
@GET("userList/{username}")
suspend fun getUserListByNickname(
@Path("username") username: String
): Response<UserList>
// 更改昵称接口
@POST("users/info")
suspend fun changeNickname(
@Body userRequest: UserRequest
): Response<ChangeNickname>
// 上传头像接口
@Multipart
@POST("users/avatar")
suspend fun uploadAvatar(
@Part("username") username: String,
@Part file: MultipartBody.Part
): Response<UploadAvatar>
}

View File

@ -1,166 +0,0 @@
package com.kaixed.kchat.repository
import androidx.lifecycle.MutableLiveData
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.model.friend.AcceptContactRequest
import com.kaixed.kchat.model.friend.ContactRequestResponse
import com.kaixed.kchat.model.response.ApplyFriend
import com.kaixed.kchat.model.response.friend.FriendList
import com.kaixed.kchat.model.response.friend.SearchFriends
import com.kaixed.kchat.network.NetworkInterface.ACCEPT_CONTACT_REQUEST
import com.kaixed.kchat.network.NetworkInterface.ADD_FRIEND
import com.kaixed.kchat.network.NetworkInterface.FRIEND_LIST
import com.kaixed.kchat.network.NetworkInterface.FRIEND_REQUEST_LIST
import com.kaixed.kchat.network.NetworkInterface.SERVER_URL
import com.kaixed.kchat.network.NetworkInterface.USER_LIST
import com.kaixed.kchat.network.NetworkRequest
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody
import okhttp3.Response
import java.io.IOException
/**
* @Author: kaixed
* @Date: 2024/10/20 17:32
*/
class ContactRepo {
fun getContactRequestList(
username: String,
): MutableLiveData<ContactRequestResponse?> {
val applyFriendMutableLiveData = MutableLiveData<ContactRequestResponse?>()
val requestBody = FormBody.Builder()
.add("userId", username)
.build()
NetworkRequest().postAsync(
"$SERVER_URL$FRIEND_REQUEST_LIST",
requestBody,
object : Callback {
override fun onFailure(call: Call, e: IOException) {
applyFriendMutableLiveData.postValue(null)
}
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val contactResponse =
Json.decodeFromString<ContactRequestResponse>(responseBody)
applyFriendMutableLiveData.postValue(contactResponse)
}
}
}
)
return applyFriendMutableLiveData
}
fun acceptContactRequest(
username: String,
contactId: String,
): MutableLiveData<AcceptContactRequest?> {
val acceptFriendMutableLiveData = MutableLiveData<AcceptContactRequest?>()
val requestBody = FormBody.Builder()
.add("requestId", contactId)
.add("receiverId", username)
.build()
NetworkRequest().postAsync(
SERVER_URL + ACCEPT_CONTACT_REQUEST,
requestBody,
object : Callback {
override fun onFailure(call: Call, e: IOException) {
acceptFriendMutableLiveData.postValue(null)
}
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val contactResponse =
Json.decodeFromString<AcceptContactRequest>(responseBody)
acceptFriendMutableLiveData.postValue(contactResponse)
} ?: acceptFriendMutableLiveData.postValue(null)
}
}
)
return acceptFriendMutableLiveData
}
fun addContact(contactId: String, message: String): MutableLiveData<ApplyFriend?> {
val mutableLiveData = MutableLiveData<ApplyFriend?>()
val requestBody = FormBody.Builder()
.add("senderId", getUsername())
.add("receiverId", contactId)
.add("message", message)
.build()
NetworkRequest().postAsync(
"$SERVER_URL$ADD_FRIEND",
requestBody,
object : Callback {
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val applyFriend =
Json.decodeFromString<ApplyFriend>(responseBody)
mutableLiveData.postValue(applyFriend)
} ?: mutableLiveData.postValue(null)
}
override fun onFailure(call: Call, e: IOException) {
mutableLiveData.postValue(null)
}
}
)
return mutableLiveData
}
fun searchContact(username: String): MutableLiveData<SearchFriends?> {
val listMutableLiveData = MutableLiveData<SearchFriends?>()
NetworkRequest().getAsync(
"$SERVER_URL$USER_LIST$username",
object : Callback {
override fun onFailure(call: Call, e: IOException) {
listMutableLiveData.postValue(null)
}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val searchFriends = Json.decodeFromString<SearchFriends>(responseBody)
listMutableLiveData.postValue(searchFriends)
} ?: listMutableLiveData.postValue(null)
}
}
)
return listMutableLiveData
}
fun getContactList(username: String): MutableLiveData<List<Contact>?> {
val applyFriendMutableLiveData = MutableLiveData<List<Contact>?>()
val requestBody = FormBody.Builder()
.add("userId", username)
.build()
NetworkRequest().postAsync(
"$SERVER_URL$FRIEND_LIST",
requestBody,
object : Callback {
override fun onFailure(call: Call, e: IOException) {
applyFriendMutableLiveData.postValue(null)
}
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val friendList = Json.decodeFromString<FriendList>(responseBody)
applyFriendMutableLiveData.postValue(friendList.data)
} ?: applyFriendMutableLiveData.postValue(null)
}
}
)
return applyFriendMutableLiveData
}
}

View File

@ -0,0 +1,75 @@
package com.kaixed.kchat.repository
import android.util.Log
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.model.friend.AcceptContactRequest
import com.kaixed.kchat.model.friend.ContactRequestResponse
import com.kaixed.kchat.model.response.ApplyFriend
import com.kaixed.kchat.model.response.friend.FriendList
import com.kaixed.kchat.model.response.friend.SearchFriends
import com.kaixed.kchat.network.RetrofitClient
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Response
class ContactRepository {
private val contactApiService = RetrofitClient.contactApiService
// 获取联系人请求列表
suspend fun getContactRequestList(username: String): Response<ContactRequestResponse> {
return try {
contactApiService.getContactRequestList(username)
} catch (e: Exception) {
Log.e("ContactRepository", "Get Contact Request List failed: ${e.message}")
Response.error(500, "".toResponseBody()) // 返回一个空的错误 Response
}
}
// 接受联系人请求
suspend fun acceptContactRequest(
username: String,
contactId: String
): Response<AcceptContactRequest> {
return try {
contactApiService.acceptContactRequest(contactId, username)
} catch (e: Exception) {
Log.e("ContactRepository", "Accept Contact Request failed: ${e.message}")
Response.error(500, "".toResponseBody())
}
}
// 添加联系人
suspend fun addContact(contactId: String, message: String): Response<ApplyFriend> {
return try {
contactApiService.addContact(
senderId = contactId,
receiverId = getUsername(),
message = message
)
} catch (e: Exception) {
Log.e("ContactRepository", "Add Contact failed: ${e.message}")
Response.error(500, "".toResponseBody())
}
}
// 搜索联系人
suspend fun searchContact(username: String): Response<SearchFriends> {
return try {
contactApiService.searchContact(username)
} catch (e: Exception) {
Log.e("ContactRepository", "Search Contact failed: ${e.message}")
Response.error(500, "".toResponseBody())
}
}
// 获取联系人列表
suspend fun getContactList(username: String): Response<FriendList> {
return try {
contactApiService.getContactList(username)
} catch (e: Exception) {
Log.e("ContactRepository", "Get Contact List failed: ${e.message}")
Response.error(500, "".toResponseBody())
}
}
}

View File

@ -1,159 +0,0 @@
package com.kaixed.kchat.repository
import androidx.lifecycle.MutableLiveData
import com.kaixed.kchat.model.request.RegisterRequest
import com.kaixed.kchat.model.request.UserRequest
import com.kaixed.kchat.model.response.login.Login
import com.kaixed.kchat.model.response.register.Register
import com.kaixed.kchat.model.response.search.UserList
import com.kaixed.kchat.model.response.user.ChangeNickname
import com.kaixed.kchat.network.NetworkInterface.SERVER_URL
import com.kaixed.kchat.network.NetworkInterface.UPDATE_USER_INFO
import com.kaixed.kchat.network.NetworkInterface.USER_LIST
import com.kaixed.kchat.network.NetworkInterface.USER_LOGIN_BY_TELEPHONE
import com.kaixed.kchat.network.NetworkInterface.USER_LOGIN_BY_USERNAME
import com.kaixed.kchat.network.NetworkInterface.USER_REGISTER
import com.kaixed.kchat.network.NetworkRequest
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody
import okhttp3.Response
import java.io.IOException
/**
* @Author: kaixed
* @Date: 2024/10/23 20:21
*/
class UserRepo {
fun register(
password: String,
avatarUrl: String,
signature: String,
nickname: String,
telephone: String
): MutableLiveData<Register?> {
val registerMutableLiveData = MutableLiveData<Register?>()
val registerRequest = RegisterRequest(
username = "",
password = password,
avatarUrl = avatarUrl,
signature = signature,
nickname = nickname,
telephone = telephone
)
val json =
Json.encodeToString(RegisterRequest.serializer(), registerRequest)
NetworkRequest().postAsync(
"$SERVER_URL$USER_REGISTER",
json,
object : Callback {
override fun onFailure(call: Call, e: IOException) {
registerMutableLiveData.postValue(null)
}
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val register = Json.decodeFromString<Register?>(responseBody)
registerMutableLiveData.postValue(register)
}
}
}
)
return registerMutableLiveData
}
fun login(
username: String,
password: String,
loginByUsername: Boolean
): MutableLiveData<Login?> {
val loginMutableLiveData = MutableLiveData<Login?>()
val url = "$SERVER_URL${
if (loginByUsername) USER_LOGIN_BY_USERNAME else USER_LOGIN_BY_TELEPHONE
}"
val requestBody = FormBody.Builder()
.add(if (loginByUsername) "username" else "telephone", username)
.add("password", password)
.build()
NetworkRequest().postAsync(
url,
requestBody,
object : Callback {
override fun onFailure(call: Call, e: IOException) {
loginMutableLiveData.postValue(null)
}
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val login = Json.decodeFromString<Login?>(responseBody)
loginMutableLiveData.postValue(login)
}
}
}
)
return loginMutableLiveData
}
fun getUserListByNickname(username: String): MutableLiveData<UserList?> {
val listMutableLiveData = MutableLiveData<UserList?>()
NetworkRequest().getAsync(
"$SERVER_URL$USER_LIST$username",
object : Callback {
override fun onFailure(call: Call, e: IOException) {
listMutableLiveData.postValue(null)
}
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val userList = Json.decodeFromString<UserList?>(responseBody)
listMutableLiveData.postValue(userList)
}
}
}
)
return listMutableLiveData
}
fun changeNickname(username: String, nickname: String): MutableLiveData<Boolean> {
val listMutableLiveData = MutableLiveData<Boolean>()
val userRequest = UserRequest(
username = username,
nickname = nickname,
)
val json =
Json.encodeToString(UserRequest.serializer(), userRequest)
NetworkRequest().postAsync(
"$SERVER_URL$UPDATE_USER_INFO",
json,
object : Callback {
override fun onFailure(call: Call, e: IOException) {
listMutableLiveData.postValue(false)
}
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { responseBody ->
val changeNickname = Json.decodeFromString<ChangeNickname>(responseBody)
when (changeNickname.code) {
"A0203" -> listMutableLiveData.postValue(true)
else -> listMutableLiveData.postValue(false)
}
}
}
}
)
return listMutableLiveData
}
}

View File

@ -0,0 +1,83 @@
package com.kaixed.kchat.repository
import android.util.Log
import com.kaixed.kchat.model.request.RegisterRequest
import com.kaixed.kchat.model.request.UserRequest
import com.kaixed.kchat.model.response.login.Login
import com.kaixed.kchat.model.response.register.Register
import com.kaixed.kchat.model.response.search.UserList
import com.kaixed.kchat.network.RetrofitClient
import okhttp3.MultipartBody
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Response
class UserRepository {
private val userApiService = RetrofitClient.userApiService
// 用户注册
suspend fun register(registerRequest: RegisterRequest): Response<Register> {
return try {
userApiService.register(registerRequest)
} catch (e: Exception) {
Log.e("UserRepository", "Register Failed: ${e.message}", e)
Response.error(500, "".toResponseBody())
}
}
// 登录请求(支持用户名或手机号)
suspend fun login(
username: String?,
password: String,
loginByUsername: Boolean
): Response<Login> {
return try {
if (loginByUsername) {
// 用户名登录
userApiService.loginByUsername(username ?: "", password)
} else {
// 电话号登录
userApiService.loginByTelephone(username ?: "", password)
}
} catch (e: Exception) {
Log.e("UserRepository", "Login Failed: ${e.message}", e)
Response.error(500, "".toResponseBody()) // 返回一个空的错误 Response
}
}
// 获取用户列表
suspend fun getUserList(username: String): Response<UserList> {
return try {
userApiService.getUserListByNickname(username)
} catch (e: Exception) {
Log.e("UserRepository", "Get User List Failed: ${e.message}", e)
Response.error(500, "".toResponseBody())
}
}
// 修改昵称
suspend fun changeNickname(userRequest: UserRequest): Response<Boolean> {
return try {
val response = userApiService.changeNickname(userRequest)
Response.success(response.isSuccessful)
} catch (e: Exception) {
Log.e("UserRepository", "Change Nickname Failed: ${e.message}", e)
Response.error(500, "".toResponseBody())
}
}
// 上传头像
suspend fun uploadAvatar(file: MultipartBody.Part, username: String): Response<String?> {
return try {
val response = userApiService.uploadAvatar(username, file)
if (response.isSuccessful) {
Response.success(response.body()?.data)
} else {
Response.success(null)
}
} catch (e: Exception) {
Log.e("UserRepository", "Upload Avatar Failed: ${e.message}", e)
Response.error(500, "".toResponseBody())
}
}
}

View File

@ -6,11 +6,11 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import com.kaixed.kchat.databinding.ActivityApplyAddFriendBinding import com.kaixed.kchat.databinding.ActivityApplyAddFriendBinding
import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.viewmodel.ApplyFriendViewModel import com.kaixed.kchat.viewmodel.ContactViewModel
class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() { class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() {
private val applyAddFriendViewModel: ApplyFriendViewModel by viewModels() private val contactViewModel: ContactViewModel by viewModels()
override fun inflateBinding(): ActivityApplyAddFriendBinding = override fun inflateBinding(): ActivityApplyAddFriendBinding =
ActivityApplyAddFriendBinding.inflate(layoutInflater) ActivityApplyAddFriendBinding.inflate(layoutInflater)
@ -29,7 +29,7 @@ class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() {
private fun sendContactRequest(contactId: String?) { private fun sendContactRequest(contactId: String?) {
contactId?.let { contactId?.let {
applyAddFriendViewModel.addContact(contactId, binding.etMessage.text.toString()) contactViewModel.addContactResponse
.observe(this) { value -> .observe(this) { value ->
runOnUiThread { runOnUiThread {
if (value?.code == "200") { if (value?.code == "200") {
@ -38,6 +38,7 @@ class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() {
} }
} }
} }
contactViewModel.addContact(contactId, binding.etMessage.text.toString())
} }
} }
} }

View File

@ -41,7 +41,7 @@ class ApproveContactRequestActivity : BaseActivity<ActivityApproveContactRequest
private fun setOnClickListener() { private fun setOnClickListener() {
binding.tvFinish.setOnClickListener { binding.tvFinish.setOnClickListener {
val contactId = intent.getStringExtra("contactId") val contactId = intent.getStringExtra("contactId")
contactViewModel.acceptContactRequest(getUsername(), contactId!!) contactViewModel.acceptContactRequestResponse
.observe(this) { value -> .observe(this) { value ->
value?.let { value?.let {
runOnUiThread { runOnUiThread {
@ -62,6 +62,8 @@ class ApproveContactRequestActivity : BaseActivity<ActivityApproveContactRequest
} }
} }
contactViewModel.acceptContactRequest(getUsername(), contactId!!)
} }
} }
} }

View File

@ -20,7 +20,7 @@ import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.kaixed.kchat.utils.DensityUtil.dpToPx import com.kaixed.kchat.utils.DensityUtil.dpToPx
import com.kaixed.kchat.utils.DrawableUtil.createDrawable import com.kaixed.kchat.utils.DrawableUtil.createDrawable
import com.kaixed.kchat.viewmodel.LoginViewModel import com.kaixed.kchat.viewmodel.UserViewModel
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import io.objectbox.Box import io.objectbox.Box
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -30,8 +30,7 @@ import kotlinx.coroutines.withContext
class LoginActivity : BaseActivity<ActivityLoginBinding>() { class LoginActivity : BaseActivity<ActivityLoginBinding>() {
private val userViewModel: UserViewModel by viewModels()
private val mViewModel: LoginViewModel by viewModels()
private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) } private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) }
@ -69,7 +68,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
if (username.isEmpty() || password.isEmpty()) { if (username.isEmpty() || password.isEmpty()) {
toast("请输入${if (loginByUsername) "用户名" else "手机号"}或密码") toast("请输入${if (loginByUsername) "用户名" else "手机号"}或密码")
} }
mViewModel.login(username, password, loginByUsername).observe(this) { loginResult -> userViewModel.loginResponse.observe(this) { loginResult ->
loginResult?.let { loginResult?.let {
when (it.code) { when (it.code) {
"A0201" -> toast("用户账户不存在") "A0201" -> toast("用户账户不存在")
@ -87,6 +86,7 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
} }
} ?: toast("登录异常") } ?: toast("登录异常")
} }
userViewModel.login(username, password, loginByUsername)
} }
private fun updateDb(userInfo: UserInfo) { private fun updateDb(userInfo: UserInfo) {

View File

@ -7,7 +7,6 @@ import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
@ -23,12 +22,12 @@ import com.kaixed.kchat.ui.fragment.MineFragment
import com.kaixed.kchat.utils.ConstantsUtil.getUsername import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.ConstantsUtil.isFirstLaunchApp import com.kaixed.kchat.utils.ConstantsUtil.isFirstLaunchApp
import com.kaixed.kchat.utils.WidgetUtil import com.kaixed.kchat.utils.WidgetUtil
import com.kaixed.kchat.viewmodel.FriendListViewModel import com.kaixed.kchat.viewmodel.ContactViewModel
import io.objectbox.Box import io.objectbox.Box
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener { class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener {
@ -36,7 +35,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener {
private val colorBlack: Int by lazy { ContextCompat.getColor(this, R.color.black) } private val colorBlack: Int by lazy { ContextCompat.getColor(this, R.color.black) }
private val contactBox: Box<Contact> by lazy { get().boxFor(Contact::class.java) } private val contactBox: Box<Contact> by lazy { get().boxFor(Contact::class.java) }
private val friendListViewModel: FriendListViewModel by viewModels() private val contactViewModel: ContactViewModel by viewModels()
companion object { companion object {
private const val KEY_HOME_FRAGMENT = 0 private const val KEY_HOME_FRAGMENT = 0
@ -76,11 +75,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener {
val dialog = WidgetUtil.showLoadingDialog(this@MainActivity, "同步数据中") val dialog = WidgetUtil.showLoadingDialog(this@MainActivity, "同步数据中")
delay(200) delay(200)
val friendListLiveData = friendListViewModel.loadFriendList(getUsername())
friendListLiveData.observe(this@MainActivity) { contacts -> contactViewModel.contactListResponse.observe(this@MainActivity) { contacts ->
contactBox.put(contacts ?: emptyList()) contactBox.put(contacts ?: emptyList())
} }
contactViewModel.loadFriendList(getUsername())
delay(2000) delay(2000)
dialog.dismiss() dialog.dismiss()
} }

View File

@ -32,12 +32,14 @@ class MessageActivity : AppCompatActivity() {
} }
private fun getItems() { private fun getItems() {
contactViewModel.getContactRequestList(getUsername()) contactViewModel.contactRequestListResponse
.observe(this) { value -> .observe(this) { value ->
if (value != null) { if (value != null) {
items.addAll(value.data.toMutableList()) items.addAll(value.data.toMutableList())
messageListAdapter.notifyDataSetChanged() messageListAdapter.notifyDataSetChanged()
} }
} }
contactViewModel.getContactRequestList(getUsername())
} }
} }

View File

@ -1,14 +1,57 @@
package com.kaixed.kchat.ui.activity package com.kaixed.kchat.ui.activity
import android.Manifest
import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.data.objectbox.entity.UserInfo_
import com.kaixed.kchat.databinding.ActivityProfileDetailBinding import com.kaixed.kchat.databinding.ActivityProfileDetailBinding
import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.ConstantsUtil.getNickName import com.kaixed.kchat.utils.Constants.AVATAR_URL
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.WidgetUtil
import com.kaixed.kchat.viewmodel.UserViewModel
import com.tencent.mmkv.MMKV
import io.objectbox.Box
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() { class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) }
private val userSessionMMKV by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
private val userViewModel: UserViewModel by viewModels()
companion object {
private const val TAG = "ProfileDetailActivity"
// 定义请求码常量
private const val REQUEST_CODE_PERMISSION = 1001
}
private val username: String by lazy { getUsername() }
// 使用 ActivityResultLauncher 来注册一个结果处理器
private lateinit var getImageLauncher: ActivityResultLauncher<Intent>
override fun inflateBinding(): ActivityProfileDetailBinding { override fun inflateBinding(): ActivityProfileDetailBinding {
return ActivityProfileDetailBinding.inflate(layoutInflater) return ActivityProfileDetailBinding.inflate(layoutInflater)
} }
@ -17,16 +60,105 @@ class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
binding.ciAvatar.setOnItemClickListener {
finish()
}
setListener() setListener()
updateContent() updateContent(username)
getImageLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val selectedImageUri: Uri? = result.data?.data
selectedImageUri?.let {
updateAvatar(it)
}
} else {
Toast.makeText(this, "未选择图片", Toast.LENGTH_SHORT).show()
}
}
}
private fun updateAvatar(uri: Uri) {
val filePath = getPathFromUri(uri)
val dialog = WidgetUtil.showLoadingDialog(this, "正在上传")
userViewModel.uploadAvatarResponse.observe(this) { value ->
value?.let {
userSessionMMKV.putString(AVATAR_URL, value)
updateDb(value, dialog)
binding.ciAvatar.setItemIcon(uri)
} ?: run {
toast("上传失败")
}
}
val file = File(filePath!!)
userViewModel.uploadAvatar(username = getUsername(), file = prepareFilePart(file))
}
private fun prepareFilePart(file: File): MultipartBody.Part {
val requestFile = file.asRequestBody("application/octet-stream".toMediaTypeOrNull())
return MultipartBody.Part.createFormData("file", file.name, requestFile)
}
private fun updateDb(url: String, dialog: Dialog) {
val userInfo =
userInfoBox.query(UserInfo_.username.equal(getUsername())).build().findFirst()
if (userInfo != null) {
userInfo.avatarUrl = url
userInfoBox.put(userInfo)
}
dialog.dismiss()
toast("上传成功")
}
private fun getPathFromUri(uri: Uri): String? {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = contentResolver.query(uri, projection, null, null, null)
cursor?.use {
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
if (cursor.moveToFirst()) {
return cursor.getString(columnIndex)
}
}
return null
}
private fun openGallery() {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
getImageLauncher.launch(intent)
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSION) {
if (checkPermission()) {
openGallery()
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_CODE_PERMISSION
)
}
}
}
private fun checkPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
} }
private fun setListener() { private fun setListener() {
binding.ciAvatar.setOnClickListener {
checkPermission()
openGallery()
}
binding.ciNickname.setOnClickListener { binding.ciNickname.setOnClickListener {
startActivity( startActivity(
Intent(this, RenameActivity::class.java) Intent(this, RenameActivity::class.java)
@ -36,10 +168,14 @@ class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateContent() updateContent(username)
} }
private fun updateContent() { private fun updateContent(username: String) {
binding.ciNickname.setItemDesc(getNickName()) val userInfo = userInfoBox.query(UserInfo_.username.equal(username)).build().findFirst()
if (userInfo != null) {
binding.ciNickname.setItemDesc(userInfo.nickname)
binding.ciAvatar.setItemIcon(userInfo.avatarUrl)
}
} }
} }

View File

@ -17,6 +17,7 @@ import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.get import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.entity.UserInfo import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.databinding.ActivityRegisterBinding import com.kaixed.kchat.databinding.ActivityRegisterBinding
import com.kaixed.kchat.model.request.RegisterRequest
import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.DensityUtil.dpToPx import com.kaixed.kchat.utils.DensityUtil.dpToPx
import com.kaixed.kchat.utils.DrawableUtil.createDrawable import com.kaixed.kchat.utils.DrawableUtil.createDrawable
@ -27,7 +28,7 @@ class RegisterActivity : BaseActivity<ActivityRegisterBinding>() {
private var tvContinueEnable: Boolean = false private var tvContinueEnable: Boolean = false
private val mViewModel: UserViewModel by viewModels() private val userViewmodel: UserViewModel by viewModels()
private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) } private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) }
@ -77,22 +78,26 @@ class RegisterActivity : BaseActivity<ActivityRegisterBinding>() {
signature: String, signature: String,
telephone: String telephone: String
) { ) {
mViewModel.register(nickname, password, avatarUrl, signature, telephone) userViewmodel.registerResponse.observe(this) { registerResult ->
.observe(this) { registerResult -> when (registerResult?.code) {
when (registerResult?.code) { "A0111" -> toast("用户名已存在,请重新注册")
"A0111" -> toast("用户名已存在,请重新注册") "200" -> registerResult.data?.username?.let {
onRegisterSuccess(it, nickname, telephone)
"200" -> registerResult.data?.username?.let {
onRegisterSuccess(
it,
nickname,
telephone
)
}
else -> toast("系统服务器异常")
} }
else -> toast("系统服务器异常")
} }
}
val registerRequest = RegisterRequest(
nickname = nickname,
password = password,
avatarUrl = avatarUrl,
signature = signature,
telephone = telephone
)
userViewmodel.register(registerRequest)
} }
private fun onRegisterSuccess(username: String, nickname: String, telephone: String) { private fun onRegisterSuccess(username: String, nickname: String, telephone: String) {

View File

@ -10,6 +10,7 @@ import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.entity.UserInfo import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.data.objectbox.entity.UserInfo_ import com.kaixed.kchat.data.objectbox.entity.UserInfo_
import com.kaixed.kchat.databinding.ActivityRenameBinding import com.kaixed.kchat.databinding.ActivityRenameBinding
import com.kaixed.kchat.model.request.UserRequest
import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.kaixed.kchat.utils.Constants.NICKNAME_KEY import com.kaixed.kchat.utils.Constants.NICKNAME_KEY
@ -65,7 +66,7 @@ class RenameActivity : BaseActivity<ActivityRenameBinding>() {
binding.ctb.setBtnEnable(false) binding.ctb.setBtnEnable(false)
val dialog = WidgetUtil.showLoadingDialog(this, "正在保存") val dialog = WidgetUtil.showLoadingDialog(this, "正在保存")
dialog.show() dialog.show()
userViewModel.changeNickname(username, newNickname).observe(this) { result -> userViewModel.changeNicknameResponse.observe(this) { result ->
if (result) { if (result) {
val user = userInfoBox val user = userInfoBox
.query(UserInfo_.username.equal(username)) .query(UserInfo_.username.equal(username))
@ -91,6 +92,13 @@ class RenameActivity : BaseActivity<ActivityRenameBinding>() {
dialog.dismiss() dialog.dismiss()
toast(if (updateSucceed) "更新成功" else "更新失败") toast(if (updateSucceed) "更新成功" else "更新失败")
}, 800) }, 800)
val userRequest = UserRequest(
username = username,
nickname = newNickname
)
userViewModel.changeNickname(userRequest)
} }
override fun onResume() { override fun onResume() {

View File

@ -21,7 +21,7 @@ import com.kaixed.kchat.databinding.ActivitySearchFriendsBinding
import com.kaixed.kchat.databinding.DialogLoadingBinding import com.kaixed.kchat.databinding.DialogLoadingBinding
import com.kaixed.kchat.model.search.User import com.kaixed.kchat.model.search.User
import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.viewmodel.SearchFriendsViewModel import com.kaixed.kchat.viewmodel.ContactViewModel
class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() { class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() {
private var isSearching = false private var isSearching = false
@ -32,7 +32,7 @@ class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() {
return ActivitySearchFriendsBinding.inflate(layoutInflater) return ActivitySearchFriendsBinding.inflate(layoutInflater)
} }
private val searchFriendsViewModel: SearchFriendsViewModel by viewModels() private val contactViewModel: ContactViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -90,7 +90,7 @@ class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() {
loadingDialog = showLoadingDialog(this) loadingDialog = showLoadingDialog(this)
val username = binding.etSearch.text.toString() val username = binding.etSearch.text.toString()
searchFriendsViewModel.searchFriends(username).observe(this) { value -> contactViewModel.searchContactResponse.observe(this) { value ->
loadingDialog.dismiss() loadingDialog.dismiss()
value?.let { value?.let {
if (value.code == "200") { if (value.code == "200") {
@ -109,6 +109,7 @@ class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() {
} }
} }
contactViewModel.searchContact(username)
} }
binding.tvCancel.setOnClickListener { binding.tvCancel.setOnClickListener {

View File

@ -1,15 +1,17 @@
package com.kaixed.kchat.ui.fragment package com.kaixed.kchat.ui.fragment
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bumptech.glide.Glide
import com.kaixed.kchat.R
import com.kaixed.kchat.databinding.FragmentMineBinding import com.kaixed.kchat.databinding.FragmentMineBinding
import com.kaixed.kchat.ui.activity.ProfileDetailActivity import com.kaixed.kchat.ui.activity.ProfileDetailActivity
import com.kaixed.kchat.ui.base.BaseFragment import com.kaixed.kchat.ui.base.BaseFragment
import com.kaixed.kchat.utils.ConstantsUtil import com.kaixed.kchat.utils.ConstantsUtil
import com.kaixed.kchat.utils.ConstantsUtil.getAvatarUrl
class MineFragment : BaseFragment<FragmentMineBinding>() { class MineFragment : BaseFragment<FragmentMineBinding>() {
@ -37,13 +39,19 @@ class MineFragment : BaseFragment<FragmentMineBinding>() {
} }
binding.ciSetting.setRedTipVisibility(true) binding.ciSetting.setRedTipVisibility(true)
} }
@SuppressLint("SetTextI18n")
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateContent()
}
private fun updateContent() {
val nickname = ConstantsUtil.getNickName() val nickname = ConstantsUtil.getNickName()
binding.tvId.text = "kid: $nickname" binding.tvNickname.text = nickname
Glide.with(requireContext()).load(getAvatarUrl())
.placeholder(R.drawable.ic_default_avatar)
.error(R.drawable.ic_default_avatar)
.into(binding.ifvAvatar)
} }
} }

View File

@ -2,13 +2,12 @@ package com.kaixed.kchat.ui.widget
import android.content.Context import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.kaixed.kchat.R import com.kaixed.kchat.R
import com.kaixed.kchat.databinding.ItemCustomBinding import com.kaixed.kchat.databinding.ItemCustomBinding
@ -34,7 +33,7 @@ class CustomItem @JvmOverloads constructor(
val itemIconSize = typedArray.getDimension(R.styleable.CustomItem_iconSize, 0f) val itemIconSize = typedArray.getDimension(R.styleable.CustomItem_iconSize, 0f)
val itemIcon = typedArray.getResourceId(R.styleable.CustomItem_itemIcon, -1) val itemIcon = typedArray.getResourceId(R.styleable.CustomItem_itemIcon, -1)
val itemLeftIcon = typedArray.getResourceId(R.styleable.CustomItem_itemLeftIcon, -1) val itemLeftIcon = typedArray.getResourceId(R.styleable.CustomItem_itemLeftIcon, -1)
val itemIconRound = typedArray.getDimension(R.styleable.CustomItem_iconRound, 0f) val itemIconRound = typedArray.getFloat(R.styleable.CustomItem_iconRound, 0f)
val itemDesc = typedArray.getString(R.styleable.CustomItem_itemDesc) ?: "" val itemDesc = typedArray.getString(R.styleable.CustomItem_itemDesc) ?: ""
val itemRedTip = typedArray.getBoolean(R.styleable.CustomItem_itemRedTip, false) val itemRedTip = typedArray.getBoolean(R.styleable.CustomItem_itemRedTip, false)
@ -87,11 +86,9 @@ class CustomItem @JvmOverloads constructor(
} }
if (itemIconRound > 0) { if (itemIconRound > 0) {
val requestOptions = binding.ivItemIcon.roundPercent = itemIconRound
RequestOptions().transform(RoundedCorners(itemIconRound.toInt())) Glide.with(context).load(itemIcon)
Glide.with(context).load(itemIcon).apply(requestOptions)
.into(binding.ivItemIcon) .into(binding.ivItemIcon)
binding.ivItemIcon.clipToOutline = true
} }
} else { } else {
binding.ivItemIcon.visibility = View.GONE binding.ivItemIcon.visibility = View.GONE
@ -116,11 +113,6 @@ class CustomItem @JvmOverloads constructor(
binding.decorationBottom.visibility = if (isShowBottomDivider) View.VISIBLE else View.GONE binding.decorationBottom.visibility = if (isShowBottomDivider) View.VISIBLE else View.GONE
} }
// 设置点击事件
fun setOnItemClickListener(listener: OnClickListener) {
binding.root.setOnClickListener(listener)
}
// 设置红点的可见性 // 设置红点的可见性
fun setRedTipVisibility(visible: Boolean) { fun setRedTipVisibility(visible: Boolean) {
binding.viewRedTip.visibility = if (visible) View.VISIBLE else View.GONE binding.viewRedTip.visibility = if (visible) View.VISIBLE else View.GONE
@ -130,4 +122,14 @@ class CustomItem @JvmOverloads constructor(
fun setItemDesc(str: String) { fun setItemDesc(str: String) {
binding.tvItemDesc.text = str binding.tvItemDesc.text = str
} }
fun setItemIcon(uri: Uri) {
Glide.with(context).load(uri)
.into(binding.ivItemIcon)
}
fun setItemIcon(url: String) {
Glide.with(context).load(url)
.into(binding.ivItemIcon)
}
} }

View File

@ -12,6 +12,7 @@ object Constants {
const val USERNAME_KEY = "username" const val USERNAME_KEY = "username"
const val NICKNAME_KEY = "nickname" const val NICKNAME_KEY = "nickname"
const val AVATAR_URL = "avatarUrl"
const val FIRST_LAUNCH_APP = "firstLaunchApp" const val FIRST_LAUNCH_APP = "firstLaunchApp"
const val USER_LOGIN_STATUS: String = "userLoginStatus" const val USER_LOGIN_STATUS: String = "userLoginStatus"
const val STATUS_BAR_HEIGHT = "status_bar_height" const val STATUS_BAR_HEIGHT = "status_bar_height"
@ -21,4 +22,5 @@ object Constants {
const val KEYBOARD_HEIGHT_RATIO = 0.15F const val KEYBOARD_HEIGHT_RATIO = 0.15F
const val KEYBOARD_DEFAULT_HEIGHT = 200 const val KEYBOARD_DEFAULT_HEIGHT = 200
const val STATUS_BAR_DEFAULT_HEIGHT: Int = 10 const val STATUS_BAR_DEFAULT_HEIGHT: Int = 10
} }

View File

@ -1,5 +1,6 @@
package com.kaixed.kchat.utils package com.kaixed.kchat.utils
import com.kaixed.kchat.utils.Constants.AVATAR_URL
import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID
import com.kaixed.kchat.utils.Constants.FIRST_LAUNCH_APP import com.kaixed.kchat.utils.Constants.FIRST_LAUNCH_APP
import com.kaixed.kchat.utils.Constants.KEYBOARD_DEFAULT_HEIGHT import com.kaixed.kchat.utils.Constants.KEYBOARD_DEFAULT_HEIGHT
@ -28,6 +29,9 @@ object ConstantsUtil {
fun getUsername(): String = fun getUsername(): String =
userSessionMMKV.getString(USERNAME_KEY, "") ?: "" userSessionMMKV.getString(USERNAME_KEY, "") ?: ""
fun getAvatarUrl(): String =
userSessionMMKV.getString(AVATAR_URL, "") ?: ""
fun getStatusBarHeight(): Int = fun getStatusBarHeight(): Int =
commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT) commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT)
@ -43,7 +47,6 @@ object ConstantsUtil {
if (isFirstLaunch) { if (isFirstLaunch) {
defaultMMKV.putBoolean(FIRST_LAUNCH_APP, false) defaultMMKV.putBoolean(FIRST_LAUNCH_APP, false)
} }
return isFirstLaunch return isFirstLaunch
} }

View File

@ -1,17 +0,0 @@
package com.kaixed.kchat.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kaixed.kchat.model.response.ApplyFriend
import com.kaixed.kchat.repository.ContactRepo
/**
* @Author: kaixed
* @Date: 2024/10/15 17:29
*/
class ApplyFriendViewModel : ViewModel() {
private var contactRepo: ContactRepo = ContactRepo()
fun addContact(contactId: String, message: String): MutableLiveData<ApplyFriend?> =
contactRepo.addContact(contactId, message)
}

View File

@ -1,28 +1,121 @@
package com.kaixed.kchat.viewmodel package com.kaixed.kchat.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.model.friend.AcceptContactRequest import com.kaixed.kchat.model.friend.AcceptContactRequest
import com.kaixed.kchat.model.friend.ContactRequestResponse import com.kaixed.kchat.model.friend.ContactRequestResponse
import com.kaixed.kchat.model.response.ApplyFriend
import com.kaixed.kchat.model.response.friend.SearchFriends import com.kaixed.kchat.model.response.friend.SearchFriends
import com.kaixed.kchat.repository.ContactRepo import com.kaixed.kchat.repository.ContactRepository
import com.kaixed.kchat.utils.Pinyin4jUtil
import kotlinx.coroutines.launch
/**
* @Author: kaixed
* @Date: 2024/10/20 17:40
*/
class ContactViewModel : ViewModel() { class ContactViewModel : ViewModel() {
private val contactRepo = ContactRepo()
fun getContactRequestList(username: String): MutableLiveData<ContactRequestResponse?> = private val contactRepository = ContactRepository()
contactRepo.getContactRequestList(username)
fun searchFriends(username: String): MutableLiveData<SearchFriends?> = // 获取联系人请求列表
contactRepo.searchContact(username) private val _contactRequestListResponse = MutableLiveData<ContactRequestResponse?>()
val contactRequestListResponse: LiveData<ContactRequestResponse?> = _contactRequestListResponse
fun acceptContactRequest( // 接受联系人请求
username: String, private val _acceptContactRequestResponse = MutableLiveData<AcceptContactRequest?>()
contactId: String val acceptContactRequestResponse: LiveData<AcceptContactRequest?> =
): MutableLiveData<AcceptContactRequest?> = _acceptContactRequestResponse
contactRepo.acceptContactRequest(username, contactId)
// 添加联系人
private val _addContactResponse = MutableLiveData<ApplyFriend?>()
val addContactResponse: LiveData<ApplyFriend?> = _addContactResponse
// 搜索联系人
private val _searchContactResponse = MutableLiveData<SearchFriends?>()
val searchContactResponse: LiveData<SearchFriends?> = _searchContactResponse
// 获取联系人列表
private val _contactListResponse = MutableLiveData<List<Contact>?>()
val contactListResponse: LiveData<List<Contact>?> = _contactListResponse
// 获取联系人请求列表
fun getContactRequestList(username: String) {
viewModelScope.launch {
val response = contactRepository.getContactRequestList(username)
if (response.isSuccessful) {
_contactRequestListResponse.value = response.body()
} else {
_contactRequestListResponse.value = null
}
}
}
// 接受联系人请求
fun acceptContactRequest(username: String, contactId: String) {
viewModelScope.launch {
val response = contactRepository.acceptContactRequest(username, contactId)
if (response.isSuccessful) {
_acceptContactRequestResponse.value = response.body()
} else {
_acceptContactRequestResponse.value = null
}
}
}
// 添加联系人
fun addContact(contactId: String, message: String) {
viewModelScope.launch {
val response = contactRepository.addContact(contactId, message)
if (response.isSuccessful) {
_addContactResponse.value = response.body()
} else {
_addContactResponse.value = null
}
}
}
// 搜索联系人
fun searchContact(username: String) {
viewModelScope.launch {
val response = contactRepository.searchContact(username)
if (response.isSuccessful) {
_searchContactResponse.value = response.body()
} else {
_searchContactResponse.value = null
}
}
}
fun loadFriendList(username: String) {
viewModelScope.launch {
val response = contactRepository.getContactList(username)
if (response.isSuccessful) {
val friendListResponse = response.body()?.data
val uiFriendList = friendListResponse?.map { networkItem ->
mapToFriendItem(networkItem) // 数据转换
}?.sortedWith { f1, f2 ->
// 拼音排序
Pinyin4jUtil.compare(f1.nickname, f2.nickname)
}
// 设置是否显示分组头
uiFriendList?.forEachIndexed { index, item ->
item.showHeader =
(index == 0 || item.quanpin?.get(index) != uiFriendList[index - 1].quanpin?.get(
index - 1
))
}
_contactListResponse.value = uiFriendList
} else {
_contactListResponse.value = null
}
}
}
private fun mapToFriendItem(response: Contact): Contact {
val pinyin = Pinyin4jUtil.toPinyin(response.nickname)
response.quanpin = pinyin
return response
}
} }

View File

@ -1,46 +0,0 @@
package com.kaixed.kchat.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.repository.ContactRepo
import com.kaixed.kchat.utils.Pinyin4jUtil
/**
* @Author: kaixed
* @Date: 2024/10/17 22:02
*/
class FriendListViewModel : ViewModel() {
private val contactRepo = ContactRepo()
fun loadFriendList(username: String): LiveData<List<Contact>?> {
val friendListLiveData = MutableLiveData<List<Contact>?>()
contactRepo.getContactList(username).observeForever { friendList ->
val uiFriendList = friendList?.map { networkItem ->
mapToFriendItem(networkItem)
}?.sortedWith { f1, f2 ->
Pinyin4jUtil.compare(f1.nickname, f2.nickname)
}
uiFriendList?.forEachIndexed { index, item ->
item.showHeader =
(index == 0 || item.quanpin?.get(index) != uiFriendList[index - 1].quanpin?.get(
index - 1
))
}
friendListLiveData.value = uiFriendList
}
return friendListLiveData
}
private fun mapToFriendItem(response: Contact): Contact {
val pinyin = Pinyin4jUtil.toPinyin(response.nickname)
response.quanpin = pinyin
return response
}
}

View File

@ -1,17 +0,0 @@
package com.kaixed.kchat.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kaixed.kchat.model.response.login.Login
import com.kaixed.kchat.repository.UserRepo
/**
* @Author: kaixed
* @Date: 2024/10/23 20:19
*/
class LoginViewModel : ViewModel() {
private val userRepo: UserRepo = UserRepo()
fun login(username: String, password: String, loginByUsername :Boolean): MutableLiveData<Login?> =
userRepo.login(username, password, loginByUsername)
}

View File

@ -1,17 +0,0 @@
package com.kaixed.kchat.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kaixed.kchat.model.response.friend.SearchFriends
import com.kaixed.kchat.repository.ContactRepo
/**
* @Author: kaixed
* @Date: 2024/9/22 23:04
*/
class SearchFriendsViewModel : ViewModel() {
private var contactRepo: ContactRepo = ContactRepo()
fun searchFriends(username: String): MutableLiveData<SearchFriends?> =
contactRepo.searchContact(username)
}

View File

@ -1,31 +1,121 @@
package com.kaixed.kchat.viewmodel package com.kaixed.kchat.viewmodel
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kaixed.kchat.model.request.RegisterRequest
import com.kaixed.kchat.model.request.UserRequest
import com.kaixed.kchat.model.response.login.Login
import com.kaixed.kchat.model.response.register.Register import com.kaixed.kchat.model.response.register.Register
import com.kaixed.kchat.model.response.search.UserList import com.kaixed.kchat.model.response.search.UserList
import com.kaixed.kchat.repository.UserRepo import com.kaixed.kchat.repository.UserRepository
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
/**
* @Author: kaixed
* @Date: 2024/10/23 20:14
*/
class UserViewModel : ViewModel() { class UserViewModel : ViewModel() {
private val userRepo: UserRepo = UserRepo()
fun getUserListByNickname(username: String): MutableLiveData<UserList?> = private val userRepo = UserRepository()
userRepo.getUserListByNickname(username)
fun register( // 注册返回
password: String, private val _registerResponse = MutableLiveData<Register?>()
nickname: String, val registerResponse: LiveData<Register?> = _registerResponse
avatarUrl: String,
signature: String,
telephone: String
): MutableLiveData<Register?> =
userRepo.register(password, nickname, avatarUrl, signature, telephone)
fun changeNickname(username: String, nickname: String): MutableLiveData<Boolean> = // 获取用户列表返回
userRepo.changeNickname(username, nickname) private val _userListResponse = MutableLiveData<UserList?>()
val userListResponse: LiveData<UserList?> = _userListResponse
// 修改昵称返回
private val _changeNicknameResponse = MutableLiveData<Boolean>()
val changeNicknameResponse: LiveData<Boolean> = _changeNicknameResponse
// 上传头像返回
private val _uploadAvatarResponse = MutableLiveData<String?>()
val uploadAvatarResponse: LiveData<String?> = _uploadAvatarResponse
// 登录返回
private val _loginResponse = MutableLiveData<Login?>()
val loginResponse: LiveData<Login?> = _loginResponse
// 注册请求
fun register(registerRequest: RegisterRequest) {
viewModelScope.launch {
try {
val response = userRepo.register(registerRequest)
if (response.isSuccessful) {
_registerResponse.value = response.body()
} else {
_registerResponse.value = null
}
} catch (e: Exception) {
Log.e("UserViewModel", "Register failed: ${e.message}")
_registerResponse.value = null
}
}
}
// 登录方法
fun login(username: String?, password: String, loginByUsername: Boolean) {
viewModelScope.launch {
try {
val response = userRepo.login(username, password, loginByUsername)
if (response.isSuccessful) {
_loginResponse.value = response.body()
} else {
_loginResponse.value = null // 登录失败时
}
} catch (e: Exception) {
Log.e("UserViewModel", "Login failed: ${e.message}")
_loginResponse.value = null
}
}
}
// 获取用户列表
fun getUserList(username: String) {
viewModelScope.launch {
try {
val response = userRepo.getUserList(username)
if (response.isSuccessful) {
_userListResponse.value = response.body()
} else {
_userListResponse.value = null
}
} catch (e: Exception) {
Log.e("UserViewModel", "Get User List failed: ${e.message}")
_userListResponse.value = null
}
}
}
// 修改昵称
fun changeNickname(userRequest: UserRequest) {
viewModelScope.launch {
try {
val response = userRepo.changeNickname(userRequest)
_changeNicknameResponse.value = response.isSuccessful
} catch (e: Exception) {
Log.e("UserViewModel", "Change Nickname failed: ${e.message}")
_changeNicknameResponse.value = false
}
}
}
// 上传头像
fun uploadAvatar(file: MultipartBody.Part, username: String) {
viewModelScope.launch {
try {
val response = userRepo.uploadAvatar(file, username)
if (response.isSuccessful) {
_uploadAvatarResponse.value = response.body()
} else {
_uploadAvatarResponse.value = null
}
} catch (e: Exception) {
Log.e("UserViewModel", "Upload Avatar failed: ${e.message}")
_uploadAvatarResponse.value = null
}
}
}
} }

View File

@ -28,7 +28,7 @@
android:id="@+id/ci_avatar" android:id="@+id/ci_avatar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:iconRound="4dp" app:iconRound="0.2"
app:iconSize="45dp" app:iconSize="45dp"
app:itemIcon="@drawable/ic_avatar" app:itemIcon="@drawable/ic_avatar"
app:itemName="头像" /> app:itemName="头像" />

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个小tip"
android:textColor="@color/black"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个小tip"
android:textColor="@color/black"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -65,7 +65,7 @@
app:layout_constraintEnd_toStartOf="@id/iv_arrow_right" app:layout_constraintEnd_toStartOf="@id/iv_arrow_right"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageView <androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_item_icon" android:id="@+id/iv_item_icon"
android:layout_width="25dp" android:layout_width="25dp"
android:layout_height="25dp" android:layout_height="25dp"
@ -74,7 +74,8 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_arrow_right" app:layout_constraintEnd_toStartOf="@id/iv_arrow_right"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:roundPercent="0.2" />
<ImageView <ImageView
android:id="@+id/iv_arrow_right" android:id="@+id/iv_arrow_right"

View File

@ -16,7 +16,7 @@
<attr name="isTopDividerVisible" format="boolean" /> <attr name="isTopDividerVisible" format="boolean" />
<attr name="isBottomDividerVisible" format="boolean" /> <attr name="isBottomDividerVisible" format="boolean" />
<attr name="iconSize" format="dimension" /> <attr name="iconSize" format="dimension" />
<attr name="iconRound" format="dimension" /> <attr name="iconRound" format="float" />
<attr name="itemIcon" format="reference" /> <attr name="itemIcon" format="reference" />
<attr name="itemLeftIcon" format="reference" /> <attr name="itemLeftIcon" format="reference" />
<attr name="itemRedTip" format="boolean" /> <attr name="itemRedTip" format="boolean" />

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config>
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">100.66.152.184</domain> <domain includeSubdomains="true">192.168.235.209</domain>
</domain-config> </domain-config>
</network-security-config> </network-security-config>

View File

@ -1,5 +1,6 @@
[versions] [versions]
agp = "8.3.2" agp = "8.3.2"
converterGson = "2.11.0"
emoji2 = "1.5.0" emoji2 = "1.5.0"
glide = "4.16.0" glide = "4.16.0"
gson = "2.11.0" gson = "2.11.0"
@ -9,6 +10,7 @@ espressoCore = "3.6.1"
appcompat = "1.7.0" appcompat = "1.7.0"
kotlinxCoroutinesCore = "1.7.3" kotlinxCoroutinesCore = "1.7.3"
kotlinxSerializationJson = "1.6.3" kotlinxSerializationJson = "1.6.3"
loggingInterceptorVersion = "5.0.0-alpha.2"
lottie = "6.5.2" lottie = "6.5.2"
material = "1.12.0" material = "1.12.0"
activity = "1.9.3" activity = "1.9.3"
@ -17,6 +19,7 @@ mmkv = "1.3.9"
okhttp = "4.12.0" okhttp = "4.12.0"
pinyin4j = "2.5.1" pinyin4j = "2.5.1"
preference = "1.2.1" preference = "1.2.1"
retrofit = "2.11.0"
shapedrawable = "3.2" shapedrawable = "3.2"
shapeview = "9.2" shapeview = "9.2"
therouter = "1.2.2" therouter = "1.2.2"
@ -30,7 +33,10 @@ objectbox = "4.0.2"
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesCore" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesCore" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptorVersion" }
pinyin4j = { module = "com.belerweb:pinyin4j", version.ref = "pinyin4j" } pinyin4j = { module = "com.belerweb:pinyin4j", version.ref = "pinyin4j" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
shapedrawable = { module = "com.github.getActivity:ShapeDrawable", version.ref = "shapedrawable" } shapedrawable = { module = "com.github.getActivity:ShapeDrawable", version.ref = "shapedrawable" }
shapeview = { module = "com.github.getActivity:ShapeView", version.ref = "shapeview" } shapeview = { module = "com.github.getActivity:ShapeView", version.ref = "shapeview" }
therouter-ksp = { module = "cn.therouter:apt", version.ref = "therouter" } therouter-ksp = { module = "cn.therouter:apt", version.ref = "therouter" }