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 package com.example.login_v3.data.local.UserPreferences
import android.content.Context
import android.net.Uri
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences // 確保是這個 Preferences import androidx.datastore.preferences.core.Preferences // 確保是這個 Preferences
import androidx.datastore.preferences.core.edit 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.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.io.File
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
@ -15,6 +19,8 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
private object PreferencesKeys { private object PreferencesKeys {
val THEME_MODE = stringPreferencesKey("theme_mode") 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" 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) { suspend fun saveThemeMode(mode: String) {
dataStore.edit { preferences -> dataStore.edit { preferences ->
preferences[PreferencesKeys.THEME_MODE] = mode 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,96 +1,220 @@
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 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.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
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.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.RadioButton import androidx.compose.runtime.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color 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 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.home.setting.setting_detail_page.viewmodel.Theme_ViewModel
import com.example.login_v3.ui.theme.AppTheme import com.example.login_v3.ui.theme.AppTheme
import java.io.File
@Composable @Composable
fun setting_theme_page( fun setting_theme_page(
viewModel: Theme_ViewModel viewModel: Theme_ViewModel
){ ){
// 直接讀取,不需要 .value val context = LocalContext.current
// 1. 監聽狀態
val selectedTheme by viewModel.currentTheme.collectAsState() val selectedTheme by viewModel.currentTheme.collectAsState()
val wallpaperPath by viewModel.customWallpaperPath.collectAsState()
Box( // 2. 建立相簿照片選擇器 Launcher
modifier = Modifier val photoPickerLauncher = rememberLauncherForActivityResult(
.padding(10.dp) contract = ActivityResultContracts.PickVisualMedia()
) { ) { uri: Uri? ->
//glass effect // 用戶選完照片後,若不為 null 則交給 ViewModel 儲存
uri?.let { viewModel.uploadWallpaper(context, it) }
}
Column() {
// theme mode
Box( Box(
modifier = Modifier modifier = Modifier
.matchParentSize() .padding(10.dp)
.blur(10.dp) ) {
) //glass effect
Box( Box(
modifier = Modifier modifier = Modifier
.matchParentSize() .matchParentSize()
.background( .blur(10.dp)
color = MaterialTheme.colorScheme.primary, )
RoundedCornerShape(16.dp) Box(
) modifier = Modifier
.border( .matchParentSize()
1.dp, .background(
Color.White.copy(alpha = 0.3f), color = MaterialTheme.colorScheme.primary,
RoundedCornerShape(16.dp) RoundedCornerShape(16.dp)
) )
) .border(
1.dp,
Color.White.copy(alpha = 0.3f),
//dark light mode RoundedCornerShape(16.dp)
Column(modifier = Modifier.padding(16.dp)) { )
Text(
text = "主題設定",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
) )
Spacer(modifier = Modifier.height(8.dp))
// 遍歷所有 Enum 選項 //dark light mode
AppTheme.entries.forEach { theme -> Column(modifier = Modifier.padding(16.dp)) {
Row( Text(
verticalAlignment = Alignment.CenterVertically, text = "主題設定",
modifier = Modifier style = MaterialTheme.typography.headlineMedium,
.fillMaxWidth() color = MaterialTheme.colorScheme.onBackground
.selectable( )
Spacer(modifier = Modifier.height(8.dp))
// 遍歷所有 Enum 選項
AppTheme.entries.forEach { theme ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = (theme == selectedTheme),
onClick = { viewModel.updateTheme(theme) }
)
.padding(8.dp)
) {
RadioButton(
selected = (theme == selectedTheme), selected = (theme == selectedTheme),
onClick = { viewModel.updateTheme(theme) } onClick = { viewModel.updateTheme(theme) }
) )
.padding(8.dp) Text(
text = when(theme) {
AppTheme.SYSTEM -> "follow"
AppTheme.LIGHT -> "light"
AppTheme.DARK -> "dark"
},
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}
// 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)
) { ) {
RadioButton( // 左側:圖片預覽框(手機螢幕比例 9:16 微縮版)
selected = (theme == selectedTheme), Box(
onClick = { viewModel.updateTheme(theme) } modifier = Modifier
) .size(90.dp, 160.dp)
Text( .clip(RoundedCornerShape(8.dp))
text = when(theme) { .background(Color.Black.copy(alpha = 0.2f))
AppTheme.SYSTEM -> "follow" .border(1.dp, Color.White.copy(alpha = 0.2f), RoundedCornerShape(8.dp)),
AppTheme.LIGHT -> "light" contentAlignment = Alignment.Center
AppTheme.DARK -> "dark" ) {
}, val file = wallpaperPath?.let { File(it) }
modifier = Modifier.padding(start = 8.dp) 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 package com.example.login_v3.home.setting.setting_detail_page.viewmodel
import android.content.Context
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -33,6 +35,14 @@ class Theme_ViewModel @Inject constructor(
initialValue = AppTheme.DARK // 初始值 initialValue = AppTheme.DARK // 初始值
) )
// 新增 1:讀取自訂壁紙路徑的 StateFlow
val customWallpaperPath: StateFlow<String?> = repository.wallpaperPathFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null // 初始值為 null,代表還沒有自訂壁紙
)
// 4. 更新:除了更新狀態,還要存入 DataStore // 4. 更新:除了更新狀態,還要存入 DataStore
fun updateTheme(newTheme: AppTheme) { fun updateTheme(newTheme: AppTheme) {
Log.d("ThemeDebug", "ViewModel 更新主題並儲存至 DataStore: $newTheme") Log.d("ThemeDebug", "ViewModel 更新主題並儲存至 DataStore: $newTheme")
@ -40,4 +50,29 @@ class Theme_ViewModel @Inject constructor(
repository.saveThemeMode(newTheme.name) // 儲存 Enum 的名稱 (例如 "DARK") 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)
}
}
} }