auth to server-V52

This commit is contained in:
Eric85710 2026-05-05 16:43:17 +08:00
parent 356ee52cff
commit 0e8ecb74b6
4 changed files with 73 additions and 38 deletions

View file

@ -39,6 +39,12 @@ interface TecnologiaApi {
@Part file: MultipartBody.Part @Part file: MultipartBody.Part
): Response<Unit> ): Response<Unit>
@Multipart
@POST("api/me/banner")
suspend fun uploadBanner(
@Part file: MultipartBody.Part
): Response<Unit>
// 發送好友請求 // 發送好友請求
@POST("api/friends/request") @POST("api/friends/request")
suspend fun sendFriendRequest( suspend fun sendFriendRequest(

View file

@ -4,6 +4,7 @@ import com.example.login_v3.data.api.TecnologiaApi
import com.example.login_v3.data.api.api_class.UserProfile import com.example.login_v3.data.api.api_class.UserProfile
import com.example.login_v3.data.api.api_class.UserProfileUpdateRequest import com.example.login_v3.data.api.api_class.UserProfileUpdateRequest
import okhttp3.MultipartBody import okhttp3.MultipartBody
import retrofit2.Response
import javax.inject.Inject import javax.inject.Inject
class PersonalProfileRepository @Inject constructor( class PersonalProfileRepository @Inject constructor(
@ -40,10 +41,13 @@ class PersonalProfileRepository @Inject constructor(
} }
// 3. 上傳頭像圖片 (POST) // 3. 上傳頭像圖片 (POST)
suspend fun uploadAvatar(avatarPart: MultipartBody.Part): Result<Unit> { // 通用的上傳邏輯
private suspend fun uploadImage(
filePart: MultipartBody.Part,
uploadAction: suspend (MultipartBody.Part) -> Response<Unit>
): Result<Unit> {
return try { return try {
val response = api.uploadAvatar(avatarPart) val response = uploadAction(filePart)
if (response.isSuccessful) { if (response.isSuccessful) {
Result.success(Unit) Result.success(Unit)
} else { } else {
@ -53,4 +57,11 @@ class PersonalProfileRepository @Inject constructor(
Result.failure(e) Result.failure(e)
} }
} }
// 外部呼叫時依然保有語意化的名稱
suspend fun uploadAvatar(avatarPart: MultipartBody.Part) =
uploadImage(avatarPart, api::uploadAvatar)
suspend fun uploadBanner(bannerPart: MultipartBody.Part) =
uploadImage(bannerPart, api::uploadBanner)
} }

View file

@ -1,5 +1,6 @@
package com.example.login_v3.home.setting.setting_detail_page.detail_UI package com.example.login_v3.home.setting.setting_detail_page.detail_UI
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
@ -107,14 +108,17 @@ fun Loaded_setting_profile_page(
//image chooser //image chooser
val context = LocalContext.current // 務必獲取 Context,uploadAvatar 需要它來解析 Uri val context = LocalContext.current // 務必獲取 Context,uploadAvatar 需要它來解析 Uri
val launcher = rememberLauncherForActivityResult( //avatar
contract = ActivityResultContracts.PickVisualMedia() val avatarLauncher = rememberLauncherForActivityResult(
) { uri -> contract = ActivityResultContracts.PickVisualMedia() // 這裡改掉
if (uri != null) { ) { uri: Uri? ->
// ✅ 修正:呼叫上傳圖片的函式,而不是更新網址字串的函式 uri?.let { viewModel.uploadAvatar(context, it) }
viewModel.uploadAvatar(context, uri)
Log.d("AvatarURL", "目前的圖片網址為: ${profile.avatar_url}")
} }
//banner
val bannerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia() // 這裡改掉
) { uri: Uri? ->
uri?.let { viewModel.uploadBanner(context, it) }
} }
Column( Column(
@ -160,13 +164,15 @@ fun Loaded_setting_profile_page(
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
){ ){
// 1. 封面圖 (背景) // 1. 封面圖 (背景)
Image( AsyncImage(
painter = painterResource(id = R.drawable.thumbnail_v1), model = profile.banner_url,
contentDescription = null, contentDescription = "Avatar of ${profile.display_name}",
contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(180.dp) .height(180.dp)
,
contentScale = ContentScale.Crop,
error = painterResource(R.drawable.thumbnail_v1)
) )
} }
@ -189,7 +195,7 @@ fun Loaded_setting_profile_page(
.clip(CircleShape) .clip(CircleShape)
.clickable { .clickable {
// 假設你拿到了一個新網址 // 假設你拿到了一個新網址
launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) avatarLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
} }
, ,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,

View file

@ -81,38 +81,50 @@ class PersonalProfileViewModel @Inject constructor(
} }
// upload image // upload image
fun uploadAvatar(context: Context, uri: Uri) { // 1. 定義一個通用的處理邏輯
private fun handleImageUpload(
context: Context,
uri: Uri,
isAvatar: Boolean // 用來區分檔案名稱或特定邏輯
) {
viewModelScope.launch { viewModelScope.launch {
// 如果你想要在上傳時讓 UI 顯示 Loading,可以在這開啟 _uiState.value = ProfileUiState.Loading
// _uiState.value = ProfileUiState.Loading
try { try {
// 1. 從 Uri 取得 InputStream 並讀取 Bytes // 從 Uri 讀取檔案
val inputStream = context.contentResolver.openInputStream(uri) val bytes = context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
val bytes = inputStream?.use { it.readBytes() } ?: return@launch if (bytes == null) {
_uiState.value = ProfileUiState.Error("無法讀取檔案")
return@launch
}
// 2. 封裝成 RequestBody (指定為圖片格式)
val requestFile = bytes.toRequestBody("image/*".toMediaTypeOrNull()) val requestFile = bytes.toRequestBody("image/*".toMediaTypeOrNull())
// 3. 建立 MultipartBody.Part // 根據類型決定檔名(雖然通常後端只看 Part Name,但給正確副檔名比較保險)
// 注意:"avatar" 必須跟後端 @Part("avatar") 裡面的名稱一模一樣 val fileName = if (isAvatar) "avatar.jpg" else "banner.jpg"
val body = MultipartBody.Part.createFormData("file", "avatar.jpg", requestFile) val body = MultipartBody.Part.createFormData("file", fileName, requestFile)
// 4. 呼叫 Repository // 根據類型呼叫對應的 Repository 方法
val result = if (isAvatar) {
repository.uploadAvatar(body) repository.uploadAvatar(body)
.onSuccess { } else {
// 上傳成功後,最保險的做法是重新 fetch 一次個人資料 repository.uploadBanner(body)
// 這樣 UI 上的頭像網址才會更新成後端最新儲存的網址
fetchProfile()
} }
.onFailure { e ->
// 處理錯誤,例如發送一個錯誤訊息給 UI result.onSuccess {
fetchProfile() // 成功後統一刷新資料
}.onFailure { e ->
_uiState.value = ProfileUiState.Error(e.localizedMessage ?: "上傳失敗") _uiState.value = ProfileUiState.Error(e.localizedMessage ?: "上傳失敗")
} }
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = ProfileUiState.Error("讀取檔案出錯: ${e.message}") _uiState.value = ProfileUiState.Error("處理檔案時發生錯誤: ${e.message}")
} }
} }
} }
// 2. 對外暴露的簡潔方法
fun uploadAvatar(context: Context, uri: Uri) = handleImageUpload(context, uri, true)
fun uploadBanner(context: Context, uri: Uri) = handleImageUpload(context, uri, false)
} }