feat: implement splash screen with user preference toggle, update app branding with SafeBite logo across UI

- Add `splashScreenEnabled` preference to UserPreferences with default true
- Implement `SplashScreen` composable with animated logo, scale/fade transitions, and gradient background
- Add splash screen toggle in settings UI under "Interface" section
- Update navigation graph to conditionally show splash before dashboard when enabled
- Replace app launcher icons with SafeBite logo drawable
This commit is contained in:
Bruno Charest 2026-04-27 21:54:13 -04:00
parent 0e75c1888b
commit ec1c8e6940
32 changed files with 305 additions and 122 deletions

View File

@ -19,8 +19,8 @@
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:icon="@drawable/safebite_logo"
android:roundIcon="@drawable/safebite_logo"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.SafeBite"

View File

@ -26,6 +26,7 @@ object UserPreferencesKeys {
val ONBOARDING_DONE = booleanPreferencesKey("onboarding_done")
val ACTIVE_PROFILE_IDS = stringSetPreferencesKey("active_profile_ids")
val HEALTH_STRICTNESS = stringPreferencesKey("health_strictness")
val SPLASH_SCREEN_ENABLED = booleanPreferencesKey("splash_screen_enabled")
}
class UserPreferences(private val dataStore: DataStore<Preferences>) {
@ -61,6 +62,10 @@ class UserPreferences(private val dataStore: DataStore<Preferences>) {
.getOrDefault(HealthStrictness.NORMAL)
}
val splashScreenEnabled: Flow<Boolean> = dataStore.data.map {
it[UserPreferencesKeys.SPLASH_SCREEN_ENABLED] ?: true
}
suspend fun setAppLanguage(value: AppLanguage) {
dataStore.edit { it[UserPreferencesKeys.APP_LANGUAGE] = value.name }
}
@ -94,4 +99,8 @@ class UserPreferences(private val dataStore: DataStore<Preferences>) {
suspend fun setHealthStrictness(value: HealthStrictness) {
dataStore.edit { it[UserPreferencesKeys.HEALTH_STRICTNESS] = value.name }
}
suspend fun setSplashScreenEnabled(value: Boolean) {
dataStore.edit { it[UserPreferencesKeys.SPLASH_SCREEN_ENABLED] = value }
}
}

View File

@ -20,6 +20,7 @@ class SettingsRepositoryImpl @Inject constructor(
override val theme = prefs.theme
override val onboardingCompleted = prefs.onboardingCompleted
override val healthStrictness = prefs.healthStrictness
override val splashScreenEnabled = prefs.splashScreenEnabled
override suspend fun setAppLanguage(value: AppLanguage) = prefs.setAppLanguage(value)
override suspend fun setDetectionLanguage(value: DetectionLanguage) = prefs.setDetectionLanguage(value)
@ -28,4 +29,5 @@ class SettingsRepositoryImpl @Inject constructor(
override suspend fun setTheme(value: ThemePref) = prefs.setTheme(value)
override suspend fun setOnboardingCompleted(value: Boolean) = prefs.setOnboardingCompleted(value)
override suspend fun setHealthStrictness(value: HealthStrictness) = prefs.setHealthStrictness(value)
override suspend fun setSplashScreenEnabled(enabled: Boolean) = prefs.setSplashScreenEnabled(enabled)
}

View File

@ -57,6 +57,7 @@ interface SettingsRepository {
val theme: Flow<ThemePref>
val onboardingCompleted: Flow<Boolean>
val healthStrictness: Flow<HealthStrictness>
val splashScreenEnabled: Flow<Boolean>
suspend fun setAppLanguage(value: AppLanguage)
suspend fun setDetectionLanguage(value: DetectionLanguage)
@ -65,6 +66,7 @@ interface SettingsRepository {
suspend fun setTheme(value: ThemePref)
suspend fun setOnboardingCompleted(value: Boolean)
suspend fun setHealthStrictness(value: HealthStrictness)
suspend fun setSplashScreenEnabled(enabled: Boolean)
}
// =============================================================================

View File

@ -21,14 +21,28 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
data class RootUi(val onboardingDone: Boolean = false, val theme: ThemePref = ThemePref.SYSTEM, val ready: Boolean = false)
data class RootUi(
val onboardingDone: Boolean = false,
val theme: ThemePref = ThemePref.SYSTEM,
val showSplash: Boolean = false,
val ready: Boolean = false
)
@HiltViewModel
class RootViewModel @Inject constructor(
settings: SettingsRepository
) : ViewModel() {
val state: StateFlow<RootUi> = combine(settings.onboardingCompleted, settings.theme) { done, theme ->
RootUi(onboardingDone = done, theme = theme, ready = true)
val state: StateFlow<RootUi> = combine(
settings.onboardingCompleted,
settings.theme,
settings.splashScreenEnabled
) { done, theme, splashEnabled ->
RootUi(
onboardingDone = done,
theme = theme,
showSplash = splashEnabled && done,
ready = true
)
}.stateIn(viewModelScope, SharingStarted.Eagerly, RootUi())
}
@ -49,7 +63,10 @@ class MainActivity : ComponentActivity() {
}
SafeBiteTheme(darkTheme = dark) {
if (ui.ready) {
SafeBiteNavGraph(onboardingCompleted = ui.onboardingDone)
SafeBiteNavGraph(
onboardingCompleted = ui.onboardingDone,
showSplash = ui.showSplash
)
}
}
}

View File

@ -24,6 +24,7 @@ import com.safebite.app.presentation.screen.profile.ProfileListScreen
import com.safebite.app.presentation.screen.result.ResultScreen
import com.safebite.app.presentation.screen.scanner.ScannerScreen
import com.safebite.app.presentation.screen.settings.SettingsScreen
import com.safebite.app.presentation.screen.splash.SplashScreen
/**
* Graph de navigation principal de l'application SafeBite.
@ -34,9 +35,13 @@ import com.safebite.app.presentation.screen.settings.SettingsScreen
* - Écrans de navigation : Scanner, Result, OCR, Settings, etc.
*/
@Composable
fun SafeBiteNavGraph(onboardingCompleted: Boolean) {
fun SafeBiteNavGraph(onboardingCompleted: Boolean, showSplash: Boolean = false) {
val navController = rememberNavController()
val startDestination = if (onboardingCompleted) Screen.Dashboard.route else Screen.Onboarding.route
val startDestination = when {
showSplash -> Screen.Splash.route
onboardingCompleted -> Screen.Dashboard.route
else -> Screen.Onboarding.route
}
val enterAnim = fadeIn(animationSpec = tween(250)) +
slideInHorizontally(animationSpec = tween(250)) { it / 24 }
@ -53,6 +58,17 @@ fun SafeBiteNavGraph(onboardingCompleted: Boolean) {
popEnterTransition = { popEnterAnim },
popExitTransition = { popExitAnim },
) {
// ── Splash ──
composable(Screen.Splash.route) {
SplashScreen(
onFinished = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Splash.route) { inclusive = true }
}
}
)
}
// ── Onboarding ──
composable(Screen.Onboarding.route) {
OnboardingScreen(onFinished = {

View File

@ -39,6 +39,7 @@ sealed class Screen(val route: String) {
}
data object Onboarding : Screen("onboarding")
data object Settings : Screen("settings")
data object Splash : Screen("splash")
// ── Sous-écrans ──
data object ProfileList : Screen("profiles")

View File

@ -8,9 +8,13 @@ import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Settings
@ -28,7 +32,9 @@ 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.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination
@ -81,7 +87,17 @@ fun MainScreen(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(id = R.drawable.safebite_logo),
contentDescription = null,
modifier = Modifier.size(32.dp)
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.app_name))
}
},
actions = {
IconButton(onClick = onOpenSettings) {
Icon(

View File

@ -1,5 +1,6 @@
package com.safebite.app.presentation.screen.onboarding
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -31,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@ -129,8 +131,12 @@ private fun WelcomeStep(onNext: () -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("🛡️", style = MaterialTheme.typography.displayLarge)
Spacer(Modifier.height(16.dp))
Image(
painter = painterResource(id = R.drawable.safebite_logo),
contentDescription = null,
modifier = Modifier.size(120.dp)
)
Spacer(Modifier.height(24.dp))
Text(
stringResource(R.string.onboarding_welcome_title),
style = MaterialTheme.typography.headlineLarge,

View File

@ -78,6 +78,7 @@ fun SettingsScreen(
Column(verticalArrangement = Arrangement.spacedBy(dimens.spacingSm)) {
ToggleRow(stringResource(R.string.settings_haptics), ui.haptics, viewModel::setHaptics)
ToggleRow(stringResource(R.string.settings_sound), ui.sound, viewModel::setSound)
ToggleRow(stringResource(R.string.settings_splash_screen), ui.splashScreenEnabled, viewModel::setSplashScreenEnabled)
}
}

View File

@ -23,7 +23,8 @@ data class SettingsUi(
val haptics: Boolean = true,
val sound: Boolean = true,
val theme: ThemePref = ThemePref.SYSTEM,
val healthStrictness: HealthStrictness = HealthStrictness.NORMAL
val healthStrictness: HealthStrictness = HealthStrictness.NORMAL,
val splashScreenEnabled: Boolean = true
)
@HiltViewModel
@ -43,8 +44,12 @@ class SettingsViewModel @Inject constructor(
SettingsUi(lang, detection, haptics, sound, theme)
}
val state: StateFlow<SettingsUi> = combine(coreFlow, settings.healthStrictness) { core, strict ->
core.copy(healthStrictness = strict)
val state: StateFlow<SettingsUi> = combine(
coreFlow,
settings.healthStrictness,
settings.splashScreenEnabled
) { core, strict, splash ->
core.copy(healthStrictness = strict, splashScreenEnabled = splash)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SettingsUi())
fun setAppLanguage(v: AppLanguage) = viewModelScope.launch { settings.setAppLanguage(v) }
@ -53,6 +58,7 @@ class SettingsViewModel @Inject constructor(
fun setSound(v: Boolean) = viewModelScope.launch { settings.setSound(v) }
fun setTheme(v: ThemePref) = viewModelScope.launch { settings.setTheme(v) }
fun setHealthStrictness(v: HealthStrictness) = viewModelScope.launch { settings.setHealthStrictness(v) }
fun setSplashScreenEnabled(v: Boolean) = viewModelScope.launch { settings.setSplashScreenEnabled(v) }
fun clearCache() = viewModelScope.launch { productRepo.clearCache() }
fun clearHistory() = viewModelScope.launch { historyRepo.clear() }
}

View File

@ -0,0 +1,83 @@
package com.safebite.app.presentation.screen.splash
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.safebite.app.R
import com.safebite.app.presentation.theme.ShieldGradient
@Composable
fun SplashScreen(
onFinished: () -> Unit,
durationMillis: Int = 2500
) {
val scale = remember { Animatable(0.6f) }
val alpha = remember { Animatable(0f) }
LaunchedEffect(Unit) {
scale.animateTo(1f, animationSpec = tween(durationMillis = 800))
alpha.animateTo(1f, animationSpec = tween(durationMillis = 600))
kotlinx.coroutines.delay(durationMillis.toLong())
onFinished()
}
Box(
modifier = Modifier
.fillMaxSize()
.background(ShieldGradient),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(24.dp)
) {
Image(
painter = painterResource(id = R.drawable.safebite_logo),
contentDescription = null,
modifier = Modifier
.size(160.dp)
.scale(scale.value)
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold,
color = androidx.compose.ui.graphics.Color.White
),
modifier = Modifier.alpha(alpha.value)
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(R.string.onboarding_welcome_subtitle),
style = MaterialTheme.typography.bodyLarge.copy(
color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.9f)
),
modifier = Modifier.alpha(alpha.value)
)
}
}
}

View File

@ -11,110 +11,110 @@ import androidx.compose.ui.graphics.Color
// Ces couleurs sont indépendantes du thème M3 pour cohérence marque.
object SemanticColors {
// Light mode
val Safe = Color(0xFF2ECC71) // Vert sécurité
val SafeContainer = Color(0xFFE8F8F5) // Fond très clair
val Safe = Color(0xFF43A047) // Vert sécurité
val SafeContainer = Color(0xFFE8F5E9) // Fond très clair
val OnSafe = Color(0xFFFFFFFF)
val OnSafeContainer = Color(0xFF1A3A2A)
val Warning = Color(0xFFE67E22) // Orange attention
val WarningContainer = Color(0xFFFEF5E7)
val Warning = Color(0xFFFFA000) // Orange attention
val WarningContainer = Color(0xFFFFF3E0)
val OnWarning = Color(0xFFFFFFFF)
val OnWarningContainer = Color(0xFF3A2A1A)
val OnWarningContainer = Color(0xFF4A2800)
val Danger = Color(0xFFE74C3C) // Rouge danger
val DangerContainer = Color(0xFFFDEDEC)
val Danger = Color(0xFFD32F2F) // Rouge danger
val DangerContainer = Color(0xFFFFEBEE)
val OnDanger = Color(0xFFFFFFFF)
val OnDangerContainer = Color(0xFF3A1A1A)
val OnDangerContainer = Color(0xFF5C0B0B)
// Dark mode (mêmes couleurs sémantiques, containers adaptés)
val SafeDark = Color(0xFF2ECC71)
val SafeContainerDark = Color(0xFF1A3A2A)
val WarningDark = Color(0xFFE67E22)
val WarningContainerDark = Color(0xFF3A2A1A)
val DangerDark = Color(0xFFE74C3C)
val DangerContainerDark = Color(0xFF3A1A1A)
val SafeDark = Color(0xFF81C784)
val SafeContainerDark = Color(0xFF1B5E20)
val WarningDark = Color(0xFFFFB74D)
val WarningContainerDark = Color(0xFF4A2800)
val DangerDark = Color(0xFFEF9A9A)
val DangerContainerDark = Color(0xFF5C0B0B)
}
// ---- NEUTRES (spec UX §2.1) ------------------------------------------------
object NeutralColors {
val Background = Color(0xFFF5F5F0) // Gris chaud (réduit fatigue oculaire)
val Background = Color(0xFFF1F8E9) // Fond principal light
val Surface = Color(0xFFFFFFFF) // Blanc pur pour cartes
val TextPrimary = Color(0xFF2D3436) // Noir doux (pas #000)
val TextSecondary = Color(0xFF636E72) // Gris moyen
val Separator = Color(0xFFDFE6E9) // Gris clair
val TextPrimary = Color(0xFF212121) // Texte principal
val TextSecondary = Color(0xFF757575) // Texte secondaire
val Separator = Color(0xFFBDBDBD) // Séparateurs
}
// ---- Brand anchors (Material 3) --------------------------------------------
val BrandIndigo = Color(0xFF1A237E)
val BrandIndigoLight = Color(0xFFBAC3FF)
val BrandTeal = Color(0xFF00897B)
val BrandTealLight = Color(0xFF4DB6AC)
val BrandPrimary = Color(0xFF1B7A2B)
val BrandPrimaryDark = Color(0xFF0D5E1A)
val BrandPrimaryLight = Color(0xFF4CAF50)
val BrandSecondary = Color(0xFF2E7D32)
// ---- Light scheme ---------------------------------------------------------
val LightPrimary = Color(0xFF1A237E)
val LightPrimary = Color(0xFF1B7A2B)
val LightOnPrimary = Color(0xFFFFFFFF)
val LightPrimaryContainer = Color(0xFFDDE1FF)
val LightOnPrimaryContainer = Color(0xFF001159)
val LightPrimaryContainer = Color(0xFFA5D6A7)
val LightOnPrimaryContainer = Color(0xFF0D3B12)
val LightSecondary = Color(0xFF00897B)
val LightSecondary = Color(0xFF2E7D32)
val LightOnSecondary = Color(0xFFFFFFFF)
val LightSecondaryContainer = Color(0xFFB2DFDB)
val LightOnSecondaryContainer = Color(0xFF00251F)
val LightSecondaryContainer = Color(0xFFC8E6C9)
val LightOnSecondaryContainer = Color(0xFF1B5E20)
val LightTertiary = Color(0xFF7B4E9E)
val LightTertiary = Color(0xFF00796B)
val LightOnTertiary = Color(0xFFFFFFFF)
val LightTertiaryContainer = Color(0xFFF0DBFF)
val LightOnTertiaryContainer = Color(0xFF2B0A45)
val LightTertiaryContainer = Color(0xFFB2DFDB)
val LightOnTertiaryContainer = Color(0xFF004D40)
val LightError = Color(0xFFBA1A1A)
val LightError = Color(0xFFD32F2F)
val LightOnError = Color(0xFFFFFFFF)
val LightErrorContainer = Color(0xFFFFDAD6)
val LightOnErrorContainer = Color(0xFF410002)
val LightErrorContainer = Color(0xFFFFCDD2)
val LightOnErrorContainer = Color(0xFF5C0B0B)
val LightBackground = NeutralColors.Background // #F5F5F0
val LightOnBackground = NeutralColors.TextPrimary // #2D3436
val LightBackground = NeutralColors.Background // #F1F8E9
val LightOnBackground = NeutralColors.TextPrimary // #212121
val LightSurface = NeutralColors.Surface // #FFFFFF
val LightOnSurface = NeutralColors.TextPrimary // #2D3436
val LightSurfaceVariant = Color(0xFFE3E2EC)
val LightOnSurface = NeutralColors.TextPrimary // #212121
val LightSurfaceVariant = Color(0xFFE8F5E9)
val LightOnSurfaceVariant = NeutralColors.TextSecondary
val LightSurfaceTint = LightPrimary
val LightOutline = NeutralColors.Separator
val LightOutlineVariant = Color(0xFFC7C6D0)
val LightOutlineVariant = Color(0xFFE0E0E0)
val LightInverseSurface = Color(0xFF2F3033)
val LightInverseOnSurface = Color(0xFFF1F0F4)
val LightInversePrimary = Color(0xFFBAC3FF)
val LightInversePrimary = Color(0xFF81C784)
val LightScrim = Color(0xFF000000)
// ---- Dark scheme (surfaces élevées M3) ------------------------------------
val DarkPrimary = Color(0xFFBAC3FF)
val DarkOnPrimary = Color(0xFF0A1A6A)
val DarkPrimaryContainer = Color(0xFF3241A0)
val DarkOnPrimaryContainer = Color(0xFFDDE1FF)
val DarkPrimary = Color(0xFF81C784)
val DarkOnPrimary = Color(0xFF0D3B12)
val DarkPrimaryContainer = Color(0xFF1B5E20)
val DarkOnPrimaryContainer = Color(0xFFA5D6A7)
val DarkSecondary = Color(0xFF4DB6AC)
val DarkOnSecondary = Color(0xFF00332C)
val DarkSecondaryContainer = Color(0xFF00695C)
val DarkOnSecondaryContainer = Color(0xFFB2DFDB)
val DarkSecondary = Color(0xFFA5D6A7)
val DarkOnSecondary = Color(0xFF1B5E20)
val DarkSecondaryContainer = Color(0xFF2E7D32)
val DarkOnSecondaryContainer = Color(0xFFC8E6C9)
val DarkTertiary = Color(0xFFE0B6FF)
val DarkOnTertiary = Color(0xFF451F6D)
val DarkTertiaryContainer = Color(0xFF5D3785)
val DarkOnTertiaryContainer = Color(0xFFF0DBFF)
val DarkTertiary = Color(0xFF4DB6AC)
val DarkOnTertiary = Color(0xFF00332C)
val DarkTertiaryContainer = Color(0xFF00695C)
val DarkOnTertiaryContainer = Color(0xFFB2DFDB)
val DarkError = Color(0xFFFFB4AB)
val DarkError = Color(0xFFEF9A9A)
val DarkOnError = Color(0xFF690005)
val DarkErrorContainer = Color(0xFF93000A)
val DarkOnErrorContainer = Color(0xFFFFDAD6)
val DarkOnErrorContainer = Color(0xFFFFCDD2)
val DarkBackground = Color(0xFF121212)
val DarkOnBackground = Color(0xFFE6E1E5)
val DarkSurface = Color(0xFF1E1E1E)
val DarkOnSurface = Color(0xFFE6E1E5)
val DarkSurfaceVariant = Color(0xFF46464F)
val DarkOnSurfaceVariant = Color(0xFFC7C6D0)
val DarkBackground = Color(0xFF1A1C1A)
val DarkOnBackground = Color(0xFFE0E0E0)
val DarkSurface = Color(0xFF2D2F2D)
val DarkOnSurface = Color(0xFFE0E0E0)
val DarkSurfaceVariant = Color(0xFF3A3F3A)
val DarkOnSurfaceVariant = Color(0xFFBDBDBD)
val DarkSurfaceTint = DarkPrimary
val DarkOutline = Color(0xFF90909A)
@ -122,10 +122,21 @@ val DarkOutlineVariant = Color(0xFF46464F)
val DarkInverseSurface = Color(0xFFE6E1E5)
val DarkInverseOnSurface = Color(0xFF2F3033)
val DarkInversePrimary = Color(0xFF1A237E)
val DarkInversePrimary = Color(0xFF1B7A2B)
val DarkScrim = Color(0xFF000000)
// ---- Dégradé signature (rappel du fond du logo bouclier) --------------------
val ShieldGradient = androidx.compose.ui.graphics.Brush.linearGradient(
colors = listOf(
Color(0xFF4CAF50), // Vert clair (haut-gauche)
Color(0xFF1B7A2B), // Vert moyen
Color(0xFF0D5E1A) // Vert foncé (bas-droite)
),
start = androidx.compose.ui.geometry.Offset(0f, 0f),
end = androidx.compose.ui.geometry.Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
)
// ---- Legacy aliases (backward compat pour code existant) -------------------
@Deprecated("Use SemanticColors.Safe", ReplaceWith("SemanticColors.Safe"))
val StatusSafe get() = SemanticColors.Safe

View File

@ -8,128 +8,126 @@ import androidx.compose.ui.unit.sp
/**
* Typographie Material 3 complète 15 styles avec lineHeight et letterSpacing
* standardisés. Utilise la sans-serif système (Roboto sur Android) pour éviter
* standardisés. Alignée sur le guide design SafeBite (Poppins titres, Inter corps).
* Utilise la sans-serif système (Roboto sur Android) pour éviter
* toute dépendance réseau / asset.
*/
private val DisplayFamily = FontFamily.SansSerif
private val HeadlineFamily = FontFamily.SansSerif
private val TitleFamily = FontFamily.SansSerif
private val BodyFamily = FontFamily.SansSerif
private val LabelFamily = FontFamily.SansSerif
private val PoppinsFamily = FontFamily.SansSerif
private val InterFamily = FontFamily.SansSerif
val SafeBiteTypography = Typography(
// Display — pour les titres héros (onboarding, banner)
displayLarge = TextStyle(
fontFamily = DisplayFamily,
fontFamily = PoppinsFamily,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
displayMedium = TextStyle(
fontFamily = DisplayFamily,
fontFamily = PoppinsFamily,
fontWeight = FontWeight.Bold,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontFamily = DisplayFamily,
fontFamily = PoppinsFamily,
fontWeight = FontWeight.Bold,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
// Headline — sections majeures
// Headline — sections majeures (Poppins Bold/SemiBold)
headlineLarge = TextStyle(
fontFamily = HeadlineFamily,
fontFamily = PoppinsFamily,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
letterSpacing = (-0.5).sp
),
headlineMedium = TextStyle(
fontFamily = HeadlineFamily,
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontFamily = HeadlineFamily,
fontFamily = PoppinsFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
// Title — titres d'écran, de cartes
titleLarge = TextStyle(
fontFamily = TitleFamily,
headlineSmall = TextStyle(
fontFamily = PoppinsFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
lineHeight = 28.sp,
letterSpacing = 0.15.sp
),
// Title — titres d'écran, de cartes (Inter Medium)
titleLarge = TextStyle(
fontFamily = InterFamily,
fontWeight = FontWeight.Medium,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontFamily = TitleFamily,
fontWeight = FontWeight.SemiBold,
fontFamily = InterFamily,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontFamily = TitleFamily,
fontFamily = InterFamily,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
// Body — textes courants
// Body — textes courants (Inter Regular)
bodyLarge = TextStyle(
fontFamily = BodyFamily,
fontFamily = InterFamily,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = BodyFamily,
fontFamily = InterFamily,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontFamily = BodyFamily,
fontFamily = InterFamily,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
// Label — boutons, chips, tags
// Label — boutons, chips, tags (Poppins SemiBold pour boutons, Inter Medium pour petits)
labelLarge = TextStyle(
fontFamily = LabelFamily,
fontWeight = FontWeight.Medium,
fontFamily = PoppinsFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
letterSpacing = 1.25.sp
),
labelMedium = TextStyle(
fontFamily = LabelFamily,
fontFamily = InterFamily,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontFamily = LabelFamily,
fontFamily = InterFamily,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
fontSize = 10.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
letterSpacing = 1.5.sp
)
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -13,7 +13,6 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"

View File

@ -13,9 +13,8 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/safebite_logo_background"/>
<foreground android:drawable="@drawable/safebite_logo_foreground"/>
<monochrome android:drawable="@drawable/safebite_logo_foreground"/>
<foreground android:drawable="@drawable/safebite_logo"/>
<monochrome android:drawable="@drawable/safebite_logo"/>
</adaptive-icon>

View File

@ -13,7 +13,6 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/safebite_logo_background"/>
<foreground android:drawable="@drawable/safebite_logo_foreground"/>

View File

@ -122,6 +122,7 @@
<string name="settings_theme_system">System</string>
<string name="settings_clear_cache">Clear product cache</string>
<string name="settings_clear_history">Clear history</string>
<string name="settings_splash_screen">Splash screen</string>
<string name="settings_about">About</string>
<string name="settings_version">Version %1$s</string>
<string name="settings_off_attribution">Data provided by Open Food Facts</string>

View File

@ -190,6 +190,7 @@
<string name="settings_theme_system">Système</string>
<string name="settings_clear_cache">Vider le cache des produits</string>
<string name="settings_clear_history">Vider l\'historique</string>
<string name="settings_splash_screen">Écran de démarrage (splash)</string>
<string name="settings_about">À propos</string>
<string name="settings_version">Version %1$s</string>
<string name="settings_off_attribution">Données fournies par Open Food Facts</string>

View File

@ -0,0 +1,13 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21

View File

@ -11,6 +11,9 @@ pluginManagement {
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

View File

@ -1,4 +1,4 @@
MAJOR=1
MINOR=9
PATCH=2
CODE=13
MINOR=14
PATCH=0
CODE=18