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 @Part file: MultipartBody.Part
): Response<Unit> ): Response<Unit>
// contact
// friends
@GET("api/friends") @GET("api/friends")
suspend fun getFriends(): FriendListResponse suspend fun getFriends(): FriendListResponse
@GET("api/friends/pending")
suspend fun getFriendsPending(): PendingFriendsResponse
} }

View file

@ -10,23 +10,22 @@ data class FriendListResponse(
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Friend( data class Friend(
@Json(name = "friend_id") @field:Json(name = "friend_id")
val friendId: String, val friendId: String,
val username: String, val username: String,
@Json(name = "display_name") @field:Json(name = "display_name")
val displayName: String, val displayName: String,
@Json(name = "avatar_url") @field:Json(name = "avatar_url")
val avatarUrl: String?, // 建議給可空性,避免後端沒傳頭像時崩潰 val avatarUrl: String?, // 建議給可空性,避免後端沒傳頭像時崩潰
val status: String, val status: String,
@Json(name = "accepted_at") @field:Json(name = "accepted_at")
val acceptedAt: String val acceptedAt: String
) )
val Friend.fullFriendsAvatarUrl: Any val Friend.fullFriendsAvatarUrl: Any
get() = if (avatarUrl.isNullOrBlank()) { get() = if (avatarUrl.isNullOrBlank()) {
R.drawable.avatar_v1 R.drawable.avatar_v1
@ -35,3 +34,41 @@ val Friend.fullFriendsAvatarUrl: Any
} else { } else {
"http://192.168.0.217$avatarUrl" "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.TecnologiaApi
import com.example.login_v3.data.api.api_class.Friend import com.example.login_v3.data.api.api_class.Friend
import com.example.login_v3.data.api.api_class.PendingFriendApiModel
import javax.inject.Inject import javax.inject.Inject
class FriendsRepository @Inject constructor class FriendsRepository @Inject constructor(
(private val apiService: TecnologiaApi) { private val apiService: TecnologiaApi
) {
suspend fun getFriendList(): Result<List<Friend>> { suspend fun getFriendList(): Result<List<Friend>> {
return try { return try {
@ -17,4 +19,14 @@ class FriendsRepository @Inject constructor
Result.failure(e) 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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape 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.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -25,11 +33,14 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.example.login_v3.R 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.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.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.home.Message.ViewModel.Detail.ContactListViewModel
import com.example.login_v3.navigation.BottomBarViewModel import com.example.login_v3.navigation.BottomBarViewModel
@ -39,6 +50,8 @@ fun Message_contact_list(
contactListViewModel: ContactListViewModel = hiltViewModel(), contactListViewModel: ContactListViewModel = hiltViewModel(),
bottomBarViewModel: BottomBarViewModel bottomBarViewModel: BottomBarViewModel
){ ){
val uiState by contactListViewModel.uiState.collectAsStateWithLifecycle()
val friends by contactListViewModel.friends.collectAsStateWithLifecycle() val friends by contactListViewModel.friends.collectAsStateWithLifecycle()
DisposableEffect(Unit) { DisposableEffect(Unit) {
@ -56,6 +69,38 @@ fun Message_contact_list(
) { ) {
Spacer(modifier = Modifier.height(60.dp)) 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()) { if (friends.isEmpty()) {
// 如果列表是空的,顯示提示(或載入指示器) // 如果列表是空的,顯示提示(或載入指示器)
Box( Box(
@ -71,13 +116,30 @@ fun Message_contact_list(
.fillMaxSize() .fillMaxSize()
.padding(6.dp) .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) FriendRow(friend)
} }
} }
} }
} }
} }
}
}
@Composable @Composable
@ -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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.login_v3.data.api.api_class.Friend 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 com.example.login_v3.data.repository.basic.FriendsRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
data class ContactUiState(
val isLoading: Boolean = false,
val friends: List<Friend> = emptyList(),
val pendingRequests: List<PendingFriendApiModel> = emptyList(),
val error: String? = null
)
@HiltViewModel @HiltViewModel
class ContactListViewModel @Inject constructor class ContactListViewModel @Inject constructor(
(
private val repository: FriendsRepository private val repository: FriendsRepository
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(ContactUiState())
val uiState: StateFlow<ContactUiState> = _uiState.asStateFlow()
private val _friends = MutableStateFlow<List<Friend>>(emptyList()) private val _friends = MutableStateFlow<List<Friend>>(emptyList())
val friends: StateFlow<List<Friend>> = _friends val friends: StateFlow<List<Friend>> = _friends
init { init {
// 畫面一建立就自動抓取資料 // 畫面一建立就自動抓取資料
fetchFriends() fetchFriends()
fetchPendingRequests()
} }
//friends list
fun fetchFriends() { fun fetchFriends() {
viewModelScope.launch { viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
repository.getFriendList() repository.getFriendList()
.onSuccess { list -> .onSuccess { list ->
_friends.value = list _uiState.update { it.copy(isLoading = false, friends = list) }
} }
.onFailure { .onFailure { error ->
// 這裡可以處理錯誤訊息,例如顯示 Toast _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 { /* 處理錯誤 */ }
}
}
} }