Add Edit Mode With source line & on UI(Only notes)

This commit is contained in:
hasinarak3@gmail.com 2026-05-08 16:30:41 +03:00
parent 49186a76c8
commit 410046a3cc
15 changed files with 2370 additions and 321 deletions

View file

@ -30,6 +30,10 @@ class MainActivity : ComponentActivity() {
1 1
) )
enableEdgeToEdge() enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setFilePickerActivity(this) setFilePickerActivity(this)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)

View file

@ -399,7 +399,7 @@ actual fun rememberPdfExportAction(
nbStanza, songKey, songAut, songComp, songRythm nbStanza, songKey, songAut, songComp, songRythm
) )
val chosenPath = withContext(Dispatchers.Main) { val chosenPath = withContext(Dispatchers.Main) {
fileRepository.pickSavePath(computedFileName) fileRepository.pickSavePath(computedFileName, "Exporter en PDF")
} }
if (chosenPath != null) { if (chosenPath != null) {
fileRepository.saveLocalFile(chosenPath, pdfBytes) fileRepository.saveLocalFile(chosenPath, pdfBytes)

View file

@ -2,8 +2,8 @@
"themeMode": "DARK", "themeMode": "DARK",
"fontSize": 18.5, "fontSize": 18.5,
"playlist": [ "playlist": [
"assets://a.txt",
"assets://ffpm-617.txt", "assets://ffpm-617.txt",
"assets://a.txt",
"assets://ews-127.txt", "assets://ews-127.txt",
"assets://ffpm-521.txt", "assets://ffpm-521.txt",
"assets://ews-126.txt", "assets://ews-126.txt",

View file

@ -1,23 +1,38 @@
package mg.dot.feufaro package mg.dot.feufaro
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.List
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.material3.IconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.ui.unit.TextUnitType import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
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.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable @Composable
fun ContextualMenu (onMenuItemClick: (String) -> Unit){ fun ContextualMenu (onMenuItemClick: (String) -> Unit){
Column( Column(
@ -32,7 +47,7 @@ fun ContextualMenu (onMenuItemClick: (String) -> Unit){
} }
@Composable @Composable
fun MenuItem(icon: androidx.compose.ui.graphics.vector.ImageVector, text: String, onClick: () -> Unit) { fun MenuItem(icon: ImageVector, text: String, onClick: () -> Unit) {
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) { Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
Icon(icon, contentDescription = text, tint = Color.White) Icon(icon, contentDescription = text, tint = Color.White)

View file

@ -13,13 +13,21 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@ -50,6 +58,8 @@ import cafe.adriel.voyager.koin.koinScreenModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mg.dot.feufaro.data.GridTUOData import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.solfa.ColorPrefixB
import mg.dot.feufaro.solfa.EditSourceCompose
import mg.dot.feufaro.ui.MyVerticalScrollbar import mg.dot.feufaro.ui.MyVerticalScrollbar
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
import java.io.ObjectStreamException import java.io.ObjectStreamException
@ -64,7 +74,7 @@ object ScreenSolfa : Screen {
val tuoList by sharedScreenModel.tuoList.collectAsState() val tuoList by sharedScreenModel.tuoList.collectAsState()
val measure by sharedScreenModel.measure.collectAsState() val measure by sharedScreenModel.measure.collectAsState()
val stanza by sharedScreenModel.stanza.collectAsState() val stanza by sharedScreenModel.stanza.collectAsState()
val gridTUOData = GridTUOData(measure, tuoList, stanza) val gridTUOData = remember(tuoList, measure, stanza) { GridTUOData(measure, tuoList, stanza) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var showContextualMenu = false var showContextualMenu = false
@ -81,6 +91,16 @@ object ScreenSolfa : Screen {
// DeepLinkHandler.consumePending("") // DeepLinkHandler.consumePending("")
} }
val tuoEditState by sharedScreenModel.tuoEditState.collectAsState()
LaunchedEffect(tuoEditState) {
tuoEditState?.let { state ->
coroutineScope.launch(Dispatchers.IO) {
solfaScreenModel.saveTUOEdit(state)
}
sharedScreenModel.closeTUOEditor()
}
}
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
DeepLinkHandler.onSongReceived = null DeepLinkHandler.onSongReceived = null
@ -97,7 +117,10 @@ object ScreenSolfa : Screen {
solfaScrollState = scrollState solfaScrollState = scrollState
) { paddingValues -> ) { paddingValues ->
Box( Box(
Modifier.fillMaxSize().padding(paddingValues) Modifier.fillMaxSize()
.padding(paddingValues)
.windowInsetsPadding(WindowInsets.safeDrawing)
.windowInsetsPadding(WindowInsets.ime)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Column( Column(
@ -139,7 +162,7 @@ object ScreenSolfa : Screen {
) { ) {
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing) //.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(start = 8.dp, end = 8.dp, top = 8.dp), .padding(start = 8.dp, end = 8.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
@ -163,10 +186,23 @@ object ScreenSolfa : Screen {
sharedScreenModel = sharedScreenModel, sharedScreenModel = sharedScreenModel,
onGridWidthMeasured = { width -> gridWidthPx = width } onGridWidthMeasured = { width -> gridWidthPx = width }
) )
EditSourceCompose(sharedScreenModel, solfaScreenModel)
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
val isEditMode = sharedScreenModel.editModeState.value
IconToggleButton(
checked = isEditMode,
onCheckedChange = { sharedScreenModel.toggleEditMode() },
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = if (isEditMode) Icons.Filled.Check else Icons.Filled.Edit,
contentDescription = "Mode édition",
tint = if (isEditMode) ColorPrefixB else Color.Gray
)
}
MGButton(onClick = { MGButton(onClick = {
//showContent = !showContent //showContent = !showContent
@ -184,7 +220,7 @@ object ScreenSolfa : Screen {
} }
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.windowInsetsPadding(WindowInsets.safeDrawing) //.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(start = 8.dp, end = 8.dp, top = 8.dp), .padding(start = 8.dp, end = 8.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)

View file

@ -9,10 +9,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mg.dot.feufaro.FileRepository import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.SaveSettings import mg.dot.feufaro.SaveSettings
import mg.dot.feufaro.getConfigDirectoryPath import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.launchFilePicker import mg.dot.feufaro.launchFilePicker
import mg.dot.feufaro.midi.MidiPitch import mg.dot.feufaro.midi.MidiPitch
import mg.dot.feufaro.midi.MidiWriterKotlin import mg.dot.feufaro.midi.MidiWriterKotlin
import java.io.File
import kotlin.math.min import kotlin.math.min
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212 //@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
@ -71,16 +72,23 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
private val meta: MutableMap<String, String> = mutableMapOf() private val meta: MutableMap<String, String> = mutableMapOf()
private val lyricsComment: MutableList<String> = mutableListOf() private val lyricsComment: MutableList<String> = mutableListOf()
suspend fun nextTimeUnitObject() { suspend fun nextTimeUnitObject(accumulator: MutableList<TimeUnitObject>) {
val lastTUO = sharedScreenModel.lastTUO() val lastTUO = accumulator.lastOrNull()
nextTIndex++ nextTIndex++
if (nextTIndex == 5) { if (T.getOrNull(nextTIndex) == null) {
withContext(Dispatchers.Main) {
sharedScreenModel.updateFullTUOList(accumulator)
//sharedScreenModel.doneTUOList()
}
return
}
/*if (nextTIndex == 5) {
sharedScreenModel.doneTUOList() sharedScreenModel.doneTUOList()
} }
if (T.getOrNull(nextTIndex) == null) { if (T.getOrNull(nextTIndex) == null) {
sharedScreenModel.doneTUOList() sharedScreenModel.doneTUOList()
return return
} }*/
val pTemplate = T[nextTIndex] val pTemplate = T[nextTIndex]
val unitObject = TimeUnitObject(pTemplate, lastTUO, N.size) val unitObject = TimeUnitObject(pTemplate, lastTUO, N.size)
if (lastTUO == null) { if (lastTUO == null) {
@ -132,9 +140,10 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} }
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
sharedScreenModel.addTUO(unitObject) //sharedScreenModel.addTUO(unitObject)
accumulator.add(unitObject)
} }
nextTimeUnitObject() nextTimeUnitObject(accumulator)
} }
fun loadSolfa() { fun loadSolfa() {
@ -157,7 +166,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
screenModelScope.launch { screenModelScope.launch {
val initialPath = stateSettings.loadLastUsedDir() val initialPath = stateSettings.loadLastUsedDir()
val homedir = getConfigDirectoryPath(); val homedir = fileRepository.getAppPublicFolder().absolutePath
launchFilePicker( launchFilePicker(
mimeTypes = arrayOf("text/plain"), mimeTypes = arrayOf("text/plain"),
@ -186,7 +195,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
sharedScreenModel.reset() sharedScreenModel.reset()
} }
fun parse(sourceFile: String) { fun parse(sourceFile: String) {
println("37: lFL bien reçu $sourceFile") //println("37: lFL bien reçu $sourceFile")
currentFile = sourceFile currentFile = sourceFile
val parseScope = CoroutineScope(Dispatchers.Default) val parseScope = CoroutineScope(Dispatchers.Default)
parseScope.launch { parseScope.launch {
@ -229,7 +238,11 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
preloadN(it) preloadN(it)
} }
try { try {
nextTimeUnitObject() val localAccumulator = mutableListOf<TimeUnitObject>()
withContext(Dispatchers.Main) {
sharedScreenModel.updateFullTUOList(emptyList())
}
nextTimeUnitObject(localAccumulator)
} catch (e: Exception) { } catch (e: Exception) {
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}") println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
} }
@ -240,6 +253,707 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
sharedScreenModel.setStanza(1) sharedScreenModel.setStanza(1)
} }
} }
fun justeBuild(sourceFile: String) {
currentFile = sourceFile
val parseScope = CoroutineScope(Dispatchers.Default)
parseScope.launch {
val lines = try {
fileRepository.readFileLines(sourceFile)
} catch (e: Exception) {
println("Opening $sourceFile raised exception {${e.message}")
emptyList()
}
val contentString = fileRepository.readFileLines(sourceFile).joinToString("\n")
sharedScreenModel.setFileContent(contentString, sourceFile)
O.clear()
T.clear()
N.clear()
L.clear()
if (pitches.isNotEmpty()) {
pitches[0].reset()
}
pitches.clear()
refrainBeginsAt = -1
unparsedNote.clear()
templateString = ""
nextTIndex = -1
nextNIndex = -1
nextLIndex = -1
lyricsComment.clear()
TimeUnitObject.hasMarker(false)
lines.forEach { line ->
run {
parseOneLine(line)
}
}
unparsedNote.forEach {
preloadN(it)
}
try {
val localAccumulator = mutableListOf<TimeUnitObject>()
withContext(Dispatchers.Main) {
sharedScreenModel.updateFullTUOList(emptyList())
}
nextTimeUnitObject(localAccumulator)
} catch (e: Exception) {
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
}
val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
val midiWriter = MidiWriterKotlin(fileRepository)
midiWriter.process(pitches)
midiWriter.save("whawyd3.mid")
}
}
suspend fun saveTUOEdit(editState: TUOEditState) {
println(editState)
/* N.mapIndexed { index, note ->
note.oneVoiceNote.mapIndexed { index, notes ->
println(notes.note)
}
}*/
val filePath = currentFile
if (filePath.isBlank()) return
// inclusion -----
val includedContent = expandInclusions(filePath)
val lines = includedContent.split("\n").toMutableList()
val targetIdx = editState.tuoIndex
val t0Idx = lines.indexOfFirst { it.startsWith("T0:") }
val u0Idx = lines.indexOfFirst { it.startsWith("U0:") }
val noteS = lines[lines.indexOfFirst { it.startsWith("N1") }]
val noteA = lines[lines.indexOfFirst { it.startsWith("N2") }]
val noteT = lines[lines.indexOfFirst { it.startsWith("N3") }]
val noteB = lines[lines.indexOfFirst { it.startsWith("N4") }]
val currentStanza = sharedScreenModel.stanza.value
val lyrIdx = lines.indexOfFirst { line ->
line.contains(Regex("^[EY]$currentStanza:"))
}
val lyrics = lines[lyrIdx]
println("Parole sur $currentStanza: $lyrics")
val content = smartYLyrics(lyrics).substringAfter(":")
val indexedLyrics = content.split('_', ' ', '/')
.filter { it.isNotBlank() }
.toMutableList()
/*indexedLyrics.forEachIndexed { index, syllable ->
println("Index $index : [$syllable]")
}*/
val measureString = sharedScreenModel.measure.value
val templateString = when {
t0Idx != -1 -> {
lines[t0Idx]
}
u0Idx != -1 -> {
lines[u0Idx]
}
else -> ""
}
val templateIndices = when {
t0Idx != -1 -> {
t0Idx
}
u0Idx != -1 -> {
u0Idx
}
else -> 0
}
when {
t0Idx != -1 -> {
val line = lines[t0Idx]
val prefix = line.substringBefore('{') + "{"
val suffix = "}"
val body = line.substringAfter('{').substringBeforeLast('}')
val regex = Regex("([^:|/!]*[:|/!])|([^:|/!]+)")
val fragments = regex.findAll(body).map { it.value }.toMutableList()
// println("Nombre de blocs détectés : ${fragments.size}")
val markerRegex = Regex("""\$\{.*?\}|\$.""")
if (targetIdx in fragments.indices) {
val oldFrag = fragments[targetIdx]
val delimiter = if (oldFrag.isNotEmpty() && oldFrag.last() in ":|/!") {
oldFrag.last().toString()
} else ""
val cleanBody = fragments.joinToString("") { it.replace(markerRegex, "") }
lines[t0Idx] = prefix + cleanBody + suffix
println("AVANT: $line")
println("APRÈS: ${lines[t0Idx]}")
} else {
println("Index $targetIdx hors limites pour T0 (taille=${fragments.size})")
return
}
}
u0Idx != -1 -> {
val fullLine = lines[u0Idx]
// println("=== DEBUG U0 ===")
// println("fullLine = '$fullLine'")
val afterU0 = fullLine.substringAfter("U0:")
val blankPrefixRegex = Regex("^(z[048CEKO]:)")
val match = blankPrefixRegex.find(afterU0)
val blankPrefix = match?.value ?: ""
val body = if (match != null) {
afterU0.substring(blankPrefix.length)
} else {
afterU0
}
val measure = measureString.split("/")[0].toIntOrNull() ?: 4
val parser = ParseULine(body, measure)
val symboles = parser.parsed()
val newLigneT = "U0:"+blankPrefix+symboles
println("Ligne de U0 ==> $newLigneT")
lines[u0Idx] = newLigneT
}
else -> {
println("Aucune ligne T0 ou U0 trouvée dans le fichier")
return
}
}
val templatArray = templateToArray(if(u0Idx != -1) lines[u0Idx] else lines[t0Idx])
/* println("index | contenuTUO")
templatArray.forEachIndexed { index, block ->
println("$index $block")
}*/
/*
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)
// RESTAURATION DES TEMPLATES
if (templateString != "") {
updatedNotes[templateIndices] = templateString
}
val finalString = updatedNotes.joinToString("\n")
val tempDir = System.getProperty("java.io.tmpdir")
val fileName = filePath.substringAfterLast('/')
val currentTempFile = File(tempDir, fileName)
currentTempFile?.deleteOnExit()
val normalizedTempDir = File(tempDir).absolutePath
val originalFilePath = if (filePath.startsWith(normalizedTempDir)) {
filePath
} else {
""
}
// println("Fichier Modifier:\n" +
// "$finalString")
//fileRepository.saveFile(filePath, finalString.toByteArray())
currentTempFile?.writeBytes(finalString.toByteArray())
withContext(Dispatchers.Main) {
justeBuild(currentTempFile.absolutePath)
}
}
private suspend fun expandInclusions(filePath: String):String {
val result = StringBuilder()
val lastSlash = filePath.lastIndexOf('/')
val directory = if (lastSlash != -1) filePath.substring(0, lastSlash + 1) else ""
val myLines = fileRepository.readFileLines(filePath)
myLines.forEach { line ->
if (line.startsWith("I0:")) {
try {
val parts = line.substring(3).split(":")
val fileName = parts[0]
val ignorePattern = if (parts.size > 1) parts[1] else ""
val fullPath = directory + fileName
val includedLines = fileRepository.readFileLines(fullPath)
val regexIgnore = if (ignorePattern.isNotEmpty()) Regex(ignorePattern) else null
includedLines.forEach { incLine ->
if (regexIgnore == null || !regexIgnore.containsMatchIn(incLine)) {
result.append(incLine).append("\n")
}
}
} catch (e: Exception) {
result.append("// Erreur inclusion: ${e.message}\n")
}
} else {
result.append(line).append("\n")
}
}
return result.toString().trimEnd()
}
private fun updateSourceLines(
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)
/* MODIF LYRICS */
//updateLyricsInLines(lines, fragments, targetIdx, deleteCount, editState)
return lines
}
private fun updateLyricsInLines(
lines: MutableList<String>,
fragments: List<String>,
targetIdx: Int,
deleteCount: Int,
editState: TUOEditState
) {
editState.lyricsByStanza.forEach { (stanzaNum, newSyllable) ->
val lineYIdx = lines.indexOfFirst { it.matches(Regex("^[Y]$stanzaNum:.*")) }
val lineEIdx = lines.indexOfFirst { it.matches(Regex("^[E]$stanzaNum:.*")) }
val lineIdx = if(lineYIdx != -1) lineYIdx else lineEIdx
if (lineIdx != -1) {
val currentLine = lines[lineIdx]
val prefix = currentLine.substringBefore(":") + ":"
val lyricsBody = currentLine.substringAfter(":")
// On traite les paroles pour avoir la liste brute
val lyricsContent = if(lineYIdx != -1) smartYLyrics(lyricsBody) else smartELyrics(lyricsBody)
// Découpage strict pour correspondre au template
println("Lyrics BODY $lyricsBody")
println("LyricsC $lyricsContent")
val lyricsInLines = sharedScreenModel.synchronizedSyllables.value
lyricsInLines.mapIndexed { index, string ->
//println("SYNRO $index => {$string}")
}
val allTokens = lyricsInLines.toMutableList()
// println("--- TABLEAU DE CORRESPONDANCE (Strophe $stanzaNum) ---")
// println(String.format("%-5s | %-10s | %-15s", "Idx", "Template", "Syllabe"))
// println("-------------------------------------------")
var actualTokenPointer = 0
for (i in fragments.indices) {
val fragment = fragments[i]
val isProlongation = fragment.trim() == "-"
val associatedSyllable = allTokens.getOrNull(actualTokenPointer) ?: ""
actualTokenPointer++
val marker = if (i == targetIdx) " <== [CIBLE]" else ""
// println(String.format("%-5d | %-10s | %-15s %s",
// i,
// "[$fragment]",
// "[$associatedSyllable]",
// marker))
}
println("\nAction finale : Remplacer l'index $targetIdx (valeur: ${allTokens.getOrNull(targetIdx)})")
if (targetIdx < allTokens.size) {
allTokens[targetIdx] = newSyllable
if(deleteCount > 1){
var deleted = 0
var j = targetIdx + 1
while (deleted < deleteCount - 1 && j < allTokens.size) {
if (allTokens[j].isNotEmpty()) {
allTokens[j] = ""
deleted++
}
j++
}
}
}
val rawResult = allTokens.filter { it.isNotEmpty() }.joinToString(" ")
println("allToken ${allTokens.joinToString("")}")
// println("RAWRes ${rawResult}")
val formattedResult = rawResult
.replace(Regex("-\\s+"), "")
.replace(Regex("-"), "")
println("FORMATTER $formattedResult")
val lyrIdx = lines.indexOfFirst { line ->
line.contains(Regex("^[EY]$stanzaNum:"))
}
val lyrics = lines[lyrIdx]
println("Originl $lyrics")
lines[lineIdx] = prefix + formattedResult
//lines[lineIdx] = prefix + reconstructLyrics(finalTokens)
}
}
// println("--- FIN DEBUG ---")
}
private fun updateNotesInLines(
lines: MutableList<String>,
fragments: List<String>,
targetIdx: Int,
deleteCount: Int,
editState: TUOEditState
) {
val notesByVoice = editState.notesByVoice
val originalNotes = editState.originalNotes
for (voiceNum in 0..3) {
val linePrefix = "N${voiceNum + 1}:"
val lineIdx = lines.indexOfFirst { it.startsWith(linePrefix) }
if (lineIdx != -1) {
val currentLine = lines[lineIdx]
val prefix = currentLine.substringBefore(":") + ":"
val noteBody = currentLine.substringAfter(":")
val hasAnchor = noteBody.contains("#")
val noteExpanded = expandNotes(noteBody)
// println("Notes===>$noteExpanded")
val newNot = notesByVoice[voiceNum] ?: ""
val oldNot = originalNotes[voiceNum] ?: ""
val isNoteModified = newNot != oldNot
// println("est-ce? $isNoteModified = $newNot != $oldNot")
if (!isNoteModified) {
// println("Voix $linePrefix : Aucune modification détectée (Note: $oldNot ).")
continue
}
val newNote = notesByVoice[voiceNum] ?: ""
if (newNote.isEmpty()) continue
val cleanNewNote = revertMusicalInput(newNote)
val regex = Regex("#\\S[',]*|\\s#\\S[',]*|/|\\(|\\)|[drmfsltDRFSTzw][0-9'¹²³⁴⁵₁₂₃₄₅,]*|[-.]")
val allTokens = regex.findAll(noteExpanded).map { it.value }.toList()
val targetPointer = getNotePointer(fragments, targetIdx, currentLine)
val resultTokens = mutableListOf<String>()
var currentLogicalIdx = 0
var i = 0
// println("\nSuivi pour la voix $linePrefix (Pointeur cible: $targetPointer)")
// println(
// String.format(
// "%-5s | %-10s | %-8s | %-12s | %-10s",
// "Idx P",
// "Token",
// "Type",
// "LogiqueIdx",
// "Action"
// )
// )
while (i < allTokens.size) {
val token = allTokens[i]
val isStructural = token.contains("#") || token == "/" || token == "(" || token == ")"
if (isStructural) {
// println(String.format("%-5d | %-10s | %-8s | %-12s | %-10s", i, token, "STRUCT", "-", "Keep"))
resultTokens.add(token)
i++
continue
}
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)
} 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
val newNotCleaned = revertMusicalInput(newNot)
val oldNotCleaned = revertMusicalInput(oldNot)
val (newBase, newLevel) = parseNoteAndOctave(newNotCleaned)
val (oldBase, oldLevel) = parseNoteAndOctave(oldNotCleaned)
val sourceLevel = newLevel - anchorLevel
val formattedOctave = formatOctave(sourceLevel)
val finalPureNew = newBase + formattedOctave
val pureOldBase = oldBase + formatOctave(oldLevel - anchorLevel)
// println("Ancre niveau: $anchorLevel | UI Level: $newLevel -> Source Level: $sourceLevel")
//
if (finalPureNew.startsWith(pureOldBase) && pureOldBase.isNotEmpty()) {
val addedPart = finalPureNew.substring(pureOldBase.length)
originalToken + addedPart
} else {
finalPureNew
}
}
// 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++
} else if (hasAnchor) {
resultTokens.add(nextToken)
}
i++
}
currentLogicalIdx += deleteCount
} else {
// AFFICHAGE NOTE NORMALE
// println(
// String.format(
// "%-5d | %-10s | %-8s | %-12d | %-10s",
// i,
// token,
// "NOTE",
// currentLogicalIdx,
// "Keep"
// )
// )
resultTokens.add(token)
currentLogicalIdx++
i++
}
}
lines[lineIdx] = prefix + resultTokens.joinToString("")
// println("-".repeat(55))
// println("RÉSULTAT : ${lines[lineIdx]}")
}
}
}
private fun parseNoteAndOctave(s: String): Pair<String, Int> {
val base = s.replace(Regex("[^drmfsltDRFSTzw]"), "")
var level = 0
level += s.count { it == '\'' || it == '¹' || it == '²' || it == '³' }
level -= s.count { it == ',' || it == '₁' || it == '₂' || it == '₃' }
return Pair(base, level)
}
private fun formatOctave(level: Int): String {
if (level == 0) return ""
return if (level > 0) "'".repeat(level) else ",".repeat(Math.abs(level))
}
private fun revertMusicalInput(input: String): String {
return input
.replace("• ,", "")
.replace("―•", "")
.replace("", "-")
.replace("", "")
.replace("³", "'''")
.replace("", ",,,")
.replace("²", "''")
.replace("", ",,")
.replace("¹", "'")
.replace("", ",")
.replace("di", "D")
.replace("ri", "R")
.replace("fi", "F")
.replace("si", "S")
.replace("ta", "T")
.replace("(","")
.replace(")","")
}
private fun parseNoteLineToArray(noteLine: String): List<String> {
val content = noteLine.substringAfter(":").replace(" ", "")
val result = mutableListOf<String>()
var i = 0
while (i < content.length) {
val char = content[i]
if (char == '/' || char == ' '/* || char == 'z'*/) {
i++
continue
}
if (char == '#') {
i += 2
continue
}
if (char.isLetter() || char == '-') {
val noteBuilder = StringBuilder(char.toString())
var nextIdx = i + 1
while (nextIdx < content.length && (content[nextIdx] == ',' || content[nextIdx] == '\'')) {
noteBuilder.append(content[nextIdx])
nextIdx++
}
val fullNote = noteBuilder.toString()
if (nextIdx < content.length && content[nextIdx].isDigit()) {
val repeatCount = content[nextIdx].toString().toInt()
repeat(repeatCount) {
result.add(fullNote)
}
nextIdx++
} else {
result.add(fullNote)
}
i = nextIdx
continue
}
i++
}
return result
}
private fun templateToArray(templateLine: String): List<String> {
var templateArray: List<String> = emptyList()
val templateType = if(templateLine.startsWith("T0")) "T" else "U"
if(templateType == "T") {
var content = templateLine.substringAfter("{").substringBeforeLast("}")
// println("AVANT T: $content")
content = content.replace(Regex("\\\$\\{.*?\\}"), "")
val rawBlocks = content.split(Regex("[:|/!]"))
templateArray = rawBlocks.filter { it.isNotBlank() }
// println("\n\nLine = $templateLine \n," +
// "Array = ${templateArray.joinToString("|")}")
// println("APRèS T: ${templateArray.joinToString("|")}")
} else {
val body = templateLine.replaceFirst(Regex("^U0:z.:"), "")
//println("Mon contenu est: $body.")
val result = mutableListOf<String>()
var currentBlock = StringBuilder()
var i = 0
while (i < body.length) {
val char = body[i]
// 1. GESTION DES MARQUEURS ${...} -> ON IGNORE TOUT LE BLOC
if (char == '$' && i + 1 < body.length && body[i + 1] == '{') {
val closingBrace = body.indexOf('}', i)
if (closingBrace != -1) {
i = closingBrace + 1 // On saute jusqu'après le '}'
continue
}
}
if (char == '$') {
i += 2 // On saute le '$' et la lettre qui suit
continue
}
// 3. SÉPARATEURS -> ON COUPE LE BLOC
if (char in listOf(':', '|', '!', '/')) {
if (currentBlock.isNotEmpty()) {
result.add(currentBlock.toString())
currentBlock.clear()
}
}
// 4. IGNORER LES ESPACES
else if (char == ' ') {
// ne rien faire
}
// 5. TOUT LE RESTE (D, z, -, ., ,, (, ) )
else {
currentBlock.append(char)
}
i++
}
// Ajouter le dernier morceau s'il n'y a pas de séparateur à la fin
if (currentBlock.isNotEmpty()) result.add(currentBlock.toString())
templateArray = result
}
// println("\n\nLine = $templateLine \n," +
// "Array = ${templateArray.joinToString("|")}")
return templateArray
}
private fun expandNotes(text: String): String {
val regex = Regex("([drmfsltDRFSTzw])([,']*)(\\d+)")
return regex.replace(text) { matchResult ->
val note = matchResult.groupValues[1]
val octaves = matchResult.groupValues[2]
val count = matchResult.groupValues[3].toInt()
(note + octaves).repeat(count)
}
}
private fun rearrangeNote(noteString: String, infiniteIter: Int = 0): String { private fun rearrangeNote(noteString: String, infiniteIter: Int = 0): String {
var result: String = noteString var result: String = noteString
@ -968,4 +1682,96 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
this.add(element) this.add(element)
} }
} }
fun getNotePointerWithDebug(templatArray: List<String>, targetIdx: Int, sourceLine: String): Int {
println("\n=== DÉBOGAGE ALIGNEMENT RYTHME / NOTES (CORRIGÉ) ===")
val bodyOnly = sourceLine.substringAfter(":")
val expandedBody = expandNotes(bodyOnly)
// On suppose que parseNoteLineToArray découpe chaque "événement" (note ou prolongement)
val notesList = parseNoteLineToArray("N:$expandedBody")
println("\n=== ALIGNEMENT STRICT : TEMPLATE -> SOURCE ===")
println("Ligne Source : ${sourceLine.substringBefore(":")}")
println("----------------------------------------------------------------------")
println("%-5s | %-12s | %-15s | %-10s".format("IDX", "BLOC TEMP.", "NOTES SRC", "POINTEUR"))
println("----------------------------------------------------------------------")
var currentPointer = 0
var notesInTargetBlock = mutableListOf<String>()
var countInTarget = 0
for (i in templatArray.indices) {
val block = templatArray[i]
// NETTOYAGE : On ignore les marqueurs $Q, ${DC} pour le comptage
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
val cleanBlock = block.replace(markerRegex, "")
// RÈGLE : On compte uniquement les LETTRES (S, F, D, R, M, L, T, d, r...)
val nbLettres = cleanBlock.count { it.isLetter() && it.lowercaseChar() != 'z' }
// On récupère les notes correspondantes dans le fichier source
val blocNotes = (0 until nbLettres).map { offset ->
notesList.getOrNull(currentPointer + offset) ?: "[VIDE]"
}
if (i == targetIdx) {
countInTarget = nbLettres
notesInTargetBlock.addAll(blocNotes)
}
val marker = if (i == targetIdx) " [ CIBLE ]" else ""
println("[%3d] | %-12s | %-15s | Index: %d %s"
.format(i, block, blocNotes.joinToString(","), currentPointer, marker))
// On incrémente le pointeur pour le prochain bloc
// SEULEMENT si nous n'avons pas encore dépassé la cible
// if (i < targetIdx) {
currentPointer += nbLettres
// }
}
println("----------------------------------------------------------------------")
println("RÉSULTAT : Le bloc $targetIdx contient $countInTarget lettre(s).")
println("Notes pointées dans le fichier source : ${notesInTargetBlock.joinToString(" ET ")}")
println("Pointeur de départ (offset) : $currentPointer")
println("======================================================================\n")
return currentPointer
}
fun getNotePointer(templatArray: List<String>, targetIdx: Int, sourceLine: String): Int {
val bodyOnly = sourceLine.substringAfter(":")
val expandedBody = expandNotes(bodyOnly)
val notesList = parseNoteLineToArray("N:$expandedBody")
var currentPointer = 0
var notesInTargetBlock = mutableListOf<String>()
var countInTarget = 0
for (i in templatArray.indices) {
val block = templatArray[i]
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
val cleanBlock = block.replace(markerRegex, "")
val nbLettres = cleanBlock.count { it.isLetter() && it.lowercaseChar() != 'z' }
val blocNotes = (0 until nbLettres).map { offset ->
notesList.getOrNull(currentPointer + offset) ?: "[VIDE]"
}
if (i == targetIdx) {
countInTarget = nbLettres
notesInTargetBlock.addAll(blocNotes)
}
val marker = if (i == targetIdx) " [ CIBLE ]" else ""
if (i < targetIdx) {
currentPointer += nbLettres
}
}
return currentPointer
}
} }

View file

@ -0,0 +1,210 @@
package mg.dot.feufaro.solfa
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
class SolfaVisualTransformation : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val mapping = IntArray(text.text.length + 1)
var currentOriginalIndex = 0
val transformed = buildAnnotatedString {
val lines = text.text.split("\n")
val hasU0 = lines.any { it.startsWith("U0:") }
val hasT0 = lines.any { it.startsWith("T0:") }
lines.forEachIndexed { index, line ->
val lineStartIndex = currentOriginalIndex
val startLen = this.length // Longueur actuelle du AnnotatedString
when {
line.startsWith("M0:") -> parseMetadataLine(line)
line.startsWith("U0:") && hasU0 -> parseControlLine(line)
line.startsWith("T0:") && !hasU0 && hasT0 -> parseControlLine(line)
line.startsWith("N") && line.getOrNull(2) == ':' -> parseNoteLine(line)
(line.startsWith("Y") || line.startsWith("E")) && (line.getOrNull(2) == ':' || line.getOrNull(3) == ':') -> parseLyricLine(line)
else -> append(line)
}
val lineTextLength = line.length
val transformedLineLength = this.length - startLen
for (i in 0 until lineTextLength) {
val transformedOffset = if (transformedLineLength == 0) startLen
else startLen + (i * transformedLineLength / lineTextLength)
mapping[lineStartIndex + i] = transformedOffset
}
currentOriginalIndex += lineTextLength
if (index < lines.size - 1) {
mapping[currentOriginalIndex] = this.length
append("\n")
currentOriginalIndex++
}
}
mapping[text.text.length] = this.length
}
val solfaOffsetMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return mapping.getOrElse(offset) { transformed.length }.coerceIn(0, transformed.length)
}
override fun transformedToOriginal(offset: Int): Int {
var bestOriginal = 0
for (i in mapping.indices) {
if (mapping[i] <= offset) bestOriginal = i
else break
}
return bestOriginal.coerceIn(0, text.text.length)
}
}
return TransformedText(transformed, solfaOffsetMapping)
}
}
// COULEURS DU THÈME
val ColorPrefixA = Color(0xFF6200EE) // Violet
val ColorPrefixB = Color(0xFF03DAC5) // Teal
val ColorSeparator = Color(0xFFB0A400) // Jaune
val ColorNoteOdd = Color(0xFF2196F3) // Bleu
val ColorNoteEven = Color(0xFFE91E63) // Rose
val ColorDigit = Color(0xFF4CAF50) // Vert
val ColorSpecial = Color(0xFFFF9800) // Orange (#)
val ColorValue = Color(0xFF757575) // Gris
val ColorMarker = Color(0xFF795548) // Marron
val MarkerRegex = Regex("\\$\\{.*?\\}|\\$[^ \n]")
private fun AnnotatedString.Builder.parseMetadataLine(line: String) {
val parts = line.split(":")
// M0:
pushStyle(SpanStyle(color = ColorPrefixA, fontWeight = FontWeight.Bold))
append(parts[0] + ":")
pop()
if (parts.size > 1) {
val remaining = line.substring(parts[0].length + 1)
val segments = remaining.split("|")
segments.forEachIndexed { i, seg ->
if (seg.contains(":")) {
val kv = seg.split(":")
pushStyle(SpanStyle(color = ColorPrefixB)) // m:, r:, t:
append(kv[0] + ":")
pop()
pushStyle(SpanStyle(color = ColorValue))
append(kv[1])
pop()
} else {
append(seg)
}
if (i < segments.size - 1) {
pushStyle(SpanStyle(color = ColorSeparator))
append("|")
pop()
}
}
}
}
private fun AnnotatedString.Builder.appendWithMarkers(text: String) {
var lastIndex = 0
MarkerRegex.findAll(text).forEach { match ->
append(text.substring(lastIndex, match.range.first))
pushStyle(SpanStyle(color = ColorMarker, fontWeight = FontWeight.Bold))
append(match.value)
pop()
lastIndex = match.range.last + 1
}
append(text.substring(lastIndex))
}
private fun AnnotatedString.Builder.parseControlLine(line: String) {
val regex = Regex("^(U0:)(z[0-9A-Z]:)?(.*)$")
val match = regex.find(line)
match?.let {
pushStyle(SpanStyle(color = ColorPrefixA))
append(it.groupValues[1])
pop()
pushStyle(SpanStyle(color = ColorPrefixB))
append(it.groupValues[2])
pop()
parseGenericContent(it.groupValues[3])
}
}
private fun AnnotatedString.Builder.parseNoteLine(line: String) {
val prefix = line.take(3) // N1:
pushStyle(SpanStyle(color = ColorPrefixA, fontWeight = FontWeight.Bold))
append(prefix)
pop()
val content = line.drop(3)
var noteCount = 0
var i = 0
while (i < content.length) {
val char = content[i]
when {
char == '#' -> {
pushStyle(SpanStyle(color = ColorSpecial))
append(char)
pop()
}
char == '/' -> {
pushStyle(SpanStyle(color = ColorSeparator, fontWeight = FontWeight.Bold))
append(char)
pop()
}
char.isDigit() -> {
pushStyle(SpanStyle(color = ColorDigit))
append(char)
pop()
}
char.lowercaseChar() in "drmfsltr" -> {
// Détecter la note complète avec ses octaves (',')
val start = i
while (i + 1 < content.length && (content[i+1] == '\'' || content[i+1] == ',')) {
i++
}
val fullNote = content.substring(start, i + 1)
val color = if (noteCount % 2 == 0) ColorNoteOdd else ColorNoteEven
pushStyle(SpanStyle(color = color))
append(fullNote)
pop()
noteCount++
}
else -> append(char)
}
i++
}
}
private fun AnnotatedString.Builder.parseLyricLine(line: String) {
pushStyle(SpanStyle(color = ColorPrefixA))
append(line.take(3))
pop()
parseGenericContent(line.drop(3))
}
private fun AnnotatedString.Builder.parseGenericContent(content: String) {
val parts = content.split("/")
parts.forEachIndexed { i, part ->
appendWithMarkers(part)
if (i < parts.size - 1) {
pushStyle(SpanStyle(color = ColorSeparator))
append("/")
pop()
}
}
}

View file

@ -0,0 +1,334 @@
package mg.dot.feufaro
import androidx.compose.foundation.background
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.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
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.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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TUODetailDialog(
editState: TUOEditState,
tuo: TimeUnitObject,
currentStanza: Int,
menuPosition: IntOffset,
globalIndex: Int,
isEditable: Boolean,
canAdd: Boolean,
onDismiss: () -> Unit,
onSave: (TUOEditState) -> Unit
) {
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 marker by remember { mutableStateOf(editState.marker) }
var hairPin by remember { mutableStateOf(editState.hairPin) }
val lyricsLines = remember {
mutableStateListOf<String>().apply {
val existing = editState.lyricsByStanza[currentStanza] ?: ""
if (existing.isEmpty() && canAdd) add("_") else add(existing)
}
}
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 = Color(0xFF2D2D2D).copy(0.75f),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp).fillMaxWidth()) {
Text(
text = "$globalIndex",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(2.dp))
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
// --- SECTION TEMPLATE ---
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Column(
modifier = Modifier.weight(1f)
) {
MyTextEditField(
value = templateFragment,
customFontSize = 14.sp,
color = Color.Yellow,
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isEditable = isEditable,
isAddable = canAdd,
onValueChng = { templateFragment = it }
)
}
if(!editState.marker.isNullOrEmpty()) {
Column(
modifier = Modifier.weight(1f)
) {
MyTextEditField(
value = marker,
customFontSize = 14.sp,
color = Color.Yellow,
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isEditable = isEditable,
isAddable = canAdd,
funTransform = ::transformMarkerInput,
onValueChng = { marker = it }
)
}
}
if(!editState.hairPin.isNullOrEmpty()) {
Column(
modifier = Modifier.weight(1f)
) {
MyTextEditField(
value = hairPin,
customFontSize = 14.sp,
color = Color.Yellow,
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isEditable = isEditable,
isAddable = canAdd,
onValueChng = { hairPin = it }
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
// --- SECTION NOTES ---
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
(0..3).forEach { voice ->
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
}
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
// --- SECTION LYRICS ---
lyricsLines.forEachIndexed { index, line ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 2.dp)
) {
Column(
modifier = Modifier.fillMaxWidth(0.8f)
) {
MyTextEditField(
value = line,
customFontSize = 13.sp,
color = Color.White,
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isEditable = isEditable,
isAddable = canAdd,
onValueChng = { newValue ->
lyricsLines[index] = newValue
}
)
}
if (isEditable || canAdd) {
IconButton(
onClick = {
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) Color.Green else Color.Red,
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 state = TUOEditState(
tuoIndex = globalIndex,
notesByVoice = notes.toMap(),
originalNotes = originalNotes.toMap(),
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
lyricsByStanza = mutableMapOf(currentStanza to lyricsLines.joinToString(" ")),
templateFragment = templateFragment
)
onSave(state)
},
modifier = Modifier.size(22.dp)
) {
Icon(
Icons.Default.Build,
contentDescription = null,
tint = Color.Green
)
}
}
}
}
}
}
}
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("'", "¹")
}
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,
funTransform: ((String) -> String)? = null,
onValueChng: (String) -> Unit
) {
val textToShow = if (isAddable && value == "_") "" else value
BasicTextField(
value = textToShow,
onValueChange = { newValue ->
if (isEditable || isAddable) {
val processedVal = funTransform?.invoke(newValue) ?: newValue
onValueChng(processedVal)
}
},
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password
),
textStyle = TextStyle(
color = color,
fontSize = customFontSize,
fontFamily = font
),
readOnly = !(isEditable || isAddable),
modifier = Modifier
.fillMaxWidth()
.background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(4.dp))
.padding(customPadding),
cursorBrush = customBrush
)
}

