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
|
1
|
||||||
)
|
)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setFilePickerActivity(this)
|
setFilePickerActivity(this)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,7 @@ actual fun rememberPdfExportAction(
|
||||||
nbStanza, songKey, songAut, songComp, songRythm
|
nbStanza, songKey, songAut, songComp, songRythm
|
||||||
)
|
)
|
||||||
val chosenPath = withContext(Dispatchers.Main) {
|
val chosenPath = withContext(Dispatchers.Main) {
|
||||||
fileRepository.pickSavePath(computedFileName)
|
fileRepository.pickSavePath(computedFileName, "Exporter en PDF")
|
||||||
}
|
}
|
||||||
if (chosenPath != null) {
|
if (chosenPath != null) {
|
||||||
fileRepository.saveLocalFile(chosenPath, pdfBytes)
|
fileRepository.saveLocalFile(chosenPath, pdfBytes)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
"themeMode": "DARK",
|
"themeMode": "DARK",
|
||||||
"fontSize": 18.5,
|
"fontSize": 18.5,
|
||||||
"playlist": [
|
"playlist": [
|
||||||
"assets://a.txt",
|
|
||||||
"assets://ffpm-617.txt",
|
"assets://ffpm-617.txt",
|
||||||
|
"assets://a.txt",
|
||||||
"assets://ews-127.txt",
|
"assets://ews-127.txt",
|
||||||
"assets://ffpm-521.txt",
|
"assets://ffpm-521.txt",
|
||||||
"assets://ews-126.txt",
|
"assets://ews-126.txt",
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,38 @@
|
||||||
package mg.dot.feufaro
|
package mg.dot.feufaro
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.List
|
import androidx.compose.material.icons.automirrored.filled.List
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.List
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContextualMenu (onMenuItemClick: (String) -> Unit){
|
fun ContextualMenu (onMenuItemClick: (String) -> Unit){
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -32,7 +47,7 @@ fun ContextualMenu (onMenuItemClick: (String) -> Unit){
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MenuItem(icon: androidx.compose.ui.graphics.vector.ImageVector, text: String, onClick: () -> Unit) {
|
fun MenuItem(icon: ImageVector, text: String, onClick: () -> Unit) {
|
||||||
IconButton(onClick = onClick) {
|
IconButton(onClick = onClick) {
|
||||||
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
|
||||||
Icon(icon, contentDescription = text, tint = Color.White)
|
Icon(icon, contentDescription = text, tint = Color.White)
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,21 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconToggleButton
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
|
@ -50,6 +58,8 @@ import cafe.adriel.voyager.koin.koinScreenModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mg.dot.feufaro.data.GridTUOData
|
import mg.dot.feufaro.data.GridTUOData
|
||||||
|
import mg.dot.feufaro.solfa.ColorPrefixB
|
||||||
|
import mg.dot.feufaro.solfa.EditSourceCompose
|
||||||
import mg.dot.feufaro.ui.MyVerticalScrollbar
|
import mg.dot.feufaro.ui.MyVerticalScrollbar
|
||||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||||
import java.io.ObjectStreamException
|
import java.io.ObjectStreamException
|
||||||
|
|
@ -64,7 +74,7 @@ object ScreenSolfa : Screen {
|
||||||
val tuoList by sharedScreenModel.tuoList.collectAsState()
|
val tuoList by sharedScreenModel.tuoList.collectAsState()
|
||||||
val measure by sharedScreenModel.measure.collectAsState()
|
val measure by sharedScreenModel.measure.collectAsState()
|
||||||
val stanza by sharedScreenModel.stanza.collectAsState()
|
val stanza by sharedScreenModel.stanza.collectAsState()
|
||||||
val gridTUOData = GridTUOData(measure, tuoList, stanza)
|
val gridTUOData = remember(tuoList, measure, stanza) { GridTUOData(measure, tuoList, stanza) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var showContextualMenu = false
|
var showContextualMenu = false
|
||||||
|
|
@ -81,6 +91,16 @@ object ScreenSolfa : Screen {
|
||||||
// DeepLinkHandler.consumePending("")
|
// DeepLinkHandler.consumePending("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val tuoEditState by sharedScreenModel.tuoEditState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(tuoEditState) {
|
||||||
|
tuoEditState?.let { state ->
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
solfaScreenModel.saveTUOEdit(state)
|
||||||
|
}
|
||||||
|
sharedScreenModel.closeTUOEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
DeepLinkHandler.onSongReceived = null
|
DeepLinkHandler.onSongReceived = null
|
||||||
|
|
@ -97,7 +117,10 @@ object ScreenSolfa : Screen {
|
||||||
solfaScrollState = scrollState
|
solfaScrollState = scrollState
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxSize().padding(paddingValues)
|
Modifier.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
|
.windowInsetsPadding(WindowInsets.ime)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -139,7 +162,7 @@ object ScreenSolfa : Screen {
|
||||||
) {
|
) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
//.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
.padding(start = 8.dp, end = 8.dp, top = 8.dp),
|
.padding(start = 8.dp, end = 8.dp, top = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
|
@ -163,10 +186,23 @@ object ScreenSolfa : Screen {
|
||||||
sharedScreenModel = sharedScreenModel,
|
sharedScreenModel = sharedScreenModel,
|
||||||
onGridWidthMeasured = { width -> gridWidthPx = width }
|
onGridWidthMeasured = { width -> gridWidthPx = width }
|
||||||
)
|
)
|
||||||
|
EditSourceCompose(sharedScreenModel, solfaScreenModel)
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
|
val isEditMode = sharedScreenModel.editModeState.value
|
||||||
|
IconToggleButton(
|
||||||
|
checked = isEditMode,
|
||||||
|
onCheckedChange = { sharedScreenModel.toggleEditMode() },
|
||||||
|
modifier = Modifier.size(40.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isEditMode) Icons.Filled.Check else Icons.Filled.Edit,
|
||||||
|
contentDescription = "Mode édition",
|
||||||
|
tint = if (isEditMode) ColorPrefixB else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
MGButton(onClick = {
|
MGButton(onClick = {
|
||||||
//showContent = !showContent
|
//showContent = !showContent
|
||||||
|
|
||||||
|
|
@ -184,7 +220,7 @@ object ScreenSolfa : Screen {
|
||||||
}
|
}
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
//.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
.padding(start = 8.dp, end = 8.dp, top = 8.dp),
|
.padding(start = 8.dp, end = 8.dp, top = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import mg.dot.feufaro.FileRepository
|
import mg.dot.feufaro.FileRepository
|
||||||
import mg.dot.feufaro.SaveSettings
|
import mg.dot.feufaro.SaveSettings
|
||||||
import mg.dot.feufaro.getConfigDirectoryPath
|
import mg.dot.feufaro.data.GridTUOData
|
||||||
import mg.dot.feufaro.launchFilePicker
|
import mg.dot.feufaro.launchFilePicker
|
||||||
import mg.dot.feufaro.midi.MidiPitch
|
import mg.dot.feufaro.midi.MidiPitch
|
||||||
import mg.dot.feufaro.midi.MidiWriterKotlin
|
import mg.dot.feufaro.midi.MidiWriterKotlin
|
||||||
|
import java.io.File
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
|
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
|
||||||
|
|
@ -71,16 +72,23 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
|
|
||||||
private val meta: MutableMap<String, String> = mutableMapOf()
|
private val meta: MutableMap<String, String> = mutableMapOf()
|
||||||
private val lyricsComment: MutableList<String> = mutableListOf()
|
private val lyricsComment: MutableList<String> = mutableListOf()
|
||||||
suspend fun nextTimeUnitObject() {
|
suspend fun nextTimeUnitObject(accumulator: MutableList<TimeUnitObject>) {
|
||||||
val lastTUO = sharedScreenModel.lastTUO()
|
val lastTUO = accumulator.lastOrNull()
|
||||||
nextTIndex++
|
nextTIndex++
|
||||||
if (nextTIndex == 5) {
|
if (T.getOrNull(nextTIndex) == null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
sharedScreenModel.updateFullTUOList(accumulator)
|
||||||
|
//sharedScreenModel.doneTUOList()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
/*if (nextTIndex == 5) {
|
||||||
sharedScreenModel.doneTUOList()
|
sharedScreenModel.doneTUOList()
|
||||||
}
|
}
|
||||||
if (T.getOrNull(nextTIndex) == null) {
|
if (T.getOrNull(nextTIndex) == null) {
|
||||||
sharedScreenModel.doneTUOList()
|
sharedScreenModel.doneTUOList()
|
||||||
return
|
return
|
||||||
}
|
}*/
|
||||||
val pTemplate = T[nextTIndex]
|
val pTemplate = T[nextTIndex]
|
||||||
val unitObject = TimeUnitObject(pTemplate, lastTUO, N.size)
|
val unitObject = TimeUnitObject(pTemplate, lastTUO, N.size)
|
||||||
if (lastTUO == null) {
|
if (lastTUO == null) {
|
||||||
|
|
@ -132,9 +140,10 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
sharedScreenModel.addTUO(unitObject)
|
//sharedScreenModel.addTUO(unitObject)
|
||||||
|
accumulator.add(unitObject)
|
||||||
}
|
}
|
||||||
nextTimeUnitObject()
|
nextTimeUnitObject(accumulator)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadSolfa() {
|
fun loadSolfa() {
|
||||||
|
|
@ -157,7 +166,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
|
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
val initialPath = stateSettings.loadLastUsedDir()
|
val initialPath = stateSettings.loadLastUsedDir()
|
||||||
val homedir = getConfigDirectoryPath();
|
val homedir = fileRepository.getAppPublicFolder().absolutePath
|
||||||
|
|
||||||
launchFilePicker(
|
launchFilePicker(
|
||||||
mimeTypes = arrayOf("text/plain"),
|
mimeTypes = arrayOf("text/plain"),
|
||||||
|
|
@ -186,7 +195,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
sharedScreenModel.reset()
|
sharedScreenModel.reset()
|
||||||
}
|
}
|
||||||
fun parse(sourceFile: String) {
|
fun parse(sourceFile: String) {
|
||||||
println("37: lFL bien reçu $sourceFile")
|
//println("37: lFL bien reçu $sourceFile")
|
||||||
currentFile = sourceFile
|
currentFile = sourceFile
|
||||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||||
parseScope.launch {
|
parseScope.launch {
|
||||||
|
|
@ -229,7 +238,11 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
preloadN(it)
|
preloadN(it)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
nextTimeUnitObject()
|
val localAccumulator = mutableListOf<TimeUnitObject>()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
sharedScreenModel.updateFullTUOList(emptyList())
|
||||||
|
}
|
||||||
|
nextTimeUnitObject(localAccumulator)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
|
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +253,707 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
sharedScreenModel.setStanza(1)
|
sharedScreenModel.setStanza(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun justeBuild(sourceFile: String) {
|
||||||
|
currentFile = sourceFile
|
||||||
|
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
parseScope.launch {
|
||||||
|
val lines = try {
|
||||||
|
fileRepository.readFileLines(sourceFile)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Opening $sourceFile raised exception {${e.message}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
val contentString = fileRepository.readFileLines(sourceFile).joinToString("\n")
|
||||||
|
sharedScreenModel.setFileContent(contentString, sourceFile)
|
||||||
|
O.clear()
|
||||||
|
T.clear()
|
||||||
|
N.clear()
|
||||||
|
L.clear()
|
||||||
|
if (pitches.isNotEmpty()) {
|
||||||
|
pitches[0].reset()
|
||||||
|
}
|
||||||
|
pitches.clear()
|
||||||
|
refrainBeginsAt = -1
|
||||||
|
unparsedNote.clear()
|
||||||
|
templateString = ""
|
||||||
|
nextTIndex = -1
|
||||||
|
nextNIndex = -1
|
||||||
|
nextLIndex = -1
|
||||||
|
lyricsComment.clear()
|
||||||
|
TimeUnitObject.hasMarker(false)
|
||||||
|
lines.forEach { line ->
|
||||||
|
run {
|
||||||
|
parseOneLine(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unparsedNote.forEach {
|
||||||
|
preloadN(it)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val localAccumulator = mutableListOf<TimeUnitObject>()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
sharedScreenModel.updateFullTUOList(emptyList())
|
||||||
|
}
|
||||||
|
nextTimeUnitObject(localAccumulator)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
|
||||||
|
}
|
||||||
|
val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
|
||||||
|
val midiWriter = MidiWriterKotlin(fileRepository)
|
||||||
|
midiWriter.process(pitches)
|
||||||
|
midiWriter.save("whawyd3.mid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveTUOEdit(editState: TUOEditState) {
|
||||||
|
println(editState)
|
||||||
|
/* N.mapIndexed { index, note ->
|
||||||
|
note.oneVoiceNote.mapIndexed { index, notes ->
|
||||||
|
println(notes.note)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
val filePath = currentFile
|
||||||
|
if (filePath.isBlank()) return
|
||||||
|
|
||||||
|
|
||||||
|
// inclusion -----
|
||||||
|
val includedContent = expandInclusions(filePath)
|
||||||
|
|
||||||
|
val lines = includedContent.split("\n").toMutableList()
|
||||||
|
|
||||||
|
val targetIdx = editState.tuoIndex
|
||||||
|
|
||||||
|
val t0Idx = lines.indexOfFirst { it.startsWith("T0:") }
|
||||||
|
val u0Idx = lines.indexOfFirst { it.startsWith("U0:") }
|
||||||
|
|
||||||
|
val noteS = lines[lines.indexOfFirst { it.startsWith("N1") }]
|
||||||
|
val noteA = lines[lines.indexOfFirst { it.startsWith("N2") }]
|
||||||
|
val noteT = lines[lines.indexOfFirst { it.startsWith("N3") }]
|
||||||
|
val noteB = lines[lines.indexOfFirst { it.startsWith("N4") }]
|
||||||
|
|
||||||
|
val currentStanza = sharedScreenModel.stanza.value
|
||||||
|
|
||||||
|
val lyrIdx = lines.indexOfFirst { line ->
|
||||||
|
line.contains(Regex("^[EY]$currentStanza:"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val lyrics = lines[lyrIdx]
|
||||||
|
println("Parole sur $currentStanza: $lyrics")
|
||||||
|
|
||||||
|
val content = smartYLyrics(lyrics).substringAfter(":")
|
||||||
|
|
||||||
|
val indexedLyrics = content.split('_', ' ', '/')
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
|
||||||
|
/*indexedLyrics.forEachIndexed { index, syllable ->
|
||||||
|
println("Index $index : [$syllable]")
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
val measureString = sharedScreenModel.measure.value
|
||||||
|
|
||||||
|
val templateString = when {
|
||||||
|
t0Idx != -1 -> {
|
||||||
|
lines[t0Idx]
|
||||||
|
}
|
||||||
|
u0Idx != -1 -> {
|
||||||
|
lines[u0Idx]
|
||||||
|
}
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val templateIndices = when {
|
||||||
|
t0Idx != -1 -> {
|
||||||
|
t0Idx
|
||||||
|
}
|
||||||
|
u0Idx != -1 -> {
|
||||||
|
u0Idx
|
||||||
|
}
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
t0Idx != -1 -> {
|
||||||
|
val line = lines[t0Idx]
|
||||||
|
val prefix = line.substringBefore('{') + "{"
|
||||||
|
val suffix = "}"
|
||||||
|
val body = line.substringAfter('{').substringBeforeLast('}')
|
||||||
|
|
||||||
|
val regex = Regex("([^:|/!]*[:|/!])|([^:|/!]+)")
|
||||||
|
val fragments = regex.findAll(body).map { it.value }.toMutableList()
|
||||||
|
// println("Nombre de blocs détectés : ${fragments.size}")
|
||||||
|
|
||||||
|
val markerRegex = Regex("""\$\{.*?\}|\$.""")
|
||||||
|
|
||||||
|
if (targetIdx in fragments.indices) {
|
||||||
|
val oldFrag = fragments[targetIdx]
|
||||||
|
|
||||||
|
val delimiter = if (oldFrag.isNotEmpty() && oldFrag.last() in ":|/!") {
|
||||||
|
oldFrag.last().toString()
|
||||||
|
} else ""
|
||||||
|
|
||||||
|
val cleanBody = fragments.joinToString("") { it.replace(markerRegex, "") }
|
||||||
|
lines[t0Idx] = prefix + cleanBody + suffix
|
||||||
|
|
||||||
|
println("AVANT: $line")
|
||||||
|
println("APRÈS: ${lines[t0Idx]}")
|
||||||
|
} else {
|
||||||
|
println("Index $targetIdx hors limites pour T0 (taille=${fragments.size})")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u0Idx != -1 -> {
|
||||||
|
val fullLine = lines[u0Idx]
|
||||||
|
// println("=== DEBUG U0 ===")
|
||||||
|
// println("fullLine = '$fullLine'")
|
||||||
|
|
||||||
|
val afterU0 = fullLine.substringAfter("U0:")
|
||||||
|
val blankPrefixRegex = Regex("^(z[048CEKO]:)")
|
||||||
|
val match = blankPrefixRegex.find(afterU0)
|
||||||
|
val blankPrefix = match?.value ?: ""
|
||||||
|
val body = if (match != null) {
|
||||||
|
afterU0.substring(blankPrefix.length)
|
||||||
|
} else {
|
||||||
|
afterU0
|
||||||
|
}
|
||||||
|
|
||||||
|
val measure = measureString.split("/")[0].toIntOrNull() ?: 4
|
||||||
|
|
||||||
|
val parser = ParseULine(body, measure)
|
||||||
|
val symboles = parser.parsed()
|
||||||
|
|
||||||
|
val newLigneT = "U0:"+blankPrefix+symboles
|
||||||
|
println("Ligne de U0 ==> $newLigneT")
|
||||||
|
lines[u0Idx] = newLigneT
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
println("Aucune ligne T0 ou U0 trouvée dans le fichier")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val templatArray = templateToArray(if(u0Idx != -1) lines[u0Idx] else lines[t0Idx])
|
||||||
|
/* println("index | contenuTUO")
|
||||||
|
templatArray.forEachIndexed { index, block ->
|
||||||
|
println("$index $block")
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
MODIF NOTES N1 N2 N3 N4
|
||||||
|
*/
|
||||||
|
// println("taille tA: ${templatArray.size} et le trgt $targetIdx")
|
||||||
|
val originalBlock = templatArray[targetIdx]
|
||||||
|
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
|
||||||
|
val deleteCount = originalBlock.replace(markerRegex, "").count { it.isLetter() }
|
||||||
|
|
||||||
|
val updatedNotes = updateSourceLines(lines, templatArray, targetIdx, deleteCount, editState)
|
||||||
|
|
||||||
|
// RESTAURATION DES TEMPLATES
|
||||||
|
if (templateString != "") {
|
||||||
|
updatedNotes[templateIndices] = templateString
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalString = updatedNotes.joinToString("\n")
|
||||||
|
|
||||||
|
val tempDir = System.getProperty("java.io.tmpdir")
|
||||||
|
val fileName = filePath.substringAfterLast('/')
|
||||||
|
val currentTempFile = File(tempDir, fileName)
|
||||||
|
currentTempFile?.deleteOnExit()
|
||||||
|
|
||||||
|
|
||||||
|
val normalizedTempDir = File(tempDir).absolutePath
|
||||||
|
val originalFilePath = if (filePath.startsWith(normalizedTempDir)) {
|
||||||
|
filePath
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
// println("Fichier Modifier:\n" +
|
||||||
|
// "$finalString")
|
||||||
|
//fileRepository.saveFile(filePath, finalString.toByteArray())
|
||||||
|
|
||||||
|
currentTempFile?.writeBytes(finalString.toByteArray())
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
justeBuild(currentTempFile.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun expandInclusions(filePath: String):String {
|
||||||
|
val result = StringBuilder()
|
||||||
|
val lastSlash = filePath.lastIndexOf('/')
|
||||||
|
val directory = if (lastSlash != -1) filePath.substring(0, lastSlash + 1) else ""
|
||||||
|
val myLines = fileRepository.readFileLines(filePath)
|
||||||
|
myLines.forEach { line ->
|
||||||
|
if (line.startsWith("I0:")) {
|
||||||
|
try {
|
||||||
|
val parts = line.substring(3).split(":")
|
||||||
|
val fileName = parts[0]
|
||||||
|
val ignorePattern = if (parts.size > 1) parts[1] else ""
|
||||||
|
|
||||||
|
val fullPath = directory + fileName
|
||||||
|
val includedLines = fileRepository.readFileLines(fullPath)
|
||||||
|
|
||||||
|
val regexIgnore = if (ignorePattern.isNotEmpty()) Regex(ignorePattern) else null
|
||||||
|
|
||||||
|
includedLines.forEach { incLine ->
|
||||||
|
if (regexIgnore == null || !regexIgnore.containsMatchIn(incLine)) {
|
||||||
|
result.append(incLine).append("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.append("// Erreur inclusion: ${e.message}\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.append(line).append("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString().trimEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSourceLines(
|
||||||
|
lines: MutableList<String>,
|
||||||
|
fragments: List<String>,
|
||||||
|
targetIdx: Int,
|
||||||
|
deleteCount: Int,
|
||||||
|
editState: TUOEditState
|
||||||
|
): MutableList<String> {
|
||||||
|
/* Notes N1 N2 N3 N4 */
|
||||||
|
updateNotesInLines(lines, fragments, targetIdx, deleteCount, editState)
|
||||||
|
/* MODIF LYRICS */
|
||||||
|
//updateLyricsInLines(lines, fragments, targetIdx, deleteCount, editState)
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLyricsInLines(
|
||||||
|
lines: MutableList<String>,
|
||||||
|
fragments: List<String>,
|
||||||
|
targetIdx: Int,
|
||||||
|
deleteCount: Int,
|
||||||
|
editState: TUOEditState
|
||||||
|
) {
|
||||||
|
editState.lyricsByStanza.forEach { (stanzaNum, newSyllable) ->
|
||||||
|
val lineYIdx = lines.indexOfFirst { it.matches(Regex("^[Y]$stanzaNum:.*")) }
|
||||||
|
val lineEIdx = lines.indexOfFirst { it.matches(Regex("^[E]$stanzaNum:.*")) }
|
||||||
|
|
||||||
|
val lineIdx = if(lineYIdx != -1) lineYIdx else lineEIdx
|
||||||
|
|
||||||
|
if (lineIdx != -1) {
|
||||||
|
val currentLine = lines[lineIdx]
|
||||||
|
val prefix = currentLine.substringBefore(":") + ":"
|
||||||
|
val lyricsBody = currentLine.substringAfter(":")
|
||||||
|
|
||||||
|
// On traite les paroles pour avoir la liste brute
|
||||||
|
val lyricsContent = if(lineYIdx != -1) smartYLyrics(lyricsBody) else smartELyrics(lyricsBody)
|
||||||
|
|
||||||
|
// Découpage strict pour correspondre au template
|
||||||
|
println("Lyrics BODY $lyricsBody")
|
||||||
|
println("LyricsC $lyricsContent")
|
||||||
|
|
||||||
|
val lyricsInLines = sharedScreenModel.synchronizedSyllables.value
|
||||||
|
lyricsInLines.mapIndexed { index, string ->
|
||||||
|
//println("SYNRO $index => {$string}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val allTokens = lyricsInLines.toMutableList()
|
||||||
|
|
||||||
|
// println("--- TABLEAU DE CORRESPONDANCE (Strophe $stanzaNum) ---")
|
||||||
|
// println(String.format("%-5s | %-10s | %-15s", "Idx", "Template", "Syllabe"))
|
||||||
|
// println("-------------------------------------------")
|
||||||
|
|
||||||
|
var actualTokenPointer = 0
|
||||||
|
for (i in fragments.indices) {
|
||||||
|
val fragment = fragments[i]
|
||||||
|
val isProlongation = fragment.trim() == "-"
|
||||||
|
|
||||||
|
val associatedSyllable = allTokens.getOrNull(actualTokenPointer) ?: ""
|
||||||
|
actualTokenPointer++
|
||||||
|
|
||||||
|
val marker = if (i == targetIdx) " <== [CIBLE]" else ""
|
||||||
|
|
||||||
|
// println(String.format("%-5d | %-10s | %-15s %s",
|
||||||
|
// i,
|
||||||
|
// "[$fragment]",
|
||||||
|
// "[$associatedSyllable]",
|
||||||
|
// marker))
|
||||||
|
}
|
||||||
|
|
||||||
|
println("\nAction finale : Remplacer l'index $targetIdx (valeur: ${allTokens.getOrNull(targetIdx)})")
|
||||||
|
|
||||||
|
if (targetIdx < allTokens.size) {
|
||||||
|
allTokens[targetIdx] = newSyllable
|
||||||
|
|
||||||
|
if(deleteCount > 1){
|
||||||
|
var deleted = 0
|
||||||
|
var j = targetIdx + 1
|
||||||
|
while (deleted < deleteCount - 1 && j < allTokens.size) {
|
||||||
|
if (allTokens[j].isNotEmpty()) {
|
||||||
|
allTokens[j] = ""
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rawResult = allTokens.filter { it.isNotEmpty() }.joinToString(" ")
|
||||||
|
println("allToken ${allTokens.joinToString("")}")
|
||||||
|
// println("RAWRes ${rawResult}")
|
||||||
|
val formattedResult = rawResult
|
||||||
|
.replace(Regex("-\\s+"), "")
|
||||||
|
.replace(Regex("-"), "")
|
||||||
|
|
||||||
|
println("FORMATTER $formattedResult")
|
||||||
|
|
||||||
|
val lyrIdx = lines.indexOfFirst { line ->
|
||||||
|
line.contains(Regex("^[EY]$stanzaNum:"))
|
||||||
|
}
|
||||||
|
val lyrics = lines[lyrIdx]
|
||||||
|
println("Originl $lyrics")
|
||||||
|
|
||||||
|
|
||||||
|
lines[lineIdx] = prefix + formattedResult
|
||||||
|
//lines[lineIdx] = prefix + reconstructLyrics(finalTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// println("--- FIN DEBUG ---")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun updateNotesInLines(
|
||||||
|
lines: MutableList<String>,
|
||||||
|
fragments: List<String>,
|
||||||
|
targetIdx: Int,
|
||||||
|
deleteCount: Int,
|
||||||
|
editState: TUOEditState
|
||||||
|
) {
|
||||||
|
val notesByVoice = editState.notesByVoice
|
||||||
|
val originalNotes = editState.originalNotes
|
||||||
|
|
||||||
|
for (voiceNum in 0..3) {
|
||||||
|
val linePrefix = "N${voiceNum + 1}:"
|
||||||
|
val lineIdx = lines.indexOfFirst { it.startsWith(linePrefix) }
|
||||||
|
|
||||||
|
if (lineIdx != -1) {
|
||||||
|
val currentLine = lines[lineIdx]
|
||||||
|
val prefix = currentLine.substringBefore(":") + ":"
|
||||||
|
val noteBody = currentLine.substringAfter(":")
|
||||||
|
val hasAnchor = noteBody.contains("#")
|
||||||
|
val noteExpanded = expandNotes(noteBody)
|
||||||
|
|
||||||
|
// println("Notes===>$noteExpanded")
|
||||||
|
val newNot = notesByVoice[voiceNum] ?: ""
|
||||||
|
val oldNot = originalNotes[voiceNum] ?: ""
|
||||||
|
|
||||||
|
val isNoteModified = newNot != oldNot
|
||||||
|
// println("est-ce? $isNoteModified = $newNot != $oldNot")
|
||||||
|
|
||||||
|
if (!isNoteModified) {
|
||||||
|
// println("Voix $linePrefix : Aucune modification détectée (Note: $oldNot ).")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val newNote = notesByVoice[voiceNum] ?: ""
|
||||||
|
if (newNote.isEmpty()) continue
|
||||||
|
val cleanNewNote = revertMusicalInput(newNote)
|
||||||
|
|
||||||
|
|
||||||
|
val regex = Regex("#\\S[',]*|\\s#\\S[',]*|/|\\(|\\)|[drmfsltDRFSTzw][0-9'¹²³⁴⁵₁₂₃₄₅,]*|[-.]")
|
||||||
|
|
||||||
|
|
||||||
|
val allTokens = regex.findAll(noteExpanded).map { it.value }.toList()
|
||||||
|
|
||||||
|
val targetPointer = getNotePointer(fragments, targetIdx, currentLine)
|
||||||
|
|
||||||
|
val resultTokens = mutableListOf<String>()
|
||||||
|
var currentLogicalIdx = 0
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
// println("\nSuivi pour la voix $linePrefix (Pointeur cible: $targetPointer)")
|
||||||
|
// println(
|
||||||
|
// String.format(
|
||||||
|
// "%-5s | %-10s | %-8s | %-12s | %-10s",
|
||||||
|
// "Idx P",
|
||||||
|
// "Token",
|
||||||
|
// "Type",
|
||||||
|
// "LogiqueIdx",
|
||||||
|
// "Action"
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
while (i < allTokens.size) {
|
||||||
|
val token = allTokens[i]
|
||||||
|
val isStructural = token.contains("#") || token == "/" || token == "(" || token == ")"
|
||||||
|
|
||||||
|
if (isStructural) {
|
||||||
|
// println(String.format("%-5d | %-10s | %-8s | %-12s | %-10s", i, token, "STRUCT", "-", "Keep"))
|
||||||
|
resultTokens.add(token)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLogicalIdx == targetPointer) {
|
||||||
|
val originalToken = allTokens[i]
|
||||||
|
var replacement = if (!hasAnchor) {
|
||||||
|
val (newBase, newLevel) = parseNoteAndOctave(revertMusicalInput(newNot))
|
||||||
|
val (oldBase, oldLevel) = parseNoteAndOctave(revertMusicalInput(oldNot))
|
||||||
|
val octaveDiff = newLevel - oldLevel
|
||||||
|
|
||||||
|
val (tokenBase, tokenLevel) = parseNoteAndOctave(originalToken)
|
||||||
|
newBase + formatOctave(tokenLevel + octaveDiff)
|
||||||
|
} else {
|
||||||
|
// SI y a '#'
|
||||||
|
val anchorMatch = Regex("#([drmfsltDRFST])[',]*").find(noteBody)
|
||||||
|
val anchorLevel = if (anchorMatch != null) {
|
||||||
|
val anchorStr = anchorMatch.value
|
||||||
|
anchorStr.count { it == '\'' } - anchorStr.count { it == ',' }
|
||||||
|
} else 0
|
||||||
|
|
||||||
|
val newNotCleaned = revertMusicalInput(newNot)
|
||||||
|
val oldNotCleaned = revertMusicalInput(oldNot)
|
||||||
|
|
||||||
|
val (newBase, newLevel) = parseNoteAndOctave(newNotCleaned)
|
||||||
|
val (oldBase, oldLevel) = parseNoteAndOctave(oldNotCleaned)
|
||||||
|
|
||||||
|
val sourceLevel = newLevel - anchorLevel
|
||||||
|
val formattedOctave = formatOctave(sourceLevel)
|
||||||
|
val finalPureNew = newBase + formattedOctave
|
||||||
|
|
||||||
|
val pureOldBase = oldBase + formatOctave(oldLevel - anchorLevel)
|
||||||
|
|
||||||
|
// println("Ancre niveau: $anchorLevel | UI Level: $newLevel -> Source Level: $sourceLevel")
|
||||||
|
//
|
||||||
|
if (finalPureNew.startsWith(pureOldBase) && pureOldBase.isNotEmpty()) {
|
||||||
|
val addedPart = finalPureNew.substring(pureOldBase.length)
|
||||||
|
originalToken + addedPart
|
||||||
|
} else {
|
||||||
|
finalPureNew
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// println(
|
||||||
|
// String.format(
|
||||||
|
// "%-5s | %-10s | %-8s | %-12s | %-10s",
|
||||||
|
// "NEW", replacement, "INSERT", currentLogicalIdx, "REPLACE ($originalToken)"
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
resultTokens.add(replacement)
|
||||||
|
|
||||||
|
var notesSkipped = 0
|
||||||
|
while (notesSkipped < deleteCount && i < allTokens.size) {
|
||||||
|
val nextToken = allTokens[i]
|
||||||
|
val isNextStructural =
|
||||||
|
nextToken.contains("#") || nextToken == "/" || nextToken == "(" || nextToken == ")"
|
||||||
|
|
||||||
|
if (!isNextStructural) {
|
||||||
|
notesSkipped++
|
||||||
|
} else if (hasAnchor) {
|
||||||
|
resultTokens.add(nextToken)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
currentLogicalIdx += deleteCount
|
||||||
|
} else {
|
||||||
|
// AFFICHAGE NOTE NORMALE
|
||||||
|
// println(
|
||||||
|
// String.format(
|
||||||
|
// "%-5d | %-10s | %-8s | %-12d | %-10s",
|
||||||
|
// i,
|
||||||
|
// token,
|
||||||
|
// "NOTE",
|
||||||
|
// currentLogicalIdx,
|
||||||
|
// "Keep"
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
resultTokens.add(token)
|
||||||
|
currentLogicalIdx++
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines[lineIdx] = prefix + resultTokens.joinToString("")
|
||||||
|
|
||||||
|
// println("-".repeat(55))
|
||||||
|
// println("RÉSULTAT : ${lines[lineIdx]}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun parseNoteAndOctave(s: String): Pair<String, Int> {
|
||||||
|
val base = s.replace(Regex("[^drmfsltDRFSTzw]"), "")
|
||||||
|
var level = 0
|
||||||
|
|
||||||
|
level += s.count { it == '\'' || it == '¹' || it == '²' || it == '³' }
|
||||||
|
level -= s.count { it == ',' || it == '₁' || it == '₂' || it == '₃' }
|
||||||
|
return Pair(base, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatOctave(level: Int): String {
|
||||||
|
if (level == 0) return ""
|
||||||
|
return if (level > 0) "'".repeat(level) else ",".repeat(Math.abs(level))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun revertMusicalInput(input: String): String {
|
||||||
|
return input
|
||||||
|
.replace("• ,", "")
|
||||||
|
.replace("―•", "")
|
||||||
|
.replace("―", "-")
|
||||||
|
.replace("•", "")
|
||||||
|
.replace("³", "'''")
|
||||||
|
.replace("₃", ",,,")
|
||||||
|
.replace("²", "''")
|
||||||
|
.replace("₂", ",,")
|
||||||
|
.replace("¹", "'")
|
||||||
|
.replace("₁", ",")
|
||||||
|
|
||||||
|
.replace("di", "D")
|
||||||
|
.replace("ri", "R")
|
||||||
|
.replace("fi", "F")
|
||||||
|
.replace("si", "S")
|
||||||
|
.replace("ta", "T")
|
||||||
|
|
||||||
|
.replace("(","")
|
||||||
|
.replace(")","")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNoteLineToArray(noteLine: String): List<String> {
|
||||||
|
val content = noteLine.substringAfter(":").replace(" ", "")
|
||||||
|
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
while (i < content.length) {
|
||||||
|
val char = content[i]
|
||||||
|
|
||||||
|
if (char == '/' || char == ' '/* || char == 'z'*/) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (char == '#') {
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.isLetter() || char == '-') {
|
||||||
|
val noteBuilder = StringBuilder(char.toString())
|
||||||
|
var nextIdx = i + 1
|
||||||
|
|
||||||
|
while (nextIdx < content.length && (content[nextIdx] == ',' || content[nextIdx] == '\'')) {
|
||||||
|
noteBuilder.append(content[nextIdx])
|
||||||
|
nextIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
val fullNote = noteBuilder.toString()
|
||||||
|
|
||||||
|
if (nextIdx < content.length && content[nextIdx].isDigit()) {
|
||||||
|
val repeatCount = content[nextIdx].toString().toInt()
|
||||||
|
repeat(repeatCount) {
|
||||||
|
result.add(fullNote)
|
||||||
|
}
|
||||||
|
nextIdx++
|
||||||
|
} else {
|
||||||
|
result.add(fullNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
i = nextIdx
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun templateToArray(templateLine: String): List<String> {
|
||||||
|
var templateArray: List<String> = emptyList()
|
||||||
|
|
||||||
|
val templateType = if(templateLine.startsWith("T0")) "T" else "U"
|
||||||
|
if(templateType == "T") {
|
||||||
|
var content = templateLine.substringAfter("{").substringBeforeLast("}")
|
||||||
|
// println("AVANT T: $content")
|
||||||
|
|
||||||
|
content = content.replace(Regex("\\\$\\{.*?\\}"), "")
|
||||||
|
|
||||||
|
val rawBlocks = content.split(Regex("[:|/!]"))
|
||||||
|
|
||||||
|
templateArray = rawBlocks.filter { it.isNotBlank() }
|
||||||
|
// println("\n\nLine = $templateLine \n," +
|
||||||
|
// "Array = ${templateArray.joinToString("|")}")
|
||||||
|
// println("APRèS T: ${templateArray.joinToString("|")}")
|
||||||
|
} else {
|
||||||
|
val body = templateLine.replaceFirst(Regex("^U0:z.:"), "")
|
||||||
|
|
||||||
|
//println("Mon contenu est: $body.")
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
var currentBlock = StringBuilder()
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (i < body.length) {
|
||||||
|
val char = body[i]
|
||||||
|
|
||||||
|
// 1. GESTION DES MARQUEURS ${...} -> ON IGNORE TOUT LE BLOC
|
||||||
|
if (char == '$' && i + 1 < body.length && body[i + 1] == '{') {
|
||||||
|
val closingBrace = body.indexOf('}', i)
|
||||||
|
if (closingBrace != -1) {
|
||||||
|
i = closingBrace + 1 // On saute jusqu'après le '}'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (char == '$') {
|
||||||
|
i += 2 // On saute le '$' et la lettre qui suit
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SÉPARATEURS -> ON COUPE LE BLOC
|
||||||
|
if (char in listOf(':', '|', '!', '/')) {
|
||||||
|
if (currentBlock.isNotEmpty()) {
|
||||||
|
result.add(currentBlock.toString())
|
||||||
|
currentBlock.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 4. IGNORER LES ESPACES
|
||||||
|
else if (char == ' ') {
|
||||||
|
// ne rien faire
|
||||||
|
}
|
||||||
|
// 5. TOUT LE RESTE (D, z, -, ., ,, (, ) )
|
||||||
|
else {
|
||||||
|
currentBlock.append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le dernier morceau s'il n'y a pas de séparateur à la fin
|
||||||
|
if (currentBlock.isNotEmpty()) result.add(currentBlock.toString())
|
||||||
|
|
||||||
|
templateArray = result
|
||||||
|
}
|
||||||
|
|
||||||
|
// println("\n\nLine = $templateLine \n," +
|
||||||
|
// "Array = ${templateArray.joinToString("|")}")
|
||||||
|
return templateArray
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expandNotes(text: String): String {
|
||||||
|
val regex = Regex("([drmfsltDRFSTzw])([,']*)(\\d+)")
|
||||||
|
return regex.replace(text) { matchResult ->
|
||||||
|
val note = matchResult.groupValues[1]
|
||||||
|
val octaves = matchResult.groupValues[2]
|
||||||
|
val count = matchResult.groupValues[3].toInt()
|
||||||
|
|
||||||
|
(note + octaves).repeat(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun rearrangeNote(noteString: String, infiniteIter: Int = 0): String {
|
private fun rearrangeNote(noteString: String, infiniteIter: Int = 0): String {
|
||||||
var result: String = noteString
|
var result: String = noteString
|
||||||
|
|
@ -968,4 +1682,96 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
this.add(element)
|
this.add(element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getNotePointerWithDebug(templatArray: List<String>, targetIdx: Int, sourceLine: String): Int {
|
||||||
|
println("\n=== DÉBOGAGE ALIGNEMENT RYTHME / NOTES (CORRIGÉ) ===")
|
||||||
|
|
||||||
|
val bodyOnly = sourceLine.substringAfter(":")
|
||||||
|
val expandedBody = expandNotes(bodyOnly)
|
||||||
|
// On suppose que parseNoteLineToArray découpe chaque "événement" (note ou prolongement)
|
||||||
|
val notesList = parseNoteLineToArray("N:$expandedBody")
|
||||||
|
|
||||||
|
println("\n=== ALIGNEMENT STRICT : TEMPLATE -> SOURCE ===")
|
||||||
|
println("Ligne Source : ${sourceLine.substringBefore(":")}")
|
||||||
|
println("----------------------------------------------------------------------")
|
||||||
|
println("%-5s | %-12s | %-15s | %-10s".format("IDX", "BLOC TEMP.", "NOTES SRC", "POINTEUR"))
|
||||||
|
println("----------------------------------------------------------------------")
|
||||||
|
|
||||||
|
var currentPointer = 0
|
||||||
|
var notesInTargetBlock = mutableListOf<String>()
|
||||||
|
var countInTarget = 0
|
||||||
|
|
||||||
|
for (i in templatArray.indices) {
|
||||||
|
val block = templatArray[i]
|
||||||
|
|
||||||
|
// NETTOYAGE : On ignore les marqueurs $Q, ${DC} pour le comptage
|
||||||
|
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
|
||||||
|
val cleanBlock = block.replace(markerRegex, "")
|
||||||
|
|
||||||
|
// RÈGLE : On compte uniquement les LETTRES (S, F, D, R, M, L, T, d, r...)
|
||||||
|
val nbLettres = cleanBlock.count { it.isLetter() && it.lowercaseChar() != 'z' }
|
||||||
|
|
||||||
|
// On récupère les notes correspondantes dans le fichier source
|
||||||
|
val blocNotes = (0 until nbLettres).map { offset ->
|
||||||
|
notesList.getOrNull(currentPointer + offset) ?: "[VIDE]"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == targetIdx) {
|
||||||
|
countInTarget = nbLettres
|
||||||
|
notesInTargetBlock.addAll(blocNotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val marker = if (i == targetIdx) " [ CIBLE ]" else ""
|
||||||
|
println("[%3d] | %-12s | %-15s | Index: %d %s"
|
||||||
|
.format(i, block, blocNotes.joinToString(","), currentPointer, marker))
|
||||||
|
|
||||||
|
// On incrémente le pointeur pour le prochain bloc
|
||||||
|
// SEULEMENT si nous n'avons pas encore dépassé la cible
|
||||||
|
// if (i < targetIdx) {
|
||||||
|
currentPointer += nbLettres
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
println("----------------------------------------------------------------------")
|
||||||
|
println("RÉSULTAT : Le bloc $targetIdx contient $countInTarget lettre(s).")
|
||||||
|
println("Notes pointées dans le fichier source : ${notesInTargetBlock.joinToString(" ET ")}")
|
||||||
|
println("Pointeur de départ (offset) : $currentPointer")
|
||||||
|
println("======================================================================\n")
|
||||||
|
|
||||||
|
return currentPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNotePointer(templatArray: List<String>, targetIdx: Int, sourceLine: String): Int {
|
||||||
|
val bodyOnly = sourceLine.substringAfter(":")
|
||||||
|
val expandedBody = expandNotes(bodyOnly)
|
||||||
|
val notesList = parseNoteLineToArray("N:$expandedBody")
|
||||||
|
var currentPointer = 0
|
||||||
|
var notesInTargetBlock = mutableListOf<String>()
|
||||||
|
var countInTarget = 0
|
||||||
|
|
||||||
|
for (i in templatArray.indices) {
|
||||||
|
val block = templatArray[i]
|
||||||
|
|
||||||
|
val markerRegex = Regex("""\$\{.*?\}|\$\w+""")
|
||||||
|
val cleanBlock = block.replace(markerRegex, "")
|
||||||
|
|
||||||
|
val nbLettres = cleanBlock.count { it.isLetter() && it.lowercaseChar() != 'z' }
|
||||||
|
|
||||||
|
val blocNotes = (0 until nbLettres).map { offset ->
|
||||||
|
notesList.getOrNull(currentPointer + offset) ?: "[VIDE]"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == targetIdx) {
|
||||||
|
countInTarget = nbLettres
|
||||||
|
notesInTargetBlock.addAll(blocNotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val marker = if (i == targetIdx) " [ CIBLE ]" else ""
|
||||||
|
|
||||||
|
if (i < targetIdx) {
|
||||||
|
currentPointer += nbLettres
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentPointer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
package mg.dot.feufaro.solfa
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.draw.drawBehind
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import mg.dot.feufaro.data.GridTUOData
|
|
||||||
import kotlin.math.min
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.BaselineShift
|
|
||||||
import androidx.compose.ui.text.withStyle
|
|
||||||
import SharedScreenModel
|
import SharedScreenModel
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Description
|
||||||
|
import androidx.compose.material.icons.filled.Save
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.text.TextMeasurer
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.BaselineShift
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import androidx.compose.ui.window.Popup
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mg.dot.feufaro.ContextualMenu
|
||||||
|
import mg.dot.feufaro.FileRepository
|
||||||
|
import mg.dot.feufaro.TUODetailDialog
|
||||||
|
import mg.dot.feufaro.data.GridTUOData
|
||||||
|
import mg.dot.feufaro.ui.rememberFileSaveLauncher
|
||||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||||
|
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
val FEUFAROO_TRIOLET_COLOR = Color.DarkGray
|
val FEUFAROO_TRIOLET_COLOR = Color.DarkGray
|
||||||
val FEUFAROO_KEY_CHANGE_COLOR = Color.Blue
|
val FEUFAROO_KEY_CHANGE_COLOR = Color.Blue
|
||||||
|
@Stable
|
||||||
class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, countOfN: Int) {
|
class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, countOfN: Int) {
|
||||||
var mutableNoteVersion: Int by mutableStateOf(0)
|
var mutableNoteVersion: Int by mutableStateOf(0)
|
||||||
private var lyrics: MutableList<POneStanzaLyrics> = mutableListOf()
|
private var lyrics: MutableList<POneStanzaLyrics> = mutableListOf()
|
||||||
|
|
@ -60,6 +71,7 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co
|
||||||
private var annotated : Boolean
|
private var annotated : Boolean
|
||||||
var transposeNewKey: String = ""
|
var transposeNewKey: String = ""
|
||||||
var transposeOldKey: String = ""
|
var transposeOldKey: String = ""
|
||||||
|
var firstTuoIndex: Int = -1
|
||||||
companion object {
|
companion object {
|
||||||
var nbBlock: Int = 0
|
var nbBlock: Int = 0
|
||||||
var sep1: String = ""
|
var sep1: String = ""
|
||||||
|
|
@ -543,8 +555,29 @@ fun LazyVerticalGridTUO(
|
||||||
onGridWidthMeasured: (Int) -> Unit,
|
onGridWidthMeasured: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val regexMeasure = Regex("(\\d)/\\d").find(viewModel.measure)
|
|
||||||
val tuoList = viewModel.tuoList
|
val tuoList = viewModel.tuoList
|
||||||
|
key(tuoList) {
|
||||||
|
|
||||||
|
var menuPosition by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
|
var showDetailDialog by remember { mutableStateOf(false) }
|
||||||
|
var showEditDialog by remember { mutableStateOf(false) }
|
||||||
|
var showContextualMenu by remember { mutableStateOf(false) }
|
||||||
|
var selectedTUO by remember { mutableStateOf<TimeUnitObject?>(null) }
|
||||||
|
var selectedIndex by remember { mutableStateOf(-1) }
|
||||||
|
val regexMeasure = Regex("(\\d)/\\d").find(viewModel.measure)
|
||||||
|
|
||||||
|
LaunchedEffect(tuoList) {
|
||||||
|
showContextualMenu = false
|
||||||
|
showEditDialog = false
|
||||||
|
showDetailDialog = false
|
||||||
|
showAddDialog = false
|
||||||
|
selectedIndex = -1
|
||||||
|
selectedTUO = null
|
||||||
|
}
|
||||||
|
|
||||||
|
var showFullChord by remember { mutableStateOf(false) }
|
||||||
|
val editMode by sharedScreenModel.modeEditor.collectAsState()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -618,11 +651,21 @@ fun LazyVerticalGridTUO(
|
||||||
println("Mise à jour MidiData avec ${metadataList.size}")
|
println("Mise à jour MidiData avec ${metadataList.size}")
|
||||||
sharedScreenModel.updateAndFinalizeMidiData(metadataList)
|
sharedScreenModel.updateAndFinalizeMidiData(metadataList)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(measures, sharedScreenModel.stanza.value) {
|
||||||
|
sharedScreenModel.updateSyllablesFromList(
|
||||||
|
measures = measures,
|
||||||
|
stanzaNumber = sharedScreenModel.stanza.value
|
||||||
|
)
|
||||||
|
|
||||||
|
val totalSyllables = sharedScreenModel.synchronizedSyllables.value.size
|
||||||
|
//println("Sync complète effectuée SUR stz: ${sharedScreenModel.stanza.value}! Total colonnes : $totalSyllables")
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
){
|
){
|
||||||
measures.forEachIndexed { measureIndex, measureTUOs ->
|
measures.forEachIndexed { measureIndex, measureTUOs ->
|
||||||
|
key(measureIndex) {
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -717,33 +760,204 @@ fun LazyVerticalGridTUO(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
measureTUOs.forEachIndexed { indexInMeasure, tuo ->
|
||||||
|
val chordMap = (1..4).mapNotNull { voice ->
|
||||||
|
val noteStr = tuo.tuNotes.getOrNull(voice)?.toString()
|
||||||
|
if (noteStr != null) voice to noteStr else null
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
val degreeName = remember(chordMap) {
|
||||||
|
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeDegree(chordMap) else null
|
||||||
|
}
|
||||||
|
val chordName = remember(chordMap) {
|
||||||
|
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, sharedScreenModel.songKey.value) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(gridWidthDp / gridColumnCount)
|
||||||
|
) {
|
||||||
|
// 1. Définir une plage de tailles de police
|
||||||
|
val maxFontSize = 13.sp
|
||||||
|
val minFontSize = 8.sp
|
||||||
|
var currentFontSize by remember { mutableStateOf(maxFontSize) }
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = { showFullChord = !showFullChord },
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
modifier = Modifier.height(24.dp).fillMaxWidth() // fillMaxWidth pour occuper toute la colonne
|
||||||
|
) {
|
||||||
|
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
val textToDisplay = if (showFullChord) (chordName ?: "") else (degreeName ?: "")
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = textToDisplay,
|
||||||
|
onTextLayout = { textLayoutResult ->
|
||||||
|
if (textLayoutResult.hasVisualOverflow && currentFontSize > minFontSize) {
|
||||||
|
currentFontSize = (currentFontSize.value * 0.9f).sp
|
||||||
|
}
|
||||||
|
},
|
||||||
|
softWrap = false, // Empêche le retour à la ligne
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
style = TextStyle(
|
||||||
|
color = if (showFullChord) Color(0xFF1B5E20) else Color(0xFF0D47A1),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = currentFontSize // Utilisation de la taille dynamique[cite: 1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
|
measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
|
||||||
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
|
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
|
||||||
val isActive = (globalIndex == activeRowIndex)
|
val isActive = (globalIndex == activeRowIndex)
|
||||||
|
|
||||||
val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L }
|
val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L }
|
||||||
|
var menuOffset by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
|
||||||
Box(modifier = Modifier.width(gridWidthDp / gridColumnCount)
|
key(oneTUO.numBlock) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.width(gridWidthDp / gridColumnCount)
|
||||||
|
.background(
|
||||||
|
if (isHovered) Color.Black.copy(alpha = 0.05f) else Color.Transparent,
|
||||||
|
RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = LocalIndication.current,
|
||||||
onClick = {
|
onClick = {
|
||||||
// println("je suis sur grille no: $globalIndex")
|
if(!editMode) {
|
||||||
sharedScreenModel.seekToGrid(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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)) {
|
)
|
||||||
|
.pointerInput(globalIndex) {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = { offset ->
|
||||||
|
if(editMode) {
|
||||||
|
menuOffset = offset
|
||||||
|
selectedTUO = oneTUO
|
||||||
|
selectedIndex = globalIndex
|
||||||
|
showContextualMenu = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
TimeUnitComposable(
|
TimeUnitComposable(
|
||||||
tuo = oneTUO,
|
tuo = oneTUO,
|
||||||
stanzaNumber = currentStanza,
|
stanzaNumber = currentStanza,
|
||||||
gridColumnCount = gridColumnCount,
|
gridColumnCount = gridColumnCount,
|
||||||
gridActive = isActive
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -765,6 +979,8 @@ fun LazyVerticalGridTUO(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allTemps.forEachIndexed { syls_i, syllables ->
|
allTemps.forEachIndexed { syls_i, syllables ->
|
||||||
|
/*val fragmentU0 = measureTUOs[syls_i].pTemplate.template
|
||||||
|
println("Fragment=[$fragmentU0] -> Syllabes=$syllables")*/
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(columnWidthDp)
|
.width(columnWidthDp)
|
||||||
|
|
@ -821,6 +1037,8 @@ fun LazyVerticalGridTUO(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun makeSpaceBetweenSyllables(
|
fun makeSpaceBetweenSyllables(
|
||||||
|
|
@ -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.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
|
@ -23,14 +24,17 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import mg.dot.feufaro.pdf.PrintSettings
|
import mg.dot.feufaro.pdf.PrintSettings
|
||||||
import mg.dot.feufaro.pdf.defaultPrintSettings
|
import mg.dot.feufaro.pdf.defaultPrintSettings
|
||||||
import mg.dot.feufaro.ui.PrintSettingsDialog
|
import mg.dot.feufaro.ui.PrintSettingsDialog
|
||||||
import mg.dot.feufaro.pdf.rememberPdfExportAction
|
import mg.dot.feufaro.pdf.rememberPdfExportAction
|
||||||
import mg.dot.feufaro.solfa.Solfa
|
import mg.dot.feufaro.solfa.Solfa
|
||||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -98,6 +102,43 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
sharedScreenModel.loadNewSong("$midiFile")
|
sharedScreenModel.loadNewSong("$midiFile")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val isEditMode by sharedScreenModel.modeEditor.collectAsState()
|
||||||
|
val currentActiveFilePath = sharedScreenModel.activeFilePath.value
|
||||||
|
val fileName = currentActiveFilePath.substringAfterLast('/')
|
||||||
|
|
||||||
|
val saveLauncher = rememberFileSaveLauncher { chosenPath ->
|
||||||
|
if (chosenPath != null) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
delay(500)
|
||||||
|
val tempDir = System.getProperty("java.io.tmpdir")
|
||||||
|
|
||||||
|
val sourceFileName = File(currentActiveFilePath).name
|
||||||
|
val sourceFile = File(tempDir, sourceFileName)
|
||||||
|
|
||||||
|
if (sourceFile.exists() && sourceFile.isFile) {
|
||||||
|
val data = sourceFile.readBytes()
|
||||||
|
|
||||||
|
if (data.isNotEmpty()) {
|
||||||
|
solfaScreenModel.fileRepository.saveLocalFile(chosenPath, data)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
println("Succès : ${data.size} octets copiés vers $chosenPath")
|
||||||
|
solfaScreenModel.loadExternalFile(chosenPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("Erreur : Le fichier source dans /tmp est vide.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("Erreur : Fichier source introuvable à l'adresse ${sourceFile.path} ${sourceFile.absolutePath}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
|
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
|
||||||
SimpleDrawerContent(
|
SimpleDrawerContent(
|
||||||
items,
|
items,
|
||||||
|
|
@ -197,6 +238,61 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
|
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
|
||||||
verticalArrangement = Arrangement.spacedBy(7.dp)
|
verticalArrangement = Arrangement.spacedBy(7.dp)
|
||||||
) {
|
) {
|
||||||
|
if (isEditMode) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = true,
|
||||||
|
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
||||||
|
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
//sharedScreenModel.
|
||||||
|
}, modifier = Modifier.alpha(0.45f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Default.Undo,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
AnimatedVisibility(
|
||||||
visible = isExpanded and !showMidiCtrl,
|
visible = isExpanded and !showMidiCtrl,
|
||||||
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
||||||
|
|
@ -230,27 +326,7 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
/*AnimatedVisibility(
|
|
||||||
visible = isExpanded and !showMidiCtrl,
|
|
||||||
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
|
||||||
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
|
|
||||||
) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
sharedScreenModel.toggleEditMode()
|
|
||||||
sharedScreenModel.setExpandedFAB(false)
|
|
||||||
}, modifier = Modifier.alpha(0.45f)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Edit,
|
|
||||||
contentDescription = "null",
|
|
||||||
tint = Color.Blue
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded and !showMidiCtrl,
|
visible = isExpanded and !showMidiCtrl,
|
||||||
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
||||||
|
|
@ -364,11 +440,11 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}) { paddingValues ->
|
}) { paddingValues ->
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize().padding(paddingValues).consumeWindowInsets(paddingValues).windowInsetsPadding(WindowInsets.ime)
|
modifier = Modifier.fillMaxSize().padding(paddingValues).consumeWindowInsets(paddingValues).windowInsetsPadding(WindowInsets.safeDrawing.union(WindowInsets.ime))
|
||||||
) {
|
) {
|
||||||
content(PaddingValues(0.dp))
|
content(PaddingValues(0.dp))
|
||||||
if (sharedScreenModel.isQRCodeVisible.value) {
|
if (sharedScreenModel.isQRCodeVisible.value) {
|
||||||
|
|
@ -424,7 +500,7 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize().fillMaxWidth(0.75f).align(Alignment.Center)
|
modifier = Modifier.fillMaxSize().fillMaxWidth(0.75f).align(Alignment.Center)
|
||||||
.padding(paddingValues)
|
.padding(paddingValues).windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.align(Alignment.TopEnd).padding(16.dp),
|
modifier = Modifier.align(Alignment.TopEnd).padding(16.dp),
|
||||||
|
|
@ -464,9 +540,8 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(
|
if(!isEditMode) {
|
||||||
|
Column {
|
||||||
) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
sharedScreenModel.showSearchMenu(!isSearchActive)
|
sharedScreenModel.showSearchMenu(!isSearchActive)
|
||||||
|
|
@ -483,7 +558,7 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -60,11 +61,49 @@ fun SimpleDrawerContent(
|
||||||
var externalExpanded by remember { mutableStateOf(false) }
|
var externalExpanded by remember { mutableStateOf(false) }
|
||||||
var playListExpanded by remember { mutableStateOf(false) }
|
var playListExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val editMode by sharedScreenModel.modeEditor.collectAsState()
|
||||||
|
var state1 by remember { mutableStateOf(false) }
|
||||||
|
var state2 by remember { mutableStateOf(false) }
|
||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.width(300.dp)
|
modifier = Modifier.width(300.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Box(modifier = Modifier.weight(1f).padding(0.dp, 20.dp)) {
|
// SWITCHES
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
CustomSwitchItem(
|
||||||
|
checked = editMode,
|
||||||
|
onCheckedChange = { newState ->
|
||||||
|
sharedScreenModel.toggleEditorMode(newState)
|
||||||
|
},
|
||||||
|
label = "Mode Edit",
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
CustomSwitchItem(
|
||||||
|
checked = state1,
|
||||||
|
onCheckedChange = { state1 = it },
|
||||||
|
label = "Analyse chords",
|
||||||
|
color = Color(0xFFE57373)
|
||||||
|
)
|
||||||
|
|
||||||
|
CustomSwitchItem(
|
||||||
|
checked = state2,
|
||||||
|
onCheckedChange = { state2 = it },
|
||||||
|
label = "Titre 3",
|
||||||
|
color = Color(0xFFBFBF11)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.weight(1f).padding(0.dp, 10.dp)) {
|
||||||
|
|
||||||
MyVerticalScrollbar(
|
MyVerticalScrollbar(
|
||||||
lazyListState = listState,
|
lazyListState = listState,
|
||||||
|
|
@ -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.data.getCombinedList
|
||||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||||
import mg.dot.feufaro.midi.FMediaPlayer
|
import mg.dot.feufaro.midi.FMediaPlayer
|
||||||
|
import mg.dot.feufaro.solfa.TUOEditState
|
||||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
@ -217,6 +218,41 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
private val _isQRCodeVisible = mutableStateOf(false)
|
private val _isQRCodeVisible = mutableStateOf(false)
|
||||||
val isQRCodeVisible: State<Boolean> = _isQRCodeVisible
|
val isQRCodeVisible: State<Boolean> = _isQRCodeVisible
|
||||||
|
|
||||||
|
private val _editModeState = mutableStateOf(false)
|
||||||
|
val editModeState: State<Boolean> = _editModeState
|
||||||
|
|
||||||
|
private val _modeEditor = MutableStateFlow(false)
|
||||||
|
val modeEditor: StateFlow<Boolean> = _modeEditor
|
||||||
|
|
||||||
|
fun toggleEditorMode(enabled: Boolean) {
|
||||||
|
_modeEditor.value = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _synchronizedSYllables = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
val synchronizedSyllables: StateFlow<List<String>> = _synchronizedSYllables.asStateFlow()
|
||||||
|
|
||||||
|
fun updateSyllablesFromList(measures: List<List<TimeUnitObject>>, stanzaNumber: Int) {
|
||||||
|
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
|
||||||
|
|
||||||
|
// println("\n--- DEBUG updateSyllablesFromList (Strophe $stanzaNumber) ---")
|
||||||
|
|
||||||
|
var absoluteIndex = 0
|
||||||
|
_synchronizedSYllables.value = measures.flatten().map { tuo ->
|
||||||
|
val rawSyllablesList = tuo.getSingleSyllable(stanzaNumber)
|
||||||
|
|
||||||
|
val processedSyllable = rawSyllablesList.joinToString(" ") { syllable ->
|
||||||
|
val cleaned = syllable.replace(REGEX_CLEAN_PREFIX, "").trim()
|
||||||
|
cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// println("TUO #$absoluteIndex | Template: ${tuo.pTemplate.template} | Raw: $rawSyllablesList | Final: [$processedSyllable]")
|
||||||
|
|
||||||
|
absoluteIndex++
|
||||||
|
processedSyllable
|
||||||
|
}
|
||||||
|
|
||||||
|
// println("--- FIN DEBUG (Total TUOs: $absoluteIndex) ---\n")
|
||||||
|
}
|
||||||
private val _activeFilePath = mutableStateOf("")
|
private val _activeFilePath = mutableStateOf("")
|
||||||
val activeFilePath: State<String> = _activeFilePath
|
val activeFilePath: State<String> = _activeFilePath
|
||||||
|
|
||||||
|
|
@ -227,6 +263,9 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
fun toggleQRCodeVisibility() {
|
fun toggleQRCodeVisibility() {
|
||||||
_isQRCodeVisible.value = !_isQRCodeVisible.value
|
_isQRCodeVisible.value = !_isQRCodeVisible.value
|
||||||
}
|
}
|
||||||
|
fun toggleEditMode() {
|
||||||
|
_editModeState.value = !_editModeState.value
|
||||||
|
}
|
||||||
val qrCodeContent: State<String?>
|
val qrCodeContent: State<String?>
|
||||||
get() = mutableStateOf(
|
get() = mutableStateOf(
|
||||||
_activeFilePath.value.takeIf { it.isNotBlank() }?.let { path ->
|
_activeFilePath.value.takeIf { it.isNotBlank() }?.let { path ->
|
||||||
|
|
@ -479,6 +518,25 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
println("Markers finalisés et mis à jour : ${finalizedList.size}")
|
println("Markers finalisés et mis à jour : ${finalizedList.size}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _tuoEditState = MutableStateFlow<TUOEditState?>(null)
|
||||||
|
val tuoEditState: StateFlow<TUOEditState?> = _tuoEditState.asStateFlow()
|
||||||
|
|
||||||
|
fun openTUOEditor(editState: TUOEditState) {
|
||||||
|
_tuoEditState.value = editState
|
||||||
|
println("nMrk = [${editState.marker}] template [${editState.templateFragment}] hp [${editState.hairPin}]")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeTUOEditor() {
|
||||||
|
_tuoEditState.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTUOEdit(new: TUOEditState) {
|
||||||
|
_tuoEditState.value = new
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFullTUOList(newList: List<TimeUnitObject>) {
|
||||||
|
_tuoList.value = newList
|
||||||
|
}
|
||||||
fun loadNewSong(newMidiFile: String) {
|
fun loadNewSong(newMidiFile: String) {
|
||||||
_mediaPlayer?.stop()
|
_mediaPlayer?.stop()
|
||||||
_mediaPlayer?.release()
|
_mediaPlayer?.release()
|
||||||
|
|
@ -488,6 +546,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
_currentPos.value = 0f
|
_currentPos.value = 0f
|
||||||
_dcDone.value = false
|
_dcDone.value = false
|
||||||
_dsDone.value = false
|
_dsDone.value = false
|
||||||
|
_editModeState.value = false
|
||||||
try {
|
try {
|
||||||
val midiFileName = fileRepository.getFileName(newMidiFile)
|
val midiFileName = fileRepository.getFileName(newMidiFile)
|
||||||
println("Opening xx129 $midiFileName")
|
println("Opening xx129 $midiFileName")
|
||||||
|
|
@ -587,6 +646,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
_showMidiCtrl.value = false
|
_showMidiCtrl.value = false
|
||||||
_expandedFAB.value = false
|
_expandedFAB.value = false
|
||||||
_showSearchMenu.value = false
|
_showSearchMenu.value = false
|
||||||
|
_editModeState.value = false
|
||||||
_midiMarkersList.value = emptyList()
|
_midiMarkersList.value = emptyList()
|
||||||
_tuoTimestamps.value = emptyList()
|
_tuoTimestamps.value = emptyList()
|
||||||
updateSearchTxt("")
|
updateSearchTxt("")
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,13 @@ package mg.dot.feufaro.viewmodel
|
||||||
|
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import mg.dot.feufaro.DeepLinkHandler
|
import mg.dot.feufaro.DeepLinkHandler
|
||||||
import mg.dot.feufaro.FileRepository
|
import mg.dot.feufaro.FileRepository
|
||||||
import mg.dot.feufaro.solfa.Solfa
|
import mg.dot.feufaro.solfa.Solfa
|
||||||
|
import mg.dot.feufaro.solfa.TUOEditState
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class SolfaScreenModel(
|
class SolfaScreenModel(
|
||||||
val fileRepository: FileRepository,
|
val fileRepository: FileRepository,
|
||||||
|
|
@ -38,4 +42,13 @@ class SolfaScreenModel(
|
||||||
fun loadFromFile(path: String) {
|
fun loadFromFile(path: String) {
|
||||||
solfa.parse(path)
|
solfa.parse(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun justeCompile(path:String) {
|
||||||
|
solfa.justeBuild(path)
|
||||||
|
}
|
||||||
|
fun saveTUOEdit(editState: TUOEditState) {
|
||||||
|
screenModelScope.launch(Dispatchers.IO) {
|
||||||
|
solfa.saveTUOEdit(editState)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -598,7 +598,7 @@ actual fun rememberPdfExportAction(
|
||||||
settings
|
settings
|
||||||
)
|
)
|
||||||
val chosenPath = withContext(Dispatchers.Main) {
|
val chosenPath = withContext(Dispatchers.Main) {
|
||||||
fileRepository.pickSavePath(computedFileName)
|
fileRepository.pickSavePath(computedFileName, "Exporter en PDF")
|
||||||
}
|
}
|
||||||
if (chosenPath != null) {
|
if (chosenPath != null) {
|
||||||
fileRepository.saveLocalFile(chosenPath, pdfBytes)
|
fileRepository.saveLocalFile(chosenPath, pdfBytes)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue