dark mode, lignht mode
This commit is contained in:
@@ -4,18 +4,31 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.vaultdrop.mobile.domain.ThemePreferenceStore
|
||||||
import com.vaultdrop.mobile.ui.navigation.VaultDropApp
|
import com.vaultdrop.mobile.ui.navigation.VaultDropApp
|
||||||
import com.vaultdrop.mobile.ui.theme.VaultDropTheme
|
import com.vaultdrop.mobile.ui.theme.VaultDropTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var themePreferenceStore: ThemePreferenceStore
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
lifecycleScope.launch { themePreferenceStore.load() }
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
VaultDropTheme {
|
val theme by themePreferenceStore.theme.collectAsStateWithLifecycle()
|
||||||
|
VaultDropTheme(theme = theme) {
|
||||||
VaultDropApp()
|
VaultDropApp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ object PrefKeys {
|
|||||||
const val DEVICE_USER_ID = "device_user_id"
|
const val DEVICE_USER_ID = "device_user_id"
|
||||||
const val ACTIVE_USER_ID = "active_user_id"
|
const val ACTIVE_USER_ID = "active_user_id"
|
||||||
const val SYNC_MODE = "sync_mode"
|
const val SYNC_MODE = "sync_mode"
|
||||||
|
const val THEME = "theme"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.vaultdrop.mobile.domain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Préférence d'affichage de la page Réglages, persistée dans `user_preferences`.
|
||||||
|
* `SYSTEM` (défaut) = suivre le thème du système ; `LIGHT`/`DARK` = forcer.
|
||||||
|
*/
|
||||||
|
enum class ThemePreference(val storedValue: String) {
|
||||||
|
SYSTEM("system"),
|
||||||
|
LIGHT("light"),
|
||||||
|
DARK("dark"),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromStored(value: String?): ThemePreference =
|
||||||
|
entries.firstOrNull { it.storedValue == value } ?: SYSTEM
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.vaultdrop.mobile.domain
|
||||||
|
|
||||||
|
import com.vaultdrop.mobile.data.local.dao.UserPreferenceDao
|
||||||
|
import com.vaultdrop.mobile.data.local.entity.UserPreferenceEntity
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Préférence de thème (clair/sombre) : valeur par défaut SYSTEM, chargée une
|
||||||
|
* fois depuis `user_preferences` puis exposée en [StateFlow]. La source le
|
||||||
|
* thread-safe : écritures et lectures passent par le DAO Room (IO).
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class ThemePreferenceStore @Inject constructor(
|
||||||
|
private val userPreferenceDao: UserPreferenceDao,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val loaded = AtomicBoolean(false)
|
||||||
|
|
||||||
|
private val _theme = MutableStateFlow<ThemePreference>(ThemePreference.SYSTEM)
|
||||||
|
val theme: StateFlow<ThemePreference> = _theme.asStateFlow()
|
||||||
|
|
||||||
|
/** Charge la valeur persistée au boot (idempotent). */
|
||||||
|
suspend fun load() {
|
||||||
|
if (!loaded.compareAndSet(false, true)) return
|
||||||
|
val stored = userPreferenceDao.getValue(PrefKeys.THEME)
|
||||||
|
_theme.value = ThemePreference.fromStored(stored)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun set(theme: ThemePreference) {
|
||||||
|
_theme.value = theme
|
||||||
|
userPreferenceDao.upsert(
|
||||||
|
UserPreferenceEntity(PrefKeys.THEME, theme.storedValue, System.currentTimeMillis()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
package com.vaultdrop.mobile.ui.settings
|
package com.vaultdrop.mobile.ui.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vaultdrop.mobile.R
|
import com.vaultdrop.mobile.R
|
||||||
|
import com.vaultdrop.mobile.domain.ThemePreference
|
||||||
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
|
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
|
||||||
import com.vaultdrop.mobile.ui.navigation.NavTab
|
import com.vaultdrop.mobile.ui.navigation.NavTab
|
||||||
|
|
||||||
@@ -20,7 +34,15 @@ import com.vaultdrop.mobile.ui.navigation.NavTab
|
|||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
selectedTab: NavTab,
|
selectedTab: NavTab,
|
||||||
onTabSelected: (NavTab) -> Unit,
|
onTabSelected: (NavTab) -> Unit,
|
||||||
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val theme by viewModel.theme.collectAsStateWithLifecycle()
|
||||||
|
val darkNow = when (theme) {
|
||||||
|
ThemePreference.DARK -> true
|
||||||
|
ThemePreference.LIGHT -> false
|
||||||
|
ThemePreference.SYSTEM -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(title = { Text(stringResource(R.string.settings)) })
|
TopAppBar(title = { Text(stringResource(R.string.settings)) })
|
||||||
@@ -29,14 +51,62 @@ fun SettingsScreen(
|
|||||||
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
contentAlignment = Alignment.Center,
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
) {
|
) {
|
||||||
Text(
|
HorizontalDivider()
|
||||||
text = stringResource(R.string.settings),
|
SettingsRow(
|
||||||
textAlign = TextAlign.Center,
|
icon = { Icon(Icons.Filled.DarkMode, contentDescription = null) },
|
||||||
|
title = stringResource(R.string.settings_dark_mode),
|
||||||
|
subtitle = stringResource(
|
||||||
|
if (darkNow) R.string.settings_dark_mode_on
|
||||||
|
else R.string.settings_dark_mode_off,
|
||||||
|
),
|
||||||
|
trailing = {
|
||||||
|
Switch(
|
||||||
|
checked = darkNow,
|
||||||
|
onCheckedChange = { on ->
|
||||||
|
viewModel.setTheme(if (on) ThemePreference.DARK else ThemePreference.LIGHT)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsRow(
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
trailing: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.layout.Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
icon()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
trailing()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.vaultdrop.mobile.ui.settings
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vaultdrop.mobile.domain.ThemePreference
|
||||||
|
import com.vaultdrop.mobile.domain.ThemePreferenceStore
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettingsViewModel @Inject constructor(
|
||||||
|
private val themePreferenceStore: ThemePreferenceStore,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val theme: StateFlow<ThemePreference> = themePreferenceStore.theme
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch { themePreferenceStore.load() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTheme(theme: ThemePreference) {
|
||||||
|
viewModelScope.launch { themePreferenceStore.set(theme) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.vaultdrop.mobile.domain.ThemePreference
|
||||||
|
|
||||||
private val LightColors = lightColorScheme(
|
private val LightColors = lightColorScheme(
|
||||||
primary = BrandBlue,
|
primary = BrandBlue,
|
||||||
@@ -17,6 +18,19 @@ private val DarkColors = darkColorScheme(
|
|||||||
onPrimary = androidx.compose.ui.graphics.Color.Black,
|
onPrimary = androidx.compose.ui.graphics.Color.Black,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VaultDropTheme(
|
||||||
|
theme: ThemePreference,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val darkTheme = when (theme) {
|
||||||
|
ThemePreference.DARK -> true
|
||||||
|
ThemePreference.LIGHT -> false
|
||||||
|
ThemePreference.SYSTEM -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
VaultDropTheme(darkTheme = darkTheme, content = content)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VaultDropTheme(
|
fun VaultDropTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
<string name="nav_search">Search</string>
|
<string name="nav_search">Search</string>
|
||||||
<string name="nav_settings">Settings</string>
|
<string name="nav_settings">Settings</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
|
<string name="settings_dark_mode">Dark mode</string>
|
||||||
|
<string name="settings_dark_mode_on">Dark theme active</string>
|
||||||
|
<string name="settings_dark_mode_off">Light theme active</string>
|
||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@
|
|||||||
<string name="nav_search">Recherche</string>
|
<string name="nav_search">Recherche</string>
|
||||||
<string name="nav_settings">Réglages</string>
|
<string name="nav_settings">Réglages</string>
|
||||||
<string name="settings">Réglages</string>
|
<string name="settings">Réglages</string>
|
||||||
|
<string name="settings_dark_mode">Mode sombre</string>
|
||||||
|
<string name="settings_dark_mode_on">Thème sombre actif</string>
|
||||||
|
<string name="settings_dark_mode_off">Thème clair actif</string>
|
||||||
|
|
||||||
<!-- Recherche -->
|
<!-- Recherche -->
|
||||||
<string name="search">Recherche</string>
|
<string name="search">Recherche</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user