auth to server-V66

This commit is contained in:
Eric85710 2026-05-07 16:41:41 +08:00
parent 7f95b6b187
commit e346444e97
5 changed files with 206 additions and 35 deletions

View file

@ -45,7 +45,12 @@ interface TecnologiaApi {
@Part file: MultipartBody.Part
): Response<Unit>
// contact
// friends
@GET("api/friends")
suspend fun getFriends(): FriendListResponse
@GET("api/friends/pending")
suspend fun getFriendsPending(): PendingFriendsResponse
}

View file

@ -10,23 +10,22 @@ data class FriendListResponse(
)
@JsonClass(generateAdapter = true)
data class Friend(
@Json(name = "friend_id")
@field:Json(name = "friend_id")
val friendId: String,
val username: String,
@Json(name = "display_name")
@field:Json(name = "display_name")
val displayName: String,
@Json(name = "avatar_url")
@field:Json(name = "avatar_url")
val avatarUrl: String?, // 建議給可空性,避免後端沒傳頭像時崩潰
val status: String,
@Json(name = "accepted_at")
@field:Json(name = "accepted_at")
val acceptedAt: String
)
val Friend.fullFriendsAvatarUrl: Any
get() = if (avatarUrl.isNullOrBlank()) {
R.drawable.avatar_v1
@ -35,3 +34,41 @@ val Friend.fullFriendsAvatarUrl: Any
} else {
"http://192.168.0.217$avatarUrl"
}
//friends pending request
@JsonClass(generateAdapter = true)
data class PendingFriendsResponse(
@field:Json(name = "pending")
val pendingRequests: List<PendingFriendApiModel>
)
@JsonClass(generateAdapter = true)
data class PendingFriendApiModel(
@field:Json(name = "friendship_id")
val friendshipId: String,
val username: String,
@field:Json(name = "display_name")
val displayName: String,
@field:Json(name = "avatar_url")
val PendingAvatarUrl: String?,
@field:Json(name = "from_user_id")
val fromUserId: String,
@field:Json(name = "created_at")
val createdAt: String
)
val PendingFriendApiModel.fullPendingAvatarUrl: Any
get() = if (PendingAvatarUrl.isNullOrBlank()) {
R.drawable.avatar_v1
} else if (PendingAvatarUrl.startsWith("http")) {
PendingAvatarUrl
} else {
"http://192.168.0.217$PendingAvatarUrl"
}

View file

@ -2,10 +2,12 @@ package com.example.login_v3.data.repository.basic
import com.example.login_v3.data.api.TecnologiaApi
import com.example.login_v3.data.api.api_class.Friend
import com.example.login_v3.data.api.api_class.PendingFriendApiModel
import javax.inject.Inject
class FriendsRepository @Inject constructor
(private val apiService: TecnologiaApi) {
class FriendsRepository @Inject constructor(
private val apiService: TecnologiaApi
) {
suspend fun getFriendList(): Result<List<Friend>> {
return try {
@ -17,4 +19,14 @@ class FriendsRepository @Inject constructor
Result.failure(e)
}
}
suspend fun getPendingRequests(): Result<List<PendingFriendApiModel>> {
return try {
val response = apiService.getFriendsPending()
// 這裡回傳 API Model,轉換邏輯可以放在 ViewModel
Result.success(response.pendingRequests)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View file

@ -2,14 +2,22 @@ package com.example.login_v3.home.Message.UI.Detail
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -25,11 +33,14 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.example.login_v3.R
import com.example.login_v3.data.api.api_class.Friend
import com.example.login_v3.data.api.api_class.PendingFriendApiModel
import com.example.login_v3.data.api.api_class.fullFriendsAvatarUrl
import com.example.login_v3.data.api.api_class.fullPendingAvatarUrl
import com.example.login_v3.home.Message.ViewModel.Detail.ContactListViewModel
import com.example.login_v3.navigation.BottomBarViewModel
@ -39,6 +50,8 @@ fun Message_contact_list(
contactListViewModel: ContactListViewModel = hiltViewModel(),
bottomBarViewModel: BottomBarViewModel
){
val uiState by contactListViewModel.uiState.collectAsStateWithLifecycle()
val friends by contactListViewModel.friends.collectAsStateWithLifecycle()
DisposableEffect(Unit) {
@ -56,6 +69,38 @@ fun Message_contact_list(
) {
Spacer(modifier = Modifier.height(60.dp))
//pending friends list
Column(
modifier = Modifier
.fillMaxWidth()
) {
if (uiState.pendingRequests.isNotEmpty()) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
) {
item {
Text(
"好友請求 (${uiState.pendingRequests.size})",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
}
items(uiState.pendingRequests) { request ->
PendingFriendRow(request)
}
item { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) }
}
}
}
//friends list
Column(
modifier = Modifier
.fillMaxWidth()
) {
if (friends.isEmpty()) {
// 如果列表是空的,顯示提示(或載入指示器)
Box(
@ -71,12 +116,29 @@ fun Message_contact_list(
.fillMaxSize()
.padding(6.dp)
) {
items(friends) { friend ->
item {
Text(
"所有好友",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
}
if (uiState.friends.isEmpty() && !uiState.isLoading) {
item {
Box(modifier = Modifier.fillMaxWidth().padding(32.dp), contentAlignment = Alignment.Center) {
Text("目前沒有好友")
}
}
} else {
items(uiState.friends) { friend ->
FriendRow(friend)
}
}
}
}
}
}
}
@ -108,3 +170,30 @@ fun FriendRow(friend: Friend) {
}
)
}
@Composable
fun PendingFriendRow(request: PendingFriendApiModel) {
ListItem(
headlineContent = { Text(request.displayName) },
supportingContent = { Text("想加你為好友") },
leadingContent = {
AsyncImage(
// 這裡可以使用我們之前寫的擴充函式 .toFullImageUrl()
model = request.fullPendingAvatarUrl,
contentDescription = null,
modifier = Modifier.size(40.dp).clip(CircleShape),
error = painterResource(id = R.drawable.avatar_v1)
)
},
trailingContent = {
Row {
IconButton(onClick = { /* TODO: 呼叫 ViewModel 拒絕 */ }) {
Icon(Icons.Default.Close, contentDescription = "拒絕", tint = Color.Red)
}
IconButton(onClick = { /* TODO: 呼叫 ViewModel 接受 */ }) {
Icon(Icons.Default.Check, contentDescription = "接受", tint = Color.Green)
}
}
}
)
}

View file

@ -3,36 +3,64 @@ package com.example.login_v3.home.Message.ViewModel.Detail
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.login_v3.data.api.api_class.Friend
import com.example.login_v3.data.api.api_class.PendingFriendApiModel
import com.example.login_v3.data.repository.basic.FriendsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ContactListViewModel @Inject constructor
(
private val repository: FriendsRepository
) : ViewModel() {
data class ContactUiState(
val isLoading: Boolean = false,
val friends: List<Friend> = emptyList(),
val pendingRequests: List<PendingFriendApiModel> = emptyList(),
val error: String? = null
)
@HiltViewModel
class ContactListViewModel @Inject constructor(
private val repository: FriendsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ContactUiState())
val uiState: StateFlow<ContactUiState> = _uiState.asStateFlow()
private val _friends = MutableStateFlow<List<Friend>>(emptyList())
val friends: StateFlow<List<Friend>> = _friends
init {
// 畫面一建立就自動抓取資料
fetchFriends()
fetchPendingRequests()
}
//friends list
fun fetchFriends() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
repository.getFriendList()
.onSuccess { list ->
_friends.value = list
_uiState.update { it.copy(isLoading = false, friends = list) }
}
.onFailure {
// 這裡可以處理錯誤訊息,例如顯示 Toast
.onFailure { error ->
_uiState.update { it.copy(isLoading = false, error = error.message) }
}
}
}
// 獲取待處理請求 (API: /api/friends/pending)
fun fetchPendingRequests() {
viewModelScope.launch {
repository.getPendingRequests()
.onSuccess { requests ->
_uiState.update { it.copy(pendingRequests = requests) }
}
.onFailure { /* 處理錯誤 */ }
}
}
}