Compare commits

...

3 commits

5 changed files with 221 additions and 172 deletions

View file

@ -1,15 +1,9 @@
package mg.dot.feufaro.solfa
import SharedScreenModel
import androidx.compose.runtime.remember
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.SaveSettings
import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.launchFilePicker
import mg.dot.feufaro.midi.MidiPitch
import mg.dot.feufaro.midi.MidiWriterKotlin
@ -450,11 +444,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
MODIF NOTES N1 N2 N3 N4
*/
// println("taille tA: ${templatArray.size} et le trgt $targetIdx")
val originalBlock = templatArray[targetIdx]
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
val deleteCount = originalBlock.replace(markerRegex, "").count { it.isLetter() }
val updatedNotes = updateSourceLines(lines, templatArray, targetIdx, deleteCount, editState)
val updatedNotes = updateSourceLines(lines, templatArray, targetIdx, editState)
// RESTAURATION DES TEMPLATES
if (templateString != "") {
@ -523,11 +513,10 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
lines: MutableList<String>,
fragments: List<String>,
targetIdx: Int,
deleteCount: Int,
editState: TUOEditState
): MutableList<String> {
/* Notes N1 N2 N3 N4 */
updateNotesInLines(lines, fragments, targetIdx, deleteCount, editState)
updateNotesInLines(lines, fragments, targetIdx, editState)
/* MODIF LYRICS */
//updateLyricsInLines(lines, fragments, targetIdx, deleteCount, editState)
return lines
@ -632,7 +621,6 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
lines: MutableList<String>,
fragments: List<String>,
targetIdx: Int,
deleteCount: Int,
editState: TUOEditState
) {
val notesByVoice = editState.notesByVoice
@ -647,7 +635,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val prefix = currentLine.substringBefore(":") + ":"
val noteBody = currentLine.substringAfter(":")
val hasAnchor = noteBody.contains("#")
val noteExpanded = expandNotes(noteBody)
val noteExpanded = expandNotes(noteBody.replace(Regex("\\s+"), ""))
// println("Notes===>$noteExpanded")
val newNot = notesByVoice[voiceNum] ?: ""
@ -666,10 +654,11 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val cleanNewNote = revertMusicalInput(newNote)
val regex = Regex("#\\S[',]*|\\s#\\S[',]*|/|\\(|\\)|[drmfsltDRFSTzw][0-9'¹²³⁴⁵₁₂₃₄₅,]*|[-.]")
val allTokens = regex.findAll(noteExpanded).map { it.value }.toList()
val regex = Regex("#\\S[',]*|\\s#\\S[',]*|/|\\(|\\)|[drmfsltDRFSTzw][0-9'¹²³⁴⁵₁₂₃₄₅,]*|[-.―•]")
val allTokens = regex.findAll(noteExpanded)
.map { it.value }
.filter { it != "." }
.toList()
val targetPointer = getNotePointer(fragments, targetIdx, currentLine)
@ -693,7 +682,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val isStructural = token.contains("#") || token == "/" || token == "(" || token == ")"
if (isStructural) {
// println(String.format("%-5d | %-10s | %-8s | %-12s | %-10s", i, token, "STRUCT", "-", "Keep"))
println(String.format("%-5d | %-10s | %-8s | %-12s | %-10s", i, token, "STRUCT", "-", "Keep"))
resultTokens.add(token)
i++
continue
@ -701,65 +690,62 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
if (currentLogicalIdx == targetPointer) {
val originalToken = allTokens[i]
var replacement = if (!hasAnchor) {
val (newBase, newLevel) = parseNoteAndOctave(revertMusicalInput(newNot))
val (oldBase, oldLevel) = parseNoteAndOctave(revertMusicalInput(oldNot))
val octaveDiff = newLevel - oldLevel
val (tokenBase, tokenLevel) = parseNoteAndOctave(originalToken)
newBase + formatOctave(tokenLevel + octaveDiff)
//println("Insertition Multiples : $newNot")
val rawUserTokens = regex.findAll(revertMusicalInput(newNot)).map { it.value }.toList()
/** Si y a '-' dans template inutile d' ajouter au notes */
val hasDashInTemplate = editState.templateFragment.contains("-")
val newUserTokens = if (hasDashInTemplate) {
rawUserTokens.filter { it.matches(Regex("[drmfsltDRFSTzw].*")) }
} else {
// SI y a '#'
val anchorMatch = Regex("#([drmfsltDRFST])[',]*").find(noteBody)
val anchorLevel = if (anchorMatch != null) {
val anchorStr = anchorMatch.value
anchorStr.count { it == '\'' } - anchorStr.count { it == ',' }
} else 0
rawUserTokens
}
//val newUserTokens = regex.findAll(revertMusicalInput(newNot)).map { it.value }.toList()
val oldUserTokens = regex.findAll(revertMusicalInput(oldNot)).map { it.value }.toList()
val newNotCleaned = revertMusicalInput(newNot)
val oldNotCleaned = revertMusicalInput(oldNot)
val firstNewNote = newUserTokens.firstOrNull { it.matches(Regex("[drmfsltDRFSTzw].*")) } ?: ""
val firstOldNote = oldUserTokens.firstOrNull { it.matches(Regex("[drmfsltDRFSTzw].*")) } ?: ""
val (newBase, newLevel) = parseNoteAndOctave(newNotCleaned)
val (oldBase, oldLevel) = parseNoteAndOctave(oldNotCleaned)
val (_, newLvl) = parseNoteAndOctave(firstNewNote)
val (_, oldLvl) = parseNoteAndOctave(firstOldNote)
val octaveDiff = newLvl - oldLvl
val sourceLevel = newLevel - anchorLevel
val formattedOctave = formatOctave(sourceLevel)
val finalPureNew = newBase + formattedOctave
val pureOldBase = oldBase + formatOctave(oldLevel - anchorLevel)
val oldTokensForThisFragment = regex.findAll(revertMusicalInput(oldNot))
.map { it.value }
.filter { it.matches(Regex("[drmfsltDRFSTzw].*|[-―]")) }
.toList()
val dynamicDeleteCount = oldTokensForThisFragment.size
// println("Ancre niveau: $anchorLevel | UI Level: $newLevel -> Source Level: $sourceLevel")
//
if (finalPureNew.startsWith(pureOldBase) && pureOldBase.isNotEmpty()) {
val addedPart = finalPureNew.substring(pureOldBase.length)
originalToken + addedPart
newUserTokens.forEach { ut ->
val processedToken = if (ut.matches(Regex("[drmfsltDRFSTzw].*"))) {
val (fileBase, fileLvl) = parseNoteAndOctave(allTokens[i])
val (uBase, _) = parseNoteAndOctave(ut)
uBase + formatOctave(fileLvl + octaveDiff)
} else {
finalPureNew
ut
}
// println("j'aoute $processedToken")
resultTokens.add(processedToken)
if (processedToken.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) {
println(String.format("NEW | %-10s | INSERT | %-12d | REPLACE", processedToken, currentLogicalIdx))
currentLogicalIdx++
}
}
// println(
// String.format(
// "%-5s | %-10s | %-8s | %-12s | %-10s",
// "NEW", replacement, "INSERT", currentLogicalIdx, "REPLACE ($originalToken)"
// )
// )
resultTokens.add(replacement)
var notesSkipped = 0
while (notesSkipped < deleteCount && i < allTokens.size) {
val nextToken = allTokens[i]
val isNextStructural =
nextToken.contains("#") || nextToken == "/" || nextToken == "(" || nextToken == ")"
if (!isNextStructural) {
notesSkipped++
var skipped = 0
while (skipped < dynamicDeleteCount && i < allTokens.size) {
val nextT = allTokens[i]
if (!nextT.contains("#") && nextT != "/" && nextT != "(" && nextT != ")") {
skipped++
} else if (hasAnchor) {
resultTokens.add(nextToken)
resultTokens.add(nextT) // Garder les ancres si on est en mode ancre
}
i++
}
currentLogicalIdx += deleteCount
} else {
// AFFICHAGE NOTE NORMALE
// println(
@ -804,7 +790,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
private fun revertMusicalInput(input: String): String {
return input
.replace("• ,", "")
.replace("", "")
.replace("", "")
.replace("", "-")
.replace("", "")
.replace("³", "'''")

View file

@ -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
}

View file

@ -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 ->
@ -1300,3 +1228,49 @@ fun EditorActionButtons(onUndo: () -> Unit, onBuild: () -> Unit, onSave: () -> U
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()
}

View file

@ -106,6 +106,20 @@ fun MainScreenWithDrawer(
val currentActiveFilePath = sharedScreenModel.activeFilePath.value
val fileName = currentActiveFilePath.substringAfterLast('/')
var sourceTitle = sharedScreenModel.songTitle.value
val originalPath = remember(sourceTitle) {
if (!currentActiveFilePath.contains("_tmp") && !currentActiveFilePath.contains("/tmp/")) {
currentActiveFilePath
} else {
currentActiveFilePath
}
}
var sourceContent = sharedScreenModel.fileContent.value ?: ""
var codeContent by remember { mutableStateOf(sourceContent?: "") }
LaunchedEffect(sourceContent) {
codeContent = sourceContent
}
val saveLauncher = rememberFileSaveLauncher { chosenPath ->
if (chosenPath != null) {
scope.launch(Dispatchers.IO) {
@ -246,7 +260,13 @@ fun MainScreenWithDrawer(
) {
FloatingActionButton(
onClick = {
//sharedScreenModel.
scope.launch {
codeContent = sourceContent
withContext(Dispatchers.Main) {
solfaScreenModel.loadExternalFile(originalPath)
}
}
}, modifier = Modifier.alpha(0.45f)
) {
Icon(

View file

@ -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()
}