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.compose.setContent
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.theme.VaultDropTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var themePreferenceStore: ThemePreferenceStore
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch { themePreferenceStore.load() }
setContent {
VaultDropTheme {
val theme by themePreferenceStore.theme.collectAsStateWithLifecycle()
VaultDropTheme(theme = theme) {
VaultDropApp()
}
}
@@ -5,4 +5,5 @@ object PrefKeys {
const val DEVICE_USER_ID = "device_user_id"
const val ACTIVE_USER_ID = "active_user_id"
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
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.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.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.domain.ThemePreference
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab
@@ -20,7 +34,15 @@ import com.vaultdrop.mobile.ui.navigation.NavTab
fun SettingsScreen(
selectedTab: NavTab,
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(
topBar = {
TopAppBar(title = { Text(stringResource(R.string.settings)) })
@@ -29,14 +51,62 @@ fun SettingsScreen(
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
},
) { padding ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
Text(
text = stringResource(R.string.settings),
textAlign = TextAlign.Center,
HorizontalDivider()
SettingsRow(
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.lightColorScheme
import androidx.compose.runtime.Composable
import com.vaultdrop.mobile.domain.ThemePreference
private val LightColors = lightColorScheme(
primary = BrandBlue,
@@ -17,6 +18,19 @@ private val DarkColors = darkColorScheme(
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
fun VaultDropTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
@@ -27,6 +27,9 @@
<string name="nav_search">Search</string>
<string name="nav_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 -->
<string name="search">Search</string>
@@ -28,6 +28,9 @@
<string name="nav_search">Recherche</string>
<string name="nav_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 -->
<string name="search">Recherche</string>