dark mode, lignht mode

This commit is contained in:
m
2026-09-11 23:19:37 +02:00
parent 64690d11f9
commit 100bff994d
9 changed files with 196 additions and 9 deletions
@@ -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>