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
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<Float> = voiceVolumes.toList()
actual fun changeInstru(noInstru: Int) {

View file

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

View file

@ -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> = _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) {

View file

@ -7,6 +7,10 @@ data class DisplayConfig (
val themeMode: String = "LIGHT",
val fontSize: Float = 14f,
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 buttonContentColorHex: String = "#FFFFFF",
val buttonDisabledContainerColorHex: String = "#CCCCCC",

View file

@ -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() },

View file

@ -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<ColorScheme, ColorScheme> {
return when (themeModeStr.uppercase()) {
"SYSTEM", "DARK", "LIGHT" -> Pair(
fun getSelectedThemeColors(config: DisplayConfig): Pair<ColorScheme, ColorScheme> {
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,
@ -64,30 +101,24 @@ object ThemeDefaults {
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(