This commit is contained in:
Eric85710 2026-06-19 14:54:13 +08:00
parent cd2b8adfb1
commit c6aad75658
3 changed files with 294 additions and 64 deletions

View file

@ -1,5 +1,7 @@
package com.example.login_v3.data.local.UserPreferences
import android.content.Context
import android.net.Uri
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences // 確保是這個 Preferences
import androidx.datastore.preferences.core.edit
@ -8,6 +10,8 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@ -15,6 +19,8 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
private object PreferencesKeys {
val THEME_MODE = stringPreferencesKey("theme_mode")
// 新增:儲存自訂壁紙檔案路徑的 Key
val CUSTOM_WALLPAPER_PATH = stringPreferencesKey("custom_wallpaper_path")
}
// 讀取設定:直接在宣告時賦值,簡潔又安全
@ -30,9 +36,74 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
preferences[PreferencesKeys.THEME_MODE] ?: "system"
}
// 2. 新增:讀取自訂壁紙路徑(預設為 null,代表使用系統預設背景)
val wallpaperPathFlow: Flow<String?> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[PreferencesKeys.CUSTOM_WALLPAPER_PATH]
}
suspend fun saveThemeMode(mode: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.THEME_MODE] = mode
}
}
/**
* 4. 新增:將用戶選擇的圖片複製到內部存儲,並將路徑存入 DataStore
* @param context 用於獲取 filesDir 與 contentResolver
* @param uri 用戶從相簿選擇的圖片 Uri
* @return 儲存成功返回 true,失敗返回 false
*/
suspend fun saveWallpaper(context: Context, uri: Uri): Boolean {
return try {
// 將圖片固定命名為 "custom_wallpaper.jpg",這樣每次上傳新圖片會自動覆蓋舊圖
val fileName = "custom_wallpaper.jpg"
val targetFile = File(context.filesDir, fileName)
// 開啟輸入流並複製到 App 的私有目錄下
context.contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(targetFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
// 將檔案的絕對路徑存入 DataStore 記錄起來
dataStore.edit { preferences ->
preferences[PreferencesKeys.CUSTOM_WALLPAPER_PATH] = targetFile.absolutePath
}
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* 5. 新增:清除自訂壁紙設定,並刪除手機內的實體檔案(省空間)
*/
suspend fun clearWallpaper(context: Context) {
try {
// 1. 刪除實體檔案
val fileName = "custom_wallpaper.jpg"
val targetFile = File(context.filesDir, fileName)
if (targetFile.exists()) {
targetFile.delete()
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 2. 無論檔案刪除成功與否,都清除 DataStore 內的紀錄
dataStore.edit { preferences ->
preferences.remove(PreferencesKeys.CUSTOM_WALLPAPER_PATH)
}
}
}
}

View file

@ -1,37 +1,51 @@
package com.example.login_v3.home.setting.setting_detail_page.detail_UI
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.example.login_v3.home.setting.setting_detail_page.viewmodel.Theme_ViewModel
import com.example.login_v3.ui.theme.AppTheme
import java.io.File
@Composable
fun setting_theme_page(
viewModel: Theme_ViewModel
){
// 直接讀取,不需要 .value
val selectedTheme by viewModel.currentTheme.collectAsState()
val context = LocalContext.current
// 1. 監聽狀態
val selectedTheme by viewModel.currentTheme.collectAsState()
val wallpaperPath by viewModel.customWallpaperPath.collectAsState()
// 2. 建立相簿照片選擇器 Launcher
val photoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia()
) { uri: Uri? ->
// 用戶選完照片後,若不為 null 則交給 ViewModel 儲存
uri?.let { viewModel.uploadWallpaper(context, it) }
}
Column() {
// theme mode
Box(
modifier = Modifier
.padding(10.dp)
@ -95,4 +109,114 @@ fun setting_theme_page(
}
}
}
// 3. 新增:upload wallpaper 區塊
Box(
modifier = Modifier
.padding(10.dp)
) {
// glass effect (與上方卡片一致)
Box(
modifier = Modifier
.matchParentSize()
.blur(10.dp)
)
Box(
modifier = Modifier
.matchParentSize()
.background(
color = MaterialTheme.colorScheme.primary,
RoundedCornerShape(16.dp)
)
.border(
1.dp,
Color.White.copy(alpha = 0.3f),
RoundedCornerShape(16.dp)
)
)
// 卡片內元件
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "自訂背景壁紙",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 左側:圖片預覽框(手機螢幕比例 9:16 微縮版)
Box(
modifier = Modifier
.size(90.dp, 160.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black.copy(alpha = 0.2f))
.border(1.dp, Color.White.copy(alpha = 0.2f), RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) {
val file = wallpaperPath?.let { File(it) }
if (file != null && file.exists()) {
// 讀取本地檔案並顯示
AsyncImage(
model = file,
contentDescription = "Wallpaper Preview",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
// 沒上傳過,顯示預設文字
Text(
text = "未設定",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.6f)
)
}
}
// 右側:操作按鈕們
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 選擇按鈕
Button(
onClick = {
// 啟動相簿選取器
photoPickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.onPrimary,
contentColor = MaterialTheme.colorScheme.primary
)
) {
Text("選擇圖片")
}
// 刪除按鈕:有壁紙時才動畫浮現
AnimatedVisibility(visible = !wallpaperPath.isNullOrEmpty()) {
Button(
onClick = { viewModel.deleteWallpaper(context) },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer
)
) {
Text("清除壁紙")
}
}
}
}
}
}
}
}

View file

@ -1,5 +1,7 @@
package com.example.login_v3.home.setting.setting_detail_page.viewmodel
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -33,6 +35,14 @@ class Theme_ViewModel @Inject constructor(
initialValue = AppTheme.DARK // 初始值
)
// 新增 1:讀取自訂壁紙路徑的 StateFlow
val customWallpaperPath: StateFlow<String?> = repository.wallpaperPathFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null // 初始值為 null,代表還沒有自訂壁紙
)
// 4. 更新:除了更新狀態,還要存入 DataStore
fun updateTheme(newTheme: AppTheme) {
Log.d("ThemeDebug", "ViewModel 更新主題並儲存至 DataStore: $newTheme")
@ -40,4 +50,29 @@ class Theme_ViewModel @Inject constructor(
repository.saveThemeMode(newTheme.name) // 儲存 Enum 的名稱 (例如 "DARK")
}
}
/**
* 新增 2:呼叫 Repository 儲存壁紙圖片
*/
fun uploadWallpaper(context: Context, uri: Uri) {
Log.d("ThemeDebug", "ViewModel 開始上傳壁紙: $uri")
viewModelScope.launch {
val success = repository.saveWallpaper(context, uri)
if (success) {
Log.d("ThemeDebug", "壁紙儲存成功")
} else {
Log.e("ThemeDebug", "壁紙儲存失敗")
}
}
}
/**
* 新增 3:呼叫 Repository 刪除壁紙圖片並清理 DataStore
*/
fun deleteWallpaper(context: Context) {
Log.d("ThemeDebug", "ViewModel 開始刪除壁紙")
viewModelScope.launch {
repository.clearWallpaper(context)
}
}
}