From 68dd6216b23fd36a9cf12ba328f36c02a05d92e8 Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Tue, 12 May 2026 15:29:52 +0300 Subject: [PATCH] Warn user if notes was not correspond to template on edit notes & autocomplete note on ui --- .../mg/dot/feufaro/solfa/TUODetailDialog.kt | 125 +++++++++++++---- .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 126 +++++++----------- .../feufaro/viewmodel/SharedScreenModel.kt | 2 +- 3 files changed, 148 insertions(+), 105 deletions(-) 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 aab1189..d2c4900 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -1,6 +1,7 @@ package mg.dot.feufaro import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -11,14 +12,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.Save -import androidx.compose.material.icons.filled.Savings import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor @@ -26,17 +23,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.toUpperCase -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.unit.* import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties -import mg.dot.feufaro.data.GridTUOData import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.solfa.TimeUnitObject @@ -168,23 +157,47 @@ fun TUODetailDialog( verticalArrangement = Arrangement.spacedBy(5.dp) ) { (0..3).forEach { voice -> + val currentNote = notes[voice] ?: "" + val isValid = validateMusicalInput(currentNote, templateFragment) + val tooltipState = rememberTooltipState(isPersistent = false) Row( - ) { - MyTextEditField( - value = notes[voice] ?: "", - customFontSize = 14.sp, - color = Color.White, - customPadding = 8.dp, - customBrush = SolidColor(Color.White), - isEditable = isEditable, - isAddable = canAdd, - funTransform = ::transformMusicalInput, - onValueChng = { newValue -> - notes[voice] = newValue - } - ) - + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + if (!isValid) { + PlainTooltip( + containerColor = Color(0xFFF59E0B), + contentColor = Color.Black + ) { + Text("Veuillez suivre ce format: $templateFragment") + } + } + }, + state = tooltipState + ) { + MyTextEditField( + value = currentNote ?: "", + customFontSize = 14.sp, + color = Color.White, + customPadding = 8.dp, + customBrush = SolidColor(Color.White), + isEditable = isEditable, + isAddable = canAdd, + isWarn = !isValid, + funTransform = ::transformMusicalInput, + onValueChng = { newValue -> + notes[voice] = newValue + } + ) + } + } + LaunchedEffect(isValid, currentNote) { + if (!isValid && currentNote.isNotEmpty()) { + tooltipState.show() + } else { + tooltipState.dismiss() + } } } } @@ -302,6 +315,7 @@ fun MyTextEditField( customBrush: Brush, isEditable: Boolean, isAddable: Boolean, + isWarn: Boolean? = false, funTransform: ((String) -> String)? = null, onValueChng: (String) -> Unit ) { @@ -328,7 +342,62 @@ fun MyTextEditField( modifier = Modifier .fillMaxWidth() .background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(4.dp)) + .border( + if (isWarn!!) 1.dp else 0.dp, + if (isWarn!!) Color(0xFFF59E0B) else Color.Transparent, + shape = RoundedCornerShape(4.dp) + ) .padding(customPadding), cursorBrush = customBrush ) +} + +fun validateMusicalInput(input: String, template: String): Boolean { + if (template.isEmpty()) return true + + val cleanInput = input.replace("(", "").replace(")", "").replace(" ", "") + val cleanTemplate = template.replace("(", "").replace(")", "") + + val regexPattern = buildString { + cleanTemplate.forEachIndexed { index, char -> + when (char) { + in 'A'..'Y' -> { + append("(?:―|di|ri|fi|si|ta|[drmfslt])[¹²³₁₂₃]*") + } + 'z' -> { + append(" ?") + } + + '.' -> append("•") + ',' -> append(",") + '-' -> { + val prev = if (index > 0) cleanTemplate[index - 1] else null + val next = if (index < cleanTemplate.lastIndex) cleanTemplate[index + 1] else null + + if (prev == '.' && next == ',') { + append("?[\\s]*") + } else { + append("―") + } + } + else -> append(Regex.escape(char.toString())) + } + } + } + + val regex = Regex("^$regexPattern$") + val isMatched = regex.matches(cleanInput) + + /*println(""" + ┌────────────────────────────────────────────────────────── + │ DÉBOGAGE VALIDATION + ├────────────────────────────────────────────────────────── + │ Template Origine : $template + │ ️ Regex Générée : ^$regexPattern$ + │ Input Reçu : "$input" + │ Résultat : ${if (isMatched) "VALIDE (OUI)" else "INVALIDE (NON)"} + └────────────────────────────────────────────────────────── + """.trimIndent())*/ + + return isMatched } \ No newline at end of file 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 06e519b..8ae6713 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBackIos import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Close @@ -576,7 +575,6 @@ fun LazyVerticalGridTUO( selectedTUO = null } - var showFullChord by remember { mutableStateOf(false) } val editMode by sharedScreenModel.modeEditor.collectAsState() Column( @@ -760,63 +758,6 @@ fun LazyVerticalGridTUO( } } } - - /*Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - measureTUOs.forEachIndexed { indexInMeasure, tuo -> - val chordMap = (1..4).mapNotNull { voice -> - val noteStr = tuo.tuNotes.getOrNull(voice)?.toString() - if (noteStr != null) voice to noteStr else null - }.toMap() - - val degreeName = remember(chordMap) { - if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeDegree(chordMap) else null - } - val chordName = remember(chordMap) { - if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, sharedScreenModel.songKey.value) else null - } - - Column( - modifier = Modifier - .width(gridWidthDp / gridColumnCount) - ) { - // 1. Définir une plage de tailles de police - val maxFontSize = 13.sp - val minFontSize = 8.sp - var currentFontSize by remember { mutableStateOf(maxFontSize) } - - TextButton( - onClick = { showFullChord = !showFullChord }, - contentPadding = PaddingValues(0.dp), - modifier = Modifier.height(24.dp).fillMaxWidth() // fillMaxWidth pour occuper toute la colonne - ) { - BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { - val textToDisplay = if (showFullChord) (chordName ?: "") else (degreeName ?: "") - - Text( - text = textToDisplay, - onTextLayout = { textLayoutResult -> - if (textLayoutResult.hasVisualOverflow && currentFontSize > minFontSize) { - currentFontSize = (currentFontSize.value * 0.9f).sp - } - }, - softWrap = false, // Empêche le retour à la ligne - maxLines = 1, - overflow = TextOverflow.Visible, - style = TextStyle( - color = if (showFullChord) Color(0xFF1B5E20) else Color(0xFF0D47A1), - fontWeight = FontWeight.Bold, - fontSize = currentFontSize // Utilisation de la taille dynamique[cite: 1] - ) - ) - } - } - } - } - }*/ Row(modifier = Modifier.fillMaxWidth()) { measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure @@ -837,11 +778,7 @@ fun LazyVerticalGridTUO( .combinedClickable( interactionSource = interactionSource, indication = LocalIndication.current, - onClick = { - if(!editMode) { - sharedScreenModel.seekToGrid(globalIndex) - } - } + onClick = { } ) .pointerInput(globalIndex) { detectTapGestures( @@ -851,6 +788,8 @@ fun LazyVerticalGridTUO( selectedTUO = oneTUO selectedIndex = globalIndex showContextualMenu = true + } else { + sharedScreenModel.seekToGrid(globalIndex) } } ) @@ -908,18 +847,7 @@ fun LazyVerticalGridTUO( },*/ notesByVoice = (0..3).associate { i -> val rawNote = oneTUO.tuNotes.getOrNull(i + 1)?.toString() ?: "" - val fixedNote = - if (!expectedTemplate) { - rawNote - } else { - val count = rawNote.count() - - if (/*count < 2 && */!rawNote.contains("•")) { - rawNote + "•―" - } else { - rawNote - } - } + val fixedNote = autoFixNote(rawNote, template) i to fixedNote }, lyricsByStanza = (1..currentStanza).associate { s -> @@ -1299,4 +1227,50 @@ fun EditorActionButtons(onUndo: () -> Unit, onBuild: () -> Unit, onSave: () -> U IconButton(onClick = onSave) { Icon(Icons.Default.Save, null, tint = Color(0xFF4CAF50)) } IconButton(onClick = onClose) { Icon(Icons.Default.Close, null, tint = Color.Red) } } +} + +fun autoFixNote(rawNote: String, template: String): String { + if (rawNote.isEmpty() || rawNote == "_") return rawNote + + val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*") + val actualNotes = noteRegex.findAll(rawNote).map { it.value }.toList() + + if (rawNote.contains("•") || rawNote.contains("―") || rawNote.contains(",")) { + val templateNotesCount = template.count { it in 'A'..'Y' } + if (actualNotes.size >= templateNotesCount) return rawNote + } + + val result = StringBuilder() + var noteIdx = 0 + + for (i in template.indices) { + val char = template[i] + when { + char in 'A'..'Y' -> { + if (noteIdx < actualNotes.size) { + result.append(actualNotes[noteIdx]) + noteIdx++ + } else { + result.append("―") + } + } + char == '-' -> { + val prev = if (i > 0) template[i - 1] else null + val next = if (i < template.lastIndex) template[i + 1] else null + if (prev == '.' && next == ',') result.append(" ") else result.append("―") + } + char == 'z' -> result.append(" ") + char == '.' -> { + val currentText = result.toString() + if (!currentText.endsWith("•")) result.append("•") + } + char == ',' -> { + val currentText = result.toString() + if (!currentText.endsWith(",")) result.append(",") + } + char == '(' -> result.append("(") + char == ')' -> result.append(")") + } + } + return result.toString() } \ 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 0ffc11a..a1c91cf 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -649,7 +649,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode _editModeState.value = false _midiMarkersList.value = emptyList() _tuoTimestamps.value = emptyList() - updateSearchTxt("") + _searchTitle.value = "" tempTimeUnitObjectList.clear() resetGridCount() }