feufaro/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt

989 lines
No EOL
43 KiB
Kotlin
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
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
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextRange
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.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.solfa.TUOEditState
import mg.dot.feufaro.solfa.TimeUnitObject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TUODetailDialog(
editState: TUOEditState,
tuo: TimeUnitObject,
currentStanza: Int,
menuPosition: IntOffset,
globalIndex: Int,
isEditable: Boolean,
canAdd: Boolean,
onIndexChanged: (Int) -> Unit,
onDismiss: () -> Unit,
onSave: (TUOEditState) -> Unit
) {
var currentIndex by remember(globalIndex) { mutableStateOf(globalIndex) }
val notes = remember {
mutableStateMapOf<Int, String>().apply {
putAll(editState.notesByVoice)
}
}
val originalNotes = mutableStateMapOf<Int, String>().apply {
putAll(editState.notesByVoice)
}
val originalLyricsByStz = mutableStateMapOf<Int, String>().apply {
putAll(editState.lyricsByStanza)
}
var templateFragment by remember { mutableStateOf(editState.templateFragment) }
var newSep by remember { mutableStateOf(editState.sep) }
var marker by remember { mutableStateOf(editState.marker) }
val initialSep = remember { editState.sep }
val initialMarker = remember { editState.marker }
LaunchedEffect(notes.toMap()) {
val tempState = TUOEditState(
tuoIndex = editState.tuoIndex,
notesByVoice = notes.toMap(),
originalNotes = editState.originalNotes,
lyricsByStanza = editState.lyricsByStanza,
originalLyricsByStanza = editState.originalLyricsByStanza,
templateFragment = templateFragment,
marker = editState.marker
)
// Màj auto template
val newTemplate = getGlobalTemplate(tempState)
if (newTemplate != templateFragment) {
templateFragment = newTemplate
}
}
val result = remember {
mutableStateListOf<String>().apply {
val existing = editState.lyricsByStanza[currentStanza] ?: ""
if (existing.isEmpty() && canAdd) add("_") else add(existing)
}
}
val lyricsLines = remember(editState.lyricsByStanza) {
mutableStateListOf<String>().apply {
addAll(editState.lyricsByStanza.values.toList())
}
}
val editedLyricsMap = remember {
mutableStateMapOf<Int, String>().apply {
putAll(editState.lyricsByStanza)
}
}
var canAddMark = mutableStateOf(false)
Popup(
offset = menuPosition,
onDismissRequest = onDismiss,
properties = PopupProperties(
focusable = true,
dismissOnBackPress = true,
dismissOnClickOutside = true
),
) {
Surface(
modifier = Modifier
.widthIn(max=125.dp)
.heightIn(max = 400.dp),
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp).fillMaxWidth()) {
Row (
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Modif silence duration
if (currentIndex == 0) {
val silentDurationOrder = listOf(
"0", "4", "8", "A", "C", "E", "G", "K", "O", "S", "W"
)
Row(
modifier = Modifier.width(125.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(
onClick = {
val currentIdx = silentDurationOrder.indexOf(editState.silentDuration)
val newIdx = if (currentIdx > 0) currentIdx - 1 else 0
val newDuration = silentDurationOrder[newIdx]
val state = TUOEditState(
tuoIndex = globalIndex,
notesByVoice = notes.toMap(),
originalNotes = originalNotes.toMap(),
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
lyricsByStanza = mutableMapOf(currentStanza to lyricsLines.joinToString(" ")),
templateFragment = templateFragment,
marker = if(marker.isEmpty()) "_" else marker,
silentDuration = newDuration
)
onSave(state)
},
enabled = silentDurationOrder.indexOf(editState.silentDuration) >= 0,
modifier = Modifier.size(20.dp)
) {
Icon(
imageVector = Icons.Default.KeyboardDoubleArrowLeft,
contentDescription = "Diminuer",
tint = if (silentDurationOrder.indexOf(editState.silentDuration) > 0) Color.LightGray else Color.DarkGray
)
}
Text(
text = editState.silentDuration,
color = Color.White,
style = MaterialTheme.typography.bodyLarge
)
IconButton(
onClick = {
val currentIdx = silentDurationOrder.indexOf(editState.silentDuration)
val newIdx = if (currentIdx != -1 && currentIdx < silentDurationOrder.lastIndex) currentIdx + 1 else currentIdx
val newDuration = silentDurationOrder[newIdx]
val state = TUOEditState(
tuoIndex = globalIndex,
notesByVoice = notes.toMap(),
originalNotes = originalNotes.toMap(),
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
lyricsByStanza = mutableMapOf(currentStanza to lyricsLines.joinToString(" ")),
templateFragment = templateFragment,
marker = if(marker.isEmpty()) "_" else marker,
silentDuration = newDuration
)
onSave(state)
},
enabled = silentDurationOrder.indexOf(editState.silentDuration) < silentDurationOrder.lastIndex,
modifier = Modifier.size(20.dp)
) {
Icon(
imageVector = Icons.Default.KeyboardDoubleArrowRight,
contentDescription = "Augmenter",
tint = if (silentDurationOrder.indexOf(editState.silentDuration) < silentDurationOrder.lastIndex) Color.LightGray else Color.DarkGray
)
}
}
}
if (currentIndex > 0) {
IconButton(
onClick = {
currentIndex--
onIndexChanged(currentIndex)
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Précédent",
tint = Color.White
)
}
Text(
text = "$currentIndex",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
IconButton(
onClick = {
currentIndex++
onIndexChanged(currentIndex)
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Suivant",
tint = Color.White
)
}
}
Spacer(modifier = Modifier.height(2.dp))
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
// --- SECTION TEMPLATE ---
Row(horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) {
if(globalIndex != 0 && templateFragment != "-") {
Column(
modifier = Modifier.width(20.dp)
) {
val isSeparator = newSep == "/"
val tooltipText = if (isSeparator) {
"Enlever la barre de mesure"
} else {
"Ajouter une barre de mesure"
}
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip(
containerColor = Color.DarkGray,
contentColor = Color.White
) {
Text(text = tooltipText, fontSize = 12.sp)
}
},
state = rememberTooltipState()
) {
if(isSeparator) {
IconButton(
modifier = Modifier.size(20.dp),
onClick = {
newSep = ""
}) {
Text(
text = "/",
color = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
} else {
IconButton(
modifier = Modifier.size(20.dp),
onClick = {
newSep = "/"
}) {
Icon(
imageVector = Icons.Default.Add,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = null
)
}
}
}
}
}
Column(
modifier = Modifier.weight(1f)
) {
MyTextEditField(
value = templateFragment,
customFontSize = 14.sp,
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f),
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isEditable = false,
isAddable = false,
onValueChng = { templateFragment = it }
)
}
if(!editState.marker.isNullOrEmpty() || canAddMark.value) {
Column(
modifier = Modifier.weight(1f)
) {
MyTextEditField(
value = marker,
customFontSize = 14.sp,
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f),
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isEditable = isEditable,
isAddable = canAdd,
funTransform = ::transformMarkerInput,
onValueChng = { marker = it }
)
}
} else {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip(
containerColor = Color.DarkGray,
contentColor = Color.White
) {
Text(text = "Ajouter une marqueur", fontSize = 12.sp)
}
},
state = rememberTooltipState()
) {
IconButton(
modifier = Modifier.size(30.dp),
onClick = {
canAddMark.value = true
}) {
Icon(
imageVector = Icons.Default.Add,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = null
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
val activeMessages = (0..3).mapNotNull { voice ->
val currentNote = notes[voice] ?: ""
val validation = validateMusicalInput(currentNote, templateFragment)
if (validation.message.isNotEmpty() && currentNote.isNotEmpty()) validation else null
}
if (activeMessages.isNotEmpty()) {
val firstValidation = activeMessages.first()
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
shape = RoundedCornerShape(8.dp),
color = if (firstValidation.isValid) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f),
shadowElevation = 2.dp
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = firstValidation.message,
color = if (firstValidation.isValid) Color.White else Color.Black,
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}
}
// --- SECTION NOTES ---
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
(0..3).forEach { voice ->
val currentNote = notes[voice] ?: ""
val validation = validateMusicalInput(currentNote, templateFragment)
Box(modifier = Modifier.wrapContentSize()
) {
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
}
)
}
}
}
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
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 2.dp)
) {
Column(
modifier = Modifier.fillMaxWidth(0.8f)
) {
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
)
}
}
}
}
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)
) {
Icon(
Icons.Default.Build,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f)
)
}
}
}
}
}
}
}
private fun unpackLyrics(lyrics: String): String {
val comments = Solfa.REGEX_COMMENT.findAll(lyrics)
val commentsIterator = comments.iterator()
val loadedLyrics = lyrics
.replace(Solfa.REGEX_LYRICS_REPETITION) { matchResult ->
val repeating = matchResult.destructured.match.groupValues[1]
"_".repeat(repeating.toString().toInt())
}
.replace(Regex("(?<![\\?:,\\.; ])_"), "-_")
.replace(Regex("_-(?=_)"), "_")
.replace(" -_", "_")
.replace("--_", "-_")
.replace(Regex("_$"), "")
val lyricsFinal = Solfa.REGEX_COMMENT.replace(loadedLyrics) { matchResult ->
commentsIterator.next().value
}
return lyricsFinal
}
private fun smartYLyrics(lyrics: String): String {
val comments = Solfa.REGEX_COMMENT.findAll(lyrics)
val commentsIterator = comments.iterator()
val loadedLyrics = lyrics
.replace(Solfa.REGEX_VOWELS_STAGE1, "$0_")
.replace(Solfa.REGEX_VOWELS_STAGE2, "$1_")
.replace(Solfa.REGEX_VOWELS_STAGE3, "$1_")
.replace(" ", " _")
.replace("_\\ _", " ")
.replace("_\\", "")
.replace("_0", "")
.replace(Solfa.REGEX_MALAGASY_MN, "$1$2_$3")
.replace(Solfa.REGEX_MALAGASY_MN_STAGE2, "$1-_")
.replace("_n'", "n'_")
val lyricsFinal = Solfa.REGEX_COMMENT.replace(loadedLyrics) { matchResult ->
commentsIterator.next().value
}
return unpackLyrics(lyricsFinal)
}
public fun transformLyricsInput(input: String, template: String): String {
val templateCount = template.count { it.isLetter() && it.lowercaseChar() != 'z' }
val processed = smartYLyrics(input)
val currentSyllables = processed.split("_").filter { it.isNotEmpty() }
val currentCount = currentSyllables.size
// println("\n\n\tDEBUG [Start] Input: '$input' | TemplateCount: $templateCount | Found: $currentCount ${currentSyllables.joinToString("|")}")
// println("DEBUG [Segments]: $currentSyllables")
val rawResult = if (currentCount <= templateCount) {
// println("DEBUG [Status]: Pas de fusion nécessaire.")
input
} else {
val parts = currentSyllables.toMutableList()
val excess = currentCount - templateCount
// println("DEBUG [Fusion]: Besoin de fusionner $excess fois.")
for (i in 0 until excess) {
val idx = parts.size - 2/* - i*/
if (idx >= 0) {
// println("DEBUG [Loop $i]: Fusion de l'index $idx avec ${idx + 1}")
// println("DEBUG [Avant]: '${parts[idx]}' et '${parts[idx + 1]}'")
val part1 = parts[idx].trimEnd()
val part2 = parts[idx + 1].trimStart()
val cleanPart1 = part1.replace("-", "")
val lastChar = if (cleanPart1.isNotEmpty()) cleanPart1.last() else ' '
val endsWithVowel = "aeiouyòàéìỳAEIOUY".contains(lastChar)
val isVowelConsonantPair = endsWithVowel && part2.length == 1 && !"aeiouyòàéìỳAEIOUY".contains(part2.first())
if (part2.isNotEmpty()) {
if (isVowelConsonantPair) {
parts[idx] = part1 + part2
} else {
parts[idx] = part1 + "\\ " + part2
}
} else {
parts[idx] = part1 + "\\"
}
parts.removeAt(idx + 1)
}
// println("DEBUG [Après]: Résultat index $idx -> '${parts}'")
}
val result = parts.joinToString("")
// println("DEBUG [Final]: '$result'")
result
}
val REGEX_MALAGASY_MN_OUTPUT = Regex("-([mn])")
val correctedResult = rawResult.replace(REGEX_MALAGASY_MN_OUTPUT, "$1-")
if (correctedResult != rawResult) {
// println("DEBUG [Correction MN]: '$rawResult' -> '$correctedResult'")
}
return correctedResult
}
private fun transformMusicalInput(input: String): String {
return input.lowercase()
.replace(";", "• ,")
.replace(".", "")
.replace("-", "")
.replace("(?<=[a-z])[']".toRegex(), "¹")
.replace("(?<=[a-z])[,]".toRegex(), "")
.replace("¹¹", "²")
.replace("¹'", "²")
.replace("²¹", "³")
.replace("²'", "³")
.replace("³'", "")
.replace("³¹", "")
.replace("₁,", "")
.replace("₁₁", "")
.replace("₂,", "")
.replace("₂₁", "")
.replace("₃,", "")
.replace("₃₁", "")
.replace("'", "¹")
}
private fun transformMarkerInput(input: String): String {
return input
.replace("dc", "DC")
.replace("ds", "DS")
.replace(".)", "\uD834\uDD10")
}
@Composable
fun MyTextEditField(
value: String,
customFontSize: TextUnit,
font: FontFamily? = FontFamily.SansSerif,
color: Color,
customPadding: Dp,
customBrush: Brush,
isEditable: Boolean,
isAddable: Boolean,
isWarn: Boolean? = false,
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)
}
}
val textToShow = if (isAddable && value == "_") "" else textFieldValueState.text
BasicTextField(
value = textFieldValueState,
onValueChange = { newValue ->
if (isEditable || isAddable) {
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
)
onValueChng(transformedText)
}
},
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Ascii
),
textStyle = TextStyle(
color = color,
fontSize = customFontSize,
fontFamily = font
),
readOnly = !(isEditable || isAddable),
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
)
private fun validateMusicalInput(input: String, template: String): ValidNoteResult {
if (template.isEmpty()) return ValidNoteResult(false, "")
/* Ajout > */
val matchAddNote = Solfa.REGEX_NOTE_TOADD.find(input)
if (matchAddNote != null) {
val addedNote = matchAddNote.value.replace(">", "")
val inserted = matchAddNote.groupValues[1] + matchAddNote.groupValues[2]
val shifted = matchAddNote.groupValues[3] + matchAddNote.groupValues[4]
return ValidNoteResult(
isValid = true,
message = "+ $inserted$shifted"
)
}
/* Suppress < */
if (input.contains("<")) {
val matchDropNote = Solfa.REGEX_NOTE_TODROP.find(input)
val isValidDrop = matchDropNote != null/* && input.endsWith("<")*/
if (isValidDrop) {
val noteBase: String = matchDropNote.groupValues[1]
val octave: String = matchDropNote.groupValues[2]
val droppedNote = noteBase + octave
return ValidNoteResult(
isValid = true,
message = "$droppedNote ⌫ ?"
)
} else {
return ValidNoteResult(
isValid = false,
message = "< incorrect"
)
}
}
val cleanInput = input
.replace("(", "")
.replace(")", "")
.replace(" ", "")
.replace(Solfa.REGEX_NOTE_TOADD, "$3$4")
.replace(Solfa.REGEX_NOTE_TODROP, "$1$2")
val cleanTemplate = template.replace("(", "").replace(")", "")
val regexPattern = buildString {
cleanTemplate.forEachIndexed { index, char ->
when (char) {
in 'A'..'Y', in 'a'..'y' -> {
append("(?:―|di|ri|fi|si|ta|[drmfsltz])[₄₃₂₁¹²³⁴]*")
}
'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 if (isMatched) {
ValidNoteResult(isValid = true, message = "")
} else {
ValidNoteResult(isValid = false, message = "Erreur")
}
}
private fun validateLyricsInput(cleanInput: String, template: String): Boolean {
val vowels = "aeiouyòàéìỳAEIOUY"
val expectedSyllableCount = template.count { it.lowercaseChar() != 'z' && it.isLetter() }
val syllableRegex = Regex("[^aeiouyòàéìỳAEIOUY]*[aeiouyòàéìỳAEIOUY]+[^aeiouyòàéìỳAEIOUY]*", RegexOption.IGNORE_CASE)
val foundSyllables = syllableRegex.findAll(cleanInput).toList()
val rawSyllableCount = foundSyllables.size
val backslashCount = cleanInput.count { it == '\\' }
val actualSyllableCount = if (backslashCount > 0) {
(rawSyllableCount - backslashCount).coerceAtLeast(1)
} else {
rawSyllableCount
}
/*println("""
┌──────────────────────────────────────────────────────────
│ VALIDATION LYRICS
├──────────────────────────────────────────────────────────
│ Template : "$template"
│ Input : "$cleanInput"
│ Attendu : $expectedSyllableCount syllabes
│ Trouvé : $actualSyllableCount syllabes
└──────────────────────────────────────────────────────────
""".trimIndent())*/
return actualSyllableCount == expectedSyllableCount
}
public fun getGlobalTemplate(editState: TUOEditState): String {
// println("\n--- DÉBOGAGE getGlobalTemplate ---")
val voiceTemplates = (0..3).map { voiceIdx ->
val notes = editState.notesByVoice[voiceIdx] ?: ""
val filteredNotes = notes
.replace(Solfa.REGEX_NOTE_TOADD, "$3$4")
.replace("<", "")
val tpl = notesToTemplate(filteredNotes)
// println("Voix $voiceIdx | Notes: '$filteredNotes' -> Template: '$tpl'")
tpl
}
val rythmicTemplates = voiceTemplates.map { tpl -> tpl.replace(Regex("[()]"), "") }
val maxLen = rythmicTemplates.maxOfOrNull { it.length } ?: 1
// println("Longueur rythmique max détectée: $maxLen")
val openBefore = BooleanArray(maxLen)
val closeBefore = BooleanArray(maxLen)
val openAfter = BooleanArray(maxLen)
val closeAfter = BooleanArray(maxLen)
var openAtTheEnd = false
var closeAtTheEnd = false
voiceTemplates.forEach { tpl ->
var rythmicIdx = 0
for (i in tpl.indices) {
val char = tpl[i]
if (char == '(' || char == ')') {
val textAfter = tpl.substring(i + 1)
val hasRythmicAfter = textAfter.any { it != '(' && it != ')' }
if (hasRythmicAfter) {
if (rythmicIdx < maxLen) {
if (char == '(') openBefore[rythmicIdx] = true
if (char == ')') closeBefore[rythmicIdx] = true
}
} else {
if (rythmicIdx > 0) {
if (char == '(') openAfter[rythmicIdx - 1] = true
if (char == ')') closeAfter[rythmicIdx - 1] = true
} else {
if (char == '(') openAtTheEnd = true
if (char == ')') closeAtTheEnd = true
}
}
} else {
rythmicIdx++
}
}
}
val result = StringBuilder()
for (i in 0 until maxLen) {
if (openBefore[i]) result.append('(')
if (closeBefore[i]) result.append(')')
val chars = rythmicTemplates.mapNotNull { it.getOrNull(i) }.toSet()
val previousChars = if(i>0) rythmicTemplates.mapNotNull { it.getOrNull(i-1) }.toSet() else "".toSet()
val activeChars = rythmicTemplates.mapNotNull { it.getOrNull(i) }
val isAllHyphen = activeChars.isNotEmpty() && activeChars.all { it == '-' }
val combinedChar = when {
chars.contains('D') -> 'D'
isAllHyphen -> '-'
chars.contains('.') -> '.'
chars.contains(',') -> ','
previousChars.contains('.') && chars.contains(' ') -> 'z'
else -> ' '
}
if (combinedChar == ',' && result.isNotEmpty() && result.last() == ' ') {
result.deleteCharAt(result.length - 1)
}
// println("Pour $i = ${chars.toString()} on a > ${combinedChar.toString()}")
val lastChar = if (result.isNotEmpty()) result.last() else null
if (!((combinedChar == '.' && lastChar == '.') || (combinedChar == ',' && lastChar == ','))) {
result.append(combinedChar)
}
// println("all = ${result.toString()}")
if (openAfter[i]) result.append('(')
if (closeAfter[i]) result.append(')')
}
if (openAtTheEnd) result.append('(')
if (closeAtTheEnd) result.append(')')
// println("Template Global Final: '${result.toString()}'")
// println("----------------------------------\n")
return result.toString()
}
private fun notesToTemplate(notes: String): String {
if (notes.isEmpty()) return "z"
val cleaned = notes
.replace("", "-")
.replace("", ".")
.replace(Regex("[³₃²₂¹₁]"), "")
.replace("di", "D")
.replace("ri", "R")
.replace("fi", "F")
.replace("si", "S")
.replace("ta", "T")
val noteRegex = Regex("""([drmfsltDRFSTz][ia]?|[.,()\s-])""")
val template = StringBuilder()
val matches = noteRegex.findAll(cleaned)
for (match in matches) {
val value = match.value
when {
value.matches(Regex("[.,()-]")) -> template.append(value)
value.trim().isEmpty() -> template.append(" ")
else -> template.append("D")
}
}
return template.toString()
}