From 0e8ecb74b664018d4fbb85cc356fd367ea7ac5fd Mon Sep 17 00:00:00 2001 From: Eric85710 Date: Tue, 5 May 2026 16:43:17 +0800 Subject: [PATCH] auth to server-V52 --- .../login_v3/data/api/Main_API_Service.kt | 6 ++ .../basic/Personal_Profile_Repository.kt | 17 +++++- .../detail_UI/setting_Profile_Page.kt | 32 ++++++----- .../viewmodel/Profile_Viewmodel.kt | 56 +++++++++++-------- 4 files changed, 73 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt index f7c2679..5966afc 100644 --- a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt +++ b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt @@ -39,6 +39,12 @@ interface TecnologiaApi { @Part file: MultipartBody.Part ): Response + @Multipart + @POST("api/me/banner") + suspend fun uploadBanner( + @Part file: MultipartBody.Part + ): Response + // 發送好友請求 @POST("api/friends/request") suspend fun sendFriendRequest( diff --git a/app/src/main/java/com/example/login_v3/data/repository/basic/Personal_Profile_Repository.kt b/app/src/main/java/com/example/login_v3/data/repository/basic/Personal_Profile_Repository.kt index de1591c..31f8fad 100644 --- a/app/src/main/java/com/example/login_v3/data/repository/basic/Personal_Profile_Repository.kt +++ b/app/src/main/java/com/example/login_v3/data/repository/basic/Personal_Profile_Repository.kt @@ -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.UserProfileUpdateRequest import okhttp3.MultipartBody +import retrofit2.Response import javax.inject.Inject class PersonalProfileRepository @Inject constructor( @@ -40,10 +41,13 @@ class PersonalProfileRepository @Inject constructor( } // 3. 上傳頭像圖片 (POST) - suspend fun uploadAvatar(avatarPart: MultipartBody.Part): Result { + // 通用的上傳邏輯 + private suspend fun uploadImage( + filePart: MultipartBody.Part, + uploadAction: suspend (MultipartBody.Part) -> Response + ): Result { return try { - val response = api.uploadAvatar(avatarPart) - + val response = uploadAction(filePart) if (response.isSuccessful) { Result.success(Unit) } else { @@ -53,4 +57,11 @@ class PersonalProfileRepository @Inject constructor( Result.failure(e) } } + + // 外部呼叫時依然保有語意化的名稱 + suspend fun uploadAvatar(avatarPart: MultipartBody.Part) = + uploadImage(avatarPart, api::uploadAvatar) + + suspend fun uploadBanner(bannerPart: MultipartBody.Part) = + uploadImage(bannerPart, api::uploadBanner) } diff --git a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Profile_Page.kt b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Profile_Page.kt index 61c76be..6fdb43c 100644 --- a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Profile_Page.kt +++ b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Profile_Page.kt @@ -1,5 +1,6 @@ package com.example.login_v3.home.setting.setting_detail_page.detail_UI +import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -107,14 +108,17 @@ fun Loaded_setting_profile_page( //image chooser val context = LocalContext.current // 務必獲取 Context,uploadAvatar 需要它來解析 Uri - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickVisualMedia() - ) { uri -> - if (uri != null) { - // ✅ 修正:呼叫上傳圖片的函式,而不是更新網址字串的函式 - viewModel.uploadAvatar(context, uri) - Log.d("AvatarURL", "目前的圖片網址為: ${profile.avatar_url}") - } + //avatar + val avatarLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia() // 這裡改掉 + ) { uri: Uri? -> + uri?.let { viewModel.uploadAvatar(context, it) } + } + //banner + val bannerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia() // 這裡改掉 + ) { uri: Uri? -> + uri?.let { viewModel.uploadBanner(context, it) } } Column( @@ -160,13 +164,15 @@ fun Loaded_setting_profile_page( .clip(RoundedCornerShape(16.dp)) ){ // 1. 封面圖 (背景) - Image( - painter = painterResource(id = R.drawable.thumbnail_v1), - contentDescription = null, - contentScale = ContentScale.Crop, + AsyncImage( + model = profile.banner_url, + contentDescription = "Avatar of ${profile.display_name}", modifier = Modifier .fillMaxWidth() .height(180.dp) + , + contentScale = ContentScale.Crop, + error = painterResource(R.drawable.thumbnail_v1) ) } @@ -189,7 +195,7 @@ fun Loaded_setting_profile_page( .clip(CircleShape) .clickable { // 假設你拿到了一個新網址 - launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + avatarLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) } , contentScale = ContentScale.Crop, diff --git a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Profile_Viewmodel.kt b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Profile_Viewmodel.kt index e4276b0..b4469f8 100644 --- a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Profile_Viewmodel.kt +++ b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Profile_Viewmodel.kt @@ -81,38 +81,50 @@ class PersonalProfileViewModel @Inject constructor( } // upload image - fun uploadAvatar(context: Context, uri: Uri) { + // 1. 定義一個通用的處理邏輯 + private fun handleImageUpload( + context: Context, + uri: Uri, + isAvatar: Boolean // 用來區分檔案名稱或特定邏輯 + ) { viewModelScope.launch { - // 如果你想要在上傳時讓 UI 顯示 Loading,可以在這開啟 - // _uiState.value = ProfileUiState.Loading + _uiState.value = ProfileUiState.Loading try { - // 1. 從 Uri 取得 InputStream 並讀取 Bytes - val inputStream = context.contentResolver.openInputStream(uri) - val bytes = inputStream?.use { it.readBytes() } ?: return@launch + // 從 Uri 讀取檔案 + val bytes = context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + if (bytes == null) { + _uiState.value = ProfileUiState.Error("無法讀取檔案") + return@launch + } - // 2. 封裝成 RequestBody (指定為圖片格式) val requestFile = bytes.toRequestBody("image/*".toMediaTypeOrNull()) - // 3. 建立 MultipartBody.Part - // 注意:"avatar" 必須跟後端 @Part("avatar") 裡面的名稱一模一樣 - val body = MultipartBody.Part.createFormData("file", "avatar.jpg", requestFile) + // 根據類型決定檔名(雖然通常後端只看 Part Name,但給正確副檔名比較保險) + val fileName = if (isAvatar) "avatar.jpg" else "banner.jpg" + val body = MultipartBody.Part.createFormData("file", fileName, requestFile) + + // 根據類型呼叫對應的 Repository 方法 + val result = if (isAvatar) { + repository.uploadAvatar(body) + } else { + repository.uploadBanner(body) + } + + result.onSuccess { + fetchProfile() // 成功後統一刷新資料 + }.onFailure { e -> + _uiState.value = ProfileUiState.Error(e.localizedMessage ?: "上傳失敗") + } - // 4. 呼叫 Repository - repository.uploadAvatar(body) - .onSuccess { - // 上傳成功後,最保險的做法是重新 fetch 一次個人資料 - // 這樣 UI 上的頭像網址才會更新成後端最新儲存的網址 - fetchProfile() - } - .onFailure { e -> - // 處理錯誤,例如發送一個錯誤訊息給 UI - _uiState.value = ProfileUiState.Error(e.localizedMessage ?: "上傳失敗") - } } 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) + } \ No newline at end of file