Add theme color customization on Settings

This commit is contained in:
Hasinjato 2026-06-22 17:40:37 +03:00
parent 323dec9e78
commit c06053c5dc
6 changed files with 219 additions and 58 deletions

View file

@ -1,15 +1,18 @@
package mg.dot.feufaro.midi package mg.dot.feufaro.midi
import SharedScreenModel import SharedScreenModel
import com.russhwolf.settings.Settings
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.billthefarmer.mididriver.MidiDriver import org.billthefarmer.mididriver.MidiDriver
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File import java.io.File
import java.io.RandomAccessFile import java.io.RandomAccessFile
actual class FMediaPlayer actual constructor( actual class FMediaPlayer actual constructor(
private val filename: String, private val filename: String,
private val onFinished: () -> Unit private val onFinished: () -> Unit
) { ): KoinComponent {
private data class MidiEvent( private data class MidiEvent(
val tickAbsolute: Long, val tickAbsolute: Long,
val type: Int, val type: Int,
@ -137,6 +140,7 @@ actual class FMediaPlayer actual constructor(
private var syncJob: Job? = null private var syncJob: Job? = null
private var dynamicJob: Job? = null private var dynamicJob: Job? = null
private var boundModel: SharedScreenModel? = null private var boundModel: SharedScreenModel? = null
private val settings: Settings by inject()
private data class NavigationStep( private data class NavigationStep(
val marker: String, val marker: String,
@ -162,6 +166,7 @@ actual class FMediaPlayer actual constructor(
init { init {
midiDriver.start() midiDriver.start()
loadVoiceVolumes()
val file = File(filename) val file = File(filename)
if (file.exists()) { if (file.exists()) {
try { try {
@ -698,8 +703,27 @@ actual class FMediaPlayer actual constructor(
actual fun clearLoop() { isLoopingAB = false; pointA = -1L; pointB = -1L } actual fun clearLoop() { isLoopingAB = false; pointA = -1L; pointB = -1L }
actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB) actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB)
actual fun toggleVoice(index: Int) { applyVoiceStates() } 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) { 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<Float> = voiceVolumes.toList() actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
actual fun changeInstru(noInstru: Int) { actual fun changeInstru(noInstru: Int) {

View file

@ -24,7 +24,7 @@ fun App() {
val displayConfigManager = koinInject<DisplayConfigManager>() val displayConfigManager = koinInject<DisplayConfigManager>()
val currentDisplayConfig by displayConfigManager.displayConfig.collectAsState() val currentDisplayConfig by displayConfigManager.displayConfig.collectAsState()
FeufaroTheme(themeModeSelected = currentDisplayConfig.themeMode, userFontSize = currentDisplayConfig.fontSize) { FeufaroTheme(config = currentDisplayConfig) {
Navigator(screen = ScreenSolfa) { Navigator(screen = ScreenSolfa) {
CurrentScreen() CurrentScreen()
} }

View file

@ -11,6 +11,7 @@ import kotlinx.serialization.json.Json
import mg.dot.feufaro.config.AppConfigJson import mg.dot.feufaro.config.AppConfigJson
import mg.dot.feufaro.config.DisplayConfig import mg.dot.feufaro.config.DisplayConfig
import java.util.prefs.Preferences import java.util.prefs.Preferences
import com.russhwolf.settings.Settings
@Serializable @Serializable
data class AppConfigJson( data class AppConfigJson(
@ -35,10 +36,10 @@ fun AppConfigJson.toDisplayConfig(defaultConfig: DisplayConfig): DisplayConfig {
) )
} }
class DisplayConfigManager( 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()) private val _displayConfig = MutableStateFlow(DisplayConfig())
val displayConfig: StateFlow<DisplayConfig> = _displayConfig.asStateFlow() val displayConfig: StateFlow<DisplayConfig> = _displayConfig.asStateFlow()
private val configScope = CoroutineScope(Dispatchers.Default) private val configScope = CoroutineScope(Dispatchers.Default)
@ -52,15 +53,35 @@ class DisplayConfigManager(
loadConfigFromFile("assets://config.json") loadConfigFromFile("assets://config.json")
val jsonConfig = _displayConfig.value val jsonConfig = _displayConfig.value
val finalTheme = prefs.get("themeMode", jsonConfig.themeMode) val finalTheme = settings.getString("themeMode", jsonConfig.themeMode)
val finalFontSize = prefs.getFloat("fontSize", jsonConfig.fontSize) 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( _displayConfig.value = jsonConfig.copy(
themeMode = finalTheme, 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) { suspend fun loadConfigFromFile(filePath: String) {
try { try {
val jsonString = fileRepository.readFileContent(filePath) val jsonString = fileRepository.readFileContent(filePath)
@ -74,22 +95,23 @@ class DisplayConfigManager(
} }
fun updateThemeMode(themeMode: String) { fun updateThemeMode(themeMode: String) {
prefs.put("themeMode", themeMode) settings.putString("themeMode", themeMode)
prefs.flush()
_displayConfig.value = _displayConfig.value.copy(themeMode = themeMode) _displayConfig.value = _displayConfig.value.copy(themeMode = themeMode)
} }
fun updateFontSize(fontSize: Float) { fun updateFontSize(fontSize: Float) {
prefs.putFloat("fontSize", fontSize) settings.putFloat("fontSize", fontSize)
prefs.flush()
_displayConfig.value = _displayConfig.value.copy(fontSize = fontSize) _displayConfig.value = _displayConfig.value.copy(fontSize = fontSize)
} }
fun resetToDefault() { fun resetToDefault() {
try { try {
prefs.remove("themeMode") settings.remove("themeMode")
prefs.remove("fontSize") settings.remove("fontSize")
prefs.flush() settings.remove("primaryBlueHex")
settings.remove("accentYellowHex")
settings.remove("accentGreenHex")
settings.remove("accentPinkHex")
loadInitialConfig() loadInitialConfig()
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -7,6 +7,10 @@ data class DisplayConfig (
val themeMode: String = "LIGHT", val themeMode: String = "LIGHT",
val fontSize: Float = 14f, val fontSize: Float = 14f,
val playlist: List<String> = emptyList(), val playlist: List<String> = emptyList(),
val primaryBlueHex: String = "#5061FF",
val accentYellowHex: String = "#FFFFFA54",
val accentGreenHex: String = "#3EFF96",
val accentPinkHex: String = "#FFFF4FA8",
val buttonContainerColorHex: String = "#737EFC", val buttonContainerColorHex: String = "#737EFC",
val buttonContentColorHex: String = "#FFFFFF", val buttonContentColorHex: String = "#FFFFFF",
val buttonDisabledContainerColorHex: String = "#CCCCCC", val buttonDisabledContainerColorHex: String = "#CCCCCC",

View file

@ -32,7 +32,7 @@ import mg.dot.feufaro.getPlatform
import mg.dot.feufaro.midi.Dynamic import mg.dot.feufaro.midi.Dynamic
import org.koin.compose.koinInject import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun Settings( fun Settings(
sharedScreenModel: SharedScreenModel, sharedScreenModel: SharedScreenModel,
@ -146,12 +146,11 @@ fun Settings(
HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
Row( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp), .padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.spacedBy(8.dp)
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Taille de la police", text = "Taille de la police",
@ -160,8 +159,9 @@ fun Settings(
) )
Row( Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(15.dp) horizontalArrangement = Arrangement.Center
) { ) {
IconButton( IconButton(
onClick = { onClick = {
@ -169,11 +169,13 @@ fun Settings(
displayConfigManager.updateFontSize(currentDisplayConfig.fontSize - 0.5f) 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) Text("-", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleLarge, color = Color.White)
} }
Spacer(modifier = Modifier.width(16.dp))
OutlinedTextField( OutlinedTextField(
value = tempFontSize.toString(), value = tempFontSize.toString(),
onValueChange = {}, onValueChange = {},
@ -183,19 +185,97 @@ fun Settings(
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) )
Spacer(modifier = Modifier.width(16.dp))
IconButton( IconButton(
onClick = { onClick = {
if (currentDisplayConfig.fontSize < 30f) { if (currentDisplayConfig.fontSize < 30f) {
displayConfigManager.updateFontSize(currentDisplayConfig.fontSize + 0.5f) 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) 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)) Spacer(modifier = Modifier.weight(1f))
OutlinedButton( OutlinedButton(
onClick = { displayConfigManager.resetToDefault() }, onClick = { displayConfigManager.resetToDefault() },

View file

@ -1,61 +1,98 @@
package mg.dot.feufaro.ui package mg.dot.feufaro.ui
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp 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 { object ThemeDefaults {
val PrimaryBlue = Color(0xFF5061FF) val availableColors = listOf(
val AccentYellow = Color(0xFFFFFA54) "#5061FF", // Bleu (défaut)
val AccentGreen = Color(0xFF3EFF96) "#FFD662", // Jaune (défaut)
val AccentPink = Color(0xFFFF4FA8) "#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 LightBackground = Color(0xFFF9F9FB)
val DarkBackground = Color(0xFF121214) val DarkBackground = Color(0xFF121214)
fun getSelectedThemeColors(themeModeStr: String): Pair<ColorScheme, ColorScheme> { fun getSelectedThemeColors(config: DisplayConfig): Pair<ColorScheme, ColorScheme> {
return when (themeModeStr.uppercase()) { val primaryBlue = config.primaryBlueHex.toColorOrDefault(Color(0xFF5061FF))
"SYSTEM", "DARK", "LIGHT" -> Pair( val accentYellow = config.accentYellowHex.toColorOrDefault(Color(0xFFFFD662))
val accentGreen = config.accentGreenHex.toColorOrDefault(Color(0xFF3EFF96))
val accentPink = config.accentPinkHex.toColorOrDefault(Color(0xFFFF4FA8))
return Pair(
lightColorScheme( lightColorScheme(
primary = PrimaryBlue.copy(alpha = 0.85f), primary = primaryBlue.copy(alpha = 0.85f),
onPrimary = LightBackground, onPrimary = LightBackground,
secondary = AccentGreen.copy(alpha = 0.85f), secondary = accentGreen.copy(alpha = 0.85f),
onSecondary = DarkBackground, onSecondary = DarkBackground,
secondaryContainer = AccentGreen.copy(alpha = 0.85f), secondaryContainer = accentGreen.copy(alpha = 0.85f),
onSecondaryContainer = DarkBackground, onSecondaryContainer = DarkBackground,
tertiary = AccentYellow.copy(alpha = 0.85f), tertiary = accentYellow.copy(alpha = 0.85f),
onTertiary = DarkBackground, onTertiary = DarkBackground,
error = AccentPink.copy(alpha = 0.85f), error = accentPink.copy(alpha = 0.85f),
onError = LightBackground, onError = LightBackground,
background = LightBackground, background = LightBackground,
surface = LightBackground surface = LightBackground
), ),
darkColorScheme( darkColorScheme(
primary = PrimaryBlue, primary = primaryBlue,
onPrimary = LightBackground, onPrimary = LightBackground,
secondary = AccentGreen, secondary = accentGreen,
onSecondary = LightBackground, onSecondary = LightBackground,
secondaryContainer = AccentGreen, secondaryContainer = accentGreen,
onSecondaryContainer = DarkBackground, onSecondaryContainer = DarkBackground,
tertiary = AccentYellow, tertiary = accentYellow,
onTertiary = DarkBackground, onTertiary = DarkBackground,
error = AccentPink, error = accentPink,
onError = LightBackground, onError = LightBackground,
background = DarkBackground, background = DarkBackground,
@ -63,31 +100,25 @@ object ThemeDefaults {
onBackground = LightBackground, onBackground = LightBackground,
onSurface = LightBackground onSurface = LightBackground
) )
) )
else -> Pair(
lightColorScheme(primary = PrimaryBlue, background = LightBackground),
darkColorScheme(primary = PrimaryBlue, background = DarkBackground)
)
}
} }
} }
@Composable @Composable
fun FeufaroTheme( fun FeufaroTheme(
themeModeSelected: String, config: DisplayConfig,
userFontSize: Float,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val (lightScheme, darkScheme) = ThemeDefaults.getSelectedThemeColors(themeModeSelected) val (lightScheme, darkScheme) = ThemeDefaults.getSelectedThemeColors(config)
val useDarkTheme = when (themeModeSelected.uppercase()) { val useDarkTheme = when (config.themeMode.uppercase()) {
"DARK" -> true "DARK" -> true
"LIGHT" -> false "LIGHT" -> false
else -> isSystemInDarkTheme() else -> isSystemInDarkTheme()
} }
val colorScheme = if (useDarkTheme) darkScheme else lightScheme val colorScheme = if (useDarkTheme) darkScheme else lightScheme
val baseSize = userFontSize val baseSize = config.fontSize
val customTypography = Typography( val customTypography = Typography(
displayLarge = TextStyle( displayLarge = TextStyle(