From 340f72855f613e06a785ea91f106cc3959139646 Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Mon, 3 Aug 2026 11:25:36 +0300 Subject: [PATCH] Add custom keyboards for search & edit notes --- .../mg/dot/feufaro/solfa/MarkersPopup.kt | 6 +- .../mg/dot/feufaro/solfa/TUODetailDialog.kt | 496 ++++-- .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 185 ++- .../mg/dot/feufaro/ui/CustomKeyboard.kt | 123 ++ .../kotlin/mg/dot/feufaro/ui/DrawerUI.kt | 1338 +++++++++-------- .../mg/dot/feufaro/ui/SimpleDrawerContent.kt | 3 +- .../kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt | 296 ++++ .../feufaro/viewmodel/SharedScreenModel.kt | 1 - 8 files changed, 1613 insertions(+), 835 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/CustomKeyboard.kt create mode 100644 composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt index 29bc204..2652598 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt @@ -15,12 +15,14 @@ import androidx.compose.material.icons.filled.RemoveCircleOutline import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties @@ -141,6 +143,7 @@ private val markerGroups = listOf( @Composable +@OptIn(ExperimentalComposeUiApi::class) fun MarkerPopup( menuPosition: IntOffset, onDismiss: () -> Unit, @@ -155,7 +158,8 @@ fun MarkerPopup( properties = PopupProperties( focusable = true, dismissOnBackPress = true, - dismissOnClickOutside = true + dismissOnClickOutside = true, + usePlatformDefaultWidth = false ) ) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt index 90d73fe..29e3889 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -1,14 +1,11 @@ package mg.dot.feufaro import SharedScreenModel -import androidx.compose.foundation.background -import androidx.compose.foundation.border +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForward @@ -23,6 +20,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -37,7 +35,7 @@ import mg.dot.feufaro.solfa.MarkerPopup import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.solfa.TimeUnitObject -import mg.dot.feufaro.solfa.extractAnchorFromNoteLine +import mg.dot.feufaro.ui.LocalSolfaKeyboardState @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -61,6 +59,18 @@ fun TUODetailDialog( putAll(editState.notesByVoice) } } + val isAndroid = remember { getPlatform().name.startsWith("Android") } + val keyboardState = LocalSolfaKeyboardState.current + val keyboardController = LocalSoftwareKeyboardController.current + var selectedVoiceIndex by remember { mutableStateOf(0) } + + DisposableEffect(Unit) { + keyboardState.isDialogActive = true + onDispose { + keyboardState.hide() + keyboardState.isDialogActive = false + } + } val originalNotes = mutableStateMapOf().apply { putAll(editState.notesByVoice) } @@ -114,20 +124,103 @@ fun TUODetailDialog( var markerButtonPos by remember { mutableStateOf(IntOffset.Zero) } + LaunchedEffect(selectedVoiceIndex, notes[selectedVoiceIndex]) { + if (keyboardState.isVisible) { + val currentRawText = notes[selectedVoiceIndex] ?: "" + keyboardState.currentText = transformMusicalInput(currentRawText) + } + } + + fun attachKeyboardToVoice(voiceIndex: Int) { + selectedVoiceIndex = voiceIndex + if(isAndroid) { + keyboardController?.hide() + + val rawInitialText = notes[voiceIndex] ?: "" + val formattedInitialText = transformMusicalInput(rawInitialText) + keyboardState.currentText = formattedInitialText + + keyboardState.show( + initialVoice = voiceIndex, + initialValue = formattedInitialText, + onNote = { noteToAdd -> + val currentText = notes[voiceIndex] ?: "" + val newText = currentText + noteToAdd + val formattedText = transformMusicalInput(newText) + notes[voiceIndex] = formattedText + keyboardState.currentText = formattedText + }, + onDelete = { + val currentText = notes[voiceIndex] ?: "" + if (currentText.isNotEmpty()) { + val octaveRegex = Regex("['`,₁₂₃₄₅¹²³⁴⁵]+$") + + val newText = if (octaveRegex.containsMatchIn(currentText)) { + currentText.replace(octaveRegex, "") + } else { + currentText.dropLast(1) + } + + notes[voiceIndex] = transformMusicalInput(newText) + keyboardState.currentText = transformMusicalInput(newText) + } + }, + onSymbol = { symbol -> + val currentText = notes[voiceIndex] ?: "" + val newText = currentText + symbol + notes[voiceIndex] = transformMusicalInput(newText) + keyboardState.currentText = transformMusicalInput(newText) + }, + onClose = { + onDismiss() + }, + onBuild = { + val finalLyricsMap = editState.lyricsByStanza.toMutableMap() + finalLyricsMap.putAll(editedLyricsMap) + if (!finalLyricsMap.containsKey(currentStanza)) { + finalLyricsMap[currentStanza] = lyricsLines.getOrNull(0)?.trim() ?: "" + } + val state = TUOEditState( + tuoIndex = globalIndex, + notesByVoice = notes.toMap(), + originalNotes = originalNotes.toMap(), + originalLyricsByStanza = originalLyricsByStz.toMutableMap(), + lyricsByStanza = finalLyricsMap, + templateFragment = templateFragment, + marker = if(marker.isEmpty()) "_" else marker, + originalSep = initialSep, + sep = newSep + ) + onSave(state) + }, + onTab = { + val nextVoice = (selectedVoiceIndex + 1) % 4 + attachKeyboardToVoice(nextVoice) + } + ) + } + } + Popup( offset = menuPosition, - onDismissRequest = onDismiss, + onDismissRequest = { + if(!isAndroid && !keyboardState.isVisible) { + onDismiss() + } + }, properties = PopupProperties( - focusable = true, - dismissOnBackPress = true, - dismissOnClickOutside = true + focusable = if(isAndroid) false else true, + dismissOnBackPress = !isAndroid || !keyboardState.isVisible, + dismissOnClickOutside = !isAndroid || !keyboardState.isVisible ), ) { Surface( modifier = Modifier .widthIn(max=125.dp) - .heightIn(max = 400.dp), + .heightIn(max = 400.dp) + .imePadding() + .navigationBarsPadding(), shape = MaterialTheme.shapes.small, color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f), ) { @@ -500,48 +593,76 @@ fun TUODetailDialog( val validation = validateMusicalInput(currentNote, templateFragment) val voiceAnchor = anchorsByVoice[voice]?.replace("#","") val hasAnchor = !voiceAnchor.isNullOrBlank() + val isSelected = selectedVoiceIndex == voice && keyboardState.isVisible - Box(modifier = Modifier.wrapContentSize() + Box(modifier = Modifier + //.wrapContentSize() + .fillMaxWidth() ) { - MyTextEditField( - value = currentNote ?: "", - customFontSize = 14.sp, - color = Color.White, - customPadding = 8.dp, - customBrush = SolidColor(Color.White), - isEditable = isEditable, - isAddable = canAdd, - isWarn = !validation.isValid, - funTransform = ::transformMusicalInput, - onValueChng = { newValue -> - notes[voice] = newValue - } - ) - if (hasAnchor) { - Spacer(modifier = Modifier.width(2.dp)) + if(!isAndroid) { + MyTextEditField( + value = currentNote ?: "", + customFontSize = 14.sp, + color = Color.White, + customPadding = 8.dp, + customBrush = SolidColor(Color.White), + isEditable = isEditable, + isAddable = canAdd, + isWarn = !validation.isValid, + funTransform = ::transformMusicalInput, + onValueChng = { newValue -> + notes[voice] = newValue + } + ) + if (hasAnchor) { + Spacer(modifier = Modifier.width(2.dp)) - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { - Text("Ancrée sur ${transformMusicalInput(voiceAnchor)}", fontSize = 11.sp) - } - }, - state = rememberTooltipState() - ) { - Surface( - shape = RoundedCornerShape(3.dp), - color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.8f), - modifier = Modifier.padding(start = 2.dp) + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { + Text("Ancrée sur ${transformMusicalInput(voiceAnchor)}", fontSize = 11.sp) + } + }, + state = rememberTooltipState() ) { - Text( - text = "⚓", - fontSize = 10.sp, - color = Color.Black, - modifier = Modifier.padding(horizontal = 2.dp, vertical = 1.dp) - ) + Surface( + shape = RoundedCornerShape(3.dp), + color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.8f), + modifier = Modifier.padding(start = 2.dp) + ) { + Text( + text = "⚓", + fontSize = 10.sp, + color = Color.Black, + modifier = Modifier.padding(horizontal = 2.dp, vertical = 1.dp) + ) + } } } + } else { + SolfaTextEditField( + value = currentNote ?: "", + customFontSize = 14.sp, + color = if (isSelected) MaterialTheme.colorScheme.tertiary else Color.White, + customPadding = 8.dp, + customBrush = SolidColor(Color.White), + isWarn = !validation.isValid, + readOnly = isAndroid, + funTransform = ::transformMusicalInput, + onValueChng = { newValue -> + notes[voice] = newValue + } + ) + } + if(isAndroid) { + Box( + modifier = Modifier + .matchParentSize() + .clickable { + attachKeyboardToVoice(voice) + } + ) } } } @@ -549,124 +670,136 @@ fun TUODetailDialog( Spacer(modifier = Modifier.height(8.dp)) - // --- SECTION LYRICS --- - lyricsLines.forEachIndexed { index, line -> - val displayedLine = line - LaunchedEffect(displayedLine) { - if (displayedLine != line) { - lyricsLines[index] = displayedLine - val updatedLyricsMap = editState.lyricsByStanza.toMutableMap() - updatedLyricsMap[index + 1] = displayedLine + if(!isAndroid) { + // --- SECTION LYRICS --- + lyricsLines.forEachIndexed { index, line -> + val displayedLine = line + LaunchedEffect(displayedLine) { + if (displayedLine != line) { + lyricsLines[index] = displayedLine + val updatedLyricsMap = editState.lyricsByStanza.toMutableMap() + updatedLyricsMap[index + 1] = displayedLine + } } - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = 2.dp) - ) { - Column( - modifier = Modifier.fillMaxWidth(0.8f) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 2.dp) ) { - - val isLyricsValid = validateLyricsInput(line, templateFragment) - val tooltipState2 = rememberTooltipState(isPersistent = false) - Row( + Column( + modifier = Modifier.fillMaxWidth(0.8f) ) { - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - if (!isLyricsValid) { - PlainTooltip( - containerColor = Color(0XFF34EB71), - contentColor = Color.White - ) { - Text("Veuillez suivre ce format: $templateFragment") - } - } - }, - state = tooltipState2 - ) { - MyTextEditField( - value = displayedLine, - customFontSize = 13.sp, - color = Color.White, - customPadding = 8.dp, - customBrush = SolidColor(Color.White), - isEditable = isEditable, - isAddable = canAdd, - isWarn = !isLyricsValid, - funTransform = { input -> transformLyricsInput(input, templateFragment) }, - onValueChng = { newValue -> - val trimmedValue = newValue.trimStart() - val dataToSave = if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue " - lyricsLines[index] = trimmedValue - val updatedLyricsMap = editState.lyricsByStanza.toMutableMap() - editedLyricsMap[index + 1] = dataToSave - val updatedState = editState.copy(lyricsByStanza = updatedLyricsMap) + val isLyricsValid = validateLyricsInput(line, templateFragment) + val tooltipState2 = rememberTooltipState(isPersistent = false) + Row( + ) { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + if (!isLyricsValid) { + PlainTooltip( + containerColor = Color(0XFF34EB71), + contentColor = Color.White + ) { + Text("Veuillez suivre ce format: $templateFragment") + } + } + }, + state = tooltipState2 + ) { + MyTextEditField( + value = displayedLine, + customFontSize = 13.sp, + color = Color.White, + customPadding = 8.dp, + customBrush = SolidColor(Color.White), + isEditable = isEditable, + isAddable = canAdd, + isWarn = !isLyricsValid, + funTransform = { input -> + transformLyricsInput( + input, + templateFragment + ) + }, + onValueChng = { newValue -> + val trimmedValue = newValue.trimStart() + val dataToSave = + if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue " + lyricsLines[index] = trimmedValue + val updatedLyricsMap = editState.lyricsByStanza.toMutableMap() + editedLyricsMap[index + 1] = dataToSave + + val updatedState = editState.copy(lyricsByStanza = updatedLyricsMap) + } + ) + } + } + } + if (isEditable || canAdd) { + IconButton( + onClick = { + if (index >= 0) { + if (index == 0) lyricsLines.add("_") else lyricsLines.removeAt(index) } + }) { + Icon( + imageVector = if (index == 0) Icons.Default.Add else Icons.Default.Clear, + tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy( + alpha = 1.5f + ), + contentDescription = null ) } } } - if (isEditable || canAdd) { - IconButton( - onClick = { - if (index >= 0) { - if (index == 0) lyricsLines.add("_") else lyricsLines.removeAt(index) - } - }) { - Icon( - imageVector = if (index == 0) Icons.Default.Add else Icons.Default.Clear, - tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f), - contentDescription = null - ) - } - } } + Spacer(modifier = Modifier.height(10.dp)) } - Spacer(modifier = Modifier.height(10.dp)) } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { - IconButton(onClick = onDismiss, modifier = Modifier.size(20.dp)) { - Icon( - Icons.Default.Clear, - contentDescription = null, - tint = Color.LightGray - ) - } - if (isEditable || canAdd) { - IconButton( - onClick = { - val finalLyricsMap = editState.lyricsByStanza.toMutableMap() - finalLyricsMap.putAll(editedLyricsMap) - if (!finalLyricsMap.containsKey(currentStanza)) { - finalLyricsMap[currentStanza] = lyricsLines.getOrNull(0)?.trim() ?: "" - } - val state = TUOEditState( - tuoIndex = globalIndex, - notesByVoice = notes.toMap(), - originalNotes = originalNotes.toMap(), - originalLyricsByStanza = originalLyricsByStz.toMutableMap(), - lyricsByStanza = finalLyricsMap, - templateFragment = templateFragment, - marker = if(marker.isEmpty()) "_" else marker, - originalSep = initialSep, - sep = newSep - ) - onSave(state) - }, - modifier = Modifier.size(22.dp) - ) { + if(!isAndroid) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + IconButton(onClick = onDismiss, modifier = Modifier.size(20.dp)) { Icon( - Icons.Default.Build, + Icons.Default.Clear, contentDescription = null, - tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) + tint = Color.LightGray ) } + if (isEditable || canAdd) { + IconButton( + onClick = { + val finalLyricsMap = editState.lyricsByStanza.toMutableMap() + finalLyricsMap.putAll(editedLyricsMap) + if (!finalLyricsMap.containsKey(currentStanza)) { + finalLyricsMap[currentStanza] = lyricsLines.getOrNull(0)?.trim() ?: "" + } + val state = TUOEditState( + tuoIndex = globalIndex, + notesByVoice = notes.toMap(), + originalNotes = originalNotes.toMap(), + originalLyricsByStanza = originalLyricsByStz.toMutableMap(), + lyricsByStanza = finalLyricsMap, + templateFragment = templateFragment, + marker = if(marker.isEmpty()) "_" else marker, + originalSep = initialSep, + sep = newSep + ) + onSave(state) + }, + modifier = Modifier.size(22.dp) + ) { + Icon( + Icons.Default.Build, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) + ) + } + } } } } @@ -907,6 +1040,75 @@ fun MyTextEditField( ) } +@Composable +fun SolfaTextEditField( + value: String, + customFontSize: TextUnit, + font: FontFamily? = FontFamily.SansSerif, + color: Color, + customPadding: Dp, + customBrush: Brush, + isWarn: Boolean? = false, + readOnly: Boolean, + funTransform: ((String) -> String)? = null, + onValueChng: (String) -> Unit +) { + var textFieldValueState by remember { + mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length))) + } + + LaunchedEffect(value) { + if (textFieldValueState.text != value) { + val newSelection = if (textFieldValueState.selection.start <= value.length) { + textFieldValueState.selection + } else { + TextRange(value.length) + } + textFieldValueState = TextFieldValue(text = value, selection = newSelection) + } + } + + BasicTextField( + value = textFieldValueState, + onValueChange = { newValue -> + println("ça change $newValue => ${textFieldValueState.text}") + val transformedText = funTransform?.invoke(newValue.text) ?: newValue.text + val finalSelection = if (transformedText.length != newValue.text.length) { + if (newValue.selection.start <= transformedText.length) newValue.selection else TextRange(transformedText.length) + } else { + newValue.selection + } + + textFieldValueState = TextFieldValue( + text = transformedText, + selection = finalSelection + ) + println("et ça trnasforme en = $transformedText") + onValueChng(transformedText) + }, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Ascii + ), + textStyle = TextStyle( + color = color, + fontSize = customFontSize, + fontFamily = font + ), + readOnly = readOnly, + modifier = Modifier + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(4.dp)) + .border( + if (isWarn!!) 1.dp else 0.dp, + if (isWarn!!) MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f) else Color.Transparent, + shape = RoundedCornerShape(4.dp) + ) + .padding(customPadding), + cursorBrush = customBrush + ) +} + data class ValidNoteResult( val isValid: Boolean, val message: String diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 136f553..0e28039 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -265,6 +265,13 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co } } +private val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+") +private val musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""",RegexOption.IGNORE_CASE) +private val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE) +private val blankPrefixRegex =Regex("""z([0-9A-Z])[:]""") +private val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*") +private val inclusionRegex = Regex("^I([0-9]):(.*)") + @Composable fun TimeUnitComposable( tuo: TimeUnitObject, @@ -273,17 +280,34 @@ fun TimeUnitComposable( gridActive: Boolean, isCurrentBlock: Boolean = false, parentFocusRequester: FocusRequester? = null, - transpositionInterval: Int + transpositionInterval: Int, + hairpinRange: HairpinRange? = null ) { val col = if (tuo.getNum() % 2 == 0) MaterialTheme.colorScheme.tertiary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.error.copy(alpha = 0.05f) val currentDensity = LocalDensity.current - val animatedColor by animateColorAsState( - targetValue = if (gridActive) MaterialTheme.colorScheme.secondary.copy(alpha = 1f) else col, - animationSpec = tween(durationMillis = 100) // Très court pour rester réactif - ) + val animatedColor = if (gridActive) { + animateColorAsState( + targetValue = MaterialTheme.colorScheme.secondary.copy(alpha = 1f), + animationSpec = tween(durationMillis = 100) + ).value + } else { + col + } + val focusRequesters = remember { List(4) { FocusRequester() } } + // Mettre en cache les annotations et les chaînes transposées + val multiLineText = remember(tuo, transpositionInterval) { + val notes = tuo.noteAsMultiString() + Transpose.transposeText(text = notes, interval = transpositionInterval) + } + + val annotationsCache = remember(tuo, transpositionInterval) { + tuo.annotate() + tuo.annotations() + } + Column( modifier = Modifier .background(animatedColor) @@ -389,15 +413,13 @@ fun TimeUnitComposable( .fillMaxHeight() ) } - tuo.annotate() + /*tuo.annotate()*/ // Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à "" val mutableNoteVersionX = if (tuo.mutableNoteVersion == -1) "7" else "" - val multiLineNotes = tuo.noteAsMultiString() - val multiLineText = Transpose.transposeText(text = multiLineNotes, interval = transpositionInterval) var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) } Box (modifier = Modifier.fillMaxWidth() .drawBehind { - tuo.annotations().map { ta -> + annotationsCache.map { ta -> ta.underlineSpec.map { us -> var xStart = 0f val separatorLength = if (tuo.sep0 in listOf(":", "!")) 1 else 0 @@ -435,7 +457,8 @@ fun TimeUnitComposable( else -> Color.Black } val totalHeight = textLayoutResult?.size?.height ?: 0 - val nbNotes = (multiLineText + mutableNoteVersionX).split("\n").size + // # FIX + val nbNotes = multiLineText.count { it == '\n' } +1 val yPos = ta.voiceNumber * totalHeight.toFloat() / nbNotes drawLine( colorUnderline, @@ -446,22 +469,24 @@ fun TimeUnitComposable( } } ){ - val annotatedText = buildAnnotatedString { - multiLineText.split(">").mapIndexed { index, text -> - if (index %2 == 0) { - append(text) - } else { - withStyle( - style = SpanStyle( - fontSize = 10.sp, - baselineShift = BaselineShift.Superscript, - //fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.primary - )) { - append(text+" ") - } - } + val annotatedText = remember(multiLineText, MaterialTheme.colorScheme.primary) { + buildAnnotatedString { + multiLineText.split(">").mapIndexed { index, text -> + if (index %2 == 0) { + append(text) + } else { + withStyle( + style = SpanStyle( + fontSize = 10.sp, + baselineShift = BaselineShift.Superscript, + //fontStyle = FontStyle.Italic, + /*color = MaterialTheme.colorScheme.primary*/ + )) { + append(text+" ") + } + } + } } } Text( @@ -485,6 +510,8 @@ fun TimeUnitComposable( data class TUOWidthMeasure(val width: Dp, val isReady: Boolean) +data class HairpinRange(val startBlock: Int, val endBlock: Int, val symbol: Char) + @Composable fun bestTUOWidth(items: List): TUOWidthMeasure { val textMeasurer = rememberTextMeasurer() @@ -656,7 +683,9 @@ fun LazyVerticalGridTUO( val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() val activeRowIndex by sharedScreenModel.activeIndex.collectAsStateWithLifecycle() - val measures = tuoList.drop(1).chunked(gridColumnCount) + val measures = remember(tuoList, gridColumnCount) { + tuoList.drop(1).chunked(gridColumnCount) + } // Avant column affichage: val metadataList = remember(tuoList) { tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO -> @@ -691,6 +720,32 @@ fun LazyVerticalGridTUO( }.distinctBy { it.gridIndex } } + val hairpinByEndBlock: Map = remember(tuoList) { + val map = mutableMapOf() + var pendingStartBlock: Int? = null + var pendingSymbol: Char? = null + tuoList.drop(1).forEach { oneTUO -> + when (oneTUO.hasHairPin()) { + '<', '>' -> { + pendingStartBlock = oneTUO.numBlock + pendingSymbol = oneTUO.hasHairPin() + } + '=' -> { + val startBlock = pendingStartBlock + val symbol = pendingSymbol + if (startBlock != null && symbol != null) { + map[oneTUO.numBlock] = HairpinRange(startBlock, oneTUO.numBlock, symbol) + } + pendingStartBlock = null + pendingSymbol = null + } + else -> Unit + } + } + map + } + + // Envoyer les données au ViewModel une seule fois LaunchedEffect(metadataList) { if (metadataList.isNotEmpty()) { @@ -713,7 +768,6 @@ fun LazyVerticalGridTUO( val textMeasurer = rememberTextMeasurer() val containerWidthDp = gridWidthDp / gridColumnCount - val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+") val isAnySyllableTooLong = remember(currentStanza, gridWidthDp, gridColumnCount) { val allMeasuresSyllables = tuoList.drop(1).chunked(gridColumnCount).map { measureTUOss -> measureTUOss.map { it.getSingleSyllable(currentStanza) }.map { column -> @@ -786,7 +840,7 @@ fun LazyVerticalGridTUO( if (tuo.isTriolet()) { Canvas(modifier = Modifier.fillMaxSize()) { - val arcWidth = with(density) { size.width * 0.65f } + val arcWidth = size.width * 0.65f drawArc( color = FEUFAROO_TRIOLET_COLOR, startAngle = 200f, @@ -800,18 +854,14 @@ fun LazyVerticalGridTUO( ) } } - if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) { -// println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}") - val hairPinStart = TimeUnitObject.lastHairPinStart - val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol - val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount - val hairPinEndLine: Int = (tuo.numBlock - 1) / gridColumnCount -// if (hairPinStartLine == hairPinEndLine) { + + val hairpinRange = hairpinByEndBlock[tuo.numBlock] + if (hairpinRange != null) { Canvas( modifier = Modifier.fillMaxSize() ) { - val xStart = if (lastHairPinSymbol == '>') -size.width * (tuo.numBlock - hairPinStart) else size.width / 2 - val xEnd = if (lastHairPinSymbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairPinStart) + val xStart = if (hairpinRange.symbol == '>') -size.width * (tuo.numBlock - hairpinRange.startBlock) else size.width / 2 + val xEnd = if (hairpinRange.symbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairpinRange.startBlock) val offsetUp = -yHeight * 0.30f val hairpinHeight = yHeight * 0.70f @@ -832,20 +882,12 @@ fun LazyVerticalGridTUO( strokeWidth = 0.5f ) } - TimeUnitObject.endHairPin() -// } + } - } - if (hairPinSymbol != null && hairPinSymbol != '=') { - TimeUnitObject.startHairPin(hairPinSymbol, tuo.numBlock) - } // @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne // problème si template = $QD:,-$QD tuo.pTemplate.resetCalledMarker() - val musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""",RegexOption.IGNORE_CASE) - val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE) - val markerList = tuo.pTemplate.markerToList() Row( modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart), @@ -1063,7 +1105,8 @@ fun LazyVerticalGridTUO( gridActive = isActive, isCurrentBlock = isSelectedByKeyboard && editMode, parentFocusRequester = focusRequester, - transpositionInterval = currentInterval + transpositionInterval = currentInterval, + hairpinRange = hairpinByEndBlock[oneTUO.numBlock] ) if (showContextualMenu && selectedIndex == globalIndex) { @@ -1119,7 +1162,6 @@ fun LazyVerticalGridTUO( else -> "" } val afterU0T0 = fullLine.substringAfter("0:") - val blankPrefixRegex =Regex("""z([0-9A-Z])[:]""") val match = blankPrefixRegex.find(afterU0T0) val blankPrefix = match?.groups?.get(1)?.value ?: "" @@ -1217,7 +1259,6 @@ fun LazyVerticalGridTUO( horizontalArrangement = Arrangement.Start ) { val columnWidthDp = gridWidthDp / gridColumnCount - val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+") val cleanAllTemps = allTemps.map { column -> column.map { syllable -> syllable.replace(REGEX_CLEAN_PREFIX, "").trim() @@ -1295,8 +1336,9 @@ fun makeSpaceBetweenSyllables( val density = LocalDensity.current val noteWidthPx = with(density) { noteWidthDp.toPx() } val style = TextStyle(fontSize = fontSize) - val spaceWidthPx = textMeasurer.measure("\u00A0", style).size.width - + val spaceWidthPx = remember(textMeasurer, style) { + textMeasurer.measure("\u00A0", style).size.width + } return { currentSyl, allT, s_i, index -> var textAlign = Alignment.Center @@ -1308,38 +1350,25 @@ fun makeSpaceBetweenSyllables( val excessPx = currentWidth - noteWidthPx val isExcess = (excessPx > 0) - val neededSpaces = if (isExcess) (excessPx / spaceWidthPx).toInt() + 1 else 0 + val neededSpaces = if (isExcess && spaceWidthPx > 0) (excessPx / spaceWidthPx).toInt() + 1 else 0 - val nextNextSyl = allT.getOrNull(s_i + 2)?.getOrNull(index) ?: "" - // println("801: prev: [$prev_syl]${prev_syl.length}:[${textMeasurer.measure(prev_syl, style).size.width}], current [$currentSyl]:[${textMeasurer.measure(currentSyl, style).size.width}], next [$next_syl]:[${textMeasurer.measure(next_syl, style).size.width}], isExcess=$excessPx & need $neededSpaces \t spwp $spaceWidthPx\n") - - val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt() - val paddingNeeded = totalSpacesPossible - (currentSyl.length+4) val resultText = when { - !isExcess && (paddingNeeded > 0) -> { - textAlign = Alignment.TopStart - currentSyl + "\u00A0".repeat(paddingNeeded) - } - isExcess && next_syl.length < prev_syl.length -> { - "\u00A0".repeat(neededSpaces) + currentSyl - } - isExcess && next_syl.length >= prev_syl.length -> { - currentSyl + "\u00A0".repeat(neededSpaces) - } - - isExcess && next_syl.isNotEmpty() -> { - val nextWidth = textMeasurer.measure(next_syl, style).size.width - val nextExcess = nextWidth - noteWidthPx - - if (nextExcess > 0 && (allT.getOrNull(s_i + 2)?.getOrNull(index)?.length ?: 0) > next_syl.length) { - // Si le mot d'après va se décaler à gauche, moi je me décale aussi à gauche - val compensationSpaces = ((nextExcess / spaceWidthPx) / 2).toInt() + 1 - "\u00A0".repeat(compensationSpaces) + currentSyl + !isExcess && (noteWidthPx > 0 && spaceWidthPx > 0) -> { + val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt() + val paddingNeeded = totalSpacesPossible - (currentSyl.length + 4) + if (paddingNeeded > 0) { + textAlign = Alignment.TopStart + currentSyl + "\u00A0".repeat(paddingNeeded) } else { currentSyl } } - + isExcess && spaceWidthPx > 0 && next_syl.length < prev_syl.length -> { + "\u00A0".repeat(neededSpaces) + currentSyl + } + isExcess && spaceWidthPx > 0 -> { + currentSyl + "\u00A0".repeat(neededSpaces) + } else -> currentSyl } resultText to textAlign @@ -1356,7 +1385,6 @@ private suspend fun expandInclusions( val lastSlash = currentFilePath.lastIndexOf('/') val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else "" - val inclusionRegex = Regex("^I([0-9]):(.*)") lines.forEach { line -> val match = inclusionRegex.find(line) @@ -1592,7 +1620,6 @@ fun autoFixNote(rawNote: String, template: String): String { // println("==================================================") if (rawNote == "_") return rawNote - val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*") val actualNotes = if (rawNote.isEmpty()) emptyList() else noteRegex.findAll(rawNote).map { it.value }.toList() // println(" • Notes détectées dans RawNote : $actualNotes (Taille: ${actualNotes.size})") /*if (rawNote.contains("•") || rawNote.contains("―") || rawNote.contains(",")) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/CustomKeyboard.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/CustomKeyboard.kt new file mode 100644 index 0000000..17e6d48 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/CustomKeyboard.kt @@ -0,0 +1,123 @@ +package mg.dot.feufaro.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun CustomKeyboard( + onDigitClick: (String) -> Unit, + onDeleteClick: () -> Unit, + onClearClick: () -> Unit, + onPrefixClick: (String) -> Unit, + isFullScreen: Boolean, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + .fillMaxWidth(), + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f), + tonalElevation = 0.dp, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) + ) { + Column( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 10.dp) + .then( + if (isFullScreen) Modifier.navigationBarsPadding() else Modifier + ), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val prefixes = listOf("EWS", "FFPM", "FF", "Antema", "Tsanta", "Salamo") + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 1.dp) + ) { + items(prefixes) { prefix -> + Button( + onClick = { onPrefixClick("$prefix ") }, + modifier = Modifier.height(38.dp), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.15f), + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + contentPadding = PaddingValues(horizontal = 14.dp, vertical = 0.dp) + ) { + Text( + text = prefix, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + fontWeight = FontWeight.Bold + ) + } + } + } + + val keys = listOf( + listOf("1", "2", "3"), + listOf("4", "5", "6"), + listOf("7", "8", "9"), + listOf("↓", "0", "⌫") + ) + + for (row in keys) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + for (key in row) { + val isActionKey = key == "⌫" || key == "↓" + Button( + onClick = { + when (key) { + "⌫" -> onDeleteClick() + "↓" -> onClearClick() + else -> onDigitClick(key) + } + }, + modifier = Modifier + .weight(1f) + .height(44.dp), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (isActionKey) + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.6f) + else + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f), + contentColor = if (isActionKey) + MaterialTheme.colorScheme.onErrorContainer + else + Color.White + ) + ) { + Text( + text = key, + fontSize = MaterialTheme.typography.headlineLarge.fontSize, + fontWeight = FontWeight.Bold + ) + } + } + } + } + } + } +} 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 80a19fc..a567fe6 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -30,11 +30,16 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -182,11 +187,15 @@ fun MainScreenWithDrawer( val isQrVisible = sharedScreenModel.isQRCodeVisible.value + val sharedInteractionSource = remember { MutableInteractionSource() } + LaunchedEffect(drawerState.isOpen) { if (drawerState.isOpen) { sharedScreenModel.loadItems() } } + val density = LocalDensity.current + var keyboardHeightDp by remember { mutableStateOf(0.dp) } ModalNavigationDrawer(drawerState = drawerState, drawerContent = { SimpleDrawerContent( @@ -253,6 +262,22 @@ fun MainScreenWithDrawer( ) } val favoritePaths by sharedScreenModel.playlistItems.collectAsState() + + val keyboardController = LocalSoftwareKeyboardController.current + var isNumericKeyboard by remember { mutableStateOf(true) } + LaunchedEffect(isNumericKeyboard, isSearchActive) { + if (isSearchActive) { + if (isAndroid && isNumericKeyboard) { + keyboardController?.hide() + } else { + keyboardController?.show() + } + } + } + val solfaKeyboardState = remember { SolfaKeyboardState() } + + CompositionLocalProvider(LocalSolfaKeyboardState provides solfaKeyboardState) { + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val isLandscape = maxWidth > maxHeight val topAppBarHeight = if (isAndroid) { @@ -265,215 +290,305 @@ fun MainScreenWithDrawer( 50.dp } - Scaffold( - contentWindowInsets = if (isFullScreenEnabled) { - WindowInsets(0, 0, 0, 0) - } else { - if(!isLandscape) { - WindowInsets.safeDrawing - } else { + CompositionLocalProvider(LocalSolfaKeyboardState provides solfaKeyboardState) { + Scaffold( + contentWindowInsets = if (isFullScreenEnabled) { WindowInsets(0, 0, 0, 0) - } - }, - topBar = { - TopAppBar( - modifier = Modifier.height(topAppBarHeight), - windowInsets = if(!isLandscape) { - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Top) + } else { + if (!isLandscape) { + WindowInsets.safeDrawing } else { WindowInsets(0, 0, 0, 0) - }, - title = { - AnimatedContent( - targetState = isSearchActive, - label = "SearchTransition" - ) { searchActive -> - if (searchActive) { - Box( - modifier = Modifier.fillMaxHeight(), - contentAlignment = Alignment.Center - ) { - BasicTextField( - value = textInput, - onValueChange = { newValue -> - textInput = newValue - sharedScreenModel.updateSearchTxt(newValue) - }, - textStyle = LocalTextStyle.current.copy( - color = Color.White, - fontSize = 16.sp - ), - singleLine = true, - cursorBrush = SolidColor(Color.White), - modifier = Modifier - .fillMaxWidth() - .height(38.dp) - .focusRequester(focusRequester) - .background( - color = Color.White.copy(alpha = 0.15f), - shape = RoundedCornerShape(20.dp) + } + }, + topBar = { + TopAppBar( + modifier = Modifier.height(topAppBarHeight), + windowInsets = if (!isLandscape) { + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Top + ) + } else { + WindowInsets(0, 0, 0, 0) + }, + title = { + AnimatedContent( + targetState = isSearchActive, + label = "SearchTransition" + ) { searchActive -> + if (searchActive) { + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.Center + ) { + BasicTextField( + value = textInput, + onValueChange = { newValue -> + textInput = newValue + sharedScreenModel.updateSearchTxt(newValue) + }, + readOnly = isAndroid && isNumericKeyboard, + textStyle = LocalTextStyle.current.copy( + color = Color.White, + fontSize = 16.sp ), - decorationBox = { innerTextField -> - TextFieldDefaults.DecorationBox( - value = textInput, - innerTextField = innerTextField, - enabled = true, - singleLine = true, - visualTransformation = VisualTransformation.None, - interactionSource = remember { MutableInteractionSource() }, - placeholder = { - Text( - text = "FFPM 1 Andriananahary ...", - color = Color.White.copy(alpha = 0.5f), - fontSize = 16.sp - ) - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Search, - contentDescription = null, - tint = Color.White.copy(alpha = 0.8f), - modifier = Modifier.size(20.dp) - ) - }, - trailingIcon = { - if (textInput.isNotEmpty()) { - IconButton( - onClick = { - textInput = "" - sharedScreenModel.updateSearchTxt("") - }, - modifier = Modifier.size(32.dp) + singleLine = true, + cursorBrush = SolidColor(Color.White), + keyboardOptions = KeyboardOptions( + keyboardType = if (isAndroid && isNumericKeyboard) KeyboardType.Text else KeyboardType.Number + ), + modifier = Modifier + .fillMaxWidth() + .height(38.dp) + .focusRequester(focusRequester) + .background( + color = Color.White.copy(alpha = 0.15f), + shape = RoundedCornerShape(20.dp) + ), + decorationBox = { innerTextField -> + TextFieldDefaults.DecorationBox( + value = textInput, + innerTextField = innerTextField, + enabled = true, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = sharedInteractionSource, + placeholder = { + Text( + text = "FFPM 1 Andriananahary ...", + color = Color.White.copy(alpha = 0.5f), + fontSize = 16.sp + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + tint = Color.White.copy(alpha = 0.8f), + modifier = Modifier.size(20.dp) + ) + }, + trailingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) ) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = "Effacer", - tint = Color.White.copy(alpha = 0.8f), - modifier = Modifier.size(18.dp) - ) + if (isAndroid) { + IconButton( + onClick = { + isNumericKeyboard = !isNumericKeyboard + }, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = if (isNumericKeyboard) Icons.Default.Abc else Icons.Filled._123, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(22.dp) + ) + } + } + + IconButton( + onClick = { + textInput = "" + sharedScreenModel.updateSearchTxt("") + }, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = null, + tint = Color.White.copy(alpha = 0.8f), + modifier = Modifier.size(18.dp) + ) + } } - } - }, - contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent + }, + contentPadding = PaddingValues( + horizontal = 10.dp, + vertical = 0.dp + ), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) ) + } + ) + } + } else { + Column( + modifier = Modifier.fillMaxSize().verticalScroll(scrollState), + verticalArrangement = Arrangement.Center + ) { + Row { + Text( + songTitle, + modifier = Modifier.weight(1f, fill = true), + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.headlineLarge ) } + } + } + } + }, navigationIcon = { + if (!isQrVisible) { + IconButton(onClick = { + scope.launch { drawerState.open() } + }) { + Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") + } + } + }, actions = { + var tempInterval by remember(fileContent) { mutableStateOf(0) } + var isEyeVisible by remember { mutableStateOf(false) } + val keysOrder = Transpose.keyToNumber + val songKeyIndex = keysOrder.indexOf(songKey).takeIf { it != -1 } ?: 0 + val rawKeyIndex = (songKeyIndex + tempInterval) % 12 + val tempUiKey = keysOrder[if (rawKeyIndex < 0) rawKeyIndex + 12 else rawKeyIndex] + val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState() + val isPendingChange = tempInterval != appliedInterval + val isCurrentlyTransposed = appliedInterval != 0 + + Box( + modifier = Modifier.padding(end = 16.dp), + contentAlignment = Alignment.TopCenter + ) { + Row( + modifier = Modifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = tempUiKey, + style = MaterialTheme.typography.displaySmall, + fontWeight = FontWeight.Black, + textAlign = TextAlign.Center, + color = if (isCurrentlyTransposed && !isPendingChange) Color(0xFFFFD700) else Color.White, + modifier = Modifier + .clickable( + interactionSource = sharedInteractionSource, + indication = null + ) { + isEyeVisible = !isEyeVisible + } + .width(45.dp) ) } - } else { - Column( - modifier = Modifier.fillMaxSize().verticalScroll(scrollState), - verticalArrangement = Arrangement.Center - ) { - Row { - Text( - songTitle, - modifier = Modifier.weight(1f, fill = true), - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.headlineLarge - ) - } - } - } - } - }, navigationIcon = { - if(!isQrVisible) { - IconButton(onClick = { - scope.launch { drawerState.open() } - }) { - Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") - } - } - }, actions = { - var tempInterval by remember(fileContent) { mutableStateOf(0) } - var isEyeVisible by remember { mutableStateOf(false) } - val keysOrder = Transpose.keyToNumber - val songKeyIndex = keysOrder.indexOf(songKey).takeIf { it != -1 } ?: 0 - val rawKeyIndex = (songKeyIndex + tempInterval) % 12 - val tempUiKey = keysOrder[if (rawKeyIndex < 0) rawKeyIndex + 12 else rawKeyIndex] - val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState() - val isPendingChange = tempInterval != appliedInterval - val isCurrentlyTransposed = appliedInterval != 0 - Box( - modifier = Modifier.padding(end = 16.dp), - contentAlignment = Alignment.TopCenter - ) { - Row( - modifier = Modifier.fillMaxHeight(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = tempUiKey, - style = MaterialTheme.typography.displaySmall, - fontWeight = FontWeight.Black, - textAlign = TextAlign.Center, - color = if (isCurrentlyTransposed && !isPendingChange) Color(0xFFFFD700) else Color.White, - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { - isEyeVisible = !isEyeVisible - } - .width(45.dp) - ) - } - - DropdownMenu( - expanded = isEyeVisible, - onDismissRequest = { isEyeVisible = false }, - containerColor = Color(0xFF2C3135).copy(alpha = 0.75f), - shape = RoundedCornerShape(16.dp), - modifier = Modifier.padding(12.dp) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) + DropdownMenu( + expanded = isEyeVisible, + onDismissRequest = { isEyeVisible = false }, + containerColor = Color(0xFF2C3135).copy(alpha = 0.75f), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.padding(12.dp) ) { - /* < = > */ - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier.padding(horizontal = 4.dp) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - /* < */ - val currentIndex = keysOrder.indexOf(tempUiKey).takeIf { it != -1 } ?: 0 - val targetKeyLeft = keysOrder[(currentIndex - 1 + 12) % 12] - - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip( - containerColor = Color.DarkGray, - contentColor = Color.White - ) { Text(targetKeyLeft, fontSize = 12.sp) } - }, - state = rememberTooltipState() + /* < = > */ + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.padding(horizontal = 4.dp) ) { - IconButton( - modifier = Modifier.size(36.dp), - onClick = { tempInterval-- } + /* < */ + val currentIndex = keysOrder.indexOf(tempUiKey).takeIf { it != -1 } ?: 0 + val targetKeyLeft = keysOrder[(currentIndex - 1 + 12) % 12] + + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip( + containerColor = Color.DarkGray, + contentColor = Color.White + ) { Text(targetKeyLeft, fontSize = 12.sp) } + }, + state = rememberTooltipState() ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, - contentDescription = null, - tint = Color.White - ) + IconButton( + modifier = Modifier.size(36.dp), + onClick = { tempInterval-- } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = null, + tint = Color.White + ) + } + } + + val centralIcon = + if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz + val centralTint = + if (isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White + val tooltipText = + if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else "Transposer" + + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip( + containerColor = Color.DarkGray, + contentColor = Color.White + ) { Text(text = tooltipText, fontSize = 12.sp) } + }, + state = rememberTooltipState() + ) { + IconButton( + modifier = Modifier.size(36.dp), + onClick = { + if (!isPendingChange && isCurrentlyTransposed) { + tempInterval = 0 + sharedScreenModel.setTranspositionInterval(0) + } else { + sharedScreenModel.setTranspositionInterval(tempInterval) + } + solfaScreenModel.loadFromFile(sharedScreenModel.activeFilePath.value) + isEyeVisible = false + } + ) { + Icon( + imageVector = centralIcon, + contentDescription = null, + tint = centralTint + ) + } + } + + /* > */ + val targetKeyRight = keysOrder[(currentIndex + 1) % 12] + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip( + containerColor = Color.DarkGray, + contentColor = Color.White + ) { Text(targetKeyRight, fontSize = 12.sp) } + }, + state = rememberTooltipState() + ) { + IconButton( + modifier = Modifier.size(36.dp), + onClick = { tempInterval++ } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = Color.White + ) + } } } - val centralIcon = if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz - val centralTint = if (isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White - val tooltipText = if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else "Transposer" + val cIndex = keysOrder.indexOf("C") + val intervalToC = if (cIndex != -1) cIndex - songKeyIndex else 0 TooltipBox( positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), @@ -481,463 +596,476 @@ fun MainScreenWithDrawer( PlainTooltip( containerColor = Color.DarkGray, contentColor = Color.White - ) { Text(text = tooltipText, fontSize = 12.sp) } + ) { Text(text = "Transposer en Do (C)", fontSize = 12.sp) } }, state = rememberTooltipState() ) { - IconButton( - modifier = Modifier.size(36.dp), + OutlinedButton( onClick = { - if (!isPendingChange && isCurrentlyTransposed) { - tempInterval = 0 - sharedScreenModel.setTranspositionInterval(0) - } else { - sharedScreenModel.setTranspositionInterval(tempInterval) - } + tempInterval = intervalToC + sharedScreenModel.setTranspositionInterval(intervalToC) solfaScreenModel.loadFromFile(sharedScreenModel.activeFilePath.value) isEyeVisible = false - } + }, + modifier = Modifier.fillMaxWidth().height(36.dp), + shape = RoundedCornerShape(8.dp), + border = BorderStroke( + 1.dp, + MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f) + ), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.tertiary + ) ) { - Icon( - imageVector = centralIcon, - contentDescription = null, - tint = centralTint + Text( + text = "Transposer en C", + fontSize = 12.sp, + fontWeight = FontWeight.Bold ) } } - - /* > */ - val targetKeyRight = keysOrder[(currentIndex + 1) % 12] - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip( - containerColor = Color.DarkGray, - contentColor = Color.White - ) { Text(targetKeyRight, fontSize = 12.sp) } - }, - state = rememberTooltipState() - ) { - IconButton( - modifier = Modifier.size(36.dp), - onClick = { tempInterval++ } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - tint = Color.White - ) - } - } - } - - val cIndex = keysOrder.indexOf("C") - val intervalToC = if (cIndex != -1) cIndex - songKeyIndex else 0 - - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip( - containerColor = Color.DarkGray, - contentColor = Color.White - ) { Text(text = "Transposer en Do (C)", fontSize = 12.sp) } - }, - state = rememberTooltipState() - ) { - OutlinedButton( - onClick = { - tempInterval = intervalToC - sharedScreenModel.setTranspositionInterval(intervalToC) - solfaScreenModel.loadFromFile(sharedScreenModel.activeFilePath.value) - isEyeVisible = false - }, - modifier = Modifier.fillMaxWidth().height(36.dp), - shape = RoundedCornerShape(8.dp), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f)), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.tertiary - ) - ) { - Text( - text = "Transposer en C", - fontSize = 12.sp, - fontWeight = FontWeight.Bold - ) - } } } } - } - }, colors = TopAppBarColors( - containerColor = MaterialTheme.colorScheme.primary, - titleContentColor = MaterialTheme.colorScheme.onPrimary, - actionIconContentColor = MaterialTheme.colorScheme.onPrimary, - navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, - scrolledContainerColor = MaterialTheme.colorScheme.onPrimary, + }, colors = TopAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + scrolledContainerColor = MaterialTheme.colorScheme.onPrimary, + ) ) - ) - }, floatingActionButton = { - Row( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(7.dp) + }, floatingActionButton = { + Row( + modifier = Modifier.fillMaxWidth() ) { - if (isEditMode) { - AnimatedVisibility( - visible = true, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + if (!solfaKeyboardState.isVisible) { + Column( + modifier = Modifier.fillMaxWidth().padding(5.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(7.dp) ) { - MyFAB( - onClick = { - scope.launch { - codeContent = sourceContent - - withContext(Dispatchers.Main) { - solfaScreenModel.loadExternalFile(originalPath) - } - } - }, - icon = Icons.AutoMirrored.Default.Undo - ) - } - - AnimatedVisibility( - visible = true, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } - ) { - MyFAB( - onClick = { - scope.launch { - try { - val initialDir = - solfaScreenModel.fileRepository.getAppPublicFolder().absolutePath - saveLauncher.launch(fileName, initialDir) - } catch (e: Exception) { - e.printStackTrace() - } - } - }, - icon = Icons.Filled.SaveAs - ) - } - - AnimatedVisibility( - visible = true, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } - ) { - MyFAB( - onClick = { - sharedScreenModel.toggleEditorMode(false) - }, - icon = Icons.Default.Close - ) - } - } else if(!isQrVisible) { - AnimatedVisibility( - visible = isExpanded and !showMidiCtrl, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } - ) { - Row { - Column { + if (isEditMode) { + AnimatedVisibility( + visible = true, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { MyFAB( onClick = { - sharedScreenModel.descGridCount(1) - }, - size = 30.dp, - icon = Icons.Default.Remove - ) - } - Column { - MyFAB( - onClick = { - sharedScreenModel.addGridCount(1) - }, - size = 30.dp, - icon = Icons.Default.Add - ) - } - } - } - AnimatedVisibility( - visible = isExpanded and !showMidiCtrl, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } - ) { - MyFAB( - onClick = { - sharedScreenModel.toggleQRCodeVisibility() - sharedScreenModel.setExpandedFAB(false) - }, - icon = Icons.Filled.QrCode - ) - } - - - AnimatedVisibility( - visible = isExpanded and !showMidiCtrl, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } - ) { - MyFAB( - onClick = { - showPrintSettings = !showPrintSettings - }, - icon = Icons.Filled.Print - ) - } - - AnimatedVisibility( - visible = isExpanded, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } - ) { - MyFAB( - onClick = { - sharedScreenModel.setMidiCtrl(!showMidiCtrl) - }, - icon = if (showMidiCtrl) Icons.Filled.StopCircle else Icons.Filled.PlayCircle - ) - } - if (!showMidiCtrl) { - MyFAB( - onClick = { - sharedScreenModel.setExpandedFAB(!isExpanded) - refreshTrigeer++ - sharedScreenModel.loadNewSong("$midiFile") - }, - icon = if (isExpanded) Icons.Filled.Close else Icons.Filled.Menu - ) - } - AnimatedVisibility( - visible = showMidiCtrl, - enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, - exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } - ) { - Box( - modifier = Modifier.fillMaxWidth(0.9f) - ) { - if (player != null) { - MidiControlPanel( - isPause = isPos, - currentPos = currentPos, - volume = volumelevel, - duration = duration, - solfaScrollState, - onPlayPauseClick = { - sharedScreenModel.togglePlayPause() - }, - onSeek = { newPos -> - sharedScreenModel.setDragging(true) - sharedScreenModel.seekTo(newPos) scope.launch { - delay(100) - sharedScreenModel.setDragging(false) + codeContent = sourceContent + + withContext(Dispatchers.Main) { + solfaScreenModel.loadExternalFile(originalPath) + } } - //println("DrawerUI:335: mihetsika $newPos") }, - mediaPlayer = player, - onVolumeChange = { newVolume -> - sharedScreenModel.setVolume(newVolume) - // println("Changement volume $newVolume -l $volumelevel") - }, - onVoiceVolumeChange = { index, volume -> - player?.updateVoiceVolume(index, volume) - } + icon = Icons.AutoMirrored.Default.Undo ) - } else { - Text("Sélectionner un morceau") + } + + AnimatedVisibility( + visible = true, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + MyFAB( + onClick = { + scope.launch { + try { + val initialDir = + solfaScreenModel.fileRepository.getAppPublicFolder().absolutePath + saveLauncher.launch(fileName, initialDir) + } catch (e: Exception) { + e.printStackTrace() + } + } + }, + icon = Icons.Filled.SaveAs + ) + } + + AnimatedVisibility( + visible = true, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + MyFAB( + onClick = { + sharedScreenModel.toggleEditorMode(false) + }, + icon = Icons.Default.Close + ) + } + } else if (!isQrVisible) { + AnimatedVisibility( + visible = isExpanded and !showMidiCtrl, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + Row { + Column { + MyFAB( + onClick = { + sharedScreenModel.descGridCount(1) + }, + size = 30.dp, + icon = Icons.Default.Remove + ) + } + Column { + MyFAB( + onClick = { + sharedScreenModel.addGridCount(1) + }, + size = 30.dp, + icon = Icons.Default.Add + ) + } + } + } + AnimatedVisibility( + visible = isExpanded and !showMidiCtrl, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + MyFAB( + onClick = { + sharedScreenModel.toggleQRCodeVisibility() + sharedScreenModel.setExpandedFAB(false) + }, + icon = Icons.Filled.QrCode + ) + } + + + AnimatedVisibility( + visible = isExpanded and !showMidiCtrl, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + MyFAB( + onClick = { + showPrintSettings = !showPrintSettings + }, + icon = Icons.Filled.Print + ) + } + + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + MyFAB( + onClick = { + sharedScreenModel.setMidiCtrl(!showMidiCtrl) + }, + icon = if (showMidiCtrl) Icons.Filled.StopCircle else Icons.Filled.PlayCircle + ) + } + if (!showMidiCtrl && !isSearchActive) { + MyFAB( + onClick = { + sharedScreenModel.setExpandedFAB(!isExpanded) + refreshTrigeer++ + sharedScreenModel.loadNewSong("$midiFile") + }, + icon = if (isExpanded) Icons.Filled.Close else Icons.Filled.Menu + ) + } + AnimatedVisibility( + visible = showMidiCtrl, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + Box( + modifier = Modifier.fillMaxWidth(0.9f) + ) { + if (player != null) { + MidiControlPanel( + isPause = isPos, + currentPos = currentPos, + volume = volumelevel, + duration = duration, + solfaScrollState, + onPlayPauseClick = { + sharedScreenModel.togglePlayPause() + }, + onSeek = { newPos -> + sharedScreenModel.setDragging(true) + sharedScreenModel.seekTo(newPos) + scope.launch { + delay(100) + sharedScreenModel.setDragging(false) + } + //println("DrawerUI:335: mihetsika $newPos") + }, + mediaPlayer = player, + onVolumeChange = { newVolume -> + sharedScreenModel.setVolume(newVolume) + // println("Changement volume $newVolume -l $volumelevel") + }, + onVoiceVolumeChange = { index, volume -> + player?.updateVoiceVolume(index, volume) + } + ) + } else { + Text("Sélectionner un morceau") + } + } } } } } } - } - }) { paddingValues -> + }) { paddingValues -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - content(PaddingValues(0.dp)) - - AnimatedVisibility( - visible = isQrVisible, - enter = slideInVertically( - initialOffsetY = { fullHeight -> fullHeight } - ) + fadeIn(), - exit = slideOutVertically( - targetOffsetY = { fullHeight -> fullHeight } - ) + fadeOut() - ) { - QRDisplay( - sharedScreenModel = sharedScreenModel, - fileRepository = solfaScreenModel.fileRepository - ) - } - - if(createMode) { - NewPartition( - onCreate = { partMtdata -> - solfaScreenModel.newSolfa(partMtdata) - sharedScreenModel.toggleCreateMode(!createMode) - sharedScreenModel.toggleEditorMode(createMode) - if(!isAndroid) { - sharedScreenModel.toggleSourceMode() - } - }, - onDismissRequest = { - sharedScreenModel.toggleCreateMode(false) - }, - isAndroid - ) - } - - var isHovered by remember { mutableStateOf(false) } - val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp) - - AnimatedVisibility( - visible = !isEditMode && !isQrVisible && !createMode, + Column( modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 15.dp) - .offset(x = offsetX) - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent() - when (event.type) { - PointerEventType.Enter -> isHovered = true - PointerEventType.Exit -> isHovered = false + .fillMaxSize() + .padding(paddingValues) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + content(PaddingValues(0.dp)) + + androidx.compose.animation.AnimatedVisibility( + visible = isQrVisible, + enter = slideInVertically( + initialOffsetY = { fullHeight -> fullHeight } + ) + fadeIn(), + exit = slideOutVertically( + targetOffsetY = { fullHeight -> fullHeight } + ) + fadeOut() + ) { + QRDisplay( + sharedScreenModel = sharedScreenModel, + fileRepository = solfaScreenModel.fileRepository + ) + } + + if (createMode) { + NewPartition( + onCreate = { partMtdata -> + solfaScreenModel.newSolfa(partMtdata) + sharedScreenModel.toggleCreateMode(!createMode) + sharedScreenModel.toggleEditorMode(createMode) + if (!isAndroid) { + sharedScreenModel.toggleSourceMode() + } + }, + onDismissRequest = { + sharedScreenModel.toggleCreateMode(false) + }, + isAndroid + ) + } + + var isHovered by remember { mutableStateOf(false) } + val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp) + + androidx.compose.animation.AnimatedVisibility( + visible = !isEditMode && !isQrVisible && !createMode && !isExpanded, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 15.dp) + .offset(x = offsetX) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + when (event.type) { + PointerEventType.Enter -> isHovered = true + PointerEventType.Exit -> isHovered = false + } + } + } + } + ) { + IconButton( + onClick = { + if (isSearchActive) { + sharedScreenModel.showSearchMenu(false) + sharedScreenModel.updateSearchTxt("") + textInput = "" + } else { + sharedScreenModel.showSearchMenu(true) + scope.launch { + delay(100) + focusRequester.requestFocus() + } + } + }, + modifier = Modifier + .size(55.dp) + .alpha(0.6f) + .background( + color = MaterialTheme.colorScheme.tertiary, + shape = CircleShape + ) + ) { + Icon( + imageVector = if (isSearchActive) Icons.Default.Close else Icons.Default.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiary + ) + } + } + + androidx.compose.animation.AnimatedVisibility( + visible = isSearchActive && textInput.isNotEmpty(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth(0.85f) + .padding(top = 8.dp) + .padding(bottom = if (isAndroid && isNumericKeyboard) keyboardHeightDp else 0.dp) + ) { + Card( + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + modifier = Modifier.fillMaxHeight() + ) { + val sortedSongs = remember(filteredSongs) { + filteredSongs.sortedBy { item -> + item.path.startsWith("assets://") + } + } + LazyColumn(Modifier.fillMaxSize()) { + if (sortedSongs.isEmpty() && textInput.isNotEmpty()) { + item { + ListItem( + headlineContent = { + Text( + text = "Partition non trouvée pour \"$textInput\"", + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Medium, + fontSize = 14.sp + ) + }, + supportingContent = { + Text( + text = "Essayez un autre.", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + leadingContent = { + Icon( + imageVector = Icons.Default.SearchOff, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + ) + } + } + itemsIndexed(sortedSongs, key = { _, item -> item.path }) { index, item -> + val isFavorite = favoritePaths.contains(item) + ListItem( + leadingContent = { + Icon( + imageVector = if (item.path.startsWith("assets://")) Icons.Default.ArrowDropDownCircle else Icons.Default.Folder, + contentDescription = null, + modifier = Modifier.size(30.dp) + ) + }, + headlineContent = { Text(item.title) }, + supportingContent = { Text(item.contentTitle, maxLines = 1) }, + trailingContent = { + IconButton( + onClick = { sharedScreenModel.toggleFavorite(item.path) }, + modifier = Modifier.size(25.dp) + ) { + Icon( + imageVector = Icons.Default.Star, + contentDescription = null, + tint = if (!isFavorite) Color.LightGray else Color( + 0xFFFFD700 + ) + ) + } + }, + modifier = Modifier.clickable { + sharedScreenModel.updateSearchTxt("") + sharedScreenModel.reset() + solfaScreenModel.loadFromFile(item.path) + sharedScreenModel.stopMidi() + sharedScreenModel.seekTo(0f) + sharedScreenModel.setTranspositionInterval(0) + }) + HorizontalDivider() } } } } - ) { - IconButton( - onClick = { - if (isSearchActive) { - sharedScreenModel.showSearchMenu(false) - sharedScreenModel.updateSearchTxt("") - textInput = "" - } else { - sharedScreenModel.showSearchMenu(true) - scope.launch { - delay(100) - focusRequester.requestFocus() - } - } - }, - modifier = Modifier - .size(55.dp) - .alpha(0.6f) - .background( - color = MaterialTheme.colorScheme.tertiary, - shape = CircleShape + + // --- CLAVIER NUMÉRIQUE PERSONNALISÉ (Sur couche supérieure) --- + androidx.compose.animation.AnimatedVisibility( + visible = isSearchActive && isAndroid && isNumericKeyboard, + enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }) + fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter) + ) { + CustomKeyboard( + modifier = Modifier.onGloballyPositioned { coordinates -> + keyboardHeightDp = with(density) { coordinates.size.height.toDp() } + }, + onDigitClick = { digit -> + textInput += digit + sharedScreenModel.updateSearchTxt(textInput) + }, + onDeleteClick = { + if (textInput.isNotEmpty()) { + textInput = textInput.dropLast(1) + sharedScreenModel.updateSearchTxt(textInput) + } + }, + onClearClick = { + textInput = "" + sharedScreenModel.updateSearchTxt("") + }, + onPrefixClick = { prefix -> + sharedScreenModel.updateSearchTxt(prefix) + }, + isFullScreen = isFullScreenEnabled ) + } + + } + + AnimatedVisibility( + visible = solfaKeyboardState.isVisible && solfaKeyboardState.isDialogActive && isAndroid, + enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }) + fadeOut() ) { - Icon( - imageVector = if (isSearchActive) Icons.Default.Close else Icons.Default.Search, - contentDescription = null, - tint = MaterialTheme.colorScheme.onTertiary + SolfaKeyboard( + currentVoiceIndex = solfaKeyboardState.activeVoiceIndex, + currentValue = solfaKeyboardState.currentText, + onNoteClick = { note -> solfaKeyboardState.onNoteInput?.invoke(note) }, + onTabClick = { solfaKeyboardState.onTabNextVoice?.invoke() }, + onBackspaceClick = { solfaKeyboardState.onBackspace?.invoke() }, + onCloseClick = { solfaKeyboardState.onCloseDialog?.invoke() }, + onBuildClick = { solfaKeyboardState.onBuildEdit?.invoke() }, + onCustomSymbolClick = { symbol -> solfaKeyboardState.onCustomSymbol?.invoke(symbol) }, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.3f) ) } } - AnimatedVisibility( - visible = isSearchActive && textInput.isNotEmpty(), - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxWidth(0.85f) - .padding(top = 8.dp) - ) { - Card( - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - ) { - val sortedSongs = remember(filteredSongs) { - filteredSongs.sortedBy { item -> - item.path.startsWith("assets://") - } - } - LazyColumn(Modifier.fillMaxSize()) { - if (sortedSongs.isEmpty() && textInput.isNotEmpty()) { - item { - ListItem( - headlineContent = { - Text( - text = "Partition non trouvée pour \"$textInput\"", - color = MaterialTheme.colorScheme.error, - fontWeight = FontWeight.Medium, - fontSize = 14.sp - ) - }, - supportingContent = { - Text( - text = "Essayez un autre.", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - leadingContent = { - Icon( - imageVector = Icons.Default.SearchOff, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } - ) - } - } - itemsIndexed(sortedSongs, key = { _, item -> item.path }) { index, item -> - val isFavorite = favoritePaths.contains(item) - ListItem( - leadingContent = { - Icon( - imageVector = if (item.path.startsWith("assets://")) Icons.Default.ArrowDropDownCircle else Icons.Default.Folder, - contentDescription = null, - modifier = Modifier.size(30.dp) - ) - }, - headlineContent = { Text(item.title) }, - supportingContent = { Text(item.contentTitle, maxLines = 1) }, - trailingContent = { - IconButton( - onClick = { sharedScreenModel.toggleFavorite(item.path) }, - modifier = Modifier.size(25.dp) - ) { - Icon( - imageVector = Icons.Default.Star, - contentDescription = null, - tint = if (!isFavorite) Color.LightGray else Color(0xFFFFD700) - ) - } - }, - modifier = Modifier.clickable { - sharedScreenModel.updateSearchTxt("") - sharedScreenModel.reset() - solfaScreenModel.loadFromFile(item.path) - sharedScreenModel.stopMidi() - sharedScreenModel.seekTo(0f) - sharedScreenModel.setTranspositionInterval(0) - }) - HorizontalDivider() - } - } - } - } } } } + } }) } 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 86dab1a..4ad72e8 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -59,6 +59,7 @@ fun SimpleDrawerContent( val internalList by sharedScreenModel.internalItems.collectAsState() val externalList by sharedScreenModel.externalItems.collectAsState() val playList by sharedScreenModel.playlistItems.collectAsState() + val favoriteLists by sharedScreenModel.playlistItems.collectAsState() val listState = rememberLazyListState() var internalExpanded by remember { mutableStateOf(false) } @@ -154,7 +155,6 @@ fun SimpleDrawerContent( if (internalExpanded) { items(internalList) { item -> val isSelected = item.path == activePath - val favoriteLists by sharedScreenModel.playlistItems.collectAsState() val isFavorite = favoriteLists.contains(item) NavigationDrawerItem( @@ -207,7 +207,6 @@ fun SimpleDrawerContent( if (externalExpanded) { items(externalList) { item -> val isSelected = item.path == activePath - val favoriteLists by sharedScreenModel.playlistItems.collectAsState() val isFavorite = favoriteLists.contains(item) if (item.path != "") { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt new file mode 100644 index 0000000..50b6f56 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt @@ -0,0 +1,296 @@ +package mg.dot.feufaro.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardBackspace +import androidx.compose.material.icons.automirrored.filled.KeyboardTab +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import mg.dot.feufaro.solfa.Transpose + +enum class OctaveMode { + LOWER, NORMAL, UPPER +} +val SOLFA_NOTES = Transpose.noteToNumber + +@Composable +fun SolfaKeyboard( + currentVoiceIndex: Int, + currentValue: String = "", + onNoteClick: (String) -> Unit, + onTabClick: () -> Unit, + onBackspaceClick: () -> Unit, + onBuildClick: () -> Unit, + onCloseClick: () -> Unit, + onCustomSymbolClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + var selectedOctave by remember { mutableStateOf("") } + val voiceLabels = listOf("S", "A", "T", "B") + + Surface( + modifier = modifier + .fillMaxWidth() + .background(Color(0xFF232323)), + color = Color(0xFF232323), + tonalElevation = 8.dp + ) { + Row( + modifier = Modifier + .padding(6.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onTabClick, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp), + modifier = Modifier.height(36.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.KeyboardTab, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.width(2.dp)) + Text( + text = voiceLabels.getOrElse(currentVoiceIndex) { "S" }, + fontSize = 11.sp, + fontWeight = FontWeight.Bold + ) + } + + BasicTextField( + value = currentValue, + onValueChange = {}, + readOnly = true, + textStyle = TextStyle( + color = Color.White, + fontSize = 13.sp, + fontWeight = FontWeight.Medium + ), + modifier = Modifier + .weight(1.5f) + .height(36.dp) + .background(Color.Black.copy(alpha = 0.4f), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 8.dp) + ) + + OutlinedButton( + onClick = { + onNoteClick(",") + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF383838) + ), + modifier = Modifier.height(36.dp).weight(0.7f) + ) { + Text(",", fontSize = 14.sp, fontWeight = FontWeight.Bold) + } + OutlinedButton( + onClick = { + onNoteClick("'") + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF383838) + ), + modifier = Modifier.height(36.dp) + ) { + Text("'", fontSize = 14.sp, fontWeight = FontWeight.Bold) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Spacer(modifier = Modifier.weight(0.5f)) + KeyButton("di", Modifier.weight(1f)) { onNoteClick("di") } + KeyButton("ri", Modifier.weight(1f)) { onNoteClick("ri") } + Spacer(modifier = Modifier.weight(1.3f)) + KeyButton("fi", Modifier.weight(1f)) { onNoteClick("fi") } + KeyButton("si", Modifier.weight(1f)) { onNoteClick("si") } + KeyButton("ta", Modifier.weight(1f)) { onNoteClick("ta") } + Spacer(modifier = Modifier.weight(0.5f)) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + val diatonicNotes = listOf("d", "r", "m", "f", "s", "l", "t") + for (note in diatonicNotes) { + KeyButton(note, Modifier.weight(1f)) { onNoteClick(note) } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + KeyButton("―", Modifier.weight(1f)) { onCustomSymbolClick("-") } + KeyButton("• ,", Modifier.weight(1f)) { onCustomSymbolClick(";") } + KeyButton("•", Modifier.weight(0.5f)) { onCustomSymbolClick(".") } + KeyButton(",", Modifier.weight(0.5f)) { onCustomSymbolClick(" ,") } + KeyButton("Espace", Modifier.weight(1.6f)) { onCustomSymbolClick(" ") } + KeyButton("(", Modifier.weight(0.6f)) { onCustomSymbolClick("(") } + KeyButton(")", Modifier.weight(0.6f)) { onCustomSymbolClick(")") } + KeyButton("<", Modifier.weight(0.5f)) { onCustomSymbolClick("<") } + KeyButton(">", Modifier.weight(0.5f)) { onCustomSymbolClick(">") } + KeyButton("z", Modifier.weight(1f)) { onNoteClick("z") } + } + } + + Column( + modifier = Modifier + .width(48.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Button( + onClick = onCloseClick, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.errorContainer), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + tint = MaterialTheme.colorScheme.onErrorContainer + ) + } + + Button( + onClick = onBackspaceClick, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4A4A4A)), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.KeyboardBackspace, + contentDescription = null, + tint = Color.White + ) + } + + Button( + onClick = onBuildClick, + modifier = Modifier + .fillMaxWidth() + .weight(1.2f), + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = null, + tint = Color.White + ) + } + } + } + } +} + +@Composable +private fun KeyButton( + text: String, + modifier: Modifier = Modifier, + containerColor: Color = Color(0xFF383838), + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = modifier.height(49.dp), + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors(containerColor = containerColor), + contentPadding = PaddingValues(0.dp) + ) { + Text( + text = text, + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } +} + +class SolfaKeyboardState { + var isVisible by mutableStateOf(false) + private set + + var isDialogActive by mutableStateOf(false) + var activeVoiceIndex by mutableStateOf(0) + private set + + var currentText by mutableStateOf("") + var onNoteInput: ((String) -> Unit)? = null + var onBackspace: (() -> Unit)? = null + var onCloseDialog: (() -> Unit)? = null + var onBuildEdit: (() -> Unit)? = null + var onCustomSymbol: ((String) -> Unit)? = null + var onTabNextVoice: (() -> Unit)? = null + var onMarker: (() -> Unit)? = null + + fun show( + initialVoice: Int = 0, + initialValue: String = "", + onNote: (String) -> Unit, + onDelete: () -> Unit, + onSymbol: (String) -> Unit, + onBuild: () -> Unit, + onClose: () -> Unit, + onTab: () -> Unit + ) { + if (!isDialogActive) return + activeVoiceIndex = initialVoice + onNoteInput = onNote + onBackspace = onDelete + onCustomSymbol = onSymbol + onCloseDialog = onClose + onBuildEdit = onBuild + onTabNextVoice = onTab + isVisible = true + } + + fun hide() { + isVisible = false + } +} + +@Composable +fun rememberSolfaKeyboardState() = remember { SolfaKeyboardState() } + +val LocalSolfaKeyboardState = staticCompositionLocalOf { + error("Aucun SolfaKeyboardState fourni") +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index 729eb92..4195a9d 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -612,7 +612,6 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode _dcDone.value = false _dsDone.value = false _sourceModeState.value = false - setTranspositionInterval(0) try { val midiFileName = fileRepository.getFileName(newMidiFile) println("Opening xx129 $midiFileName")