View file

@ -0,0 +1,12 @@
package mg.dot.feufaro.solfa
data class TUOEditState(
val tuoIndex: Int,
val notesByVoice: Map<Int, String> = emptyMap(),
val originalNotes: Map<Int, String> = emptyMap(),
val lyricsByStanza: MutableMap<Int, String> = mutableMapOf(),
val originalLyricsByStanza: MutableMap<Int, String> = mutableMapOf(),
val templateFragment: String = "",
val marker: String = "",
val hairPin: String = ""
)

View file

@ -1,55 +1,66 @@
package mg.dot.feufaro.solfa package mg.dot.feufaro.solfa
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.material3.Text
import androidx.compose.material3.LocalTextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import mg.dot.feufaro.data.GridTUOData
import kotlin.math.min
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.withStyle
import SharedScreenModel import SharedScreenModel
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.runtime.collectAsState import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
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
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.text.TextMeasurer import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mg.dot.feufaro.ContextualMenu
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.TUODetailDialog
import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.ui.rememberFileSaveLauncher
import mg.dot.feufaro.viewmodel.MidiMarkers import mg.dot.feufaro.viewmodel.MidiMarkers
import mg.dot.feufaro.viewmodel.SolfaScreenModel
import java.io.File
import kotlin.math.min
import kotlin.math.roundToInt
val FEUFAROO_TRIOLET_COLOR = Color.DarkGray val FEUFAROO_TRIOLET_COLOR = Color.DarkGray
val FEUFAROO_KEY_CHANGE_COLOR = Color.Blue val FEUFAROO_KEY_CHANGE_COLOR = Color.Blue
@Stable
class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, countOfN: Int) { class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, countOfN: Int) {
var mutableNoteVersion: Int by mutableStateOf(0) var mutableNoteVersion: Int by mutableStateOf(0)
private var lyrics: MutableList<POneStanzaLyrics> = mutableListOf() private var lyrics: MutableList<POneStanzaLyrics> = mutableListOf()
@ -60,6 +71,7 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co
private var annotated : Boolean private var annotated : Boolean
var transposeNewKey: String = "" var transposeNewKey: String = ""
var transposeOldKey: String = "" var transposeOldKey: String = ""
var firstTuoIndex: Int = -1
companion object { companion object {
var nbBlock: Int = 0 var nbBlock: Int = 0
var sep1: String = "" var sep1: String = ""
@ -543,8 +555,29 @@ fun LazyVerticalGridTUO(
onGridWidthMeasured: (Int) -> Unit, onGridWidthMeasured: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val regexMeasure = Regex("(\\d)/\\d").find(viewModel.measure)
val tuoList = viewModel.tuoList val tuoList = viewModel.tuoList
key(tuoList) {
var menuPosition by remember { mutableStateOf(Offset.Zero) }
var showAddDialog by remember { mutableStateOf(false) }
var showDetailDialog by remember { mutableStateOf(false) }
var showEditDialog by remember { mutableStateOf(false) }
var showContextualMenu by remember { mutableStateOf(false) }
var selectedTUO by remember { mutableStateOf<TimeUnitObject?>(null) }
var selectedIndex by remember { mutableStateOf(-1) }
val regexMeasure = Regex("(\\d)/\\d").find(viewModel.measure)
LaunchedEffect(tuoList) {
showContextualMenu = false
showEditDialog = false
showDetailDialog = false
showAddDialog = false
selectedIndex = -1
selectedTUO = null
}
var showFullChord by remember { mutableStateOf(false) }
val editMode by sharedScreenModel.modeEditor.collectAsState()
Column( Column(
modifier = Modifier modifier = Modifier
@ -618,11 +651,21 @@ fun LazyVerticalGridTUO(
println("Mise à jour MidiData avec ${metadataList.size}") println("Mise à jour MidiData avec ${metadataList.size}")
sharedScreenModel.updateAndFinalizeMidiData(metadataList) sharedScreenModel.updateAndFinalizeMidiData(metadataList)
} }
}
LaunchedEffect(measures, sharedScreenModel.stanza.value) {
sharedScreenModel.updateSyllablesFromList(
measures = measures,
stanzaNumber = sharedScreenModel.stanza.value
)
val totalSyllables = sharedScreenModel.synchronizedSyllables.value.size
//println("Sync complète effectuée SUR stz: ${sharedScreenModel.stanza.value}! Total colonnes : $totalSyllables")
} }
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
){ ){
measures.forEachIndexed { measureIndex, measureTUOs -> measures.forEachIndexed { measureIndex, measureTUOs ->
key(measureIndex) {
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
Row( Row(
@ -717,33 +760,204 @@ 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()) { Row(modifier = Modifier.fillMaxWidth()) {
measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
val isActive = (globalIndex == activeRowIndex) val isActive = (globalIndex == activeRowIndex)
val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L }
var menuOffset by remember { mutableStateOf(Offset.Zero) }
val interactionSource = remember { MutableInteractionSource() }
val isHovered by interactionSource.collectIsHoveredAsState()
Box(modifier = Modifier.width(gridWidthDp / gridColumnCount) key(oneTUO.numBlock) {
.combinedClickable( Box(modifier = Modifier
onClick = { .width(gridWidthDp / gridColumnCount)
// println("je suis sur grille no: $globalIndex") .background(
sharedScreenModel.seekToGrid(globalIndex) if (isHovered) Color.Black.copy(alpha = 0.05f) else Color.Transparent,
} , RoundedCornerShape(4.dp)
onDoubleClick = { )
val m = sharedScreenModel.getFullMarkers() .combinedClickable(
m.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> interactionSource = interactionSource,
println("Allmarker $marker in $gridIndex on $timestamp ms") indication = LocalIndication.current,
// & note= $note & nBefore =$noteBefore & separateur = $separat onClick = {
if(!editMode) {
sharedScreenModel.seekToGrid(globalIndex)
}
}
)
.pointerInput(globalIndex) {
detectTapGestures(
onTap = { offset ->
if(editMode) {
menuOffset = offset
selectedTUO = oneTUO
selectedIndex = globalIndex
showContextualMenu = true
}
}
)
}
) {
TimeUnitComposable(
tuo = oneTUO,
stanzaNumber = currentStanza,
gridColumnCount = gridColumnCount,
gridActive = isActive
)
if (showContextualMenu && selectedIndex == globalIndex) {
Popup(
alignment = Alignment.TopStart,
offset = IntOffset(
menuOffset.x.roundToInt(),
menuOffset.y.roundToInt()
),
onDismissRequest = { showContextualMenu = false }
) {
ContextualMenu(onMenuItemClick = { item ->
println("Clicked in TUO $globalIndex: $item")
showContextualMenu = false
when(item) {
"Modifier" -> {
showEditDialog = true
}
"Ajouter" -> {
showAddDialog = true
}
"Liste" -> {
showDetailDialog = true
}
else -> {
showDetailDialog = false
}
}
})
}
}
val currentSelected = selectedTUO
if ((showEditDialog || showAddDialog || showDetailDialog) && currentSelected != null && selectedIndex == globalIndex) {
val canAdd = showAddDialog
val canEdit = showEditDialog || canAdd
val template = oneTUO.pTemplate.template
val expectedTemplate = template.count { it.isLetter() } == 2 && template.contains(".")
val editState = TUOEditState(
tuoIndex = oneTUO.firstTuoIndex,
/*notesByVoice = (0..3).associate { i ->
i to (oneTUO.tuNotes.getOrNull(i + 1)?.toString() ?: "")
},*/
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
}
}
i to fixedNote
},
lyricsByStanza = (1..currentStanza).associate { s ->
s to oneTUO.getSingleSyllable(s).firstOrNull().orEmpty()
}.toMutableMap(),
templateFragment = oneTUO.pTemplate.template,
marker = oneTUO.pTemplate.markerToString(),
hairPin = oneTUO.hasHairPin()?.toString() ?: ""
)
// oneTUO.tuNotes.mapIndexed { index, note ->
//
// println("i$index => ${note.toString()}")
// }
TUODetailDialog(
editState = editState,
tuo = currentSelected,
currentStanza = currentStanza,
menuPosition = IntOffset(
menuOffset.x.roundToInt(),
menuOffset.y.roundToInt()
),
globalIndex = globalIndex,
isEditable = canEdit,
canAdd = canAdd,
onDismiss = {
showAddDialog = false
showDetailDialog = false
showEditDialog = false
},
onSave = { newState ->
sharedScreenModel.openTUOEditor(newState)
showAddDialog = false
showEditDialog = false
}
)
} }
} }
)) {
TimeUnitComposable(
tuo = oneTUO,
stanzaNumber = currentStanza,
gridColumnCount = gridColumnCount,
gridActive = isActive
)
} }
} }
} }
@ -765,6 +979,8 @@ fun LazyVerticalGridTUO(
} }
} }
allTemps.forEachIndexed { syls_i, syllables -> allTemps.forEachIndexed { syls_i, syllables ->
/*val fragmentU0 = measureTUOs[syls_i].pTemplate.template
println("Fragment=[$fragmentU0] -> Syllabes=$syllables")*/
Column( Column(
modifier = Modifier modifier = Modifier
.width(columnWidthDp) .width(columnWidthDp)
@ -818,8 +1034,10 @@ fun LazyVerticalGridTUO(
} }
} }
} }
}
} }
} }
}
} }
@Composable @Composable
@ -881,4 +1099,204 @@ fun makeSpaceBetweenSyllables(
resultText to textAlign resultText to textAlign
} }
} }
}
private suspend fun expandInclusions(
content: String,
currentFilePath: String,
fileRepository: FileRepository
): String {
val lines = content.split("\n")
val result = StringBuilder()
val lastSlash = currentFilePath.lastIndexOf('/')
val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else ""
lines.forEach { line ->
if (line.startsWith("I0:")) {
try {
val parts = line.substring(3).split(":")
val fileName = parts[0]
val ignorePattern = if (parts.size > 1) parts[1] else ""
val fullPath = directory + fileName
val includedLines = fileRepository.readFileLines(fullPath)
val regexIgnore = if (ignorePattern.isNotEmpty()) Regex(ignorePattern) else null
includedLines.forEach { incLine ->
if (regexIgnore == null || !regexIgnore.containsMatchIn(incLine)) {
result.append(incLine).append("\n")
}
}
} catch (e: Exception) {
result.append("// Erreur inclusion: ${e.message}\n")
}
} else {
result.append(line).append("\n")
}
}
return result.toString().trimEnd()
}
@Composable
fun EditSourceCompose(
sharedScreenModel: SharedScreenModel,
solfaScreenModel: SolfaScreenModel
) {
var isEditorVisible = sharedScreenModel.editModeState.value
var sourcePath = sharedScreenModel.activeFilePath.value
var sourceContent = sharedScreenModel.fileContent.value ?: ""
var sourceTitle = sharedScreenModel.songTitle.value
// println("path $sourcePath\n" +
// "contenu $sourceContent")
val originalPath = remember(sourceTitle) {
if (!sourcePath.contains("_tmp") && !sourcePath.contains("/tmp/")) {
sourcePath
} else {
sourcePath
}
}
val fileName = originalPath.substringAfterLast('/').substringAfterLast(':')
val scope = rememberCoroutineScope()
var currentTempFile by remember { mutableStateOf<File?>(null) }
var codeContent by remember { mutableStateOf(sourceContent?: "") }
LaunchedEffect(sourceContent) {
codeContent = sourceContent
}
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
val saveLauncher = rememberFileSaveLauncher { chosenPath ->
if (chosenPath != null) {
val contentToSave = codeContent
scope.launch(Dispatchers.IO) {
try {
val data = codeContent.toByteArray(Charsets.UTF_8)
solfaScreenModel.fileRepository.saveLocalFile(chosenPath, data)
withContext(Dispatchers.Main) {
println("Fichier sauvegardé avec succès")
solfaScreenModel.loadExternalFile(chosenPath)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} else {
println("Sauvegarde annulée par l'utilisateur")
}
}
if(sourcePath.isNotEmpty() && sourceContent.isNotEmpty()) {
Column(modifier = Modifier.fillMaxSize()) {
if (isEditorVisible) {
Card(
modifier = Modifier.fillMaxSize().padding(4.dp),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column {
Row(
Modifier.fillMaxWidth().background(Color(0xFFF5F5F5)).padding(2.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Description, contentDescription = null, modifier = Modifier.size(16.dp))
Text(
text = sourceTitle.ifEmpty { "nouveau_feufaro.txt" },
modifier = Modifier.padding(start = 8.dp).widthIn(max = 160.dp),
style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium)
)
Spacer(modifier = Modifier.weight(1f))
EditorActionButtons(
onUndo = {
scope.launch {
codeContent = sourceContent
withContext(Dispatchers.Main) {
solfaScreenModel.loadExternalFile(originalPath)
}
currentTempFile?.delete()
currentTempFile = null
}
},
onBuild = {
scope.launch {
try {
val expandedContent = expandInclusions(
codeContent,
sourcePath,
solfaScreenModel.fileRepository
)
if (currentTempFile == null || !currentTempFile!!.exists()) {
val tempDir = System.getProperty("java.io.tmpdir")
currentTempFile = File(tempDir, fileName)
currentTempFile?.deleteOnExit()
}
currentTempFile?.writeText(expandedContent)
currentTempFile?.let { file ->
withContext(Dispatchers.Main) {
solfaScreenModel.justeCompile(file.absolutePath)
}
}
println("Compilation sur le fichier : ${currentTempFile?.absolutePath}")
} catch (e: Exception) {
e.printStackTrace()
}
}
},
onSave = {
scope.launch {
try {
saveLauncher.launch(fileName)
} catch (e: Exception) {
e.printStackTrace()
}
}
},
onClose = { sharedScreenModel.toggleEditMode() }
)
}
Row(modifier = Modifier.fillMaxSize().background(Color.White)) {
BasicTextField(
value = codeContent,
onValueChange = { codeContent = it },
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password
),
modifier = Modifier.fillMaxSize().padding(start = 8.dp, top = 8.dp),
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
lineHeight = 20.sp
),
visualTransformation = SolfaVisualTransformation(),
cursorBrush = SolidColor(Color.Black)
)
}
}
}
}
}
}
}
@Composable
fun EditorActionButtons(onUndo: () -> Unit, onBuild: () -> Unit, onSave: () -> Unit, onClose: () -> Unit) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
IconButton(onClick = onUndo) { Icon(Icons.AutoMirrored.Filled.Undo, null, tint = Color.Gray) }
IconButton(onClick = onBuild) { Icon(Icons.Default.Build, null, tint = Color(0xFF2196F3)) }
IconButton(onClick = onSave) { Icon(Icons.Default.Save, null, tint = Color(0xFF4CAF50)) }
IconButton(onClick = onClose) { Icon(Icons.Default.Close, null, tint = Color.Red) }
}
} }

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -23,14 +24,17 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow 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.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mg.dot.feufaro.pdf.PrintSettings import mg.dot.feufaro.pdf.PrintSettings
import mg.dot.feufaro.pdf.defaultPrintSettings import mg.dot.feufaro.pdf.defaultPrintSettings
import mg.dot.feufaro.ui.PrintSettingsDialog import mg.dot.feufaro.ui.PrintSettingsDialog
import mg.dot.feufaro.pdf.rememberPdfExportAction import mg.dot.feufaro.pdf.rememberPdfExportAction
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
import java.io.File
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -67,25 +71,25 @@ fun MainScreenWithDrawer(
val isExpanded by sharedScreenModel.expandedFAB.collectAsState() val isExpanded by sharedScreenModel.expandedFAB.collectAsState()
val showMidiCtrl by sharedScreenModel.showMidiCtrl.collectAsState() val showMidiCtrl by sharedScreenModel.showMidiCtrl.collectAsState()
val isPlay by sharedScreenModel.isPlay.collectAsState() val isPlay by sharedScreenModel.isPlay.collectAsState()
val isPos by sharedScreenModel.isPos.collectAsState() val isPos by sharedScreenModel.isPos.collectAsState()
var isDragging = sharedScreenModel.isDragging var isDragging = sharedScreenModel.isDragging
val currentPos by sharedScreenModel.currentPos.collectAsState() val currentPos by sharedScreenModel.currentPos.collectAsState()
val duration by sharedScreenModel.duration.collectAsState() val duration by sharedScreenModel.duration.collectAsState()
val midiFile = "whawyd3.mid" //What do we gain by all our hard work? val midiFile = "whawyd3.mid" //What do we gain by all our hard work?
var refreshTrigeer by remember { mutableStateOf(0)} var refreshTrigeer by remember { mutableStateOf(0)}
val volumelevel by sharedScreenModel.volumeLevel.collectAsState() val volumelevel by sharedScreenModel.volumeLevel.collectAsState()
val player = sharedScreenModel.mediaPlayer val player = sharedScreenModel.mediaPlayer
LaunchedEffect(isPlay, isPos) { LaunchedEffect(isPlay, isPos) {
if (isPlay && !isPos) { if (isPlay && !isPos) {
while (true) { while (true) {
sharedScreenModel.updateProgress() sharedScreenModel.updateProgress()
delay(100) delay(100)
}
} }
} }
}
LaunchedEffect(isSearchActive) { LaunchedEffect(isSearchActive) {
if (isSearchActive) { if (isSearchActive) {
@ -98,6 +102,43 @@ LaunchedEffect(isPlay, isPos) {
sharedScreenModel.loadNewSong("$midiFile") sharedScreenModel.loadNewSong("$midiFile")
} }
} }
val isEditMode by sharedScreenModel.modeEditor.collectAsState()
val currentActiveFilePath = sharedScreenModel.activeFilePath.value
val fileName = currentActiveFilePath.substringAfterLast('/')
val saveLauncher = rememberFileSaveLauncher { chosenPath ->
if (chosenPath != null) {
scope.launch(Dispatchers.IO) {
try {
delay(500)
val tempDir = System.getProperty("java.io.tmpdir")
val sourceFileName = File(currentActiveFilePath).name
val sourceFile = File(tempDir, sourceFileName)
if (sourceFile.exists() && sourceFile.isFile) {
val data = sourceFile.readBytes()
if (data.isNotEmpty()) {
solfaScreenModel.fileRepository.saveLocalFile(chosenPath, data)
withContext(Dispatchers.Main) {
println("Succès : ${data.size} octets copiés vers $chosenPath")
solfaScreenModel.loadExternalFile(chosenPath)
}
} else {
println("Erreur : Le fichier source dans /tmp est vide.")
}
} else {
println("Erreur : Fichier source introuvable à l'adresse ${sourceFile.path} ${sourceFile.absolutePath}")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
ModalNavigationDrawer(drawerState = drawerState, drawerContent = { ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
SimpleDrawerContent( SimpleDrawerContent(
items, items,
@ -113,7 +154,7 @@ LaunchedEffect(isPlay, isPos) {
onSongSelected = { newSong -> onSongSelected = { newSong ->
sharedScreenModel.loadNewSong("$midiFile") sharedScreenModel.loadNewSong("$midiFile")
} }
) )
}, content = { }, content = {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showPrintSettings by remember { mutableStateOf(false) } var showPrintSettings by remember { mutableStateOf(false) }
@ -148,46 +189,46 @@ LaunchedEffect(isPlay, isPos) {
Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = { Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = {
TopAppBar( TopAppBar(
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.safeDrawing), modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.safeDrawing),
title = { title = {
Column( Column(
modifier = Modifier.fillMaxSize().verticalScroll(scrollState), modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Row { Row {
Text(
songTitle,
modifier = Modifier.weight(1f, fill = true),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
}
}
}, navigationIcon = {
IconButton(onClick = {
scope.launch { drawerState.open() }
}) {
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu")
}
}, actions = {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text( Text(
songTitle, text = songKey,
modifier = Modifier.weight(1f, fill = true), fontSize = 25.sp,
maxLines = 1, fontWeight = FontWeight.Black,
softWrap = false, modifier = Modifier.padding(end = 16.dp)
overflow = TextOverflow.Ellipsis,
) )
} }
} }, colors = TopAppBarColors(
}, navigationIcon = { containerColor = MaterialTheme.colorScheme.primary,
IconButton(onClick = { titleContentColor = MaterialTheme.colorScheme.onPrimary,
scope.launch { drawerState.open() } actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
}) { navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") scrolledContainerColor = MaterialTheme.colorScheme.onPrimary,
} )
}, actions = {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(
text = songKey,
fontSize = 25.sp,
fontWeight = FontWeight.Black,
modifier = Modifier.padding(end = 16.dp)
)
}
}, colors = TopAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
scrolledContainerColor = MaterialTheme.colorScheme.onPrimary,
)
) )
}, floatingActionButton = { }, floatingActionButton = {
Row( Row(
@ -197,178 +238,213 @@ LaunchedEffect(isPlay, isPos) {
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End, modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(7.dp) verticalArrangement = Arrangement.spacedBy(7.dp)
) { ) {
AnimatedVisibility( if (isEditMode) {
visible = isExpanded and !showMidiCtrl, AnimatedVisibility(
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, visible = true,
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
) { exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
Row {
Column {
FloatingActionButton(
onClick = {
sharedScreenModel.descGridCount(1)
}, modifier = Modifier.size(30.dp).alpha(0.45f)
) {
Icon(
Icons.Default.Remove,
contentDescription = null,
tint = Color.Blue
)
}
}
Column {
FloatingActionButton(
onClick = {
sharedScreenModel.addGridCount(1)
}, modifier = Modifier.size(30.dp).alpha(0.45f)
) {
Icon(
Icons.Default.Add,
contentDescription = null,
tint = Color.Blue
)
}
}
}
}
/*AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
sharedScreenModel.toggleEditMode()
sharedScreenModel.setExpandedFAB(false)
}, modifier = Modifier.alpha(0.45f)
) { ) {
Icon( FloatingActionButton(
imageVector = Icons.Filled.Edit, onClick = {
contentDescription = "null", //sharedScreenModel.
tint = Color.Blue }, modifier = Modifier.alpha(0.45f)
) ) {
} Icon(
}*/ imageVector = Icons.AutoMirrored.Default.Undo,
AnimatedVisibility( contentDescription = null,
visible = isExpanded and !showMidiCtrl, tint = Color.Blue
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
sharedScreenModel.toggleQRCodeVisibility()
sharedScreenModel.setExpandedFAB(false)
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Filled.QrCode,
contentDescription = "share qr",
tint = Color.Blue
)
}
}
AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
showPrintSettings = !showPrintSettings
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Filled.Print,
contentDescription = "Imprimer",
tint = Color.Blue
)
}
}
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
sharedScreenModel.setMidiCtrl(!showMidiCtrl)
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = if(showMidiCtrl) Icons.Filled.StopCircle else Icons.Filled.PlayCircle,
contentDescription = "Jouer",
tint = Color.Blue
)
}
}
if (!showMidiCtrl) {
FloatingActionButton(
onClick = {
sharedScreenModel.setExpandedFAB(!isExpanded)
refreshTrigeer++
sharedScreenModel.loadNewSong("$midiFile")
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = if (isExpanded) Icons.Filled.Close else Icons.Filled.Menu,
contentDescription = "MenuFermer",
tint = Color.Blue
)
}
}
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") }
AnimatedVisibility(
visible = true,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
saveLauncher.launch(fileName)
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Filled.SaveAs,
contentDescription = null,
tint = Color.Blue
)
}
}
AnimatedVisibility(
visible = true,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
sharedScreenModel.toggleEditorMode(false)
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = Color.Blue
)
}
}
} else {
AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
Row {
Column {
FloatingActionButton(
onClick = {
sharedScreenModel.descGridCount(1)
}, modifier = Modifier.size(30.dp).alpha(0.45f)
) {
Icon(
Icons.Default.Remove,
contentDescription = null,
tint = Color.Blue
)
}
}
Column {
FloatingActionButton(
onClick = {
sharedScreenModel.addGridCount(1)
}, modifier = Modifier.size(30.dp).alpha(0.45f)
) {
Icon(
Icons.Default.Add,
contentDescription = null,
tint = Color.Blue
)
}
}
}
}
AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
sharedScreenModel.toggleQRCodeVisibility()
sharedScreenModel.setExpandedFAB(false)
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Filled.QrCode,
contentDescription = "share qr",
tint = Color.Blue
)
}
}
AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
showPrintSettings = !showPrintSettings
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Filled.Print,
contentDescription = "Imprimer",
tint = Color.Blue
)
}
}
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
sharedScreenModel.setMidiCtrl(!showMidiCtrl)
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = if(showMidiCtrl) Icons.Filled.StopCircle else Icons.Filled.PlayCircle,
contentDescription = "Jouer",
tint = Color.Blue
)
}
}
if (!showMidiCtrl) {
FloatingActionButton(
onClick = {
sharedScreenModel.setExpandedFAB(!isExpanded)
refreshTrigeer++
sharedScreenModel.loadNewSong("$midiFile")
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = if (isExpanded) Icons.Filled.Close else Icons.Filled.Menu,
contentDescription = "MenuFermer",
tint = Color.Blue
)
}
}
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( Box(
modifier = Modifier.fillMaxSize().padding(paddingValues).consumeWindowInsets(paddingValues).windowInsetsPadding(WindowInsets.ime) modifier = Modifier.fillMaxSize().padding(paddingValues).consumeWindowInsets(paddingValues).windowInsetsPadding(WindowInsets.safeDrawing.union(WindowInsets.ime))
) { ) {
content(PaddingValues(0.dp)) content(PaddingValues(0.dp))
if (sharedScreenModel.isQRCodeVisible.value) { if (sharedScreenModel.isQRCodeVisible.value) {
@ -424,7 +500,7 @@ LaunchedEffect(isPlay, isPos) {
Box( Box(
modifier = Modifier.fillMaxSize().fillMaxWidth(0.75f).align(Alignment.Center) modifier = Modifier.fillMaxSize().fillMaxWidth(0.75f).align(Alignment.Center)
.padding(paddingValues) .padding(paddingValues).windowInsetsPadding(WindowInsets.safeDrawing)
) { ) {
Row( Row(
modifier = Modifier.align(Alignment.TopEnd).padding(16.dp), modifier = Modifier.align(Alignment.TopEnd).padding(16.dp),
@ -464,26 +540,25 @@ LaunchedEffect(isPlay, isPos) {
} }
} }
} }
Column( if(!isEditMode) {
Column {
) { IconButton(
IconButton( onClick = {
onClick = { sharedScreenModel.showSearchMenu(!isSearchActive)
sharedScreenModel.showSearchMenu(!isSearchActive) sharedScreenModel.updateSearchTxt("")
sharedScreenModel.updateSearchTxt("") textInput = ""
textInput = "" }, modifier = Modifier.size(56.dp).alpha(0.45f).background(
}, modifier = Modifier.size(56.dp).alpha(0.45f).background(
color = Color.Blue, shape = CircleShape color = Color.Blue, shape = CircleShape
) )
) { ) {
Icon( Icon(
if(isSearchActive) Icons.Default.Close else Icons.Default.Search, if(isSearchActive) Icons.Default.Close else Icons.Default.Search,
contentDescription = "la recherche", contentDescription = "la recherche",
tint = Color.White tint = Color.White
) )
}
} }
} }
} }
} }

View file

@ -21,6 +21,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -60,11 +61,49 @@ fun SimpleDrawerContent(
var externalExpanded by remember { mutableStateOf(false) } var externalExpanded by remember { mutableStateOf(false) }
var playListExpanded by remember { mutableStateOf(false) } var playListExpanded by remember { mutableStateOf(false) }
val editMode by sharedScreenModel.modeEditor.collectAsState()
var state1 by remember { mutableStateOf(false) }
var state2 by remember { mutableStateOf(false) }
ModalDrawerSheet( ModalDrawerSheet(
modifier = Modifier.width(300.dp) modifier = Modifier.width(300.dp)
) { ) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Box(modifier = Modifier.weight(1f).padding(0.dp, 20.dp)) { // SWITCHES
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CustomSwitchItem(
checked = editMode,
onCheckedChange = { newState ->
sharedScreenModel.toggleEditorMode(newState)
},
label = "Mode Edit",
color = MaterialTheme.colorScheme.primary
)
CustomSwitchItem(
checked = state1,
onCheckedChange = { state1 = it },
label = "Analyse chords",
color = Color(0xFFE57373)
)
CustomSwitchItem(
checked = state2,
onCheckedChange = { state2 = it },
label = "Titre 3",
color = Color(0xFFBFBF11)
)
}
}
Box(modifier = Modifier.weight(1f).padding(0.dp, 10.dp)) {
MyVerticalScrollbar( MyVerticalScrollbar(
lazyListState = listState, lazyListState = listState,
@ -80,7 +119,7 @@ fun SimpleDrawerContent(
stickyHeader { stickyHeader {
DrawerHeaderSticky( DrawerHeaderSticky(
title = "Solfa disponibles", title = "Solfa disponibles",
icon = Icons.AutoMirrored.Filled.Note, icon = Icons.AutoMirrored.Filled.Note,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
isExpanded = internalExpanded, isExpanded = internalExpanded,
onToggle = { internalExpanded = !internalExpanded }, onToggle = { internalExpanded = !internalExpanded },
@ -471,4 +510,31 @@ fun DrawerFavorisItemLabel(item: DrawerItem, index: Int, isSelected: Boolean, sh
) )
} }
} }
}
@Composable
fun CustomSwitchItem(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
label: String,
color: Color
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = color,
uncheckedTrackColor = color.copy(alpha = 0.3f)
),
modifier = Modifier.scale(0.7f)
)
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }

View file

@ -23,6 +23,7 @@ import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.data.getCombinedList import mg.dot.feufaro.data.getCombinedList
import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.midi.FMediaPlayer import mg.dot.feufaro.midi.FMediaPlayer
import mg.dot.feufaro.solfa.TUOEditState
import mg.dot.feufaro.viewmodel.MidiMarkers import mg.dot.feufaro.viewmodel.MidiMarkers
import java.io.File import java.io.File
@ -217,6 +218,41 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
private val _isQRCodeVisible = mutableStateOf(false) private val _isQRCodeVisible = mutableStateOf(false)
val isQRCodeVisible: State<Boolean> = _isQRCodeVisible val isQRCodeVisible: State<Boolean> = _isQRCodeVisible
private val _editModeState = mutableStateOf(false)
val editModeState: State<Boolean> = _editModeState
private val _modeEditor = MutableStateFlow(false)
val modeEditor: StateFlow<Boolean> = _modeEditor
fun toggleEditorMode(enabled: Boolean) {
_modeEditor.value = enabled
}
private val _synchronizedSYllables = MutableStateFlow<List<String>>(emptyList())
val synchronizedSyllables: StateFlow<List<String>> = _synchronizedSYllables.asStateFlow()
fun updateSyllablesFromList(measures: List<List<TimeUnitObject>>, stanzaNumber: Int) {
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
// println("\n--- DEBUG updateSyllablesFromList (Strophe $stanzaNumber) ---")
var absoluteIndex = 0
_synchronizedSYllables.value = measures.flatten().map { tuo ->
val rawSyllablesList = tuo.getSingleSyllable(stanzaNumber)
val processedSyllable = rawSyllablesList.joinToString(" ") { syllable ->
val cleaned = syllable.replace(REGEX_CLEAN_PREFIX, "").trim()
cleaned
}
// println("TUO #$absoluteIndex | Template: ${tuo.pTemplate.template} | Raw: $rawSyllablesList | Final: [$processedSyllable]")
absoluteIndex++
processedSyllable
}
// println("--- FIN DEBUG (Total TUOs: $absoluteIndex) ---\n")
}
private val _activeFilePath = mutableStateOf("") private val _activeFilePath = mutableStateOf("")
val activeFilePath: State<String> = _activeFilePath val activeFilePath: State<String> = _activeFilePath
@ -227,6 +263,9 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun toggleQRCodeVisibility() { fun toggleQRCodeVisibility() {
_isQRCodeVisible.value = !_isQRCodeVisible.value _isQRCodeVisible.value = !_isQRCodeVisible.value
} }
fun toggleEditMode() {
_editModeState.value = !_editModeState.value
}
val qrCodeContent: State<String?> val qrCodeContent: State<String?>
get() = mutableStateOf( get() = mutableStateOf(
_activeFilePath.value.takeIf { it.isNotBlank() }?.let { path -> _activeFilePath.value.takeIf { it.isNotBlank() }?.let { path ->
@ -479,6 +518,25 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
println("Markers finalisés et mis à jour : ${finalizedList.size}") println("Markers finalisés et mis à jour : ${finalizedList.size}")
} }
private val _tuoEditState = MutableStateFlow<TUOEditState?>(null)
val tuoEditState: StateFlow<TUOEditState?> = _tuoEditState.asStateFlow()
fun openTUOEditor(editState: TUOEditState) {
_tuoEditState.value = editState
println("nMrk = [${editState.marker}] template [${editState.templateFragment}] hp [${editState.hairPin}]")
}
fun closeTUOEditor() {
_tuoEditState.value = null
}
fun updateTUOEdit(new: TUOEditState) {
_tuoEditState.value = new
}
fun updateFullTUOList(newList: List<TimeUnitObject>) {
_tuoList.value = newList
}
fun loadNewSong(newMidiFile: String) { fun loadNewSong(newMidiFile: String) {
_mediaPlayer?.stop() _mediaPlayer?.stop()
_mediaPlayer?.release() _mediaPlayer?.release()
@ -488,6 +546,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_currentPos.value = 0f _currentPos.value = 0f
_dcDone.value = false _dcDone.value = false
_dsDone.value = false _dsDone.value = false
_editModeState.value = false
try { try {
val midiFileName = fileRepository.getFileName(newMidiFile) val midiFileName = fileRepository.getFileName(newMidiFile)
println("Opening xx129 $midiFileName") println("Opening xx129 $midiFileName")
@ -587,6 +646,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_showMidiCtrl.value = false _showMidiCtrl.value = false
_expandedFAB.value = false _expandedFAB.value = false
_showSearchMenu.value = false _showSearchMenu.value = false
_editModeState.value = false
_midiMarkersList.value = emptyList() _midiMarkersList.value = emptyList()
_tuoTimestamps.value = emptyList() _tuoTimestamps.value = emptyList()
updateSearchTxt("") updateSearchTxt("")

View file

@ -2,9 +2,13 @@ package mg.dot.feufaro.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mg.dot.feufaro.DeepLinkHandler import mg.dot.feufaro.DeepLinkHandler
import mg.dot.feufaro.FileRepository import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.solfa.TUOEditState
import java.io.File
class SolfaScreenModel( class SolfaScreenModel(
val fileRepository: FileRepository, val fileRepository: FileRepository,
@ -38,4 +42,13 @@ class SolfaScreenModel(
fun loadFromFile(path: String) { fun loadFromFile(path: String) {
solfa.parse(path) solfa.parse(path)
} }
fun justeCompile(path:String) {
solfa.justeBuild(path)
}
fun saveTUOEdit(editState: TUOEditState) {
screenModelScope.launch(Dispatchers.IO) {
solfa.saveTUOEdit(editState)
}
}
} }

View file

@ -598,7 +598,7 @@ actual fun rememberPdfExportAction(
settings settings
) )
val chosenPath = withContext(Dispatchers.Main) { val chosenPath = withContext(Dispatchers.Main) {
fileRepository.pickSavePath(computedFileName) fileRepository.pickSavePath(computedFileName, "Exporter en PDF")
} }
if (chosenPath != null) { if (chosenPath != null) {
fileRepository.saveLocalFile(chosenPath, pdfBytes) fileRepository.saveLocalFile(chosenPath, pdfBytes)