Add theme color customization on Settings
This commit is contained in:
parent
323dec9e78
commit
c06053c5dc
6 changed files with 219 additions and 58 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue