diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt index 77f7419..7c6c219 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt @@ -30,6 +30,10 @@ class MainActivity : ComponentActivity() { 1 ) 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) setFilePickerActivity(this) WindowCompat.setDecorFitsSystemWindows(window, false) diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt index 3f3469c..62df64a 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt @@ -399,7 +399,7 @@ actual fun rememberPdfExportAction( nbStanza, songKey, songAut, songComp, songRythm ) val chosenPath = withContext(Dispatchers.Main) { - fileRepository.pickSavePath(computedFileName) + fileRepository.pickSavePath(computedFileName, "Exporter en PDF") } if (chosenPath != null) { fileRepository.saveLocalFile(chosenPath, pdfBytes) diff --git a/composeApp/src/commonMain/composeResources/files/config.json b/composeApp/src/commonMain/composeResources/files/config.json index 8bfa415..1995db9 100644 --- a/composeApp/src/commonMain/composeResources/files/config.json +++ b/composeApp/src/commonMain/composeResources/files/config.json @@ -2,8 +2,8 @@ "themeMode": "DARK", "fontSize": 18.5, "playlist": [ - "assets://a.txt", "assets://ffpm-617.txt", + "assets://a.txt", "assets://ews-127.txt", "assets://ffpm-521.txt", "assets://ews-126.txt", diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ContextualMenu.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ContextualMenu.kt index f64354b..9e074b2 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ContextualMenu.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ContextualMenu.kt @@ -1,23 +1,38 @@ package mg.dot.feufaro import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement 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.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List 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.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.IconButton 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 fun ContextualMenu (onMenuItemClick: (String) -> Unit){ Column( @@ -32,7 +47,7 @@ fun ContextualMenu (onMenuItemClick: (String) -> Unit){ } @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) { Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) { Icon(icon, contentDescription = text, tint = Color.White) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt index aa0c053..9b43244 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt @@ -13,13 +13,21 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape 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.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -50,6 +58,8 @@ import cafe.adriel.voyager.koin.koinScreenModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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.viewmodel.SolfaScreenModel import java.io.ObjectStreamException @@ -64,7 +74,7 @@ object ScreenSolfa : Screen { val tuoList by sharedScreenModel.tuoList.collectAsState() val measure by sharedScreenModel.measure.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 scrollState = rememberScrollState() var showContextualMenu = false @@ -81,6 +91,16 @@ object ScreenSolfa : Screen { // DeepLinkHandler.consumePending("") } + val tuoEditState by sharedScreenModel.tuoEditState.collectAsState() + + LaunchedEffect(tuoEditState) { + tuoEditState?.let { state -> + coroutineScope.launch(Dispatchers.IO) { + solfaScreenModel.saveTUOEdit(state) + } + sharedScreenModel.closeTUOEditor() + } + } DisposableEffect(Unit) { onDispose { DeepLinkHandler.onSongReceived = null @@ -97,7 +117,10 @@ object ScreenSolfa : Screen { solfaScrollState = scrollState ) { paddingValues -> Box( - Modifier.fillMaxSize().padding(paddingValues) + Modifier.fillMaxSize() + .padding(paddingValues) + .windowInsetsPadding(WindowInsets.safeDrawing) + .windowInsetsPadding(WindowInsets.ime) .padding(horizontal = 16.dp) ) { Column( @@ -139,7 +162,7 @@ object ScreenSolfa : Screen { ) { FlowRow( modifier = Modifier.fillMaxWidth() - .windowInsetsPadding(WindowInsets.safeDrawing) + //.windowInsetsPadding(WindowInsets.safeDrawing) .padding(start = 8.dp, end = 8.dp, top = 8.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp) @@ -163,10 +186,23 @@ object ScreenSolfa : Screen { sharedScreenModel = sharedScreenModel, onGridWidthMeasured = { width -> gridWidthPx = width } ) + EditSourceCompose(sharedScreenModel, solfaScreenModel) FlowRow( horizontalArrangement = Arrangement.spacedBy(16.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 = { //showContent = !showContent @@ -184,7 +220,7 @@ object ScreenSolfa : Screen { } FlowRow( modifier = Modifier.fillMaxWidth() - .windowInsetsPadding(WindowInsets.safeDrawing) + //.windowInsetsPadding(WindowInsets.safeDrawing) .padding(start = 8.dp, end = 8.dp, top = 8.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt index 14a810f..3e47528 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt @@ -9,10 +9,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mg.dot.feufaro.FileRepository 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.midi.MidiPitch import mg.dot.feufaro.midi.MidiWriterKotlin +import java.io.File import kotlin.math.min //@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 = mutableMapOf() private val lyricsComment: MutableList = mutableListOf() - suspend fun nextTimeUnitObject() { - val lastTUO = sharedScreenModel.lastTUO() + suspend fun nextTimeUnitObject(accumulator: MutableList) { + val lastTUO = accumulator.lastOrNull() nextTIndex++ - if (nextTIndex == 5) { + if (T.getOrNull(nextTIndex) == null) { + withContext(Dispatchers.Main) { + sharedScreenModel.updateFullTUOList(accumulator) + //sharedScreenModel.doneTUOList() + } + return + } + /*if (nextTIndex == 5) { sharedScreenModel.doneTUOList() } if (T.getOrNull(nextTIndex) == null) { sharedScreenModel.doneTUOList() return - } + }*/ val pTemplate = T[nextTIndex] val unitObject = TimeUnitObject(pTemplate, lastTUO, N.size) if (lastTUO == null) { @@ -132,9 +140,10 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository } } withContext(Dispatchers.Main) { - sharedScreenModel.addTUO(unitObject) + //sharedScreenModel.addTUO(unitObject) + accumulator.add(unitObject) } - nextTimeUnitObject() + nextTimeUnitObject(accumulator) } fun loadSolfa() { @@ -157,7 +166,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository screenModelScope.launch { val initialPath = stateSettings.loadLastUsedDir() - val homedir = getConfigDirectoryPath(); + val homedir = fileRepository.getAppPublicFolder().absolutePath launchFilePicker( mimeTypes = arrayOf("text/plain"), @@ -186,7 +195,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository sharedScreenModel.reset() } fun parse(sourceFile: String) { - println("37: lFL bien reçu $sourceFile") + //println("37: lFL bien reçu $sourceFile") currentFile = sourceFile val parseScope = CoroutineScope(Dispatchers.Default) parseScope.launch { @@ -229,7 +238,11 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository preloadN(it) } try { - nextTimeUnitObject() + val localAccumulator = mutableListOf() + withContext(Dispatchers.Main) { + sharedScreenModel.updateFullTUOList(emptyList()) + } + nextTimeUnitObject(localAccumulator) } catch (e: Exception) { 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) } } + 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() + 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, + fragments: List, + targetIdx: Int, + deleteCount: Int, + editState: TUOEditState + ): MutableList { + /* 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, + fragments: List, + 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, + fragments: List, + 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() + 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 { + 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 { + val content = noteLine.substringAfter(":").replace(" ", "") + + val result = mutableListOf() + 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 { + var templateArray: List = 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() + 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 { var result: String = noteString @@ -968,4 +1682,96 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository this.add(element) } } + + fun getNotePointerWithDebug(templatArray: List, 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() + 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, targetIdx: Int, sourceLine: String): Int { + val bodyOnly = sourceLine.substringAfter(":") + val expandedBody = expandNotes(bodyOnly) + val notesList = parseNoteLineToArray("N:$expandedBody") + var currentPointer = 0 + var notesInTargetBlock = mutableListOf() + 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 + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/SolfaVisualTransformation.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/SolfaVisualTransformation.kt new file mode 100644 index 0000000..367723d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/SolfaVisualTransformation.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt new file mode 100644 index 0000000..aab1189 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -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().apply { + putAll(editState.notesByVoice) + } + } + val originalNotes = mutableStateMapOf().apply { + putAll(editState.notesByVoice) + } + val originalLyricsByStz = mutableStateMapOf().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().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 = "N°$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 + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUOEditState.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUOEditState.kt new file mode 100644 index 0000000..d0c9c2e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUOEditState.kt @@ -0,0 +1,12 @@ +package mg.dot.feufaro.solfa + +data class TUOEditState( + val tuoIndex: Int, + val notesByVoice: Map = emptyMap(), + val originalNotes: Map = emptyMap(), + val lyricsByStanza: MutableMap = mutableMapOf(), + val originalLyricsByStanza: MutableMap = mutableMapOf(), + val templateFragment: String = "", + val marker: String = "", + val hairPin: String = "" +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 14ac9bd..06e519b 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -1,55 +1,66 @@ 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 androidx.compose.animation.animateColorAsState 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.text.TextMeasurer -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.Modifier +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.SolfaScreenModel +import java.io.File +import kotlin.math.min +import kotlin.math.roundToInt val FEUFAROO_TRIOLET_COLOR = Color.DarkGray val FEUFAROO_KEY_CHANGE_COLOR = Color.Blue +@Stable class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, countOfN: Int) { var mutableNoteVersion: Int by mutableStateOf(0) private var lyrics: MutableList = mutableListOf() @@ -60,6 +71,7 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co private var annotated : Boolean var transposeNewKey: String = "" var transposeOldKey: String = "" + var firstTuoIndex: Int = -1 companion object { var nbBlock: Int = 0 var sep1: String = "" @@ -543,8 +555,29 @@ fun LazyVerticalGridTUO( onGridWidthMeasured: (Int) -> Unit, modifier: Modifier = Modifier ) { - val regexMeasure = Regex("(\\d)/\\d").find(viewModel.measure) 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(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( modifier = Modifier @@ -618,11 +651,21 @@ fun LazyVerticalGridTUO( println("Mise à jour MidiData avec ${metadataList.size}") 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( modifier = Modifier.fillMaxWidth() ){ measures.forEachIndexed { measureIndex, measureTUOs -> + key(measureIndex) { Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { 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()) { measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure val isActive = (globalIndex == activeRowIndex) 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) - .combinedClickable( - onClick = { -// println("je suis sur grille no: $globalIndex") - sharedScreenModel.seekToGrid(globalIndex) - } , - onDoubleClick = { - val m = sharedScreenModel.getFullMarkers() - m.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> - println("Allmarker $marker in $gridIndex on $timestamp ms") - // & note= $note & nBefore =$noteBefore & separateur = $separat + key(oneTUO.numBlock) { + Box(modifier = Modifier + .width(gridWidthDp / gridColumnCount) + .background( + if (isHovered) Color.Black.copy(alpha = 0.05f) else Color.Transparent, + RoundedCornerShape(4.dp) + ) + .combinedClickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + 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 -> + /*val fragmentU0 = measureTUOs[syls_i].pTemplate.template + println("Fragment=[$fragmentU0] -> Syllabes=$syllables")*/ Column( modifier = Modifier .width(columnWidthDp) @@ -818,8 +1034,10 @@ fun LazyVerticalGridTUO( } } } + } } } + } } @Composable @@ -881,4 +1099,204 @@ fun makeSpaceBetweenSyllables( 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(null) } + + var codeContent by remember { mutableStateOf(sourceContent?: "") } + LaunchedEffect(sourceContent) { + codeContent = sourceContent + } + var textLayoutResult by remember { mutableStateOf(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) } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index 6e94e0f..30ac05c 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.filled.* import androidx.compose.material3.* 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.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import mg.dot.feufaro.pdf.PrintSettings import mg.dot.feufaro.pdf.defaultPrintSettings import mg.dot.feufaro.ui.PrintSettingsDialog import mg.dot.feufaro.pdf.rememberPdfExportAction import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.viewmodel.SolfaScreenModel +import java.io.File @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -67,25 +71,25 @@ fun MainScreenWithDrawer( val isExpanded by sharedScreenModel.expandedFAB.collectAsState() val showMidiCtrl by sharedScreenModel.showMidiCtrl.collectAsState() -val isPlay by sharedScreenModel.isPlay.collectAsState() -val isPos by sharedScreenModel.isPos.collectAsState() -var isDragging = sharedScreenModel.isDragging -val currentPos by sharedScreenModel.currentPos.collectAsState() -val duration by sharedScreenModel.duration.collectAsState() -val midiFile = "whawyd3.mid" //What do we gain by all our hard work? -var refreshTrigeer by remember { mutableStateOf(0)} + val isPlay by sharedScreenModel.isPlay.collectAsState() + val isPos by sharedScreenModel.isPos.collectAsState() + var isDragging = sharedScreenModel.isDragging + val currentPos by sharedScreenModel.currentPos.collectAsState() + val duration by sharedScreenModel.duration.collectAsState() + val midiFile = "whawyd3.mid" //What do we gain by all our hard work? + var refreshTrigeer by remember { mutableStateOf(0)} -val volumelevel by sharedScreenModel.volumeLevel.collectAsState() + val volumelevel by sharedScreenModel.volumeLevel.collectAsState() -val player = sharedScreenModel.mediaPlayer -LaunchedEffect(isPlay, isPos) { - if (isPlay && !isPos) { - while (true) { - sharedScreenModel.updateProgress() - delay(100) + val player = sharedScreenModel.mediaPlayer + LaunchedEffect(isPlay, isPos) { + if (isPlay && !isPos) { + while (true) { + sharedScreenModel.updateProgress() + delay(100) + } } } -} LaunchedEffect(isSearchActive) { if (isSearchActive) { @@ -98,6 +102,43 @@ LaunchedEffect(isPlay, isPos) { 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 = { SimpleDrawerContent( items, @@ -113,7 +154,7 @@ LaunchedEffect(isPlay, isPos) { onSongSelected = { newSong -> sharedScreenModel.loadNewSong("$midiFile") } - ) + ) }, content = { val scope = rememberCoroutineScope() var showPrintSettings by remember { mutableStateOf(false) } @@ -148,46 +189,46 @@ LaunchedEffect(isPlay, isPos) { Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = { TopAppBar( modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.safeDrawing), - title = { - Column( - modifier = Modifier.fillMaxSize().verticalScroll(scrollState), - verticalArrangement = Arrangement.Center - ) { - Row { + title = { + Column( + modifier = Modifier.fillMaxSize().verticalScroll(scrollState), + verticalArrangement = Arrangement.Center + ) { + 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( - songTitle, - modifier = Modifier.weight(1f, fill = true), - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, + text = songKey, + fontSize = 25.sp, + fontWeight = FontWeight.Black, + modifier = Modifier.padding(end = 16.dp) ) } - } - }, navigationIcon = { - IconButton(onClick = { - scope.launch { drawerState.open() } - }) { - Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") - } - }, 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, - ) + }, colors = TopAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + scrolledContainerColor = MaterialTheme.colorScheme.onPrimary, + ) ) }, floatingActionButton = { Row( @@ -197,178 +238,213 @@ LaunchedEffect(isPlay, isPos) { modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(7.dp) ) { - 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.toggleEditMode() - sharedScreenModel.setExpandedFAB(false) - }, modifier = Modifier.alpha(0.45f) + if (isEditMode) { + AnimatedVisibility( + visible = true, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } ) { - Icon( - imageVector = Icons.Filled.Edit, - 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) - } + FloatingActionButton( + onClick = { + //sharedScreenModel. + }, modifier = Modifier.alpha(0.45f) + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.Undo, + contentDescription = null, + tint = Color.Blue ) - } 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 -> 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)) if (sharedScreenModel.isQRCodeVisible.value) { @@ -424,7 +500,7 @@ LaunchedEffect(isPlay, isPos) { Box( modifier = Modifier.fillMaxSize().fillMaxWidth(0.75f).align(Alignment.Center) - .padding(paddingValues) + .padding(paddingValues).windowInsetsPadding(WindowInsets.safeDrawing) ) { Row( modifier = Modifier.align(Alignment.TopEnd).padding(16.dp), @@ -464,26 +540,25 @@ LaunchedEffect(isPlay, isPos) { } } } - Column( - - ) { - IconButton( - onClick = { - sharedScreenModel.showSearchMenu(!isSearchActive) - sharedScreenModel.updateSearchTxt("") - textInput = "" - }, modifier = Modifier.size(56.dp).alpha(0.45f).background( + if(!isEditMode) { + Column { + IconButton( + onClick = { + sharedScreenModel.showSearchMenu(!isSearchActive) + sharedScreenModel.updateSearchTxt("") + textInput = "" + }, modifier = Modifier.size(56.dp).alpha(0.45f).background( color = Color.Blue, shape = CircleShape ) - ) { - Icon( - if(isSearchActive) Icons.Default.Close else Icons.Default.Search, - contentDescription = "la recherche", - tint = Color.White - ) + ) { + Icon( + if(isSearchActive) Icons.Default.Close else Icons.Default.Search, + contentDescription = "la recherche", + tint = Color.White + ) + } } } - } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt index 51c0283..d3f1f0f 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight @@ -60,11 +61,49 @@ fun SimpleDrawerContent( var externalExpanded 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( modifier = Modifier.width(300.dp) ) { 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( lazyListState = listState, @@ -80,7 +119,7 @@ fun SimpleDrawerContent( stickyHeader { DrawerHeaderSticky( title = "Solfa disponibles", - icon = Icons.AutoMirrored.Filled.Note, + icon = Icons.AutoMirrored.Filled.Note, color = MaterialTheme.colorScheme.primary, isExpanded = 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 + ) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index 7cb3bc2..0ffc11a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -23,6 +23,7 @@ import mg.dot.feufaro.data.GridTUOData import mg.dot.feufaro.data.getCombinedList import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.midi.FMediaPlayer +import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.viewmodel.MidiMarkers import java.io.File @@ -217,6 +218,41 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode private val _isQRCodeVisible = mutableStateOf(false) val isQRCodeVisible: State = _isQRCodeVisible + private val _editModeState = mutableStateOf(false) + val editModeState: State = _editModeState + + private val _modeEditor = MutableStateFlow(false) + val modeEditor: StateFlow = _modeEditor + + fun toggleEditorMode(enabled: Boolean) { + _modeEditor.value = enabled + } + + private val _synchronizedSYllables = MutableStateFlow>(emptyList()) + val synchronizedSyllables: StateFlow> = _synchronizedSYllables.asStateFlow() + + fun updateSyllablesFromList(measures: List>, 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("") val activeFilePath: State = _activeFilePath @@ -227,6 +263,9 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode fun toggleQRCodeVisibility() { _isQRCodeVisible.value = !_isQRCodeVisible.value } + fun toggleEditMode() { + _editModeState.value = !_editModeState.value + } val qrCodeContent: State get() = mutableStateOf( _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}") } + private val _tuoEditState = MutableStateFlow(null) + val tuoEditState: StateFlow = _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) { + _tuoList.value = newList + } fun loadNewSong(newMidiFile: String) { _mediaPlayer?.stop() _mediaPlayer?.release() @@ -488,6 +546,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode _currentPos.value = 0f _dcDone.value = false _dsDone.value = false + _editModeState.value = false try { val midiFileName = fileRepository.getFileName(newMidiFile) println("Opening xx129 $midiFileName") @@ -587,6 +646,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode _showMidiCtrl.value = false _expandedFAB.value = false _showSearchMenu.value = false + _editModeState.value = false _midiMarkersList.value = emptyList() _tuoTimestamps.value = emptyList() updateSearchTxt("") diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt index d5638d1..5df5c3c 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt @@ -2,9 +2,13 @@ package mg.dot.feufaro.viewmodel import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import mg.dot.feufaro.DeepLinkHandler import mg.dot.feufaro.FileRepository import mg.dot.feufaro.solfa.Solfa +import mg.dot.feufaro.solfa.TUOEditState +import java.io.File class SolfaScreenModel( val fileRepository: FileRepository, @@ -38,4 +42,13 @@ class SolfaScreenModel( fun loadFromFile(path: String) { solfa.parse(path) } + + fun justeCompile(path:String) { + solfa.justeBuild(path) + } + fun saveTUOEdit(editState: TUOEditState) { + screenModelScope.launch(Dispatchers.IO) { + solfa.saveTUOEdit(editState) + } + } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt index 02d6748..3cdf935 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt @@ -598,7 +598,7 @@ actual fun rememberPdfExportAction( settings ) val chosenPath = withContext(Dispatchers.Main) { - fileRepository.pickSavePath(computedFileName) + fileRepository.pickSavePath(computedFileName, "Exporter en PDF") } if (chosenPath != null) { fileRepository.saveLocalFile(chosenPath, pdfBytes)