dm-134
This commit is contained in:
parent
cd2b8adfb1
commit
c6aad75658
3 changed files with 294 additions and 64 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +1,220 @@
|
|||
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 context = LocalContext.current
|
||||
|
||||
// 1. 監聽狀態
|
||||
val selectedTheme by viewModel.currentTheme.collectAsState()
|
||||
val wallpaperPath by viewModel.customWallpaperPath.collectAsState()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
) {
|
||||
//glass effect
|
||||
// 2. 建立相簿照片選擇器 Launcher
|
||||
val photoPickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia()
|
||||
) { uri: Uri? ->
|
||||
// 用戶選完照片後,若不為 null 則交給 ViewModel 儲存
|
||||
uri?.let { viewModel.uploadWallpaper(context, it) }
|
||||
}
|
||||
|
||||
Column() {
|
||||
|
||||
// theme mode
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
//dark light mode
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "主題設定",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
.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)
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 遍歷所有 Enum 選項
|
||||
AppTheme.entries.forEach { theme ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
//dark light mode
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "主題設定",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
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),
|
||||
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(
|
||||
selected = (theme == selectedTheme),
|
||||
onClick = { viewModel.updateTheme(theme) }
|
||||
)
|
||||
Text(
|
||||
text = when(theme) {
|
||||
AppTheme.SYSTEM -> "follow"
|
||||
AppTheme.LIGHT -> "light"
|
||||
AppTheme.DARK -> "dark"
|
||||
},
|
||||
modifier = Modifier.padding(start = 8.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("清除壁紙")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue