From c06053c5dc7a7cd7d7e8229de49b621654468964 Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Mon, 22 Jun 2026 17:40:37 +0300 Subject: [PATCH] Add theme color customization on Settings --- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 28 ++++- .../commonMain/kotlin/mg/dot/feufaro/App.kt | 2 +- .../mg/dot/feufaro/DisplayConfigManager.kt | 46 +++++--- .../mg/dot/feufaro/config/DisplayConfig.kt | 4 + .../kotlin/mg/dot/feufaro/ui/Settings.kt | 96 +++++++++++++++-- .../kotlin/mg/dot/feufaro/ui/Theme.kt | 101 ++++++++++++------ 6 files changed, 219 insertions(+), 58 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 1293ff6..8b8da24 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -1,15 +1,18 @@ package mg.dot.feufaro.midi import SharedScreenModel +import com.russhwolf.settings.Settings import kotlinx.coroutines.* import org.billthefarmer.mididriver.MidiDriver +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import java.io.File import java.io.RandomAccessFile actual class FMediaPlayer actual constructor( private val filename: String, private val onFinished: () -> Unit -) { +): KoinComponent { private data class MidiEvent( val tickAbsolute: Long, val type: Int, @@ -137,6 +140,7 @@ actual class FMediaPlayer actual constructor( private var syncJob: Job? = null private var dynamicJob: Job? = null private var boundModel: SharedScreenModel? = null + private val settings: Settings by inject() private data class NavigationStep( val marker: String, @@ -162,6 +166,7 @@ actual class FMediaPlayer actual constructor( init { midiDriver.start() + loadVoiceVolumes() val file = File(filename) if (file.exists()) { try { @@ -698,8 +703,27 @@ actual class FMediaPlayer actual constructor( actual fun clearLoop() { isLoopingAB = false; pointA = -1L; pointB = -1L } actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB) actual fun toggleVoice(index: Int) { applyVoiceStates() } + private fun saveVoicesVolumes() { + val data = voiceVolumes.joinToString(",") + settings.putString("voices_volumes", data) + } + + private fun loadVoiceVolumes() { + val data = settings.getString("voices_volumes", "127,127,127,127") + val volumesArray = data.split(",") + + if (volumesArray.size == 4) { + for (i in 0 until 4) { + voiceVolumes[i] = volumesArray[i].toFloatOrNull() ?: 127f + } + } + } actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) { - if (voiceIndex in 0..3) { voiceVolumes[voiceIndex] = newVolume; applyVoiceStates() } + if (voiceIndex in 0..3) { + voiceVolumes[voiceIndex] = newVolume; + saveVoicesVolumes() + applyVoiceStates() + } } actual fun getVoiceVolumes(): List = voiceVolumes.toList() actual fun changeInstru(noInstru: Int) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt index bacd134..25ad579 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt @@ -24,7 +24,7 @@ fun App() { val displayConfigManager = koinInject() val currentDisplayConfig by displayConfigManager.displayConfig.collectAsState() - FeufaroTheme(themeModeSelected = currentDisplayConfig.themeMode, userFontSize = currentDisplayConfig.fontSize) { + FeufaroTheme(config = currentDisplayConfig) { Navigator(screen = ScreenSolfa) { CurrentScreen() } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/DisplayConfigManager.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/DisplayConfigManager.kt index 60dc138..fa0bbd8 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/DisplayConfigManager.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/DisplayConfigManager.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.json.Json import mg.dot.feufaro.config.AppConfigJson import mg.dot.feufaro.config.DisplayConfig import java.util.prefs.Preferences +import com.russhwolf.settings.Settings @Serializable data class AppConfigJson( @@ -35,10 +36,10 @@ fun AppConfigJson.toDisplayConfig(defaultConfig: DisplayConfig): DisplayConfig { ) } class DisplayConfigManager( - private val fileRepository: FileRepository + private val fileRepository: FileRepository, + private val settings: Settings ) { - private val prefs = Preferences.userRoot().node("mg.dot.feufaro") private val _displayConfig = MutableStateFlow(DisplayConfig()) val displayConfig: StateFlow = _displayConfig.asStateFlow() private val configScope = CoroutineScope(Dispatchers.Default) @@ -52,15 +53,35 @@ class DisplayConfigManager( loadConfigFromFile("assets://config.json") val jsonConfig = _displayConfig.value - val finalTheme = prefs.get("themeMode", jsonConfig.themeMode) - val finalFontSize = prefs.getFloat("fontSize", jsonConfig.fontSize) + val finalTheme = settings.getString("themeMode", jsonConfig.themeMode) + val finalFontSize = settings.getFloat("fontSize", jsonConfig.fontSize) + + val pBlue = settings.getString("primaryBlueHex", jsonConfig.primaryBlueHex) + val aYellow = settings.getString("accentYellowHex", jsonConfig.accentYellowHex) + val aGreen = settings.getString("accentGreenHex", jsonConfig.accentGreenHex) + val aPink = settings.getString("accentPinkHex", jsonConfig.accentPinkHex) _displayConfig.value = jsonConfig.copy( themeMode = finalTheme, - fontSize = finalFontSize + fontSize = finalFontSize, + primaryBlueHex = pBlue, + accentYellowHex = aYellow, + accentGreenHex = aGreen, + accentPinkHex = aPink ) } } + fun updateCustomColor(key: String, hexValue: String) { + settings.putString(key, hexValue) + + _displayConfig.value = when(key) { + "primaryBlueHex" -> _displayConfig.value.copy(primaryBlueHex = hexValue) + "accentYellowHex" -> _displayConfig.value.copy(accentYellowHex = hexValue) + "accentGreenHex" -> _displayConfig.value.copy(accentGreenHex = hexValue) + "accentPinkHex" -> _displayConfig.value.copy(accentPinkHex = hexValue) + else -> _displayConfig.value + } + } suspend fun loadConfigFromFile(filePath: String) { try { val jsonString = fileRepository.readFileContent(filePath) @@ -74,22 +95,23 @@ class DisplayConfigManager( } fun updateThemeMode(themeMode: String) { - prefs.put("themeMode", themeMode) - prefs.flush() + settings.putString("themeMode", themeMode) _displayConfig.value = _displayConfig.value.copy(themeMode = themeMode) } fun updateFontSize(fontSize: Float) { - prefs.putFloat("fontSize", fontSize) - prefs.flush() + settings.putFloat("fontSize", fontSize) _displayConfig.value = _displayConfig.value.copy(fontSize = fontSize) } fun resetToDefault() { try { - prefs.remove("themeMode") - prefs.remove("fontSize") - prefs.flush() + settings.remove("themeMode") + settings.remove("fontSize") + settings.remove("primaryBlueHex") + settings.remove("accentYellowHex") + settings.remove("accentGreenHex") + settings.remove("accentPinkHex") loadInitialConfig() } catch (e: Exception) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/config/DisplayConfig.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/config/DisplayConfig.kt index b36558b..32d4c3e 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/config/DisplayConfig.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/config/DisplayConfig.kt @@ -7,6 +7,10 @@ data class DisplayConfig ( val themeMode: String = "LIGHT", val fontSize: Float = 14f, val playlist: List = emptyList(), + val primaryBlueHex: String = "#5061FF", + val accentYellowHex: String = "#FFFFFA54", + val accentGreenHex: String = "#3EFF96", + val accentPinkHex: String = "#FFFF4FA8", val buttonContainerColorHex: String = "#737EFC", val buttonContentColorHex: String = "#FFFFFF", val buttonDisabledContainerColorHex: String = "#CCCCCC", diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt index c9f78ef..a0b827e 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt @@ -32,7 +32,7 @@ import mg.dot.feufaro.getPlatform import mg.dot.feufaro.midi.Dynamic import org.koin.compose.koinInject -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun Settings( sharedScreenModel: SharedScreenModel, @@ -146,12 +146,11 @@ fun Settings( HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = "Taille de la police", @@ -160,8 +159,9 @@ fun Settings( ) Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(15.dp) + horizontalArrangement = Arrangement.Center ) { IconButton( onClick = { @@ -169,11 +169,13 @@ fun Settings( displayConfigManager.updateFontSize(currentDisplayConfig.fontSize - 0.5f) } }, - modifier = Modifier.size(25.dp).background(MaterialTheme.colorScheme.primary, CircleShape) + modifier = Modifier.size(32.dp).background(MaterialTheme.colorScheme.primary, CircleShape) ) { Text("-", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleLarge, color = Color.White) } + Spacer(modifier = Modifier.width(16.dp)) + OutlinedTextField( value = tempFontSize.toString(), onValueChange = {}, @@ -183,19 +185,97 @@ fun Settings( shape = RoundedCornerShape(12.dp) ) + Spacer(modifier = Modifier.width(16.dp)) + IconButton( onClick = { if (currentDisplayConfig.fontSize < 30f) { displayConfigManager.updateFontSize(currentDisplayConfig.fontSize + 0.5f) } }, - modifier = Modifier.size(25.dp).background(MaterialTheme.colorScheme.primary, CircleShape) + modifier = Modifier.size(32.dp).background(MaterialTheme.colorScheme.primary, CircleShape) ) { Text("+", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleLarge, color = Color.White) } } } + HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Personnalisation des couleurs", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold + ) + + val colorPickers = listOf( + Triple("Couleur Primaire", "primaryBlueHex", currentDisplayConfig.primaryBlueHex), + Triple("Couleur Secondaire", "accentGreenHex", currentDisplayConfig.accentGreenHex), + Triple("Couleur Tertiaire", "accentYellowHex", currentDisplayConfig.accentYellowHex), + Triple("Autre couleur", "accentPinkHex", currentDisplayConfig.accentPinkHex) + ) + + val availableColors = ThemeDefaults.availableColors + + colorPickers.forEach { (label, configKey, currentHex) -> + var showLocalPicker by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(label, style = MaterialTheme.typography.bodyMedium) + + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(currentHex.toColorOrDefault(Color.Gray)) + .border(2.dp, MaterialTheme.colorScheme.outline, CircleShape) + .clickable { showLocalPicker = !showLocalPicker } + ) + } + + AnimatedVisibility(visible = showLocalPicker) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + availableColors.forEach { colorHex -> + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(colorHex.toColorOrDefault(Color.Gray)) + .border( + width = if (currentHex.uppercase() == colorHex.uppercase()) 2.5.dp else 0.dp, + color = MaterialTheme.colorScheme.onSurface, + shape = CircleShape + ) + .clickable { + displayConfigManager.updateCustomColor(configKey, colorHex) + } + ) + } + } + } + } + } + } + Spacer(modifier = Modifier.weight(1f)) OutlinedButton( onClick = { displayConfigManager.resetToDefault() }, diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Theme.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Theme.kt index eac852b..c0b3703 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Theme.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Theme.kt @@ -1,61 +1,98 @@ package mg.dot.feufaro.ui import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Typography -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp +import mg.dot.feufaro.config.DisplayConfig +fun String.toColorOrDefault(defaultColor: Color): Color { + return try { + val cleaned = this.removePrefix("#").trim() + + when (cleaned.length) { + 6 -> { + val longValue = "FF$cleaned".toLong(16) + Color(longValue) + } + 8 -> { + val longValue = cleaned.toLong(16) + Color(longValue) + } + else -> defaultColor + } + } catch (e: Exception) { + defaultColor + } +} object ThemeDefaults { - val PrimaryBlue = Color(0xFF5061FF) - val AccentYellow = Color(0xFFFFFA54) - val AccentGreen = Color(0xFF3EFF96) - val AccentPink = Color(0xFFFF4FA8) + val availableColors = listOf( + "#5061FF", // Bleu (défaut) + "#FFD662", // Jaune (défaut) + "#3EFF96", // Vert (défaut) + "#FF4FA8", // Rose (défaut) + "#FF1744", // Rouge + "#FF8C42", // Orange + "#FFC857", // Or + "#6BCB77", // Vert nature + "#00C2A8", // Turquoise + "#4D96FF", // Bleu ciel + "#422057", // Violet + "#D65DB1", // Magenta + "#7B61FF", // Lavande + "#00B8D9", // Cyan + "#009688", // Sarcelle + "#8BC34A", // Vert lime + "#795548", // Marron + "#607D8B", // Bleu gris + "#E91E63", // Fuchsia + "#1A1A1A", // Noir profond + ) val LightBackground = Color(0xFFF9F9FB) val DarkBackground = Color(0xFF121214) - fun getSelectedThemeColors(themeModeStr: String): Pair { - return when (themeModeStr.uppercase()) { - "SYSTEM", "DARK", "LIGHT" -> Pair( + fun getSelectedThemeColors(config: DisplayConfig): Pair { + val primaryBlue = config.primaryBlueHex.toColorOrDefault(Color(0xFF5061FF)) + val accentYellow = config.accentYellowHex.toColorOrDefault(Color(0xFFFFD662)) + val accentGreen = config.accentGreenHex.toColorOrDefault(Color(0xFF3EFF96)) + val accentPink = config.accentPinkHex.toColorOrDefault(Color(0xFFFF4FA8)) + + return Pair( lightColorScheme( - primary = PrimaryBlue.copy(alpha = 0.85f), + primary = primaryBlue.copy(alpha = 0.85f), onPrimary = LightBackground, - secondary = AccentGreen.copy(alpha = 0.85f), + secondary = accentGreen.copy(alpha = 0.85f), onSecondary = DarkBackground, - secondaryContainer = AccentGreen.copy(alpha = 0.85f), + secondaryContainer = accentGreen.copy(alpha = 0.85f), onSecondaryContainer = DarkBackground, - tertiary = AccentYellow.copy(alpha = 0.85f), + tertiary = accentYellow.copy(alpha = 0.85f), onTertiary = DarkBackground, - error = AccentPink.copy(alpha = 0.85f), + error = accentPink.copy(alpha = 0.85f), onError = LightBackground, background = LightBackground, surface = LightBackground ), darkColorScheme( - primary = PrimaryBlue, + primary = primaryBlue, onPrimary = LightBackground, - secondary = AccentGreen, + secondary = accentGreen, onSecondary = LightBackground, - secondaryContainer = AccentGreen, + secondaryContainer = accentGreen, onSecondaryContainer = DarkBackground, - tertiary = AccentYellow, + tertiary = accentYellow, onTertiary = DarkBackground, - error = AccentPink, + error = accentPink, onError = LightBackground, background = DarkBackground, @@ -63,31 +100,25 @@ object ThemeDefaults { onBackground = LightBackground, onSurface = LightBackground ) - ) - else -> Pair( - lightColorScheme(primary = PrimaryBlue, background = LightBackground), - darkColorScheme(primary = PrimaryBlue, background = DarkBackground) - ) - } + ) } } + @Composable fun FeufaroTheme( - themeModeSelected: String, - userFontSize: Float, + config: DisplayConfig, content: @Composable () -> Unit ) { - val (lightScheme, darkScheme) = ThemeDefaults.getSelectedThemeColors(themeModeSelected) - val useDarkTheme = when (themeModeSelected.uppercase()) { + val (lightScheme, darkScheme) = ThemeDefaults.getSelectedThemeColors(config) + val useDarkTheme = when (config.themeMode.uppercase()) { "DARK" -> true "LIGHT" -> false else -> isSystemInDarkTheme() } - val colorScheme = if (useDarkTheme) darkScheme else lightScheme - val baseSize = userFontSize + val baseSize = config.fontSize val customTypography = Typography( displayLarge = TextStyle(