diff --git a/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt b/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt index da1e1f1..3507c9a 100644 --- a/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt +++ b/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt @@ -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) { 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[PreferencesKeys.THEME_MODE] ?: "system" } + // 2. 新增:讀取自訂壁紙路徑(預設為 null,代表使用系統預設背景) + val wallpaperPathFlow: Flow = 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) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Theme_Page.kt b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Theme_Page.kt index 3bb43d9..fba06b6 100644 --- a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Theme_Page.kt +++ b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/detail_UI/setting_Theme_Page.kt @@ -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("清除壁紙") + } + } + } } } } diff --git a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Theme_ViewModel.kt b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Theme_ViewModel.kt index b6b0e41..8e9de69 100644 --- a/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Theme_ViewModel.kt +++ b/app/src/main/java/com/example/login_v3/home/setting/setting_detail_page/viewmodel/Theme_ViewModel.kt @@ -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 = 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) + } + } } \ No newline at end of file