diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt index 3d6d408..89160e0 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt @@ -1,6 +1,8 @@ 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), PP (33, "pp", 0.66f), 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), FF (112, "ff", 1.35f), 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 { + 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 = entries.minByOrNull { kotlin.math.abs(it.velocity - v) } ?: MF + + fun resetToDefaults() { + entries.forEach { it.savedFactor = it.defaultFactor } + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index 41eb19d..b632a88 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -50,6 +50,7 @@ fun MainScreenWithDrawer( content: @Composable (PaddingValues) -> Unit, ) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + var showSettingsDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val items by sharedScreenModel.drawerItems.collectAsState() @@ -175,6 +176,9 @@ fun MainScreenWithDrawer( }, onSongSelected = { newSong -> sharedScreenModel.loadNewSong("$midiFile") + }, + onSettingsCheck = { + showSettingsDialog = true } ) }, content = { @@ -207,6 +211,13 @@ fun MainScreenWithDrawer( } ) } + if (showSettingsDialog) { + Settings( + onDismissRequest = { + showSettingsDialog = false + } + ) + } val favoritePaths by sharedScreenModel.playlistItems.collectAsState() Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = { TopAppBar( diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt new file mode 100644 index 0000000..2ff6639 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt @@ -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() + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt index 776ea4d..eb7fa8c 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -1,15 +1,16 @@ package mg.dot.feufaro.ui import SharedScreenModel -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons 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.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch 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.viewmodel.SolfaScreenModel -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SimpleDrawerContent( items: List, @@ -45,6 +45,7 @@ fun SimpleDrawerContent( scope: CoroutineScope, onScannerButtonClick: () -> Unit, onSongSelected: (String) -> Unit, + onSettingsCheck: () -> Unit ) { var searchTxt by remember { mutableStateOf("") } @@ -76,7 +77,8 @@ fun SimpleDrawerContent( ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically ) { CustomSwitchItem( checked = editMode, @@ -228,152 +230,57 @@ fun SimpleDrawerContent( Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceEvenly + .padding(vertical = 8.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically ) { - Button(onClick = { - scope.launch { drawerState.close() } - }) { - Text("Scanner") - } - Button(onClick = { - solfaScreenModel.loadCustomFile() - scope.launch { drawerState.close() } - }) { - Text("Importer") - } - } - } - } -} - -@Composable -fun DrawerAccordionSection( - title: String, - items: List, - 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 ( - + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { PlainTooltip { Text("") } }, + state = rememberTooltipState() ) { - Spacer(modifier = Modifier.width(10.dp)) - Button( - onClick = { - - } - ) { - Text("Scanner") + IconButton(onClick = { + scope.launch { drawerState.close() } + }) { + Icon(Icons.Default.GraphicEq, contentDescription = null, tint = MaterialTheme.colorScheme.primary) } - Spacer(modifier = Modifier.width(10.dp)) - Button( - onClick = { - scope.launch { - //drawerState.close() - } - solfaScreenModel.loadCustomFile() - } - ) { - Text("Importer") + } + + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { PlainTooltip { Text("Importer un fichier") } }, + state = rememberTooltipState() + ) { + IconButton(onClick = { + solfaScreenModel.loadCustomFile() + 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) } } }