989 lines
No EOL
43 KiB
Kotlin
989 lines
No EOL
43 KiB
Kotlin
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 = "N°$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()
|
||
} |