Add Settings Page on Drawer & Controls Dynamic velocity

This commit is contained in:
Hasinjato 2026-06-11 14:37:54 +03:00
parent 89d0b850f8
commit e2a5d18a20
4 changed files with 481 additions and 148 deletions

View file

@ -1,6 +1,8 @@
package mg.dot.feufaro.midi package mg.dot.feufaro.midi
enum class Dynamic(val velocity: Int, val label: String, val factor: Float) { import java.util.prefs.Preferences
enum class Dynamic(val velocity: Int, val label: String, val defaultFactor: Float) {
PPP(16, "ppp", 0.55f), PPP(16, "ppp", 0.55f),
PP (33, "pp", 0.66f), PP (33, "pp", 0.66f),
P (49, "p", 0.77f), P (49, "p", 0.77f),
@ -9,9 +11,29 @@ enum class Dynamic(val velocity: Int, val label: String, val factor: Float) {
F (96, "f", 1.15f), F (96, "f", 1.15f),
FF (112, "ff", 1.35f), FF (112, "ff", 1.35f),
FFF(126, "fff", 1.60f); FFF(126, "fff", 1.60f);
private val prefs = Preferences.userRoot().node("mg.dot.feufaro")
var savedFactor: Float = prefs.getFloat("dynamic_${name.lowercase()}", defaultFactor)
set(value) {
field = value
prefs.putFloat("dynamic_${name.lowercase()}", value)
}
val factor: Float
get() = if (isGloballyEnabled) savedFactor else 1.00f
companion object { companion object {
private val globalPrefs = Preferences.userRoot().node("mg.dot.feufaro")
var isGloballyEnabled: Boolean = globalPrefs.getBoolean("dynamics_enabled", true)
set(value) {
field = value
globalPrefs.putBoolean("dynamics_enabled", value)
}
fun fromVelocity(v: Int): Dynamic = fun fromVelocity(v: Int): Dynamic =
entries.minByOrNull { kotlin.math.abs(it.velocity - v) } ?: MF entries.minByOrNull { kotlin.math.abs(it.velocity - v) } ?: MF
fun resetToDefaults() {
entries.forEach { it.savedFactor = it.defaultFactor }
}
} }
} }

View file

@ -50,6 +50,7 @@ fun MainScreenWithDrawer(
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
) { ) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
var showSettingsDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val items by sharedScreenModel.drawerItems.collectAsState() val items by sharedScreenModel.drawerItems.collectAsState()
@ -175,6 +176,9 @@ fun MainScreenWithDrawer(
}, },
onSongSelected = { newSong -> onSongSelected = { newSong ->
sharedScreenModel.loadNewSong("$midiFile") sharedScreenModel.loadNewSong("$midiFile")
},
onSettingsCheck = {
showSettingsDialog = true
} }
) )
}, content = { }, content = {
@ -207,6 +211,13 @@ fun MainScreenWithDrawer(
} }
) )
} }
if (showSettingsDialog) {
Settings(
onDismissRequest = {
showSettingsDialog = false
}
)
}
val favoritePaths by sharedScreenModel.playlistItems.collectAsState() val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = { Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = {
TopAppBar( TopAppBar(

View file

@ -0,0 +1,393 @@
package mg.dot.feufaro.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mg.dot.feufaro.midi.Dynamic
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Settings(
onDismissRequest: () -> Unit
) {
var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) }
var refreshTrigger by remember { mutableStateOf(0) }
var expandedGeneral by remember { mutableStateOf(false) }
var expandedPrint by remember { mutableStateOf(false) }
var expandedAudio by remember { mutableStateOf(true) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = {
Text(
text = "Paramètres",
fontWeight = FontWeight.Black,
fontSize = 22.sp
)
},
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
SettingsAccordion(
title = "Paramètres Général",
isExpanded = expandedGeneral,
onToggle = { expandedGeneral = !expandedGeneral }
) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
}
}
}
SettingsAccordion(
title = "Paramètres Impression",
isExpanded = expandedPrint,
onToggle = { expandedPrint = !expandedPrint }
) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
}
}
}
SettingsAccordion(
title = "Paramètres Audio & Nuances",
isExpanded = expandedAudio,
onToggle = { expandedAudio = !expandedAudio }
) {
Column(modifier = Modifier.padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(if(isDynamicEnabled) "Désactiver" else "Activer", fontWeight = FontWeight.Bold, fontSize = 15.sp)
Switch(
checked = isDynamicEnabled,
onCheckedChange = { enabled ->
isDynamicEnabled = enabled
Dynamic.isGloballyEnabled = enabled
refreshTrigger++
}
)
OutlinedButton(
onClick = {
Dynamic.resetToDefaults()
isDynamicEnabled = true
refreshTrigger++
},
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
Text("Réinitialiser", fontSize = 14.sp)
}
}
}
key(refreshTrigger) {
val dynamicEntries = Dynamic.entries
val minF = 0.22f
val maxF = 2.33f
val step = 0.05f
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFF121212).copy(alpha = 0.6f), RoundedCornerShape(16.dp))
.padding(8.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
dynamicEntries.forEachIndexed { index, dynamicItem ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(1.dp),
modifier = Modifier.padding(vertical = 0.dp)
) {
// (-)
if(index == 0) {
FilledIconButton(
onClick = {
if (isDynamicEnabled) {
val delta = - step
var insideBounds = true
dynamicEntries.forEach { item ->
val projectedValue = item.savedFactor + delta
if (projectedValue < minF || projectedValue > maxF) {
insideBounds = false
}
}
if (insideBounds) {
dynamicEntries.forEach { item ->
item.savedFactor = (item.savedFactor + delta).coerceIn(minF, maxF)
}
refreshTrigger++
}
}
},
enabled = isDynamicEnabled,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = Color(0xFF2C2C2C),
contentColor = Color.White
),
modifier = Modifier.size(32.dp)
) {
Text("-", fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
} else {
Spacer(modifier = Modifier.width(32.dp))
}
Text(
text = dynamicItem.label.lowercase(),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = if (isDynamicEnabled) Color.White else Color.Gray,
textAlign = TextAlign.Center,
modifier = Modifier.width(30.dp)
)
Slider(
value = dynamicItem.savedFactor,
onValueChange = { newValue ->
if (isDynamicEnabled) {
val oldValue = dynamicItem.savedFactor
val delta = newValue - oldValue
var insideBounds = true
dynamicEntries.forEach { item ->
val projectedValue = item.savedFactor + delta
if (projectedValue < minF || projectedValue > maxF) {
insideBounds = false
}
}
if (insideBounds) {
dynamicEntries.forEach { item ->
item.savedFactor = (item.savedFactor + delta).coerceIn(minF, maxF)
}
refreshTrigger++
}
}
},
valueRange = minF..maxF,
enabled = isDynamicEnabled,
track = { sliderState ->
SliderDefaults.Track(
sliderState = sliderState,
enabled = isDynamicEnabled,
colors = SliderDefaults.colors(
activeTrackColor = if (isDynamicEnabled) Color.Blue else Color.Gray,
inactiveTrackColor = Color.LightGray
),
modifier = Modifier.height(6.dp)
)
},
thumb = {
SliderDefaults.Thumb(
interactionSource = remember { MutableInteractionSource() },
colors = SliderDefaults.colors(thumbColor = if(isDynamicEnabled) Color(0xFFFF4081) else Color.LightGray),
modifier = Modifier.size(12.dp),
)
},
colors = SliderDefaults.colors(
activeTrackColor = Color(0xFF7C4DFF),
inactiveTrackColor = Color(0xFF2C2C2C)
),
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
.height(16.dp)
)
Text(
text = String.format("%.2f", if (isDynamicEnabled) dynamicItem.savedFactor else 1.00f),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = if (isDynamicEnabled) Color(0xFFDEFF9A) else Color.DarkGray,
textAlign = TextAlign.Center,
modifier = Modifier.width(30.dp)
)
// (+)
if(index == 0) {
FilledIconButton(
onClick = {
if (isDynamicEnabled) {
val delta = step
var insideBounds = true
dynamicEntries.forEach { item ->
val projectedValue = item.savedFactor + delta
if (projectedValue < minF || projectedValue > maxF) {
insideBounds = false
}
}
if (insideBounds) {
dynamicEntries.forEach { item ->
item.savedFactor =
(item.savedFactor + delta).coerceIn(minF, maxF)
}
refreshTrigger++
}
}
},
enabled = isDynamicEnabled,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = Color(0xFF2C2C2C),
contentColor = Color.White
),
modifier = Modifier.size(32.dp)
) {
Text("+", fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
} else {
Spacer(modifier = Modifier.width(32.dp))
}
}
}
}
}
}
}
}
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text("Enregistrer", fontWeight = FontWeight.Bold)
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text("Annuler")
}
},
containerColor = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp)
)
}
@Composable
fun SettingsAccordion(
title: String,
isExpanded: Boolean,
onToggle: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
val arrowRotationDegree by animateFloatAsState(
targetValue = if (isExpanded) 180f else 0f,
animationSpec = tween(durationMillis = 250),
label = "ArrowRotation"
)
Card(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp)),
colors = CardDefaults.cardColors(
containerColor = if (isExpanded) {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f)
}
),
border = BorderStroke(
width = 1.dp,
color = if (isExpanded) MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) else Color.Transparent
),
shape = RoundedCornerShape(16.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onToggle() }
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = title,
fontWeight = if (isExpanded) FontWeight.ExtraBold else FontWeight.SemiBold,
fontSize = 15.sp,
color = if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
)
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.rotate(arrowRotationDegree)
)
}
AnimatedVisibility(visible = isExpanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
) {
HorizontalDivider(
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f),
modifier = Modifier.padding(bottom = 12.dp)
)
content()
}
}
}
}
}

View file

@ -1,15 +1,16 @@
package mg.dot.feufaro.ui package mg.dot.feufaro.ui
import SharedScreenModel import SharedScreenModel
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.EventNote import androidx.compose.material.icons.automirrored.filled.EventNote
@ -26,7 +27,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mg.dot.feufaro.data.DrawerItem import mg.dot.feufaro.data.DrawerItem
@ -34,7 +34,7 @@ import mg.dot.feufaro.getPlatform
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SimpleDrawerContent( fun SimpleDrawerContent(
items: List<DrawerItem>, items: List<DrawerItem>,
@ -45,6 +45,7 @@ fun SimpleDrawerContent(
scope: CoroutineScope, scope: CoroutineScope,
onScannerButtonClick: () -> Unit, onScannerButtonClick: () -> Unit,
onSongSelected: (String) -> Unit, onSongSelected: (String) -> Unit,
onSettingsCheck: () -> Unit
) { ) {
var searchTxt by remember { mutableStateOf("") } var searchTxt by remember { mutableStateOf("") }
@ -76,7 +77,8 @@ fun SimpleDrawerContent(
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) { ) {
CustomSwitchItem( CustomSwitchItem(
checked = editMode, checked = editMode,
@ -228,152 +230,57 @@ fun SimpleDrawerContent(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(vertical = 8.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) { ) {
Button(onClick = { TooltipBox(
scope.launch { drawerState.close() } positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
}) { tooltip = { PlainTooltip { Text("") } },
Text("Scanner") state = rememberTooltipState()
}
Button(onClick = {
solfaScreenModel.loadCustomFile()
scope.launch { drawerState.close() }
}) {
Text("Importer")
}
}
}
}
}
@Composable
fun DrawerAccordionSection(
title: String,
items: List<DrawerItem>,
icon: ImageVector,
iconColor: Color,
activePath: String,
scope: CoroutineScope,
solfaScreenModel: SolfaScreenModel,
onItemClick: (DrawerItem) -> Unit
) {
var expanded by remember { mutableStateOf(true) }
val rotationState by animateFloatAsState(targetValue = if (expanded) 180f else 0f)
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = iconColor,
modifier = Modifier.size(20.dp)
)
Spacer(Modifier.width(12.dp))
Text(
text = title.uppercase(),
style = MaterialTheme.typography.labelLarge.copy(
letterSpacing = 1.sp,
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
Surface(
color = iconColor.copy(alpha = 0.1f),
shape = MaterialTheme.shapes.extraSmall
) {
Text(
text = "${items.size}",
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
style = MaterialTheme.typography.labelSmall,
color = iconColor
)
}
Spacer(Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.rotate(rotationState).size(20.dp)
)
}
AnimatedVisibility(visible = expanded) {
Column {
items.forEach { item ->
val isSelected = item.path == activePath
NavigationDrawerItem(
label = {
Column {
Text(
text = item.title,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = item.contentTitle,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1
)
}
},
selected = isSelected,
onClick = { onItemClick(item) },
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
shape = MaterialTheme.shapes.medium,
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = Color.Transparent
)
)
}
}
}
HorizontalDivider(
modifier = Modifier.padding(top = 8.dp),
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant
)
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row (
) { ) {
Spacer(modifier = Modifier.width(10.dp)) IconButton(onClick = {
Button( scope.launch { drawerState.close() }
onClick = { }) {
Icon(Icons.Default.GraphicEq, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
}
) {
Text("Scanner")
} }
Spacer(modifier = Modifier.width(10.dp)) }
Button(
onClick = { TooltipBox(
scope.launch { positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
//drawerState.close() tooltip = { PlainTooltip { Text("Importer un fichier") } },
} state = rememberTooltipState()
solfaScreenModel.loadCustomFile() ) {
} IconButton(onClick = {
) { solfaScreenModel.loadCustomFile()
Text("Importer") scope.launch { drawerState.close() }
}) {
Icon(Icons.Default.FolderOpen, contentDescription = null)
}
}
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip { Text("Paramètres") } },
state = rememberTooltipState()
) {
IconButton(onClick = {
onSettingsCheck()
scope.launch { drawerState.close() }
}) {
Icon(Icons.Default.Settings, contentDescription = null)
}
}
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip { Text("À propos") } },
state = rememberTooltipState()
) {
IconButton(onClick = {
scope.launch { drawerState.close() }
}) {
Icon(Icons.Default.Info, contentDescription = null)
} }
} }
} }