Add Edit Mode With source line & on UI(Only notes)
This commit is contained in:
parent
49186a76c8
commit
410046a3cc
15 changed files with 2370 additions and 321 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<String, String> = mutableMapOf()
|
||||
private val lyricsComment: MutableList<String> = mutableListOf()
|
||||
suspend fun nextTimeUnitObject() {
|
||||
val lastTUO = sharedScreenModel.lastTUO()
|
||||
suspend fun nextTimeUnitObject(accumulator: MutableList<TimeUnitObject>) {
|
||||
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<TimeUnitObject>()
|
||||
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<TimeUnitObject>()
|
||||
withContext(Dispatchers.Main) {
|
||||
sharedScreenModel.updateFullTUOList(emptyList())
|
||||
}
|
||||
nextTimeUnitObject(localAccumulator)
|
||||
} catch (e: Exception) {
|
||||
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
|
||||
}
|
||||
val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
|
||||
val midiWriter = MidiWriterKotlin(fileRepository)
|
||||
midiWriter.process(pitches)
|
||||
midiWriter.save("whawyd3.mid")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveTUOEdit(editState: TUOEditState) {
|
||||
println(editState)
|
||||
/* N.mapIndexed { index, note ->
|
||||
note.oneVoiceNote.mapIndexed { index, notes ->
|
||||
println(notes.note)
|
||||
}
|
||||
}*/
|
||||
val filePath = currentFile
|
||||
if (filePath.isBlank()) return
|
||||
|
||||
|
||||
// inclusion -----
|
||||
val includedContent = expandInclusions(filePath)
|
||||
|
||||
val lines = includedContent.split("\n").toMutableList()
|
||||
|
||||
val targetIdx = editState.tuoIndex
|
||||
|
||||
val t0Idx = lines.indexOfFirst { it.startsWith("T0:") }
|
||||
val u0Idx = lines.indexOfFirst { it.startsWith("U0:") }
|
||||
|
||||
val noteS = lines[lines.indexOfFirst { it.startsWith("N1") }]
|
||||
val noteA = lines[lines.indexOfFirst { it.startsWith("N2") }]
|
||||
val noteT = lines[lines.indexOfFirst { it.startsWith("N3") }]
|
||||
val noteB = lines[lines.indexOfFirst { it.startsWith("N4") }]
|
||||
|
||||
val currentStanza = sharedScreenModel.stanza.value
|
||||
|
||||
val lyrIdx = lines.indexOfFirst { line ->
|
||||
line.contains(Regex("^[EY]$currentStanza:"))
|
||||
}
|
||||
|
||||
val lyrics = lines[lyrIdx]
|
||||
println("Parole sur $currentStanza: $lyrics")
|
||||
|
||||
val content = smartYLyrics(lyrics).substringAfter(":")
|
||||
|
||||
val indexedLyrics = content.split('_', ' ', '/')
|
||||
.filter { it.isNotBlank() }
|
||||
.toMutableList()
|
||||
|
||||
|
||||
/*indexedLyrics.forEachIndexed { index, syllable ->
|
||||
println("Index $index : [$syllable]")
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
val measureString = sharedScreenModel.measure.value
|
||||
|
||||
val templateString = when {
|
||||
t0Idx != -1 -> {
|
||||
lines[t0Idx]
|
||||
}
|
||||
u0Idx != -1 -> {
|
||||
lines[u0Idx]
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val templateIndices = when {
|
||||
t0Idx != -1 -> {
|
||||
t0Idx
|
||||
}
|
||||
u0Idx != -1 -> {
|
||||
u0Idx
|
||||
}
|
||||
else -> 0
|
||||
}
|
||||
|
||||
when {
|
||||
t0Idx != -1 -> {
|
||||
val line = lines[t0Idx]
|
||||
val prefix = line.substringBefore('{') + "{"
|
||||
val suffix = "}"
|
||||
val body = line.substringAfter('{').substringBeforeLast('}')
|
||||
|
||||
val regex = Regex("([^:|/!]*[:|/!])|([^:|/!]+)")
|
||||
val fragments = regex.findAll(body).map { it.value }.toMutableList()
|
||||
// println("Nombre de blocs détectés : ${fragments.size}")
|
||||
|
||||
val markerRegex = Regex("""\$\{.*?\}|\$.""")
|
||||
|
||||
if (targetIdx in fragments.indices) {
|
||||
val oldFrag = fragments[targetIdx]
|
||||
|
||||
val delimiter = if (oldFrag.isNotEmpty() && oldFrag.last() in ":|/!") {
|
||||
oldFrag.last().toString()
|
||||
} else ""
|
||||
|
||||
val cleanBody = fragments.joinToString("") { it.replace(markerRegex, "") }
|
||||
lines[t0Idx] = prefix + cleanBody + suffix
|
||||
|
||||
println("AVANT: $line")
|
||||
println("APRÈS: ${lines[t0Idx]}")
|
||||
} else {
|
||||
println("Index $targetIdx hors limites pour T0 (taille=${fragments.size})")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
u0Idx != -1 -> {
|
||||
val fullLine = lines[u0Idx]
|
||||
// println("=== DEBUG U0 ===")
|
||||
// println("fullLine = '$fullLine'")
|
||||
|
||||
val afterU0 = fullLine.substringAfter("U0:")
|
||||
val blankPrefixRegex = Regex("^(z[048CEKO]:)")
|
||||
val match = blankPrefixRegex.find(afterU0)
|
||||
val blankPrefix = match?.value ?: ""
|
||||
val body = if (match != null) {
|
||||
afterU0.substring(blankPrefix.length)
|
||||
} else {
|
||||
afterU0
|
||||
}
|
||||
|
||||
val measure = measureString.split("/")[0].toIntOrNull() ?: 4
|
||||
|
||||
val parser = ParseULine(body, measure)
|
||||
val symboles = parser.parsed()
|
||||
|
||||
val newLigneT = "U0:"+blankPrefix+symboles
|
||||
println("Ligne de U0 ==> $newLigneT")
|
||||
lines[u0Idx] = newLigneT
|
||||
|
||||
}
|
||||
|
||||
else -> {
|
||||
println("Aucune ligne T0 ou U0 trouvée dans le fichier")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val templatArray = templateToArray(if(u0Idx != -1) lines[u0Idx] else lines[t0Idx])
|
||||
/* println("index | contenuTUO")
|
||||
templatArray.forEachIndexed { index, block ->
|
||||
println("$index $block")
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
MODIF NOTES N1 N2 N3 N4
|
||||
*/
|
||||
// println("taille tA: ${templatArray.size} et le trgt $targetIdx")
|
||||
val originalBlock = templatArray[targetIdx]
|
||||
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
|
||||
val deleteCount = originalBlock.replace(markerRegex, "").count { it.isLetter() }
|
||||
|
||||
val updatedNotes = updateSourceLines(lines, templatArray, targetIdx, deleteCount, editState)
|
||||
|
||||
// RESTAURATION DES TEMPLATES
|
||||
if (templateString != "") {
|
||||
updatedNotes[templateIndices] = templateString
|
||||
}
|
||||
|
||||
val finalString = updatedNotes.joinToString("\n")
|
||||
|
||||
val tempDir = System.getProperty("java.io.tmpdir")
|
||||
val fileName = filePath.substringAfterLast('/')
|
||||
val currentTempFile = File(tempDir, fileName)
|
||||
currentTempFile?.deleteOnExit()
|
||||
|
||||
|
||||
val normalizedTempDir = File(tempDir).absolutePath
|
||||
val originalFilePath = if (filePath.startsWith(normalizedTempDir)) {
|
||||
filePath
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// println("Fichier Modifier:\n" +
|
||||
// "$finalString")
|
||||
//fileRepository.saveFile(filePath, finalString.toByteArray())
|
||||
|
||||
currentTempFile?.writeBytes(finalString.toByteArray())
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
justeBuild(currentTempFile.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun expandInclusions(filePath: String):String {
|
||||
val result = StringBuilder()
|
||||
val lastSlash = filePath.lastIndexOf('/')
|
||||
val directory = if (lastSlash != -1) filePath.substring(0, lastSlash + 1) else ""
|
||||
val myLines = fileRepository.readFileLines(filePath)
|
||||
myLines.forEach { line ->
|
||||
if (line.startsWith("I0:")) {
|
||||
try {
|
||||
val parts = line.substring(3).split(":")
|
||||
val fileName = parts[0]
|
||||
val ignorePattern = if (parts.size > 1) parts[1] else ""
|
||||
|
||||
val fullPath = directory + fileName
|
||||
val includedLines = fileRepository.readFileLines(fullPath)
|
||||
|
||||
val regexIgnore = if (ignorePattern.isNotEmpty()) Regex(ignorePattern) else null
|
||||
|
||||
includedLines.forEach { incLine ->
|
||||
if (regexIgnore == null || !regexIgnore.containsMatchIn(incLine)) {
|
||||
result.append(incLine).append("\n")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.append("// Erreur inclusion: ${e.message}\n")
|
||||
}
|
||||
} else {
|
||||
result.append(line).append("\n")
|
||||
}
|
||||
}
|
||||
return result.toString().trimEnd()
|
||||
}
|
||||
|
||||
private fun updateSourceLines(
|
||||
lines: MutableList<String>,
|
||||
fragments: List<String>,
|
||||
targetIdx: Int,
|
||||
deleteCount: Int,
|
||||
editState: TUOEditState
|
||||
): MutableList<String> {
|
||||
/* Notes N1 N2 N3 N4 */
|
||||
updateNotesInLines(lines, fragments, targetIdx, deleteCount, editState)
|
||||
/* MODIF LYRICS */
|
||||
//updateLyricsInLines(lines, fragments, targetIdx, deleteCount, editState)
|
||||
return lines
|
||||
}
|
||||
|
||||
private fun updateLyricsInLines(
|
||||
lines: MutableList<String>,
|
||||
fragments: List<String>,
|
||||
targetIdx: Int,
|
||||
deleteCount: Int,
|
||||
editState: TUOEditState
|
||||
) {
|
||||
editState.lyricsByStanza.forEach { (stanzaNum, newSyllable) ->
|
||||
val lineYIdx = lines.indexOfFirst { it.matches(Regex("^[Y]$stanzaNum:.*")) }
|
||||
val lineEIdx = lines.indexOfFirst { it.matches(Regex("^[E]$stanzaNum:.*")) }
|
||||
|
||||
val lineIdx = if(lineYIdx != -1) lineYIdx else lineEIdx
|
||||
|
||||
if (lineIdx != -1) {
|
||||
val currentLine = lines[lineIdx]
|
||||
val prefix = currentLine.substringBefore(":") + ":"
|
||||
val lyricsBody = currentLine.substringAfter(":")
|
||||
|
||||
// On traite les paroles pour avoir la liste brute
|
||||
val lyricsContent = if(lineYIdx != -1) smartYLyrics(lyricsBody) else smartELyrics(lyricsBody)
|
||||
|
||||
// Découpage strict pour correspondre au template
|
||||
println("Lyrics BODY $lyricsBody")
|
||||
println("LyricsC $lyricsContent")
|
||||
|
||||
val lyricsInLines = sharedScreenModel.synchronizedSyllables.value
|
||||
lyricsInLines.mapIndexed { index, string ->
|
||||
//println("SYNRO $index => {$string}")
|
||||
}
|
||||
|
||||
val allTokens = lyricsInLines.toMutableList()
|
||||
|
||||
// println("--- TABLEAU DE CORRESPONDANCE (Strophe $stanzaNum) ---")
|
||||
// println(String.format("%-5s | %-10s | %-15s", "Idx", "Template", "Syllabe"))
|
||||
// println("-------------------------------------------")
|
||||
|
||||
var actualTokenPointer = 0
|
||||
for (i in fragments.indices) {
|
||||
val fragment = fragments[i]
|
||||
val isProlongation = fragment.trim() == "-"
|
||||
|
||||
val associatedSyllable = allTokens.getOrNull(actualTokenPointer) ?: ""
|
||||
actualTokenPointer++
|
||||
|
||||
val marker = if (i == targetIdx) " <== [CIBLE]" else ""
|
||||
|
||||
// println(String.format("%-5d | %-10s | %-15s %s",
|
||||
// i,
|
||||
// "[$fragment]",
|
||||
// "[$associatedSyllable]",
|
||||
// marker))
|
||||
}
|
||||
|
||||
println("\nAction finale : Remplacer l'index $targetIdx (valeur: ${allTokens.getOrNull(targetIdx)})")
|
||||
|
||||
if (targetIdx < allTokens.size) {
|
||||
allTokens[targetIdx] = newSyllable
|
||||
|
||||
if(deleteCount > 1){
|
||||
var deleted = 0
|
||||
var j = targetIdx + 1
|
||||
while (deleted < deleteCount - 1 && j < allTokens.size) {
|
||||
if (allTokens[j].isNotEmpty()) {
|
||||
allTokens[j] = ""
|
||||
deleted++
|
||||
}
|
||||
j++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val rawResult = allTokens.filter { it.isNotEmpty() }.joinToString(" ")
|
||||
println("allToken ${allTokens.joinToString("")}")
|
||||
// println("RAWRes ${rawResult}")
|
||||
val formattedResult = rawResult
|
||||
.replace(Regex("-\\s+"), "")
|
||||
.replace(Regex("-"), "")
|
||||
|
||||
println("FORMATTER $formattedResult")
|
||||
|
||||
val lyrIdx = lines.indexOfFirst { line ->
|
||||
line.contains(Regex("^[EY]$stanzaNum:"))
|
||||
}
|
||||
val lyrics = lines[lyrIdx]
|
||||
println("Originl $lyrics")
|
||||
|
||||
|
||||
lines[lineIdx] = prefix + formattedResult
|
||||
//lines[lineIdx] = prefix + reconstructLyrics(finalTokens)
|
||||
}
|
||||
}
|
||||
// println("--- FIN DEBUG ---")
|
||||
}
|
||||
|
||||
|
||||
private fun updateNotesInLines(
|
||||
lines: MutableList<String>,
|
||||
fragments: List<String>,
|
||||
targetIdx: Int,
|
||||
deleteCount: Int,
|
||||
editState: TUOEditState
|
||||
) {
|
||||
val notesByVoice = editState.notesByVoice
|
||||
val originalNotes = editState.originalNotes
|
||||
|
||||
for (voiceNum in 0..3) {
|
||||
val linePrefix = "N${voiceNum + 1}:"
|
||||
val lineIdx = lines.indexOfFirst { it.startsWith(linePrefix) }
|
||||
|
||||
if (lineIdx != -1) {
|
||||
val currentLine = lines[lineIdx]
|
||||
val prefix = currentLine.substringBefore(":") + ":"
|
||||
val noteBody = currentLine.substringAfter(":")
|
||||
val hasAnchor = noteBody.contains("#")
|
||||
val noteExpanded = expandNotes(noteBody)
|
||||
|
||||
// println("Notes===>$noteExpanded")
|
||||
val newNot = notesByVoice[voiceNum] ?: ""
|
||||
val oldNot = originalNotes[voiceNum] ?: ""
|
||||
|
||||
val isNoteModified = newNot != oldNot
|
||||
// println("est-ce? $isNoteModified = $newNot != $oldNot")
|
||||
|
||||
if (!isNoteModified) {
|
||||
// println("Voix $linePrefix : Aucune modification détectée (Note: $oldNot ).")
|
||||
continue
|
||||
}
|
||||
|
||||
val newNote = notesByVoice[voiceNum] ?: ""
|
||||
if (newNote.isEmpty()) continue
|
||||
val cleanNewNote = revertMusicalInput(newNote)
|
||||
|
||||
|
||||
val regex = Regex("#\\S[',]*|\\s#\\S[',]*|/|\\(|\\)|[drmfsltDRFSTzw][0-9'¹²³⁴⁵₁₂₃₄₅,]*|[-.]")
|
||||
|
||||
|
||||
val allTokens = regex.findAll(noteExpanded).map { it.value }.toList()
|
||||
|
||||
val targetPointer = getNotePointer(fragments, targetIdx, currentLine)
|
||||
|
||||
val resultTokens = mutableListOf<String>()
|
||||
var currentLogicalIdx = 0
|
||||
var i = 0
|
||||
|
||||
// println("\nSuivi pour la voix $linePrefix (Pointeur cible: $targetPointer)")
|
||||
// println(
|
||||
// String.format(
|
||||
// "%-5s | %-10s | %-8s | %-12s | %-10s",
|
||||
// "Idx P",
|
||||
// "Token",
|
||||
// "Type",
|
||||
// "LogiqueIdx",
|
||||
// "Action"
|
||||
// )
|
||||
// )
|
||||
while (i < allTokens.size) {
|
||||
val token = allTokens[i]
|
||||
val isStructural = token.contains("#") || token == "/" || token == "(" || token == ")"
|
||||
|
||||
if (isStructural) {
|
||||
// println(String.format("%-5d | %-10s | %-8s | %-12s | %-10s", i, token, "STRUCT", "-", "Keep"))
|
||||
resultTokens.add(token)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentLogicalIdx == targetPointer) {
|
||||
val originalToken = allTokens[i]
|
||||
var replacement = if (!hasAnchor) {
|
||||
val (newBase, newLevel) = parseNoteAndOctave(revertMusicalInput(newNot))
|
||||
val (oldBase, oldLevel) = parseNoteAndOctave(revertMusicalInput(oldNot))
|
||||
val octaveDiff = newLevel - oldLevel
|
||||
|
||||
val (tokenBase, tokenLevel) = parseNoteAndOctave(originalToken)
|
||||
newBase + formatOctave(tokenLevel + octaveDiff)
|
||||
} else {
|
||||
// SI y a '#'
|
||||
val anchorMatch = Regex("#([drmfsltDRFST])[',]*").find(noteBody)
|
||||
val anchorLevel = if (anchorMatch != null) {
|
||||
val anchorStr = anchorMatch.value
|
||||
anchorStr.count { it == '\'' } - anchorStr.count { it == ',' }
|
||||
} else 0
|
||||
|
||||
val newNotCleaned = revertMusicalInput(newNot)
|
||||
val oldNotCleaned = revertMusicalInput(oldNot)
|
||||
|
||||
val (newBase, newLevel) = parseNoteAndOctave(newNotCleaned)
|
||||
val (oldBase, oldLevel) = parseNoteAndOctave(oldNotCleaned)
|
||||
|
||||
val sourceLevel = newLevel - anchorLevel
|
||||
val formattedOctave = formatOctave(sourceLevel)
|
||||
val finalPureNew = newBase + formattedOctave
|
||||
|
||||
val pureOldBase = oldBase + formatOctave(oldLevel - anchorLevel)
|
||||
|
||||
// println("Ancre niveau: $anchorLevel | UI Level: $newLevel -> Source Level: $sourceLevel")
|
||||
//
|
||||
if (finalPureNew.startsWith(pureOldBase) && pureOldBase.isNotEmpty()) {
|
||||
val addedPart = finalPureNew.substring(pureOldBase.length)
|
||||
originalToken + addedPart
|
||||
} else {
|
||||
finalPureNew
|
||||
}
|
||||
}
|
||||
|
||||
// println(
|
||||
// String.format(
|
||||
// "%-5s | %-10s | %-8s | %-12s | %-10s",
|
||||
// "NEW", replacement, "INSERT", currentLogicalIdx, "REPLACE ($originalToken)"
|
||||
// )
|
||||
// )
|
||||
resultTokens.add(replacement)
|
||||
|
||||
var notesSkipped = 0
|
||||
while (notesSkipped < deleteCount && i < allTokens.size) {
|
||||
val nextToken = allTokens[i]
|
||||
val isNextStructural =
|
||||
nextToken.contains("#") || nextToken == "/" || nextToken == "(" || nextToken == ")"
|
||||
|
||||
if (!isNextStructural) {
|
||||
notesSkipped++
|
||||
} else if (hasAnchor) {
|
||||
resultTokens.add(nextToken)
|
||||
}
|
||||
i++
|
||||
}
|
||||
currentLogicalIdx += deleteCount
|
||||
} else {
|
||||
// AFFICHAGE NOTE NORMALE
|
||||
// println(
|
||||
// String.format(
|
||||
// "%-5d | %-10s | %-8s | %-12d | %-10s",
|
||||
// i,
|
||||
// token,
|
||||
// "NOTE",
|
||||
// currentLogicalIdx,
|
||||
// "Keep"
|
||||
// )
|
||||
// )
|
||||
resultTokens.add(token)
|
||||
currentLogicalIdx++
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
lines[lineIdx] = prefix + resultTokens.joinToString("")
|
||||
|
||||
// println("-".repeat(55))
|
||||
// println("RÉSULTAT : ${lines[lineIdx]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun parseNoteAndOctave(s: String): Pair<String, Int> {
|
||||
val base = s.replace(Regex("[^drmfsltDRFSTzw]"), "")
|
||||
var level = 0
|
||||
|
||||
level += s.count { it == '\'' || it == '¹' || it == '²' || it == '³' }
|
||||
level -= s.count { it == ',' || it == '₁' || it == '₂' || it == '₃' }
|
||||
return Pair(base, level)
|
||||
}
|
||||
|
||||
private fun formatOctave(level: Int): String {
|
||||
if (level == 0) return ""
|
||||
return if (level > 0) "'".repeat(level) else ",".repeat(Math.abs(level))
|
||||
}
|
||||
|
||||
private fun revertMusicalInput(input: String): String {
|
||||
return input
|
||||
.replace("• ,", "")
|
||||
.replace("―•", "")
|
||||
.replace("―", "-")
|
||||
.replace("•", "")
|
||||
.replace("³", "'''")
|
||||
.replace("₃", ",,,")
|
||||
.replace("²", "''")
|
||||
.replace("₂", ",,")
|
||||
.replace("¹", "'")
|
||||
.replace("₁", ",")
|
||||
|
||||
.replace("di", "D")
|
||||
.replace("ri", "R")
|
||||
.replace("fi", "F")
|
||||
.replace("si", "S")
|
||||
.replace("ta", "T")
|
||||
|
||||
.replace("(","")
|
||||
.replace(")","")
|
||||
}
|
||||
|
||||
private fun parseNoteLineToArray(noteLine: String): List<String> {
|
||||
val content = noteLine.substringAfter(":").replace(" ", "")
|
||||
|
||||
val result = mutableListOf<String>()
|
||||
var i = 0
|
||||
|
||||
while (i < content.length) {
|
||||
val char = content[i]
|
||||
|
||||
if (char == '/' || char == ' '/* || char == 'z'*/) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (char == '#') {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
|
||||
if (char.isLetter() || char == '-') {
|
||||
val noteBuilder = StringBuilder(char.toString())
|
||||
var nextIdx = i + 1
|
||||
|
||||
while (nextIdx < content.length && (content[nextIdx] == ',' || content[nextIdx] == '\'')) {
|
||||
noteBuilder.append(content[nextIdx])
|
||||
nextIdx++
|
||||
}
|
||||
|
||||
val fullNote = noteBuilder.toString()
|
||||
|
||||
if (nextIdx < content.length && content[nextIdx].isDigit()) {
|
||||
val repeatCount = content[nextIdx].toString().toInt()
|
||||
repeat(repeatCount) {
|
||||
result.add(fullNote)
|
||||
}
|
||||
nextIdx++
|
||||
} else {
|
||||
result.add(fullNote)
|
||||
}
|
||||
|
||||
i = nextIdx
|
||||
continue
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun templateToArray(templateLine: String): List<String> {
|
||||
var templateArray: List<String> = emptyList()
|
||||
|
||||
val templateType = if(templateLine.startsWith("T0")) "T" else "U"
|
||||
if(templateType == "T") {
|
||||
var content = templateLine.substringAfter("{").substringBeforeLast("}")
|
||||
// println("AVANT T: $content")
|
||||
|
||||
content = content.replace(Regex("\\\$\\{.*?\\}"), "")
|
||||
|
||||
val rawBlocks = content.split(Regex("[:|/!]"))
|
||||
|
||||
templateArray = rawBlocks.filter { it.isNotBlank() }
|
||||
// println("\n\nLine = $templateLine \n," +
|
||||
// "Array = ${templateArray.joinToString("|")}")
|
||||
// println("APRèS T: ${templateArray.joinToString("|")}")
|
||||
} else {
|
||||
val body = templateLine.replaceFirst(Regex("^U0:z.:"), "")
|
||||
|
||||
//println("Mon contenu est: $body.")
|
||||
val result = mutableListOf<String>()
|
||||
var currentBlock = StringBuilder()
|
||||
|
||||
var i = 0
|
||||
while (i < body.length) {
|
||||
val char = body[i]
|
||||
|
||||
// 1. GESTION DES MARQUEURS ${...} -> ON IGNORE TOUT LE BLOC
|
||||
if (char == '$' && i + 1 < body.length && body[i + 1] == '{') {
|
||||
val closingBrace = body.indexOf('}', i)
|
||||
if (closingBrace != -1) {
|
||||
i = closingBrace + 1 // On saute jusqu'après le '}'
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (char == '$') {
|
||||
i += 2 // On saute le '$' et la lettre qui suit
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. SÉPARATEURS -> ON COUPE LE BLOC
|
||||
if (char in listOf(':', '|', '!', '/')) {
|
||||
if (currentBlock.isNotEmpty()) {
|
||||
result.add(currentBlock.toString())
|
||||
currentBlock.clear()
|
||||
}
|
||||
}
|
||||
// 4. IGNORER LES ESPACES
|
||||
else if (char == ' ') {
|
||||
// ne rien faire
|
||||
}
|
||||
// 5. TOUT LE RESTE (D, z, -, ., ,, (, ) )
|
||||
else {
|
||||
currentBlock.append(char)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
// Ajouter le dernier morceau s'il n'y a pas de séparateur à la fin
|
||||
if (currentBlock.isNotEmpty()) result.add(currentBlock.toString())
|
||||
|
||||
templateArray = result
|
||||
}
|
||||
|
||||
// println("\n\nLine = $templateLine \n," +
|
||||
// "Array = ${templateArray.joinToString("|")}")
|
||||
return templateArray
|
||||
}
|
||||
|
||||
private fun expandNotes(text: String): String {
|
||||
val regex = Regex("([drmfsltDRFSTzw])([,']*)(\\d+)")
|
||||
return regex.replace(text) { matchResult ->
|
||||
val note = matchResult.groupValues[1]
|
||||
val octaves = matchResult.groupValues[2]
|
||||
val count = matchResult.groupValues[3].toInt()
|
||||
|
||||
(note + octaves).repeat(count)
|
||||
}
|
||||
}
|
||||
|
||||
private fun rearrangeNote(noteString: String, infiniteIter: Int = 0): String {
|
||||
var result: String = noteString
|
||||
|
|
@ -968,4 +1682,96 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
this.add(element)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNotePointerWithDebug(templatArray: List<String>, targetIdx: Int, sourceLine: String): Int {
|
||||
println("\n=== DÉBOGAGE ALIGNEMENT RYTHME / NOTES (CORRIGÉ) ===")
|
||||
|
||||
val bodyOnly = sourceLine.substringAfter(":")
|
||||
val expandedBody = expandNotes(bodyOnly)
|
||||
// On suppose que parseNoteLineToArray découpe chaque "événement" (note ou prolongement)
|
||||
val notesList = parseNoteLineToArray("N:$expandedBody")
|
||||
|
||||
println("\n=== ALIGNEMENT STRICT : TEMPLATE -> SOURCE ===")
|
||||
println("Ligne Source : ${sourceLine.substringBefore(":")}")
|
||||
println("----------------------------------------------------------------------")
|
||||
println("%-5s | %-12s | %-15s | %-10s".format("IDX", "BLOC TEMP.", "NOTES SRC", "POINTEUR"))
|
||||
println("----------------------------------------------------------------------")
|
||||
|
||||
var currentPointer = 0
|
||||
var notesInTargetBlock = mutableListOf<String>()
|
||||
var countInTarget = 0
|
||||
|
||||
for (i in templatArray.indices) {
|
||||
val block = templatArray[i]
|
||||
|
||||
// NETTOYAGE : On ignore les marqueurs $Q, ${DC} pour le comptage
|
||||
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
|
||||
val cleanBlock = block.replace(markerRegex, "")
|
||||
|
||||
// RÈGLE : On compte uniquement les LETTRES (S, F, D, R, M, L, T, d, r...)
|
||||
val nbLettres = cleanBlock.count { it.isLetter() && it.lowercaseChar() != 'z' }
|
||||
|
||||
// On récupère les notes correspondantes dans le fichier source
|
||||
val blocNotes = (0 until nbLettres).map { offset ->
|
||||
notesList.getOrNull(currentPointer + offset) ?: "[VIDE]"
|
||||
}
|
||||
|
||||
if (i == targetIdx) {
|
||||
countInTarget = nbLettres
|
||||
notesInTargetBlock.addAll(blocNotes)
|
||||
}
|
||||
|
||||
val marker = if (i == targetIdx) " [ CIBLE ]" else ""
|
||||
println("[%3d] | %-12s | %-15s | Index: %d %s"
|
||||
.format(i, block, blocNotes.joinToString(","), currentPointer, marker))
|
||||
|
||||
// On incrémente le pointeur pour le prochain bloc
|
||||
// SEULEMENT si nous n'avons pas encore dépassé la cible
|
||||
// if (i < targetIdx) {
|
||||
currentPointer += nbLettres
|
||||
// }
|
||||
}
|
||||
|
||||
println("----------------------------------------------------------------------")
|
||||
println("RÉSULTAT : Le bloc $targetIdx contient $countInTarget lettre(s).")
|
||||
println("Notes pointées dans le fichier source : ${notesInTargetBlock.joinToString(" ET ")}")
|
||||
println("Pointeur de départ (offset) : $currentPointer")
|
||||
println("======================================================================\n")
|
||||
|
||||
return currentPointer
|
||||
}
|
||||
|
||||
fun getNotePointer(templatArray: List<String>, targetIdx: Int, sourceLine: String): Int {
|
||||
val bodyOnly = sourceLine.substringAfter(":")
|
||||
val expandedBody = expandNotes(bodyOnly)
|
||||
val notesList = parseNoteLineToArray("N:$expandedBody")
|
||||
var currentPointer = 0
|
||||
var notesInTargetBlock = mutableListOf<String>()
|
||||
var countInTarget = 0
|
||||
|
||||
for (i in templatArray.indices) {
|
||||
val block = templatArray[i]
|
||||
|
||||
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
|
||||
val cleanBlock = block.replace(markerRegex, "")
|
||||
|
||||
val nbLettres = cleanBlock.count { it.isLetter() && it.lowercaseChar() != 'z' }
|
||||
|
||||
val blocNotes = (0 until nbLettres).map { offset ->
|
||||
notesList.getOrNull(currentPointer + offset) ?: "[VIDE]"
|
||||
}
|
||||
|
||||
if (i == targetIdx) {
|
||||
countInTarget = nbLettres
|
||||
notesInTargetBlock.addAll(blocNotes)
|
||||
}
|
||||
|
||||
val marker = if (i == targetIdx) " [ CIBLE ]" else ""
|
||||
|
||||
if (i < targetIdx) {
|
||||
currentPointer += nbLettres
|
||||
}
|
||||
}
|
||||
return currentPointer
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material.icons.filled.Savings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import mg.dot.feufaro.data.GridTUOData
|
||||
import mg.dot.feufaro.solfa.TUOEditState
|
||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TUODetailDialog(
|
||||
editState: TUOEditState,
|
||||
tuo: TimeUnitObject,
|
||||
currentStanza: Int,
|
||||
menuPosition: IntOffset,
|
||||
globalIndex: Int,
|
||||
isEditable: Boolean,
|
||||
canAdd: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (TUOEditState) -> Unit
|
||||
) {
|
||||
val notes = remember {
|
||||
mutableStateMapOf<Int, String>().apply {
|
||||
putAll(editState.notesByVoice)
|
||||
}
|
||||
}
|
||||
val originalNotes = mutableStateMapOf<Int, String>().apply {
|
||||
putAll(editState.notesByVoice)
|
||||
}
|
||||
val originalLyricsByStz = mutableStateMapOf<Int, String>().apply {
|
||||
putAll(editState.lyricsByStanza)
|
||||
}
|
||||
|
||||
var templateFragment by remember { mutableStateOf(editState.templateFragment) }
|
||||
|
||||
var marker by remember { mutableStateOf(editState.marker) }
|
||||
var hairPin by remember { mutableStateOf(editState.hairPin) }
|
||||
|
||||
val lyricsLines = remember {
|
||||
mutableStateListOf<String>().apply {
|
||||
val existing = editState.lyricsByStanza[currentStanza] ?: ""
|
||||
if (existing.isEmpty() && canAdd) add("_") else add(existing)
|
||||
}
|
||||
}
|
||||
|
||||
Popup(
|
||||
offset = menuPosition,
|
||||
onDismissRequest = onDismiss,
|
||||
properties = PopupProperties(
|
||||
focusable = true,
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true
|
||||
),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.widthIn(max=125.dp)
|
||||
.heightIn(max = 400.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = Color(0xFF2D2D2D).copy(0.75f),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp).fillMaxWidth()) {
|
||||
Text(
|
||||
text = "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
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package mg.dot.feufaro.solfa
|
||||
|
||||
data class TUOEditState(
|
||||
val tuoIndex: Int,
|
||||
val notesByVoice: Map<Int, String> = emptyMap(),
|
||||
val originalNotes: Map<Int, String> = emptyMap(),
|
||||
val lyricsByStanza: MutableMap<Int, String> = mutableMapOf(),
|
||||
val originalLyricsByStanza: MutableMap<Int, String> = mutableMapOf(),
|
||||
val templateFragment: String = "",
|
||||
val marker: String = "",
|
||||
val hairPin: String = ""
|
||||
)
|
||||
|
|
@ -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<POneStanzaLyrics> = 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<TimeUnitObject?>(null) }
|
||||
var selectedIndex by remember { mutableStateOf(-1) }
|
||||
val regexMeasure = Regex("(\\d)/\\d").find(viewModel.measure)
|
||||
|
||||
LaunchedEffect(tuoList) {
|
||||
showContextualMenu = false
|
||||
showEditDialog = false
|
||||
showDetailDialog = false
|
||||
showAddDialog = false
|
||||
selectedIndex = -1
|
||||
selectedTUO = null
|
||||
}
|
||||
|
||||
var showFullChord by remember { mutableStateOf(false) }
|
||||
val editMode by sharedScreenModel.modeEditor.collectAsState()
|
||||
|
||||
Column(
|
||||
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
|
||||
|
|
@ -882,3 +1100,203 @@ fun makeSpaceBetweenSyllables(
|
|||
}
|
||||
}
|
||||
}
|
||||
private suspend fun expandInclusions(
|
||||
content: String,
|
||||
currentFilePath: String,
|
||||
fileRepository: FileRepository
|
||||
): String {
|
||||
val lines = content.split("\n")
|
||||
val result = StringBuilder()
|
||||
val lastSlash = currentFilePath.lastIndexOf('/')
|
||||
val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else ""
|
||||
|
||||
lines.forEach { line ->
|
||||
if (line.startsWith("I0:")) {
|
||||
try {
|
||||
val parts = line.substring(3).split(":")
|
||||
val fileName = parts[0]
|
||||
val ignorePattern = if (parts.size > 1) parts[1] else ""
|
||||
|
||||
val fullPath = directory + fileName
|
||||
val includedLines = fileRepository.readFileLines(fullPath)
|
||||
|
||||
val regexIgnore = if (ignorePattern.isNotEmpty()) Regex(ignorePattern) else null
|
||||
|
||||
includedLines.forEach { incLine ->
|
||||
if (regexIgnore == null || !regexIgnore.containsMatchIn(incLine)) {
|
||||
result.append(incLine).append("\n")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.append("// Erreur inclusion: ${e.message}\n")
|
||||
}
|
||||
} else {
|
||||
result.append(line).append("\n")
|
||||
}
|
||||
}
|
||||
return result.toString().trimEnd()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditSourceCompose(
|
||||
sharedScreenModel: SharedScreenModel,
|
||||
solfaScreenModel: SolfaScreenModel
|
||||
) {
|
||||
var isEditorVisible = sharedScreenModel.editModeState.value
|
||||
var sourcePath = sharedScreenModel.activeFilePath.value
|
||||
var sourceContent = sharedScreenModel.fileContent.value ?: ""
|
||||
var sourceTitle = sharedScreenModel.songTitle.value
|
||||
|
||||
// println("path $sourcePath\n" +
|
||||
// "contenu $sourceContent")
|
||||
val originalPath = remember(sourceTitle) {
|
||||
if (!sourcePath.contains("_tmp") && !sourcePath.contains("/tmp/")) {
|
||||
sourcePath
|
||||
} else {
|
||||
sourcePath
|
||||
}
|
||||
}
|
||||
|
||||
val fileName = originalPath.substringAfterLast('/').substringAfterLast(':')
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var currentTempFile by remember { mutableStateOf<File?>(null) }
|
||||
|
||||
var codeContent by remember { mutableStateOf(sourceContent?: "") }
|
||||
LaunchedEffect(sourceContent) {
|
||||
codeContent = sourceContent
|
||||
}
|
||||
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
|
||||
|
||||
val saveLauncher = rememberFileSaveLauncher { chosenPath ->
|
||||
if (chosenPath != null) {
|
||||
val contentToSave = codeContent
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val data = codeContent.toByteArray(Charsets.UTF_8)
|
||||
solfaScreenModel.fileRepository.saveLocalFile(chosenPath, data)
|
||||
withContext(Dispatchers.Main) {
|
||||
println("Fichier sauvegardé avec succès")
|
||||
solfaScreenModel.loadExternalFile(chosenPath)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println("Sauvegarde annulée par l'utilisateur")
|
||||
}
|
||||
}
|
||||
|
||||
if(sourcePath.isNotEmpty() && sourceContent.isNotEmpty()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (isEditorVisible) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().background(Color(0xFFF5F5F5)).padding(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Description, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(
|
||||
text = sourceTitle.ifEmpty { "nouveau_feufaro.txt" },
|
||||
modifier = Modifier.padding(start = 8.dp).widthIn(max = 160.dp),
|
||||
style = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
EditorActionButtons(
|
||||
onUndo = {
|
||||
scope.launch {
|
||||
codeContent = sourceContent
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
solfaScreenModel.loadExternalFile(originalPath)
|
||||
}
|
||||
currentTempFile?.delete()
|
||||
currentTempFile = null
|
||||
}
|
||||
},
|
||||
onBuild = {
|
||||
scope.launch {
|
||||
try {
|
||||
val expandedContent = expandInclusions(
|
||||
codeContent,
|
||||
sourcePath,
|
||||
solfaScreenModel.fileRepository
|
||||
)
|
||||
|
||||
if (currentTempFile == null || !currentTempFile!!.exists()) {
|
||||
val tempDir = System.getProperty("java.io.tmpdir")
|
||||
currentTempFile = File(tempDir, fileName)
|
||||
currentTempFile?.deleteOnExit()
|
||||
}
|
||||
|
||||
currentTempFile?.writeText(expandedContent)
|
||||
|
||||
currentTempFile?.let { file ->
|
||||
withContext(Dispatchers.Main) {
|
||||
solfaScreenModel.justeCompile(file.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
println("Compilation sur le fichier : ${currentTempFile?.absolutePath}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
},
|
||||
onSave = {
|
||||
scope.launch {
|
||||
try {
|
||||
saveLauncher.launch(fileName)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClose = { sharedScreenModel.toggleEditMode() }
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize().background(Color.White)) {
|
||||
BasicTextField(
|
||||
value = codeContent,
|
||||
onValueChange = { codeContent = it },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password
|
||||
),
|
||||
modifier = Modifier.fillMaxSize().padding(start = 8.dp, top = 8.dp),
|
||||
textStyle = TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
visualTransformation = SolfaVisualTransformation(),
|
||||
cursorBrush = SolidColor(Color.Black)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditorActionButtons(onUndo: () -> Unit, onBuild: () -> Unit, onSave: () -> Unit, onClose: () -> Unit) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
IconButton(onClick = onUndo) { Icon(Icons.AutoMirrored.Filled.Undo, null, tint = Color.Gray) }
|
||||
IconButton(onClick = onBuild) { Icon(Icons.Default.Build, null, tint = Color(0xFF2196F3)) }
|
||||
IconButton(onClick = onSave) { Icon(Icons.Default.Save, null, tint = Color(0xFF4CAF50)) }
|
||||
IconButton(onClick = onClose) { Icon(Icons.Default.Close, null, tint = Color.Red) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
@ -472,3 +511,30 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean> = _isQRCodeVisible
|
||||
|
||||
private val _editModeState = mutableStateOf(false)
|
||||
val editModeState: State<Boolean> = _editModeState
|
||||
|
||||
private val _modeEditor = MutableStateFlow(false)
|
||||
val modeEditor: StateFlow<Boolean> = _modeEditor
|
||||
|
||||
fun toggleEditorMode(enabled: Boolean) {
|
||||
_modeEditor.value = enabled
|
||||
}
|
||||
|
||||
private val _synchronizedSYllables = MutableStateFlow<List<String>>(emptyList())
|
||||
val synchronizedSyllables: StateFlow<List<String>> = _synchronizedSYllables.asStateFlow()
|
||||
|
||||
fun updateSyllablesFromList(measures: List<List<TimeUnitObject>>, stanzaNumber: Int) {
|
||||
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
|
||||
|
||||
// println("\n--- DEBUG updateSyllablesFromList (Strophe $stanzaNumber) ---")
|
||||
|
||||
var absoluteIndex = 0
|
||||
_synchronizedSYllables.value = measures.flatten().map { tuo ->
|
||||
val rawSyllablesList = tuo.getSingleSyllable(stanzaNumber)
|
||||
|
||||
val processedSyllable = rawSyllablesList.joinToString(" ") { syllable ->
|
||||
val cleaned = syllable.replace(REGEX_CLEAN_PREFIX, "").trim()
|
||||
cleaned
|
||||
}
|
||||
|
||||
// println("TUO #$absoluteIndex | Template: ${tuo.pTemplate.template} | Raw: $rawSyllablesList | Final: [$processedSyllable]")
|
||||
|
||||
absoluteIndex++
|
||||
processedSyllable
|
||||
}
|
||||
|
||||
// println("--- FIN DEBUG (Total TUOs: $absoluteIndex) ---\n")
|
||||
}
|
||||
private val _activeFilePath = mutableStateOf("")
|
||||
val activeFilePath: State<String> = _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<String?>
|
||||
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<TUOEditState?>(null)
|
||||
val tuoEditState: StateFlow<TUOEditState?> = _tuoEditState.asStateFlow()
|
||||
|
||||
fun openTUOEditor(editState: TUOEditState) {
|
||||
_tuoEditState.value = editState
|
||||
println("nMrk = [${editState.marker}] template [${editState.templateFragment}] hp [${editState.hairPin}]")
|
||||
}
|
||||
|
||||
fun closeTUOEditor() {
|
||||
_tuoEditState.value = null
|
||||
}
|
||||
|
||||
fun updateTUOEdit(new: TUOEditState) {
|
||||
_tuoEditState.value = new
|
||||
}
|
||||
|
||||
fun updateFullTUOList(newList: List<TimeUnitObject>) {
|
||||
_tuoList.value = newList
|
||||
}
|
||||
fun loadNewSong(newMidiFile: String) {
|
||||
_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("")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue