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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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("清除壁紙")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue